mirror of https://github.com/vector-im/riot-web
RTE format bar enhancements
parent
0c0c44b050
commit
f0f20beae0
|
@ -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);
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue