2016-07-03 16:45:13 +00:00
import React from 'react' ;
2016-06-11 16:54:09 +00:00
import {
Editor ,
2016-07-08 07:24:28 +00:00
EditorState ,
2016-06-11 16:54:09 +00:00
Modifier ,
ContentState ,
2016-07-03 16:45:13 +00:00
ContentBlock ,
2016-06-11 16:54:09 +00:00
convertFromHTML ,
DefaultDraftBlockRenderMap ,
DefaultDraftInlineStyle ,
2016-06-21 10:16:20 +00:00
CompositeDecorator ,
2016-07-03 16:45:13 +00:00
SelectionState ,
2016-07-08 07:24:28 +00:00
Entity ,
2016-06-11 16:54:09 +00:00
} from 'draft-js' ;
2016-06-11 10:22:08 +00:00
import * as sdk from './index' ;
2016-07-02 19:41:34 +00:00
import * as emojione from 'emojione' ;
2016-05-27 04:45:55 +00:00
2016-05-28 06:28:22 +00:00
const BLOCK _RENDER _MAP = DefaultDraftBlockRenderMap . set ( 'unstyled' , {
2016-07-08 07:24:28 +00:00
element : 'span' ,
2016-06-15 15:04:37 +00:00
/ *
draft uses < div > by default which we don 't really like, so we' re using < span >
this is probably not a good idea since < span > is not a block level element but
we ' re trying to fix things in contentStateToHTML below
* /
2016-05-28 06:28:22 +00:00
} ) ;
2016-06-11 16:54:09 +00:00
const STYLES = {
2016-05-27 04:45:55 +00:00
BOLD : 'strong' ,
CODE : 'code' ,
ITALIC : 'em' ,
STRIKETHROUGH : 's' ,
2016-07-03 16:45:13 +00:00
UNDERLINE : 'u' ,
2016-05-27 04:45:55 +00:00
} ;
2016-06-11 22:52:30 +00:00
const MARKDOWN _REGEX = {
LINK : /(?:\[([^\]]+)\]\(([^\)]+)\))|\<(\w+:\/\/[^\>]+)\>/g ,
ITALIC : /([\*_])([\w\s]+?)\1/g ,
2016-07-04 16:14:35 +00:00
BOLD : /([\*_])\1([\w\s]+?)\1\1/g ,
2016-06-11 22:52:30 +00:00
} ;
const USERNAME _REGEX = /@\S+:\S+/g ;
const ROOM _REGEX = /#\S+:\S+/g ;
2016-07-04 16:14:35 +00:00
const EMOJI _REGEX = new RegExp ( emojione . unicodeRegexp , 'g' ) ;
2016-06-11 22:52:30 +00:00
2016-06-11 10:22:08 +00:00
export function contentStateToHTML ( contentState : ContentState ) : string {
return contentState . getBlockMap ( ) . map ( ( block ) => {
let elem = BLOCK _RENDER _MAP . get ( block . getType ( ) ) . element ;
let content = [ ] ;
2016-06-11 16:54:09 +00:00
block . findStyleRanges (
( ) => true , // always return true => don't filter any ranges out
( start , end ) => {
// map style names to elements
2016-06-14 13:40:35 +00:00
let tags = block . getInlineStyleAt ( start ) . map ( style => STYLES [ style ] ) . filter ( style => ! ! style ) ;
2016-06-11 16:54:09 +00:00
// combine them to get well-nested HTML
let open = tags . map ( tag => ` < ${ tag } > ` ) . join ( '' ) ;
let close = tags . map ( tag => ` </ ${ tag } > ` ) . reverse ( ) . join ( '' ) ;
// and get the HTML representation of this styled range (this .substring() should never fail)
2016-06-15 14:54:37 +00:00
let text = block . getText ( ) . substring ( start , end ) ;
// http://shebang.brandonmintern.com/foolproof-html-escaping-in-javascript/
let div = document . createElement ( 'div' ) ;
div . appendChild ( document . createTextNode ( text ) ) ;
let safeText = div . innerHTML ;
content . push ( ` ${ open } ${ safeText } ${ close } ` ) ;
2016-06-11 16:54:09 +00:00
}
) ;
2016-05-27 04:45:55 +00:00
2016-06-15 14:54:37 +00:00
let result = ` < ${ elem } > ${ content . join ( '' ) } </ ${ elem } > ` ;
// dirty hack because we don't want block level tags by default, but breaks
2016-07-08 07:24:28 +00:00
if ( elem === 'span' )
2016-06-15 14:54:37 +00:00
result += '<br />' ;
return result ;
2016-05-27 04:45:55 +00:00
} ) . join ( '' ) ;
}
2016-06-11 16:54:09 +00:00
export function HTMLtoContentState ( html : string ) : ContentState {
2016-05-27 04:45:55 +00:00
return ContentState . createFromBlockArray ( convertFromHTML ( html ) ) ;
}
2016-06-09 18:23:09 +00:00
2016-07-08 07:24:28 +00:00
function unicodeToEmojiUri ( str ) {
let replaceWith , unicode , alt ;
if ( ( ! emojione . unicodeAlt ) || ( emojione . sprites ) ) {
// if we are using the shortname as the alt tag then we need a reversed array to map unicode code point to shortnames
let mappedUnicode = emojione . mapUnicodeToShort ( ) ;
}
str = str . replace ( emojione . regUnicode , function ( unicodeChar ) {
if ( ( typeof unicodeChar === 'undefined' ) || ( unicodeChar === '' ) || ( ! ( unicodeChar in emojione . jsEscapeMap ) ) ) {
// if the unicodeChar doesnt exist just return the entire match
return unicodeChar ;
} else {
// get the unicode codepoint from the actual char
unicode = emojione . jsEscapeMap [ unicodeChar ] ;
return emojione . imagePathSVG + unicode + '.svg' + emojione . cacheBustParam ;
}
} ) ;
return str ;
}
2016-08-03 12:57:49 +00:00
// Workaround for https://github.com/facebook/draft-js/issues/414
2016-07-08 07:24:28 +00:00
let emojiDecorator = {
strategy : ( contentBlock , callback ) => {
findWithRegex ( EMOJI _REGEX , contentBlock , callback ) ;
} ,
component : ( props ) => {
let uri = unicodeToEmojiUri ( props . children [ 0 ] . props . text ) ;
let shortname = emojione . toShort ( props . children [ 0 ] . props . text ) ;
let style = {
display : 'inline-block' ,
width : '1em' ,
maxHeight : '1em' ,
background : ` url( ${ uri } ) ` ,
backgroundSize : 'contain' ,
backgroundPosition : 'center center' ,
overflow : 'hidden' ,
} ;
2016-08-03 12:57:49 +00:00
return ( < span title = { shortname } style = { style } > < span style = { { opacity : 0 } } > { props . children } < / s p a n > < / s p a n > ) ;
2016-07-08 07:24:28 +00:00
} ,
} ;
2016-06-11 10:22:08 +00:00
/ * *
* Returns a composite decorator which has access to provided scope .
* /
2016-06-11 21:13:57 +00:00
export function getScopedRTDecorators ( scope : any ) : CompositeDecorator {
2016-06-11 16:54:09 +00:00
let MemberAvatar = sdk . getComponent ( 'avatars.MemberAvatar' ) ;
2016-06-09 18:23:09 +00:00
2016-06-11 16:54:09 +00:00
let usernameDecorator = {
2016-06-09 18:23:09 +00:00
strategy : ( contentBlock , callback ) => {
findWithRegex ( USERNAME _REGEX , contentBlock , callback ) ;
} ,
component : ( props ) => {
2016-06-11 10:22:08 +00:00
let member = scope . room . getMember ( props . children [ 0 ] . props . text ) ;
2016-06-14 13:40:35 +00:00
// unused until we make these decorators immutable (autocomplete needed)
let name = member ? member . name : null ;
2016-06-11 16:54:09 +00:00
let avatar = member ? < MemberAvatar member = { member } width = { 16 } height = { 16 } / > : null ;
2016-07-08 07:24:28 +00:00
return < span className = "mx_UserPill" > { avatar } { props . children } < / s p a n > ;
2016-06-09 18:23:09 +00:00
}
} ;
2016-07-02 19:41:34 +00:00
2016-06-11 16:54:09 +00:00
let roomDecorator = {
2016-06-09 18:23:09 +00:00
strategy : ( contentBlock , callback ) => {
findWithRegex ( ROOM _REGEX , contentBlock , callback ) ;
} ,
component : ( props ) => {
return < span className = "mx_RoomPill" > { props . children } < / s p a n > ;
}
} ;
2016-07-08 07:24:28 +00:00
return [ usernameDecorator , roomDecorator , emojiDecorator ] ;
2016-06-09 18:23:09 +00:00
}
2016-06-11 21:13:57 +00:00
export function getScopedMDDecorators ( scope : any ) : CompositeDecorator {
let markdownDecorators = [ 'BOLD' , 'ITALIC' ] . map (
( style ) => ( {
strategy : ( contentBlock , callback ) => {
return findWithRegex ( MARKDOWN _REGEX [ style ] , contentBlock , callback ) ;
} ,
component : ( props ) => (
< span className = { "mx_MarkdownElement mx_Markdown_" + style } >
{ props . children }
< / s p a n >
)
} ) ) ;
markdownDecorators . push ( {
strategy : ( contentBlock , callback ) => {
return findWithRegex ( MARKDOWN _REGEX . LINK , contentBlock , callback ) ;
} ,
component : ( props ) => (
< a href = "#" className = "mx_MarkdownElement mx_Markdown_LINK" >
{ props . children }
< / a >
)
} ) ;
2016-07-08 07:24:28 +00:00
markdownDecorators . push ( emojiDecorator ) ;
2016-06-11 21:13:57 +00:00
return markdownDecorators ;
}
2016-06-11 16:54:09 +00:00
/ * *
* Utility function that looks for regex matches within a ContentBlock and invokes { callback } with ( start , end )
* From https : //facebook.github.io/draft-js/docs/advanced-topics-decorators.html
* /
function findWithRegex ( regex , contentBlock : ContentBlock , callback : ( start : number , end : number ) => any ) {
2016-06-09 18:23:09 +00:00
const text = contentBlock . getText ( ) ;
let matchArr , start ;
while ( ( matchArr = regex . exec ( text ) ) !== null ) {
start = matchArr . index ;
callback ( start , start + matchArr [ 0 ] . length ) ;
}
}
2016-06-11 16:54:09 +00:00
/ * *
* Passes rangeToReplace to modifyFn and replaces it in contentState with the result .
* /
2016-06-11 22:50:30 +00:00
export function modifyText ( contentState : ContentState , rangeToReplace : SelectionState ,
2016-06-14 13:40:35 +00:00
modifyFn : ( text : string ) => string , inlineStyle , entityKey ) : ContentState {
2016-06-11 22:50:30 +00:00
let getText = ( key ) => contentState . getBlockForKey ( key ) . getText ( ) ,
startKey = rangeToReplace . getStartKey ( ) ,
startOffset = rangeToReplace . getStartOffset ( ) ,
endKey = rangeToReplace . getEndKey ( ) ,
endOffset = rangeToReplace . getEndOffset ( ) ,
2016-06-11 16:54:09 +00:00
text = "" ;
2016-06-11 22:50:30 +00:00
2016-07-03 16:45:13 +00:00
for ( let currentKey = startKey ;
2016-06-11 22:50:30 +00:00
currentKey && currentKey !== endKey ;
currentKey = contentState . getKeyAfter ( currentKey ) ) {
2016-06-14 13:58:51 +00:00
let blockText = getText ( currentKey ) ;
text += blockText . substring ( startOffset , blockText . length ) ;
2016-06-11 22:50:30 +00:00
// from now on, we'll take whole blocks
startOffset = 0 ;
2016-06-11 16:54:09 +00:00
}
2016-06-11 22:50:30 +00:00
// add remaining part of last block
text += getText ( endKey ) . substring ( startOffset , endOffset ) ;
2016-06-14 13:40:35 +00:00
return Modifier . replaceText ( contentState , rangeToReplace , modifyFn ( text ) , inlineStyle , entityKey ) ;
2016-06-11 16:54:09 +00:00
}
2016-06-21 10:16:20 +00:00
/ * *
* Computes the plaintext offsets of the given SelectionState .
* Note that this inherently means we make assumptions about what that means ( no separator between ContentBlocks , etc )
* Used by autocomplete to show completions when the current selection lies within , or at the edges of a command .
* /
2016-07-03 16:45:13 +00:00
export function selectionStateToTextOffsets ( selectionState : SelectionState ,
contentBlocks : Array < ContentBlock > ) : { start : number , end : number } {
2016-06-21 10:16:20 +00:00
let offset = 0 , start = 0 , end = 0 ;
2016-07-08 07:24:28 +00:00
for ( let block of contentBlocks ) {
2016-07-03 16:45:13 +00:00
if ( selectionState . getStartKey ( ) === block . getKey ( ) ) {
2016-06-21 10:16:20 +00:00
start = offset + selectionState . getStartOffset ( ) ;
}
2016-07-03 16:45:13 +00:00
if ( selectionState . getEndKey ( ) === block . getKey ( ) ) {
2016-06-21 10:16:20 +00:00
end = offset + selectionState . getEndOffset ( ) ;
break ;
}
offset += block . getLength ( ) ;
}
return {
start ,
2016-07-03 16:45:13 +00:00
end ,
} ;
}
export function textOffsetsToSelectionState ( { start , end } : { start : number , end : number } ,
contentBlocks : Array < ContentBlock > ) : SelectionState {
let selectionState = SelectionState . createEmpty ( ) ;
for ( let block of contentBlocks ) {
let blockLength = block . getLength ( ) ;
if ( start !== - 1 && start < blockLength ) {
selectionState = selectionState . merge ( {
anchorKey : block . getKey ( ) ,
anchorOffset : start ,
} ) ;
start = - 1 ;
} else {
start -= blockLength ;
}
if ( end !== - 1 && end <= blockLength ) {
selectionState = selectionState . merge ( {
focusKey : block . getKey ( ) ,
focusOffset : end ,
} ) ;
end = - 1 ;
} else {
end -= blockLength ;
}
2016-06-21 10:16:20 +00:00
}
2016-07-03 16:45:13 +00:00
return selectionState ;
2016-06-21 10:16:20 +00:00
}
2016-07-08 07:24:28 +00:00
// modified version of https://github.com/draft-js-plugins/draft-js-plugins/blob/master/draft-js-emoji-plugin/src/modifiers/attachImmutableEntitiesToEmojis.js
export function attachImmutableEntitiesToEmoji ( editorState : EditorState ) : EditorState {
const contentState = editorState . getCurrentContent ( ) ;
const blocks = contentState . getBlockMap ( ) ;
let newContentState = contentState ;
blocks . forEach ( ( block ) => {
const plainText = block . getText ( ) ;
const addEntityToEmoji = ( start , end ) => {
const existingEntityKey = block . getEntityAt ( start ) ;
if ( existingEntityKey ) {
// avoid manipulation in case the emoji already has an entity
const entity = Entity . get ( existingEntityKey ) ;
if ( entity && entity . get ( 'type' ) === 'emoji' ) {
return ;
}
}
const selection = SelectionState . createEmpty ( block . getKey ( ) )
. set ( 'anchorOffset' , start )
. set ( 'focusOffset' , end ) ;
const emojiText = plainText . substring ( start , end ) ;
const entityKey = Entity . create ( 'emoji' , 'IMMUTABLE' , { emojiUnicode : emojiText } ) ;
newContentState = Modifier . replaceText (
newContentState ,
selection ,
emojiText ,
null ,
entityKey ,
) ;
} ;
findWithRegex ( EMOJI _REGEX , block , addEntityToEmoji ) ;
} ) ;
if ( ! newContentState . equals ( contentState ) ) {
return EditorState . push (
editorState ,
newContentState ,
'convert-to-immutable-emojis' ,
) ;
}
return editorState ;
}