2016-07-03 18:45:13 +02:00
import React from 'react' ;
2016-06-11 18:54:09 +02:00
import {
Editor ,
2016-07-08 09:24:28 +02:00
EditorState ,
2016-06-11 18:54:09 +02:00
Modifier ,
ContentState ,
2016-07-03 18:45:13 +02:00
ContentBlock ,
2016-06-11 18:54:09 +02:00
convertFromHTML ,
DefaultDraftBlockRenderMap ,
DefaultDraftInlineStyle ,
2016-06-21 12:16:20 +02:00
CompositeDecorator ,
2016-07-03 18:45:13 +02:00
SelectionState ,
2016-07-08 09:24:28 +02:00
Entity ,
2016-06-11 18:54:09 +02:00
} from 'draft-js' ;
2017-01-20 15:22:27 +01:00
import * as sdk from './index' ;
2016-07-02 21:41:34 +02:00
import * as emojione from 'emojione' ;
2016-09-04 17:33:40 +02:00
import { stateToHTML } from 'draft-js-export-html' ;
2016-09-13 12:11:52 +02:00
import { SelectionRange } from "./autocomplete/Autocompleter" ;
2017-03-10 16:04:31 +01:00
import { stateToMarkdown as _ _stateToMarkdown } from 'draft-js-export-markdown' ;
2016-05-27 06:45:55 +02:00
2016-06-12 00:52:30 +02:00
const MARKDOWN _REGEX = {
LINK : /(?:\[([^\]]+)\]\(([^\)]+)\))|\<(\w+:\/\/[^\>]+)\>/g ,
ITALIC : /([\*_])([\w\s]+?)\1/g ,
2016-07-04 18:14:35 +02:00
BOLD : /([\*_])\1([\w\s]+?)\1\1/g ,
2016-09-07 19:22:14 +02:00
HR : /(\n|^)((-|\*|_) *){3,}(\n|$)/g ,
CODE : /`[^`]*`/g ,
2016-09-07 23:16:56 +02:00
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 18:14:35 +02:00
const EMOJI _REGEX = new RegExp ( emojione . unicodeRegexp , 'g' ) ;
2016-06-12 00:52:30 +02:00
2017-03-10 16:04:31 +01:00
const ZWS _CODE = 8203 ;
const ZWS = String . fromCharCode ( ZWS _CODE ) ; // zero width space
export function stateToMarkdown ( state ) {
return _ _stateToMarkdown ( state )
. replace (
ZWS , // draft-js-export-markdown adds these
'' ) ; // this is *not* a zero width space, trust me :)
}
2017-02-21 22:40:15 +01:00
export const contentStateToHTML = ( contentState : ContentState ) => {
return stateToHTML ( contentState , {
inlineStyles : {
UNDERLINE : {
element : 'u'
}
}
} ) ;
} ;
2016-05-27 06:45:55 +02:00
2017-06-23 18:35:07 +02:00
export function htmlToContentState ( html : string ) : ContentState {
2016-05-27 06:45:55 +02:00
return ContentState . createFromBlockArray ( convertFromHTML ( html ) ) ;
}
2016-06-09 20:23:09 +02:00
2016-07-08 09:24:28 +02: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-09-04 17:33:40 +02: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 ) {
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 14:57:49 +02:00
// Workaround for https://github.com/facebook/draft-js/issues/414
2016-07-08 09:24:28 +02: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 14:57:49 +02:00
return ( < span title = { shortname } style = { style } > < span style = { { opacity : 0 } } > { props . children } < / s p a n > < / s p a n > ) ;
2016-07-08 09:24:28 +02:00
} ,
} ;
2016-06-11 12:22:08 +02:00
/ * *
* 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 18:54:09 +02:00
let MemberAvatar = sdk . getComponent ( 'avatars.MemberAvatar' ) ;
2016-06-09 20:23:09 +02:00
2016-06-11 18:54:09 +02:00
let usernameDecorator = {
2016-06-09 20:23:09 +02:00
strategy : ( contentBlock , callback ) => {
findWithRegex ( USERNAME _REGEX , contentBlock , callback ) ;
} ,
component : ( props ) => {
2016-06-11 12:22:08 +02:00
let member = scope . room . getMember ( props . children [ 0 ] . props . text ) ;
2016-06-14 15:40:35 +02:00
// unused until we make these decorators immutable (autocomplete needed)
let name = member ? member . name : null ;
2016-06-11 18:54:09 +02:00
let avatar = member ? < MemberAvatar member = { member } width = { 16 } height = { 16 } / > : null ;
2016-07-08 09:24:28 +02:00
return < span className = "mx_UserPill" > { avatar } { props . children } < / s p a n > ;
2016-06-09 20:23:09 +02:00
}
} ;
2017-01-20 15:22:27 +01:00
2016-06-11 18:54:09 +02:00
let roomDecorator = {
2016-06-09 20:23:09 +02:00
strategy : ( contentBlock , callback ) => {
findWithRegex ( ROOM _REGEX , contentBlock , callback ) ;
} ,
component : ( props ) => {
return < span className = "mx_RoomPill" > { props . children } < / s p a n > ;
}
} ;
2016-09-15 23:17:27 +02:00
// TODO Re-enable usernameDecorator and roomDecorator
return [ emojiDecorator ] ;
2016-06-09 20:23:09 +02:00
}
2016-06-11 23:13:57 +02:00
export function getScopedMDDecorators ( scope : any ) : CompositeDecorator {
2016-09-07 23:16:56 +02:00
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 }
< / 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-10-11 15:46:35 +02:00
// markdownDecorators.push(emojiDecorator);
// TODO Consider renabling "syntax highlighting" when we can do it properly
return [ emojiDecorator ] ;
2016-06-11 23:13:57 +02:00
}
2016-06-11 18:54:09 +02:00
/ * *
* 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 15:40:35 +02:00
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 18:54:09 +02:00
text = "" ;
2016-06-12 00:50:30 +02:00
2016-07-03 18:45:13 +02:00
for ( let currentKey = startKey ;
2016-06-12 00:50:30 +02:00
currentKey && currentKey !== endKey ;
currentKey = contentState . getKeyAfter ( currentKey ) ) {
2016-06-14 15:58:51 +02:00
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 18:54:09 +02:00
}
2016-06-12 00:50:30 +02:00
// add remaining part of last block
text += getText ( endKey ) . substring ( startOffset , endOffset ) ;
2016-06-14 15:40:35 +02:00
return Modifier . replaceText ( contentState , rangeToReplace , modifyFn ( text ) , inlineStyle , entityKey ) ;
2016-06-11 18:54:09 +02:00
}
2016-06-21 12:16:20 +02: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 18:45:13 +02:00
export function selectionStateToTextOffsets ( selectionState : SelectionState ,
contentBlocks : Array < ContentBlock > ) : { start : number , end : number } {
2016-06-21 12:16:20 +02:00
let offset = 0 , start = 0 , end = 0 ;
2016-07-08 09:24:28 +02:00
for ( let block of contentBlocks ) {
2016-07-03 18:45:13 +02:00
if ( selectionState . getStartKey ( ) === block . getKey ( ) ) {
2016-06-21 12:16:20 +02:00
start = offset + selectionState . getStartOffset ( ) ;
}
2016-07-03 18:45:13 +02:00
if ( selectionState . getEndKey ( ) === block . getKey ( ) ) {
2016-06-21 12:16:20 +02:00
end = offset + selectionState . getEndOffset ( ) ;
break ;
}
offset += block . getLength ( ) ;
}
return {
start ,
2016-07-03 18:45:13 +02:00
end ,
} ;
}
2016-09-13 12:11:52 +02:00
export function textOffsetsToSelectionState ( { start , end } : SelectionRange ,
2016-07-03 18:45:13 +02:00
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 12:16:20 +02:00
}
2016-07-03 18:45:13 +02:00
return selectionState ;
2016-06-21 12:16:20 +02:00
}
2016-07-08 09:24:28 +02: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 ) ) {
2016-09-21 03:32:53 +02:00
const oldSelection = editorState . getSelection ( ) ;
editorState = EditorState . push (
2016-07-08 09:24:28 +02:00
editorState ,
newContentState ,
'convert-to-immutable-emojis' ,
) ;
2016-09-21 03:32:53 +02:00
// 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 09:24:28 +02:00
}
return editorState ;
}