2016-07-03 22:15:13 +05:30
import React from 'react' ;
2016-06-11 22:24:09 +05:30
import {
Editor ,
2016-07-08 12:54:28 +05:30
EditorState ,
2016-06-11 22:24:09 +05:30
Modifier ,
ContentState ,
2016-07-03 22:15:13 +05:30
ContentBlock ,
2016-06-11 22:24:09 +05:30
convertFromHTML ,
DefaultDraftBlockRenderMap ,
DefaultDraftInlineStyle ,
2016-06-21 15:46:20 +05:30
CompositeDecorator ,
2016-07-03 22:15:13 +05:30
SelectionState ,
2016-07-08 12:54:28 +05:30
Entity ,
2016-06-11 22:24:09 +05:30
} from 'draft-js' ;
2017-01-20 14:22:27 +00:00
import * as sdk from './index' ;
2016-07-03 01:11:34 +05:30
import * as emojione from 'emojione' ;
2016-09-04 21:03:40 +05:30
import { stateToHTML } from 'draft-js-export-html' ;
2016-09-13 15:41:52 +05:30
import { SelectionRange } from "./autocomplete/Autocompleter" ;
2016-05-27 10:15:55 +05:30
2016-06-12 00:52:30 +02:00
const MARKDOWN_REGEX = {
LINK : /(?:\[([^\]]+)\]\(([^\)]+)\))|\<(\w+:\/\/[^\>]+)\>/g ,
ITALIC : /([\*_])([\w\s]+?)\1/g ,
2016-07-04 21:44:35 +05:30
BOLD : /([\*_])\1([\w\s]+?)\1\1/g ,
2016-09-07 22:52:14 +05:30
HR : /(\n|^)((-|\*|_) *){3,}(\n|$)/g ,
CODE : /`[^`]*`/g ,
2016-09-08 02:46:56 +05:30
STRIKETHROUGH : /~{2}[^~]*~{2}/g ,
2016-06-12 00:52:30 +02:00
};
const USERNAME_REGEX = /@\S+:\S+/g ;
const ROOM_REGEX = /#\S+:\S+/g ;
2016-07-04 21:44:35 +05:30
const EMOJI_REGEX = new RegExp ( emojione . unicodeRegexp , 'g' );
2016-06-12 00:52:30 +02:00
2016-09-04 21:03:40 +05:30
export const contentStateToHTML = stateToHTML ;
2016-05-27 10:15:55 +05:30
2016-06-11 22:24:09 +05:30
export function HTMLtoContentState ( html : string ) : ContentState {
2016-05-27 10:15:55 +05:30
return ContentState . createFromBlockArray ( convertFromHTML ( html ));
}
2016-06-09 23:53:09 +05:30
2016-07-08 12:54:28 +05:30
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-09-04 21:03:40 +05:30
/**
* 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 ) {
const text = contentBlock . getText ();
let matchArr , start ;
while (( matchArr = regex . exec ( text )) !== null ) {
start = matchArr . index ;
callback ( start , start + matchArr [ 0 ]. length );
}
}
2016-08-03 18:27:49 +05:30
// Workaround for https://github.com/facebook/draft-js/issues/414
2016-07-08 12:54:28 +05:30
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 18:27:49 +05:30
return ( < span title = { shortname } style = { style } >< span style = {{ opacity : 0 }} > { props . children } < /span></span>);
2016-07-08 12:54:28 +05:30
},
};
2016-06-11 15:52:08 +05:30
/**
* Returns a composite decorator which has access to provided scope.
*/
2016-06-11 23:13:57 +02:00
export function getScopedRTDecorators ( scope : any ) : CompositeDecorator {
2016-06-11 22:24:09 +05:30
let MemberAvatar = sdk . getComponent ( 'avatars.MemberAvatar' );
2016-06-09 23:53:09 +05:30
2016-06-11 22:24:09 +05:30
let usernameDecorator = {
2016-06-09 23:53:09 +05:30
strategy : ( contentBlock , callback ) => {
findWithRegex ( USERNAME_REGEX , contentBlock , callback );
},
component : ( props ) => {
2016-06-11 15:52:08 +05:30
let member = scope . room . getMember ( props . children [ 0 ]. props . text );
2016-06-14 19:10:35 +05:30
// unused until we make these decorators immutable (autocomplete needed)
let name = member ? member . name : null ;
2016-06-11 22:24:09 +05:30
let avatar = member ? < MemberAvatar member = { member } width = { 16 } height = { 16 } /> : null ;
2016-07-08 12:54:28 +05:30
return < span className = "mx_UserPill" > { avatar }{ props . children } < /span>;
2016-06-09 23:53:09 +05:30
}
};
2017-01-20 14:22:27 +00:00
2016-06-11 22:24:09 +05:30
let roomDecorator = {
2016-06-09 23:53:09 +05:30
strategy : ( contentBlock , callback ) => {
findWithRegex ( ROOM_REGEX , contentBlock , callback );
},
component : ( props ) => {
return < span className = "mx_RoomPill" > { props . children } < /span>;
}
};
2016-09-16 02:47:27 +05:30
// TODO Re-enable usernameDecorator and roomDecorator
return [ emojiDecorator ];
2016-06-09 23:53:09 +05:30
}
2016-06-11 23:13:57 +02:00
export function getScopedMDDecorators ( scope : any ) : CompositeDecorator {
2016-09-08 02:46:56 +05:30
let markdownDecorators = [ 'HR' , 'BOLD' , 'ITALIC' , 'CODE' , 'STRIKETHROUGH' ]. map (
2016-06-11 23:13:57 +02:00
( style ) => ({
strategy : ( contentBlock , callback ) => {
return findWithRegex ( MARKDOWN_REGEX [ style ], contentBlock , callback );
},
component : ( props ) => (
< span className = { "mx_MarkdownElement mx_Markdown_" + style } >
{ props . children }
< /span>
)
}));
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 12:54:28 +05:30
markdownDecorators . push ( emojiDecorator );
2016-06-11 23:13:57 +02:00
return markdownDecorators ;
}
2016-06-11 22:24:09 +05:30
/**
* Passes rangeToReplace to modifyFn and replaces it in contentState with the result.
*/
2016-06-12 00:50:30 +02:00
export function modifyText ( contentState : ContentState , rangeToReplace : SelectionState ,
2016-06-14 19:10:35 +05:30
modifyFn : ( text : string ) => string , inlineStyle , entityKey ) : ContentState {
2016-06-12 00:50:30 +02:00
let getText = ( key ) => contentState . getBlockForKey ( key ). getText (),
startKey = rangeToReplace . getStartKey (),
startOffset = rangeToReplace . getStartOffset (),
endKey = rangeToReplace . getEndKey (),
endOffset = rangeToReplace . getEndOffset (),
2016-06-11 22:24:09 +05:30
text = "" ;
2016-06-12 00:50:30 +02:00
2016-07-03 22:15:13 +05:30
for ( let currentKey = startKey ;
2016-06-12 00:50:30 +02:00
currentKey && currentKey !== endKey ;
currentKey = contentState . getKeyAfter ( currentKey )) {
2016-06-14 19:28:51 +05:30
let blockText = getText ( currentKey );
text += blockText . substring ( startOffset , blockText . length );
2016-06-12 00:50:30 +02:00
// from now on, we'll take whole blocks
startOffset = 0 ;
2016-06-11 22:24:09 +05:30
}
2016-06-12 00:50:30 +02:00
// add remaining part of last block
text += getText ( endKey ). substring ( startOffset , endOffset );
2016-06-14 19:10:35 +05:30
return Modifier . replaceText ( contentState , rangeToReplace , modifyFn ( text ), inlineStyle , entityKey );
2016-06-11 22:24:09 +05:30
}
2016-06-21 15:46:20 +05:30
/**
* 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 22:15:13 +05:30
export function selectionStateToTextOffsets ( selectionState : SelectionState ,
contentBlocks : Array < ContentBlock > ) : { start : number , end : number } {
2016-06-21 15:46:20 +05:30
let offset = 0 , start = 0 , end = 0 ;
2016-07-08 12:54:28 +05:30
for ( let block of contentBlocks ) {
2016-07-03 22:15:13 +05:30
if ( selectionState . getStartKey () === block . getKey ()) {
2016-06-21 15:46:20 +05:30
start = offset + selectionState . getStartOffset ();
}
2016-07-03 22:15:13 +05:30
if ( selectionState . getEndKey () === block . getKey ()) {
2016-06-21 15:46:20 +05:30
end = offset + selectionState . getEndOffset ();
break ;
}
offset += block . getLength ();
}
return {
start ,
2016-07-03 22:15:13 +05:30
end ,
};
}
2016-09-13 15:41:52 +05:30
export function textOffsetsToSelectionState ({ start , end } : SelectionRange ,
2016-07-03 22:15:13 +05:30
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 15:46:20 +05:30
}
2016-07-03 22:15:13 +05:30
return selectionState ;
2016-06-21 15:46:20 +05:30
}
2016-07-08 12:54:28 +05:30
// 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 )) {
2016-09-21 07:02:53 +05:30
const oldSelection = editorState . getSelection ();
editorState = EditorState . push (
2016-07-08 12:54:28 +05:30
editorState ,
newContentState ,
'convert-to-immutable-emojis' ,
);
2016-09-21 07:02:53 +05:30
// this is somewhat of a hack, we're undoing selection changes caused above
// it would be better not to make those changes in the first place
editorState = EditorState . forceSelection ( editorState , oldSelection );
2016-07-08 12:54:28 +05:30
}
return editorState ;
}