From 001011df2735d11c30a0010ccc660bf63440b642 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Fri, 27 May 2016 10:15:55 +0530 Subject: [PATCH 01/15] Initial version of rich text editor --- package.json | 2 + src/RichText.js | 37 ++++ .../views/rooms/MessageComposerInput.js | 201 +++++++++++------- 3 files changed, 165 insertions(+), 75 deletions(-) create mode 100644 src/RichText.js diff --git a/package.json b/package.json index 3f4a862f6f..4a2ba34343 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,8 @@ }, "dependencies": { "classnames": "^2.1.2", + "draft-js": "^0.7.0", + "draft-js-export-html": "^0.2.2", "favico.js": "^0.3.10", "filesize": "^3.1.2", "flux": "^2.0.3", diff --git a/src/RichText.js b/src/RichText.js new file mode 100644 index 0000000000..8cec5fb565 --- /dev/null +++ b/src/RichText.js @@ -0,0 +1,37 @@ +import {Editor, ContentState, convertFromHTML, DefaultDraftBlockRenderMap, DefaultDraftInlineStyle} from 'draft-js'; +const ReactDOM = require('react-dom'); + +const styles = { + BOLD: 'strong', + CODE: 'code', + ITALIC: 'em', + STRIKETHROUGH: 's', + UNDERLINE: 'u' +}; + +export function contentStateToHTML(contentState:ContentState): String { + const elem = contentState.getBlockMap().map((block) => { + const elem = DefaultDraftBlockRenderMap.get(block.getType()).element; + const content = []; + block.findStyleRanges(() => true, (s, e) => { + console.log(block.getInlineStyleAt(s)); + const tags = block.getInlineStyleAt(s).map(style => styles[style]); + const open = tags.map(tag => `<${tag}>`).join(''); + const close = tags.map(tag => ``).reverse().join(''); + content.push(`${open}${block.getText().substring(s, e)}${close}`); + }); + + return (` + <${elem}> + ${content.join('')} + + `); + }).join(''); + + + return elem; +} + +export function HTMLtoContentState(html:String): ContentState { + return ContentState.createFromBlockArray(convertFromHTML(html)); +} diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 733d9e6056..66506b3fe4 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -27,6 +27,9 @@ marked.setOptions({ smartypants: false }); +import {Editor, EditorState, RichUtils} from 'draft-js'; +import {stateToHTML} from 'draft-js-export-html'; + var MatrixClientPeg = require("../../../MatrixClientPeg"); var SlashCommands = require("../../../SlashCommands"); var Modal = require("../../../Modal"); @@ -36,6 +39,8 @@ var sdk = require('../../../index'); var dis = require("../../../dispatcher"); var KeyCode = require("../../../KeyCode"); +import {contentStateToHTML} from '../../../RichText'; + var TYPING_USER_TIMEOUT = 10000; var TYPING_SERVER_TIMEOUT = 30000; var MARKDOWN_ENABLED = true; @@ -56,26 +61,18 @@ function mdownToHtml(mdown) { /* * The textInput part of the MessageComposer */ -module.exports = React.createClass({ - displayName: 'MessageComposerInput', +module.exports = class extends React.Component { + constructor(props, context) { + super(props, context); + this.onAction = this.onAction.bind(this); + this.onInputClick = this.onInputClick.bind(this); - statics: { - // the height we limit the composer to - MAX_HEIGHT: 100, - }, + this.state = { + editorState: EditorState.createEmpty() + }; + } - propTypes: { - tabComplete: React.PropTypes.any, - - // a callback which is called when the height of the composer is - // changed due to a change in content. - onResize: React.PropTypes.func, - - // js-sdk Room object - room: React.PropTypes.object.isRequired, - }, - - componentWillMount: function() { + componentWillMount() { this.oldScrollHeight = 0; this.markdownEnabled = MARKDOWN_ENABLED; var self = this; @@ -157,21 +154,22 @@ module.exports = React.createClass({ // 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! - var text = this.element.value; - window.sessionStorage.setItem("input_" + this.roomId, text); + console.error('fixme'); + // window.sessionStorage.setItem("input_" + this.roomId, text); }, setLastTextEntry: function() { - var text = window.sessionStorage.getItem("input_" + this.roomId); - if (text) { - this.element.value = text; - self.resizeInput(); - } + console.error('fixme'); + // var text = window.sessionStorage.getItem("input_" + this.roomId); + // if (text) { + // this.element.value = text; + // self.resizeInput(); + // } } }; - }, + } - componentDidMount: function() { + componentDidMount() { this.dispatcherRef = dis.register(this.onAction); this.sentHistory.init( this.refs.textarea, @@ -181,18 +179,19 @@ module.exports = React.createClass({ if (this.props.tabComplete) { this.props.tabComplete.setTextArea(this.refs.textarea); } - }, + } - componentWillUnmount: function() { + componentWillUnmount() { dis.unregister(this.dispatcherRef); this.sentHistory.saveLastTextEntry(); - }, + } - onAction: function(payload) { - var textarea = this.refs.textarea; + onAction(payload) { + var editor = this.refs.editor; switch (payload.action) { case 'focus_composer': - textarea.focus(); + console.error('fixme'); + editor.focus(); break; case 'insert_displayname': if (textarea.value.length) { @@ -214,9 +213,9 @@ module.exports = React.createClass({ } break; } - }, + } - onKeyDown: function (ev) { + onKeyDown(ev) { if (ev.keyCode === KeyCode.ENTER && !ev.shiftKey) { var input = this.refs.textarea.value; if (input.length === 0) { @@ -252,33 +251,34 @@ module.exports = React.createClass({ self.onFinishedTyping(); } }, 10); // XXX: what is this 10ms setTimeout doing? Looks hacky :( - }, + } - resizeInput: function() { + resizeInput() { + console.error('fixme'); // scrollHeight is at least equal to clientHeight, so we have to // temporarily crimp clientHeight to 0 to get an accurate scrollHeight value - this.refs.textarea.style.height = "20px"; // 20 hardcoded from CSS - var newHeight = Math.min(this.refs.textarea.scrollHeight, - this.constructor.MAX_HEIGHT); - this.refs.textarea.style.height = Math.ceil(newHeight) + "px"; - this.oldScrollHeight = this.refs.textarea.scrollHeight; + // this.refs.textarea.style.height = "20px"; // 20 hardcoded from CSS + // var newHeight = Math.min(this.refs.textarea.scrollHeight, + // this.constructor.MAX_HEIGHT); + // this.refs.textarea.style.height = Math.ceil(newHeight) + "px"; + // this.oldScrollHeight = this.refs.textarea.scrollHeight; + // + // if (this.props.onResize) { + // // kick gemini-scrollbar to re-layout + // this.props.onResize(); + // } + } - if (this.props.onResize) { - // kick gemini-scrollbar to re-layout - this.props.onResize(); - } - }, - - onKeyUp: function(ev) { + onKeyUp(ev) { if (this.refs.textarea.scrollHeight !== this.oldScrollHeight || ev.keyCode === KeyCode.DELETE || ev.keyCode === KeyCode.BACKSPACE) { this.resizeInput(); } - }, + } - onEnter: function(ev) { + onEnter(ev) { var contentText = this.refs.textarea.value; // bodge for now to set markdown state on/off. We probably want a separate @@ -365,25 +365,25 @@ module.exports = React.createClass({ this.refs.textarea.value = ''; this.resizeInput(); ev.preventDefault(); - }, + } - onTypingActivity: function() { + onTypingActivity() { this.isTyping = true; if (!this.userTypingTimer) { this.sendTyping(true); } this.startUserTypingTimer(); this.startServerTypingTimer(); - }, + } - onFinishedTyping: function() { + onFinishedTyping() { this.isTyping = false; this.sendTyping(false); this.stopUserTypingTimer(); this.stopServerTypingTimer(); - }, + } - startUserTypingTimer: function() { + startUserTypingTimer() { this.stopUserTypingTimer(); var self = this; this.userTypingTimer = setTimeout(function() { @@ -391,16 +391,16 @@ module.exports = React.createClass({ self.sendTyping(self.isTyping); self.userTypingTimer = null; }, TYPING_USER_TIMEOUT); - }, + } - stopUserTypingTimer: function() { + stopUserTypingTimer() { if (this.userTypingTimer) { clearTimeout(this.userTypingTimer); this.userTypingTimer = null; } - }, + } - startServerTypingTimer: function() { + startServerTypingTimer() { if (!this.serverTypingTimer) { var self = this; this.serverTypingTimer = setTimeout(function() { @@ -410,39 +410,90 @@ module.exports = React.createClass({ } }, TYPING_SERVER_TIMEOUT / 2); } - }, + } - stopServerTypingTimer: function() { + stopServerTypingTimer() { if (this.serverTypingTimer) { clearTimeout(this.servrTypingTimer); this.serverTypingTimer = null; } - }, + } - sendTyping: function(isTyping) { + sendTyping(isTyping) { MatrixClientPeg.get().sendTyping( this.props.room.roomId, this.isTyping, TYPING_SERVER_TIMEOUT ).done(); - }, + } - refreshTyping: function() { + refreshTyping() { if (this.typingTimeout) { clearTimeout(this.typingTimeout); this.typingTimeout = null; } - }, + } - onInputClick: function(ev) { - this.refs.textarea.focus(); - }, + onInputClick(ev) { + this.refs.editor.focus(); + } - render: function() { + onChange(editorState) { + this.setState({editorState}); + } + + handleKeyCommand(command) { + const newState = RichUtils.handleKeyCommand(this.state.editorState, command); + if (newState) { + this.onChange(newState); + return true; + } + return false; + } + + handleReturn(ev) { + if(ev.shiftKey) + return false; + + const contentState = this.state.editorState.getCurrentContent(); + if(!contentState.hasText()) + return false; + + const contentText = contentState.getPlainText(), + contentHTML = contentStateToHTML(contentState); + + MatrixClientPeg.get().sendHtmlMessage(this.props.room.roomId, contentText, contentHTML); + + this.setState({ + editorState: EditorState.createEmpty() + }); + + return true; + } + + render() { return ( -
-