From b960d220d27524aa419fe643817a7ca6e79e00d7 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Sat, 11 Jun 2016 22:24:09 +0530 Subject: [PATCH] cleanup, better comments, markdown hotkeys --- src/RichText.js | 72 +++++++++++----- .../views/rooms/MessageComposerInput.js | 86 +++++++++++++------ 2 files changed, 109 insertions(+), 49 deletions(-) diff --git a/src/RichText.js b/src/RichText.js index 121efc871c..f1f2188d0d 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -1,11 +1,19 @@ -import {Editor, ContentState, convertFromHTML, DefaultDraftBlockRenderMap, DefaultDraftInlineStyle, CompositeDecorator} from 'draft-js'; +import { + Editor, + Modifier, + ContentState, + convertFromHTML, + DefaultDraftBlockRenderMap, + DefaultDraftInlineStyle, + CompositeDecorator +} from 'draft-js'; import * as sdk from './index'; const BLOCK_RENDER_MAP = DefaultDraftBlockRenderMap.set('unstyled', { element: 'p' // draft uses
by default which we don't really like, so we're using

}); -const styles = { +const STYLES = { BOLD: 'strong', CODE: 'code', ITALIC: 'em', @@ -17,18 +25,24 @@ export function contentStateToHTML(contentState: ContentState): string { return contentState.getBlockMap().map((block) => { let elem = BLOCK_RENDER_MAP.get(block.getType()).element; let content = []; - block.findStyleRanges(() => true, (start, end) => { - const tags = block.getInlineStyleAt(start).map(style => styles[style]); - const open = tags.map(tag => `<${tag}>`).join(''); - const close = tags.map(tag => ``).reverse().join(''); - content.push(`${open}${block.getText().substring(start, end)}${close}`); - }); + block.findStyleRanges( + () => true, // always return true => don't filter any ranges out + (start, end) => { + // map style names to elements + let tags = block.getInlineStyleAt(start).map(style => STYLES[style]); + // combine them to get well-nested HTML + let open = tags.map(tag => `<${tag}>`).join(''); + let close = tags.map(tag => ``).reverse().join(''); + // and get the HTML representation of this styled range (this .substring() should never fail) + content.push(`${open}${block.getText().substring(start, end)}${close}`); + } + ); return (`<${elem}>${content.join('')}`); }).join(''); } -export function HTMLtoContentState(html:String): ContentState { +export function HTMLtoContentState(html: string): ContentState { return ContentState.createFromBlockArray(convertFromHTML(html)); } @@ -37,29 +51,25 @@ const ROOM_REGEX = /#\S+:\S+/g; /** * Returns a composite decorator which has access to provided scope. - * - * @param scope - * @returns {*} */ -export function getScopedDecorator(scope) { - const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); +export function getScopedDecorator(scope: any): CompositeDecorator { + let MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); - const usernameDecorator = { + let usernameDecorator = { strategy: (contentBlock, callback) => { findWithRegex(USERNAME_REGEX, contentBlock, callback); }, component: (props) => { let member = scope.room.getMember(props.children[0].props.text); let name = null; - if(!!member) { - name = member.name; + if (!!member) { + name = member.name; // unused until we make these decorators immutable (autocomplete needed) } - console.log(member); - let avatar = member ? : null; + let avatar = member ? : null; return {avatar} {props.children}; } }; - const roomDecorator = { + let roomDecorator = { strategy: (contentBlock, callback) => { findWithRegex(ROOM_REGEX, contentBlock, callback); }, @@ -71,7 +81,11 @@ export function getScopedDecorator(scope) { return new CompositeDecorator([usernameDecorator, roomDecorator]); } -function findWithRegex(regex, contentBlock, callback) { +/** + * 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) { @@ -79,3 +93,19 @@ function findWithRegex(regex, contentBlock, callback) { callback(start, start + matchArr[0].length); } } + +/** + * Passes rangeToReplace to modifyFn and replaces it in contentState with the result. + */ +export function modifyText(contentState: ContentState, rangeToReplace: SelectionState, modifyFn: (text: string) => string, ...rest): ContentState { + let startKey = rangeToReplace.getStartKey(), + endKey = contentState.getKeyAfter(rangeToReplace.getEndKey()), + text = ""; + + for(let currentKey = startKey; currentKey && currentKey !== endKey; currentKey = contentState.getKeyAfter(currentKey)) { + let currentBlock = contentState.getBlockForKey(currentKey); + text += currentBlock.getText(); + } + + return Modifier.replaceText(contentState, rangeToReplace, modifyFn(text), ...rest); +} diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 56046f0798..98dd2855c4 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -42,7 +42,7 @@ var sdk = require('../../../index'); var dis = require("../../../dispatcher"); var KeyCode = require("../../../KeyCode"); -import {contentStateToHTML, HTMLtoContentState, getScopedDecorator} from '../../../RichText'; +import * as RichText from '../../../RichText'; const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000; @@ -69,7 +69,7 @@ export default class MessageComposerInput extends React.Component { this.onInputClick = this.onInputClick.bind(this); this.state = { - isRichtextEnabled: true, + isRichtextEnabled: false, editorState: null }; @@ -95,7 +95,7 @@ export default class MessageComposerInput extends React.Component { let func = contentState ? EditorState.createWithContent : EditorState.createEmpty; let args = contentState ? [contentState] : []; if(this.state.isRichtextEnabled) { - args.push(getScopedDecorator(this.props)); + args.push(RichText.getScopedDecorator(this.props)); } return func.apply(null, args); } @@ -114,7 +114,7 @@ export default class MessageComposerInput extends React.Component { // The textarea element to set text to. element: null, - init: function (element, roomId) { + init: function(element, roomId) { this.roomId = roomId; this.element = element; this.position = -1; @@ -129,7 +129,7 @@ export default class MessageComposerInput extends React.Component { } }, - push: function (text) { + push: function(text) { // store a message in the sent history this.data.unshift(text); window.sessionStorage.setItem( @@ -142,7 +142,7 @@ export default class MessageComposerInput extends React.Component { }, // move in the history. Returns true if we managed to move. - next: function (offset) { + next: function(offset) { if (this.position === -1) { // user is going into the history, save the current line. this.originalText = this.element.value; @@ -175,7 +175,7 @@ export default class MessageComposerInput extends React.Component { return true; }, - saveLastTextEntry: function () { + saveLastTextEntry: function() { // save the currently entered text in order to restore it later. // NB: This isn't 'originalText' because we want to restore // sent history items too! @@ -183,7 +183,7 @@ export default class MessageComposerInput extends React.Component { window.sessionStorage.setItem("input_" + this.roomId, contentJSON); }, - setLastTextEntry: function () { + setLastTextEntry: function() { let contentJSON = window.sessionStorage.getItem("input_" + this.roomId); if (contentJSON) { let content = convertFromRaw(JSON.parse(contentJSON)); @@ -404,7 +404,7 @@ export default class MessageComposerInput extends React.Component { this.refs.editor.focus(); } - onChange(editorState) { + onChange(editorState: EditorState) { this.setState({editorState}); if(editorState.getCurrentContent().hasText()) { @@ -414,30 +414,60 @@ export default class MessageComposerInput extends React.Component { } } - handleKeyCommand(command) { - if(command === 'toggle-mode') { + enableRichtext(enabled: boolean) { + this.setState({ + isRichtextEnabled: enabled + }); + + if(!this.state.isRichtextEnabled) { + let html = mdownToHtml(this.state.editorState.getCurrentContent().getPlainText()); this.setState({ - isRichtextEnabled: !this.state.isRichtextEnabled + editorState: this.createEditorState(RichText.HTMLtoContentState(html)) }); + } else { + let markdown = stateToMarkdown(this.state.editorState.getCurrentContent()); + let contentState = ContentState.createFromText(markdown); + this.setState({ + editorState: this.createEditorState(contentState) + }); + } + } - if(!this.state.isRichtextEnabled) { - let html = mdownToHtml(this.state.editorState.getCurrentContent().getPlainText()); - this.setState({ - editorState: this.createEditorState(HTMLtoContentState(html)) - }); - } else { - let markdown = stateToMarkdown(this.state.editorState.getCurrentContent()); - let contentState = ContentState.createFromText(markdown); - this.setState({ - editorState: this.createEditorState(contentState) - }); - } - + handleKeyCommand(command: string): boolean { + if(command === 'toggle-mode') { + this.enableRichtext(!this.state.isRichtextEnabled); return true; } - let newState = RichUtils.handleKeyCommand(this.state.editorState, command); - if (newState) { + let newState: ?EditorState = null; + + // Draft handles rich text mode commands by default but we need to do it ourselves for Markdown. + if(!this.state.isRichtextEnabled) { + let contentState = this.state.editorState.getCurrentContent(), + selection = this.state.editorState.getSelection(); + + let modifyFn = { + bold: text => `**${text}**`, + italic: text => `*${text}*`, + underline: text => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug* + code: text => `\`${text}\`` + }[command]; + + if(modifyFn) { + newState = EditorState.push( + this.state.editorState, + RichText.modifyText(contentState, selection, modifyFn), + 'insert-characters' + ); + } + console.log(modifyFn); + console.log(newState); + } + + if(newState == null) + newState = RichUtils.handleKeyCommand(this.state.editorState, command); + + if (newState != null) { this.onChange(newState); return true; } @@ -455,7 +485,7 @@ export default class MessageComposerInput extends React.Component { let contentText = contentState.getPlainText(), contentHTML; if(this.state.isRichtextEnabled) { - contentHTML = contentStateToHTML(contentState); + contentHTML = RichText.contentStateToHTML(contentState); } else { contentHTML = mdownToHtml(contentText); }