From f0f20beae0bf0f62b8c5f5f4d9790a98bc763dd4 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Wed, 7 Sep 2016 22:52:14 +0530 Subject: [PATCH] RTE format bar enhancements --- src/RichText.js | 4 +- src/UserSettingsStore.js | 4 +- src/components/views/rooms/MessageComposer.js | 54 +++++++++----- .../views/rooms/MessageComposerInput.js | 70 ++++++++++--------- 4 files changed, 79 insertions(+), 53 deletions(-) diff --git a/src/RichText.js b/src/RichText.js index 073d873945..aebd6f5765 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -20,6 +20,8 @@ const MARKDOWN_REGEX = { LINK: /(?:\[([^\]]+)\]\(([^\)]+)\))|\<(\w+:\/\/[^\>]+)\>/g, ITALIC: /([\*_])([\w\s]+?)\1/g, BOLD: /([\*_])\1([\w\s]+?)\1\1/g, + HR: /(\n|^)((-|\*|_) *){3,}(\n|$)/g, + CODE: /`[^`]*`/g, }; const USERNAME_REGEX = /@\S+:\S+/g; @@ -119,7 +121,7 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator { } export function getScopedMDDecorators(scope: any): CompositeDecorator { - let markdownDecorators = ['BOLD', 'ITALIC'].map( + let markdownDecorators = ['HR', 'BOLD', 'ITALIC', 'CODE'].map( (style) => ({ strategy: (contentBlock, callback) => { return findWithRegex(MARKDOWN_REGEX[style], contentBlock, callback); diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index f4eb4f0d83..3e0c7127c1 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -130,9 +130,9 @@ module.exports = { return event ? event.getContent() : {}; }, - getSyncedSetting: function(type) { + getSyncedSetting: function(type, defaultValue = null) { var settings = this.getSyncedSettings(); - return settings[type]; + return settings.hasOwnProperty(type) ? settings[type] : null; }, setSyncedSetting: function(type, value) { diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index dbc970511a..971024eb57 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -21,6 +21,7 @@ var Modal = require('../../../Modal'); var sdk = require('../../../index'); var dis = require('../../../dispatcher'); import Autocomplete from './Autocomplete'; +import classNames from 'classnames'; import UserSettingsStore from '../../../UserSettingsStore'; @@ -48,9 +49,10 @@ export default class MessageComposer extends React.Component { inputState: { style: [], blockType: null, - isRichtextEnabled: true, + isRichtextEnabled: UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', true), + wordCount: 0, }, - showFormatting: false, + showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false), }; } @@ -168,17 +170,20 @@ export default class MessageComposer extends React.Component { } } - onFormatButtonClicked(name: "bold" | "italic" | "strike" | "quote" | "bullet" | "numbullet", event) { + onFormatButtonClicked(name: "bold" | "italic" | "strike" | "code" | "underline" | "quote" | "bullet" | "numbullet", event) { event.preventDefault(); this.messageComposerInput.onFormatButtonClicked(name, event); } onToggleFormattingClicked() { + UserSettingsStore.setSyncedSetting('MessageComposer.showFormatting', !this.state.showFormatting); this.setState({showFormatting: !this.state.showFormatting}); + this.messageComposerInput.focus(); } onToggleMarkdownClicked() { this.messageComposerInput.enableRichtext(!this.state.inputState.isRichtextEnabled); + this.messageComposerInput.focus(); } render() { @@ -238,7 +243,8 @@ export default class MessageComposer extends React.Component { title="Show Text Formatting Toolbar" src="img/button-text-formatting.svg" onClick={this.onToggleFormattingClicked} - style={{visibility: this.state.showFormatting ? 'hidden' : 'visible'}} + style={{visibility: this.state.showFormatting || + !UserSettingsStore.isFeatureEnabled('rich_text_editor') ? 'hidden' : 'visible'}} key="controls_formatting" /> ); @@ -281,12 +287,21 @@ export default class MessageComposer extends React.Component { const {style, blockType} = this.state.inputState; - const formatButtons = ["bold", "italic", "strike", "quote", "bullet", "numbullet"].map( + const formatButtons = ["bold", "italic", "strike", "underline", "code", "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 ; + const disabled = !this.state.inputState.isRichtextEnabled && ['strike', 'underline'].includes(name); + const className = classNames("mx_MessageComposer_format_button", { + mx_MessageComposer_format_button_disabled: disabled, + }); + return ; }, ); @@ -299,18 +314,21 @@ export default class MessageComposer extends React.Component { {UserSettingsStore.isFeatureEnabled('rich_text_editor') ? -
- {formatButtons} -
- - -
: null +
+
+ {formatButtons} +
+ {this.state.inputState.wordCount} + + +
+
: null } ); diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 49b0706499..32f4497326 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -83,7 +83,7 @@ export default class MessageComposerInput extends React.Component { constructor(props, context) { super(props, context); this.onAction = this.onAction.bind(this); - this.onInputClick = this.onInputClick.bind(this); + this.focus = this.focus.bind(this); this.handleReturn = this.handleReturn.bind(this); this.handleKeyCommand = this.handleKeyCommand.bind(this); this.setEditorState = this.setEditorState.bind(this); @@ -91,8 +91,9 @@ export default class MessageComposerInput extends React.Component { this.onDownArrow = this.onDownArrow.bind(this); this.onTab = this.onTab.bind(this); this.onConfirmAutocompletion = this.onConfirmAutocompletion.bind(this); + this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this); - const isRichtextEnabled = UserSettingsStore.isFeatureEnabled('rich_text_editor'); + const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', true); this.state = { isRichtextEnabled, @@ -240,6 +241,7 @@ export default class MessageComposerInput extends React.Component { if (this.props.onInputStateChanged && nextState !== this.state) { const state = this.getSelectionInfo(nextState.editorState); state.isRichtextEnabled = nextState.isRichtextEnabled; + state.wordCount = nextState.editorState.getCurrentContent().getPlainText().split(' ').filter(w => !!w).length; this.props.onInputStateChanged(state); } } @@ -377,7 +379,7 @@ export default class MessageComposerInput extends React.Component { } } - onInputClick(ev) { + focus(ev) { this.refs.editor.focus(); } @@ -410,11 +412,11 @@ export default class MessageComposerInput extends React.Component { this.setEditorState(this.createEditorState(enabled, contentState)); } - window.localStorage.setItem('mx_editor_rte_enabled', enabled); - this.setState({ - isRichtextEnabled: enabled + isRichtextEnabled: enabled, }); + + UserSettingsStore.setSyncedSetting('MessageComposerInput.isRichTextEnabled', enabled); } handleKeyCommand(command: string): boolean { @@ -426,7 +428,17 @@ export default class MessageComposerInput extends React.Component { 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) { + if (this.state.isRichtextEnabled) { + // These are block types, not handled by RichUtils by default. + const blockCommands = ['code-block', 'blockquote', 'unordered-list-item', 'ordered-list-item']; + + if (blockCommands.includes(command)) { + this.setEditorState(RichUtils.toggleBlockType(this.state.editorState, command)); + } else if (command === 'strike') { + // this is the only inline style not handled by Draft by default + this.setEditorState(RichUtils.toggleInlineStyle(this.state.editorState, 'STRIKETHROUGH')); + } + } else { let contentState = this.state.editorState.getCurrentContent(), selection = this.state.editorState.getSelection(); @@ -435,6 +447,9 @@ export default class MessageComposerInput extends React.Component { italic: text => `*${text}*`, underline: text => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug* code: text => `\`${text}\``, + blockquote: text => text.split('\n').map(line => `> ${line}\n`).join(''), + 'unordered-list-item': text => text.split('\n').map(line => `- ${line}\n`).join(''), + 'ordered-list-item': text => text.split('\n').map((line, i) => `${i+1}. ${line}\n`).join(''), }[command]; if (modifyFn) { @@ -573,30 +588,14 @@ export default class MessageComposerInput extends React.Component { setTimeout(() => this.refs.editor.focus(), 50); } - onFormatButtonClicked(name: "bold" | "italic" | "strike" | "quote" | "bullet" | "numbullet", e) { - const style = { - bold: 'BOLD', - italic: 'ITALIC', - strike: 'STRIKETHROUGH', - }[name]; - - 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.`); - } - } + onFormatButtonClicked(name: "bold" | "italic" | "strike" | "code" | "underline" | "quote" | "bullet" | "numbullet", e) { + const command = { + code: 'code-block', + quote: 'blockquote', + bullet: 'unordered-list-item', + numbullet: 'ordered-list-item', + }[name] || name; + this.handleKeyCommand(command); } /* returns inline style and block type of current SelectionState so MessageComposer can render formatting @@ -606,6 +605,7 @@ export default class MessageComposerInput extends React.Component { BOLD: 'bold', ITALIC: 'italic', STRIKETHROUGH: 'strike', + UNDERLINE: 'underline', }; const originalStyle = editorState.getCurrentInlineStyle().toArray(); @@ -614,6 +614,7 @@ export default class MessageComposerInput extends React.Component { .filter(styleName => !!styleName); const blockName = { + 'code-block': 'code', blockquote: 'quote', 'unordered-list-item': 'bullet', 'ordered-list-item': 'numbullet', @@ -629,6 +630,10 @@ export default class MessageComposerInput extends React.Component { }; } + onMarkdownToggleClicked() { + this.enableRichtext(!this.state.isRichtextEnabled); + } + render() { const {editorState} = this.state; @@ -649,8 +654,9 @@ export default class MessageComposerInput extends React.Component { return (
+ onClick={ this.focus }>