diff --git a/package.json b/package.json
index d1b9122a62..f8f0106a32 100644
--- a/package.json
+++ b/package.json
@@ -26,10 +26,9 @@
"dependencies": {
"browser-request": "^0.3.3",
"classnames": "^2.1.2",
- "draft-js": "^0.7.0",
- "draft-js-export-html": "^0.2.2",
+ "draft-js": "^0.8.1",
+ "draft-js-export-html": "^0.4.0",
"draft-js-export-markdown": "^0.2.0",
- "draft-js-import-markdown": "^0.1.6",
"emojione": "2.2.3",
"favico.js": "^0.3.10",
"filesize": "^3.1.2",
diff --git a/src/RichText.js b/src/RichText.js
index 7cd78a14c9..073d873945 100644
--- a/src/RichText.js
+++ b/src/RichText.js
@@ -14,23 +14,7 @@ import {
} from 'draft-js';
import * as sdk from './index';
import * as emojione from 'emojione';
-
-const BLOCK_RENDER_MAP = DefaultDraftBlockRenderMap.set('unstyled', {
- element: 'span',
- /*
- draft uses
by default which we don't really like, so we're using
- this is probably not a good idea since is not a block level element but
- we're trying to fix things in contentStateToHTML below
- */
-});
-
-const STYLES = {
- BOLD: 'strong',
- CODE: 'code',
- ITALIC: 'em',
- STRIKETHROUGH: 's',
- UNDERLINE: 'u',
-};
+import {stateToHTML} from 'draft-js-export-html';
const MARKDOWN_REGEX = {
LINK: /(?:\[([^\]]+)\]\(([^\)]+)\))|\<(\w+:\/\/[^\>]+)\>/g,
@@ -42,36 +26,7 @@ const USERNAME_REGEX = /@\S+:\S+/g;
const ROOM_REGEX = /#\S+:\S+/g;
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp, 'g');
-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, // 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]).filter(style => !!style);
- // 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)
- 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}`);
- }
- );
-
- let result = `<${elem}>${content.join('')}${elem}>`;
-
- // dirty hack because we don't want block level tags by default, but breaks
- if (elem === 'span')
- result += '
';
- return result;
- }).join('');
-}
+export const contentStateToHTML = stateToHTML;
export function HTMLtoContentState(html: string): ContentState {
return ContentState.createFromBlockArray(convertFromHTML(html));
@@ -98,6 +53,19 @@ function unicodeToEmojiUri(str) {
return str;
}
+/**
+ * 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);
+ }
+}
+
// Workaround for https://github.com/facebook/draft-js/issues/414
let emojiDecorator = {
strategy: (contentBlock, callback) => {
@@ -178,19 +146,6 @@ export function getScopedMDDecorators(scope: any): CompositeDecorator {
return markdownDecorators;
}
-/**
- * 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);
- }
-}
-
/**
* Passes rangeToReplace to modifyFn and replaces it in contentState with the result.
*/
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 7b84d394e0..3d01052ccf 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -42,6 +42,10 @@ export default class MessageComposer extends React.Component {
this.state = {
autocompleteQuery: '',
selection: null,
+ selectionInfo: {
+ style: [],
+ blockType: null,
+ },
};
}
@@ -127,10 +131,11 @@ export default class MessageComposer extends React.Component {
});
}
- onInputContentChanged(content: string, selection: {start: number, end: number}) {
+ onInputContentChanged(content: string, selection: {start: number, end: number}, selectionInfo) {
this.setState({
autocompleteQuery: content,
selection,
+ selectionInfo,
});
}
@@ -155,6 +160,10 @@ export default class MessageComposer extends React.Component {
}
}
+ onFormatButtonClicked(name: "bold" | "italic" | "strike" | "quote" | "bullet" | "numbullet", event) {
+ this.messageComposerInput.onFormatButtonClicked(name, event);
+ }
+
render() {
var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
var uploadInputStyle = {display: 'none'};
@@ -207,6 +216,12 @@ export default class MessageComposer extends React.Component {
);
+ const formattingButton = (
+
+ );
+
controls.push(
this.messageComposerInput = c}
@@ -218,6 +233,7 @@ export default class MessageComposer extends React.Component {
onDownArrow={this.onDownArrow}
tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete
onContentChanged={this.onInputContentChanged} />,
+ formattingButton,
uploadButton,
hangupButton,
callButton,
@@ -242,6 +258,17 @@ export default class MessageComposer extends React.Component {
;
}
+
+ const {style, blockType} = this.state.selectionInfo;
+ const formatButtons = ["bold", "italic", "strike", "quote", "bullet", "numbullet"].map(
+ name => {
+ const active = style.includes(name) || blockType === name;
+ const suffix = active ? '-o-n' : '';
+ const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name);
+ return
;
+ },
+ );
+
return (
{autoComplete}
@@ -250,6 +277,11 @@ export default class MessageComposer extends React.Component {
{controls}
+ {UserSettingsStore.isFeatureEnabled('rich_text_editor') ?
+
+ {formatButtons}
+
: null
+ }
);
}
diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js
index 2d42b65246..aebb1855f3 100644
--- a/src/components/views/rooms/MessageComposerInput.js
+++ b/src/components/views/rooms/MessageComposerInput.js
@@ -32,6 +32,7 @@ import {Editor, EditorState, RichUtils, CompositeDecorator,
getDefaultKeyBinding, KeyBindingUtil, ContentState} from 'draft-js';
import {stateToMarkdown} from 'draft-js-export-markdown';
+import classNames from 'classnames';
import MatrixClientPeg from '../../../MatrixClientPeg';
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
@@ -359,9 +360,12 @@ export default class MessageComposerInput extends React.Component {
}
if (this.props.onContentChanged) {
- this.props.onContentChanged(editorState.getCurrentContent().getPlainText(),
- RichText.selectionStateToTextOffsets(editorState.getSelection(),
- editorState.getCurrentContent().getBlocksAsArray()));
+ const textContent = editorState.getCurrentContent().getPlainText();
+ const selection = RichText.selectionStateToTextOffsets(editorState.getSelection(),
+ editorState.getCurrentContent().getBlocksAsArray());
+ const selectionInfo = this.getSelectionInfo(editorState);
+
+ this.props.onContentChanged(textContent, selection, selectionInfo);
}
}
@@ -418,6 +422,7 @@ export default class MessageComposerInput extends React.Component {
this.setEditorState(newState);
return true;
}
+
return false;
}
@@ -536,12 +541,79 @@ export default class MessageComposerInput extends React.Component {
setTimeout(() => this.refs.editor.focus(), 50);
}
- render() {
- let className = "mx_MessageComposer_input";
+ onFormatButtonClicked(name: "bold" | "italic" | "strike" | "quote" | "bullet" | "numbullet", e) {
+ const style = {
+ bold: 'BOLD',
+ italic: 'ITALIC',
+ strike: 'STRIKETHROUGH',
+ }[name];
- if (this.state.isRichtextEnabled) {
- className += " mx_MessageComposer_input_rte"; // placeholder indicator for RTE mode
+ if (style) {
+ e.preventDefault();
+ this.setEditorState(RichUtils.toggleInlineStyle(this.state.editorState, style));
+ } else {
+ const blockType = {
+ quote: 'blockquote',
+ bullet: 'unordered-list-item',
+ numbullet: 'ordered-list-item',
+ }[name];
+
+ if (blockType) {
+ e.preventDefault();
+ this.setEditorState(RichUtils.toggleBlockType(this.state.editorState, blockType));
+ } else {
+ console.error(`Unknown formatting style "${name}", ignoring.`);
+ }
}
+ }
+
+ /* returns inline style and block type of current SelectionState so MessageComposer can render formatting
+ buttons. */
+ getSelectionInfo(editorState: EditorState) {
+ const styleName = {
+ BOLD: 'bold',
+ ITALIC: 'italic',
+ STRIKETHROUGH: 'strike',
+ };
+
+ const originalStyle = editorState.getCurrentInlineStyle().toArray();
+ const style = originalStyle
+ .map(style => styleName[style] || null)
+ .filter(styleName => !!styleName);
+
+ const blockName = {
+ blockquote: 'quote',
+ 'unordered-list-item': 'bullet',
+ 'ordered-list-item': 'numbullet',
+ };
+ const originalBlockType = editorState.getCurrentContent()
+ .getBlockForKey(editorState.getSelection().getStartKey())
+ .getType();
+ const blockType = blockName[originalBlockType] || null;
+
+ return {
+ style,
+ blockType,
+ };
+ }
+
+ render() {
+ const {editorState} = this.state;
+
+ // From https://github.com/facebook/draft-js/blob/master/examples/rich/rich.html#L92
+ // If the user changes block type before entering any text, we can
+ // either style the placeholder or hide it.
+ let hidePlaceholder = false;
+ const contentState = editorState.getCurrentContent();
+ if (!contentState.hasText()) {
+ if (contentState.getBlockMap().first().getType() !== 'unstyled') {
+ hidePlaceholder = true;
+ }
+ }
+
+ const className = classNames('mx_MessageComposer_input', {
+ mx_MessageComposer_input_empty: hidePlaceholder,
+ });
return (