RTE format bar enhancements

pull/21833/head
Aviral Dasgupta 2016-09-07 22:52:14 +05:30
parent 0c0c44b050
commit f0f20beae0
4 changed files with 79 additions and 53 deletions

View File

@ -20,6 +20,8 @@ const MARKDOWN_REGEX = {
LINK: /(?:\[([^\]]+)\]\(([^\)]+)\))|\<(\w+:\/\/[^\>]+)\>/g, LINK: /(?:\[([^\]]+)\]\(([^\)]+)\))|\<(\w+:\/\/[^\>]+)\>/g,
ITALIC: /([\*_])([\w\s]+?)\1/g, ITALIC: /([\*_])([\w\s]+?)\1/g,
BOLD: /([\*_])\1([\w\s]+?)\1\1/g, BOLD: /([\*_])\1([\w\s]+?)\1\1/g,
HR: /(\n|^)((-|\*|_) *){3,}(\n|$)/g,
CODE: /`[^`]*`/g,
}; };
const USERNAME_REGEX = /@\S+:\S+/g; const USERNAME_REGEX = /@\S+:\S+/g;
@ -119,7 +121,7 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator {
} }
export function getScopedMDDecorators(scope: any): CompositeDecorator { export function getScopedMDDecorators(scope: any): CompositeDecorator {
let markdownDecorators = ['BOLD', 'ITALIC'].map( let markdownDecorators = ['HR', 'BOLD', 'ITALIC', 'CODE'].map(
(style) => ({ (style) => ({
strategy: (contentBlock, callback) => { strategy: (contentBlock, callback) => {
return findWithRegex(MARKDOWN_REGEX[style], contentBlock, callback); return findWithRegex(MARKDOWN_REGEX[style], contentBlock, callback);

View File

@ -130,9 +130,9 @@ module.exports = {
return event ? event.getContent() : {}; return event ? event.getContent() : {};
}, },
getSyncedSetting: function(type) { getSyncedSetting: function(type, defaultValue = null) {
var settings = this.getSyncedSettings(); var settings = this.getSyncedSettings();
return settings[type]; return settings.hasOwnProperty(type) ? settings[type] : null;
}, },
setSyncedSetting: function(type, value) { setSyncedSetting: function(type, value) {

View File

@ -21,6 +21,7 @@ var Modal = require('../../../Modal');
var sdk = require('../../../index'); var sdk = require('../../../index');
var dis = require('../../../dispatcher'); var dis = require('../../../dispatcher');
import Autocomplete from './Autocomplete'; import Autocomplete from './Autocomplete';
import classNames from 'classnames';
import UserSettingsStore from '../../../UserSettingsStore'; import UserSettingsStore from '../../../UserSettingsStore';
@ -48,9 +49,10 @@ export default class MessageComposer extends React.Component {
inputState: { inputState: {
style: [], style: [],
blockType: null, 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(); event.preventDefault();
this.messageComposerInput.onFormatButtonClicked(name, event); this.messageComposerInput.onFormatButtonClicked(name, event);
} }
onToggleFormattingClicked() { onToggleFormattingClicked() {
UserSettingsStore.setSyncedSetting('MessageComposer.showFormatting', !this.state.showFormatting);
this.setState({showFormatting: !this.state.showFormatting}); this.setState({showFormatting: !this.state.showFormatting});
this.messageComposerInput.focus();
} }
onToggleMarkdownClicked() { onToggleMarkdownClicked() {
this.messageComposerInput.enableRichtext(!this.state.inputState.isRichtextEnabled); this.messageComposerInput.enableRichtext(!this.state.inputState.isRichtextEnabled);
this.messageComposerInput.focus();
} }
render() { render() {
@ -238,7 +243,8 @@ export default class MessageComposer extends React.Component {
title="Show Text Formatting Toolbar" title="Show Text Formatting Toolbar"
src="img/button-text-formatting.svg" src="img/button-text-formatting.svg"
onClick={this.onToggleFormattingClicked} 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" /> key="controls_formatting" />
); );
@ -281,12 +287,21 @@ export default class MessageComposer extends React.Component {
const {style, blockType} = this.state.inputState; 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 => { name => {
const active = style.includes(name) || blockType === name; const active = style.includes(name) || blockType === name;
const suffix = active ? '-o-n' : ''; const suffix = active ? '-o-n' : '';
const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name); const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name);
return <img className="mx_MessageComposer_format_button" title={name} onClick={onFormatButtonClicked} key={name} src={`img/button-text-${name}${suffix}.svg`} height="17" />; const disabled = !this.state.inputState.isRichtextEnabled && ['strike', 'underline'].includes(name);
const className = classNames("mx_MessageComposer_format_button", {
mx_MessageComposer_format_button_disabled: disabled,
});
return <img className={className}
title={name}
onClick={disabled ? null : onFormatButtonClicked}
key={name}
src={`img/button-text-${name}${suffix}.svg`}
height="17" />;
}, },
); );
@ -299,18 +314,21 @@ export default class MessageComposer extends React.Component {
</div> </div>
</div> </div>
{UserSettingsStore.isFeatureEnabled('rich_text_editor') ? {UserSettingsStore.isFeatureEnabled('rich_text_editor') ?
<div className="mx_MessageComposer_formatbar" style={this.state.showFormatting ? {} : {display: 'none'}}> <div className="mx_MessageComposer_formatbar_wrapper">
{formatButtons} <div className="mx_MessageComposer_formatbar" style={this.state.showFormatting ? {} : {display: 'none'}}>
<div style={{flex: 1}}></div> {formatButtons}
<img title={`Turn Markdown ${this.state.inputState.isRichtextEnabled ? 'on' : 'off'}`} <div style={{flex: 1}}></div>
onClick={this.onToggleMarkdownClicked} <strong>{this.state.inputState.wordCount}</strong>
className="mx_MessageComposer_formatbar_markdown" <img title={`Turn Markdown ${this.state.inputState.isRichtextEnabled ? 'on' : 'off'}`}
src={`img/button-md-${!this.state.inputState.isRichtextEnabled}.png`} /> onClick={this.onToggleMarkdownClicked}
<img title="Hide Text Formatting Toolbar" className="mx_MessageComposer_formatbar_markdown"
onClick={this.onToggleFormattingClicked} src={`img/button-md-${!this.state.inputState.isRichtextEnabled}.png`} />
className="mx_MessageComposer_formatbar_cancel" <img title="Hide Text Formatting Toolbar"
src="img/icon-text-cancel.svg" /> onClick={this.onToggleFormattingClicked}
</div> : null className="mx_MessageComposer_formatbar_cancel"
src="img/icon-text-cancel.svg" />
</div>
</div>: null
} }
</div> </div>
); );

View File

@ -83,7 +83,7 @@ export default class MessageComposerInput extends React.Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.onAction = this.onAction.bind(this); 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.handleReturn = this.handleReturn.bind(this);
this.handleKeyCommand = this.handleKeyCommand.bind(this); this.handleKeyCommand = this.handleKeyCommand.bind(this);
this.setEditorState = this.setEditorState.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.onDownArrow = this.onDownArrow.bind(this);
this.onTab = this.onTab.bind(this); this.onTab = this.onTab.bind(this);
this.onConfirmAutocompletion = this.onConfirmAutocompletion.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 = { this.state = {
isRichtextEnabled, isRichtextEnabled,
@ -240,6 +241,7 @@ export default class MessageComposerInput extends React.Component {
if (this.props.onInputStateChanged && nextState !== this.state) { if (this.props.onInputStateChanged && nextState !== this.state) {
const state = this.getSelectionInfo(nextState.editorState); const state = this.getSelectionInfo(nextState.editorState);
state.isRichtextEnabled = nextState.isRichtextEnabled; state.isRichtextEnabled = nextState.isRichtextEnabled;
state.wordCount = nextState.editorState.getCurrentContent().getPlainText().split(' ').filter(w => !!w).length;
this.props.onInputStateChanged(state); this.props.onInputStateChanged(state);
} }
} }
@ -377,7 +379,7 @@ export default class MessageComposerInput extends React.Component {
} }
} }
onInputClick(ev) { focus(ev) {
this.refs.editor.focus(); this.refs.editor.focus();
} }
@ -410,11 +412,11 @@ export default class MessageComposerInput extends React.Component {
this.setEditorState(this.createEditorState(enabled, contentState)); this.setEditorState(this.createEditorState(enabled, contentState));
} }
window.localStorage.setItem('mx_editor_rte_enabled', enabled);
this.setState({ this.setState({
isRichtextEnabled: enabled isRichtextEnabled: enabled,
}); });
UserSettingsStore.setSyncedSetting('MessageComposerInput.isRichTextEnabled', enabled);
} }
handleKeyCommand(command: string): boolean { handleKeyCommand(command: string): boolean {
@ -426,7 +428,17 @@ export default class MessageComposerInput extends React.Component {
let newState: ?EditorState = null; let newState: ?EditorState = null;
// Draft handles rich text mode commands by default but we need to do it ourselves for Markdown. // 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(), let contentState = this.state.editorState.getCurrentContent(),
selection = this.state.editorState.getSelection(); selection = this.state.editorState.getSelection();
@ -435,6 +447,9 @@ export default class MessageComposerInput extends React.Component {
italic: text => `*${text}*`, italic: text => `*${text}*`,
underline: text => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug* underline: text => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug*
code: text => `\`${text}\``, 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]; }[command];
if (modifyFn) { if (modifyFn) {
@ -573,30 +588,14 @@ export default class MessageComposerInput extends React.Component {
setTimeout(() => this.refs.editor.focus(), 50); setTimeout(() => this.refs.editor.focus(), 50);
} }
onFormatButtonClicked(name: "bold" | "italic" | "strike" | "quote" | "bullet" | "numbullet", e) { onFormatButtonClicked(name: "bold" | "italic" | "strike" | "code" | "underline" | "quote" | "bullet" | "numbullet", e) {
const style = { const command = {
bold: 'BOLD', code: 'code-block',
italic: 'ITALIC', quote: 'blockquote',
strike: 'STRIKETHROUGH', bullet: 'unordered-list-item',
}[name]; numbullet: 'ordered-list-item',
}[name] || name;
if (style) { this.handleKeyCommand(command);
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 /* 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', BOLD: 'bold',
ITALIC: 'italic', ITALIC: 'italic',
STRIKETHROUGH: 'strike', STRIKETHROUGH: 'strike',
UNDERLINE: 'underline',
}; };
const originalStyle = editorState.getCurrentInlineStyle().toArray(); const originalStyle = editorState.getCurrentInlineStyle().toArray();
@ -614,6 +614,7 @@ export default class MessageComposerInput extends React.Component {
.filter(styleName => !!styleName); .filter(styleName => !!styleName);
const blockName = { const blockName = {
'code-block': 'code',
blockquote: 'quote', blockquote: 'quote',
'unordered-list-item': 'bullet', 'unordered-list-item': 'bullet',
'ordered-list-item': 'numbullet', 'ordered-list-item': 'numbullet',
@ -629,6 +630,10 @@ export default class MessageComposerInput extends React.Component {
}; };
} }
onMarkdownToggleClicked() {
this.enableRichtext(!this.state.isRichtextEnabled);
}
render() { render() {
const {editorState} = this.state; const {editorState} = this.state;
@ -649,8 +654,9 @@ export default class MessageComposerInput extends React.Component {
return ( return (
<div className={className} <div className={className}
onClick={ this.onInputClick }> onClick={ this.focus }>
<img className="mx_MessageComposer_input_markdownIndicator" <img className="mx_MessageComposer_input_markdownIndicator"
onClick={this.onMarkdownToggleClicked}
title={`Markdown is ${this.state.isRichtextEnabled ? 'disabled' : 'enabled'}`} title={`Markdown is ${this.state.isRichtextEnabled ? 'disabled' : 'enabled'}`}
src={`img/button-md-${!this.state.isRichtextEnabled}.png`} /> src={`img/button-md-${!this.state.isRichtextEnabled}.png`} />
<Editor ref="editor" <Editor ref="editor"