Formatting toggle, markdown indicator, quoting

Fixes vector-im/vector-web#1825
pull/21833/head
Aviral Dasgupta 2016-09-05 17:38:53 +05:30
parent 71251293e4
commit 0c0c44b050
2 changed files with 97 additions and 30 deletions

View File

@ -38,14 +38,19 @@ export default class MessageComposer extends React.Component {
this.onDownArrow = this.onDownArrow.bind(this); this.onDownArrow = this.onDownArrow.bind(this);
this._tryComplete = this._tryComplete.bind(this); this._tryComplete = this._tryComplete.bind(this);
this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this); this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this);
this.onToggleFormattingClicked = this.onToggleFormattingClicked.bind(this);
this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this);
this.onInputStateChanged = this.onInputStateChanged.bind(this);
this.state = { this.state = {
autocompleteQuery: '', autocompleteQuery: '',
selection: null, selection: null,
selectionInfo: { inputState: {
style: [], style: [],
blockType: null, blockType: null,
isRichtextEnabled: true,
}, },
showFormatting: false,
}; };
} }
@ -131,14 +136,17 @@ export default class MessageComposer extends React.Component {
}); });
} }
onInputContentChanged(content: string, selection: {start: number, end: number}, selectionInfo) { onInputContentChanged(content: string, selection: {start: number, end: number}) {
this.setState({ this.setState({
autocompleteQuery: content, autocompleteQuery: content,
selection, selection,
selectionInfo,
}); });
} }
onInputStateChanged(inputState) {
this.setState({inputState});
}
onUpArrow() { onUpArrow() {
return this.refs.autocomplete.onUpArrow(); return this.refs.autocomplete.onUpArrow();
} }
@ -161,9 +169,18 @@ export default class MessageComposer extends React.Component {
} }
onFormatButtonClicked(name: "bold" | "italic" | "strike" | "quote" | "bullet" | "numbullet", event) { onFormatButtonClicked(name: "bold" | "italic" | "strike" | "quote" | "bullet" | "numbullet", event) {
event.preventDefault();
this.messageComposerInput.onFormatButtonClicked(name, event); this.messageComposerInput.onFormatButtonClicked(name, event);
} }
onToggleFormattingClicked() {
this.setState({showFormatting: !this.state.showFormatting});
}
onToggleMarkdownClicked() {
this.messageComposerInput.enableRichtext(!this.state.inputState.isRichtextEnabled);
}
render() { render() {
var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId); var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
var uploadInputStyle = {display: 'none'}; var uploadInputStyle = {display: 'none'};
@ -217,8 +234,11 @@ export default class MessageComposer extends React.Component {
); );
const formattingButton = ( const formattingButton = (
<img title="Text Formatting" <img className="mx_MessageComposer_formatting"
title="Show Text Formatting Toolbar"
src="img/button-text-formatting.svg" src="img/button-text-formatting.svg"
onClick={this.onToggleFormattingClicked}
style={{visibility: this.state.showFormatting ? 'hidden' : 'visible'}}
key="controls_formatting" /> key="controls_formatting" />
); );
@ -232,7 +252,8 @@ export default class MessageComposer extends React.Component {
onUpArrow={this.onUpArrow} onUpArrow={this.onUpArrow}
onDownArrow={this.onDownArrow} onDownArrow={this.onDownArrow}
tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete
onContentChanged={this.onInputContentChanged} />, onContentChanged={this.onInputContentChanged}
onInputStateChanged={this.onInputStateChanged} />,
formattingButton, formattingButton,
uploadButton, uploadButton,
hangupButton, hangupButton,
@ -259,7 +280,7 @@ export default class MessageComposer extends React.Component {
} }
const {style, blockType} = this.state.selectionInfo; const {style, blockType} = this.state.inputState;
const formatButtons = ["bold", "italic", "strike", "quote", "bullet", "numbullet"].map( const formatButtons = ["bold", "italic", "strike", "quote", "bullet", "numbullet"].map(
name => { name => {
const active = style.includes(name) || blockType === name; const active = style.includes(name) || blockType === name;
@ -278,8 +299,17 @@ 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"> <div className="mx_MessageComposer_formatbar" style={this.state.showFormatting ? {} : {display: 'none'}}>
{formatButtons} {formatButtons}
<div style={{flex: 1}}></div>
<img title={`Turn Markdown ${this.state.inputState.isRichtextEnabled ? 'on' : 'off'}`}
onClick={this.onToggleMarkdownClicked}
className="mx_MessageComposer_formatbar_markdown"
src={`img/button-md-${!this.state.inputState.isRichtextEnabled}.png`} />
<img title="Hide Text Formatting Toolbar"
onClick={this.onToggleFormattingClicked}
className="mx_MessageComposer_formatbar_cancel"
src="img/icon-text-cancel.svg" />
</div> : null </div> : null
} }
</div> </div>

View File

@ -29,10 +29,11 @@ marked.setOptions({
import {Editor, EditorState, RichUtils, CompositeDecorator, import {Editor, EditorState, RichUtils, CompositeDecorator,
convertFromRaw, convertToRaw, Modifier, EditorChangeType, convertFromRaw, convertToRaw, Modifier, EditorChangeType,
getDefaultKeyBinding, KeyBindingUtil, ContentState} from 'draft-js'; getDefaultKeyBinding, KeyBindingUtil, ContentState, SelectionState} from 'draft-js';
import {stateToMarkdown} from 'draft-js-export-markdown'; import {stateToMarkdown} from 'draft-js-export-markdown';
import classNames from 'classnames'; import classNames from 'classnames';
import escape from 'lodash/escape';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix'; import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
@ -42,6 +43,7 @@ import sdk from '../../../index';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import KeyCode from '../../../KeyCode'; import KeyCode from '../../../KeyCode';
import UserSettingsStore from '../../../UserSettingsStore';
import * as RichText from '../../../RichText'; import * as RichText from '../../../RichText';
@ -90,14 +92,10 @@ export default class MessageComposerInput extends React.Component {
this.onTab = this.onTab.bind(this); this.onTab = this.onTab.bind(this);
this.onConfirmAutocompletion = this.onConfirmAutocompletion.bind(this); this.onConfirmAutocompletion = this.onConfirmAutocompletion.bind(this);
let isRichtextEnabled = window.localStorage.getItem('mx_editor_rte_enabled'); const isRichtextEnabled = UserSettingsStore.isFeatureEnabled('rich_text_editor');
if (isRichtextEnabled == null) {
isRichtextEnabled = 'true';
}
isRichtextEnabled = isRichtextEnabled === 'true';
this.state = { this.state = {
isRichtextEnabled: isRichtextEnabled, isRichtextEnabled,
editorState: null, editorState: null,
}; };
@ -237,8 +235,18 @@ export default class MessageComposerInput extends React.Component {
this.sentHistory.saveLastTextEntry(); this.sentHistory.saveLastTextEntry();
} }
componentWillUpdate(nextProps, nextState) {
// this is dirty, but moving all this state to MessageComposer is dirtier
if (this.props.onInputStateChanged && nextState !== this.state) {
const state = this.getSelectionInfo(nextState.editorState);
state.isRichtextEnabled = nextState.isRichtextEnabled;
this.props.onInputStateChanged(state);
}
}
onAction(payload) { onAction(payload) {
let editor = this.refs.editor; let editor = this.refs.editor;
let contentState = this.state.editorState.getCurrentContent();
switch (payload.action) { switch (payload.action) {
case 'focus_composer': case 'focus_composer':
@ -247,20 +255,44 @@ export default class MessageComposerInput extends React.Component {
// TODO change this so we insert a complete user alias // TODO change this so we insert a complete user alias
case 'insert_displayname': case 'insert_displayname': {
if (this.state.editorState.getCurrentContent().hasText()) { contentState = Modifier.replaceText(
console.log(payload); contentState,
let contentState = Modifier.replaceText( this.state.editorState.getSelection(),
this.state.editorState.getCurrentContent(), `${payload.displayname}: `
this.state.editorState.getSelection(), );
payload.displayname let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
); editorState = EditorState.forceSelection(editorState, contentState.getSelectionAfter());
this.setState({ this.setEditorState(editorState);
editorState: EditorState.push(this.state.editorState, contentState, 'insert-characters'), editor.focus();
}); }
break;
case 'quote': {
let {event: {content: {body, formatted_body}}} = payload.event || {};
formatted_body = formatted_body || escape(body);
if (formatted_body) {
let content = RichText.HTMLtoContentState(`<blockquote>${formatted_body}</blockquote>`);
if (!this.state.isRichtextEnabled) {
content = ContentState.createFromText(stateToMarkdown(content));
}
const blockMap = content.getBlockMap();
let startSelection = SelectionState.createEmpty(contentState.getFirstBlock().getKey());
contentState = Modifier.splitBlock(contentState, startSelection);
startSelection = SelectionState.createEmpty(contentState.getFirstBlock().getKey());
contentState = Modifier.replaceWithFragment(contentState,
startSelection,
blockMap);
startSelection = SelectionState.createEmpty(contentState.getFirstBlock().getKey());
if (this.state.isRichtextEnabled)
contentState = Modifier.setBlockType(contentState, startSelection, 'blockquote');
let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
this.setEditorState(editorState);
editor.focus(); editor.focus();
} }
break; }
break;
} }
} }
@ -363,9 +395,8 @@ export default class MessageComposerInput extends React.Component {
const textContent = editorState.getCurrentContent().getPlainText(); const textContent = editorState.getCurrentContent().getPlainText();
const selection = RichText.selectionStateToTextOffsets(editorState.getSelection(), const selection = RichText.selectionStateToTextOffsets(editorState.getSelection(),
editorState.getCurrentContent().getBlocksAsArray()); editorState.getCurrentContent().getBlocksAsArray());
const selectionInfo = this.getSelectionInfo(editorState);
this.props.onContentChanged(textContent, selection, selectionInfo); this.props.onContentChanged(textContent, selection);
} }
} }
@ -428,7 +459,8 @@ export default class MessageComposerInput extends React.Component {
handleReturn(ev) { handleReturn(ev) {
if (ev.shiftKey) { if (ev.shiftKey) {
return false; this.setEditorState(RichUtils.insertSoftNewline(this.state.editorState));
return true;
} }
const contentState = this.state.editorState.getCurrentContent(); const contentState = this.state.editorState.getCurrentContent();
@ -469,7 +501,7 @@ export default class MessageComposerInput extends React.Component {
return true; return true;
} }
if(this.state.isRichtextEnabled) { if (this.state.isRichtextEnabled) {
contentHTML = RichText.contentStateToHTML(contentState); contentHTML = RichText.contentStateToHTML(contentState);
} else { } else {
contentHTML = mdownToHtml(contentText); contentHTML = mdownToHtml(contentText);
@ -618,6 +650,9 @@ export default class MessageComposerInput extends React.Component {
return ( return (
<div className={className} <div className={className}
onClick={ this.onInputClick }> onClick={ this.onInputClick }>
<img className="mx_MessageComposer_input_markdownIndicator"
title={`Markdown is ${this.state.isRichtextEnabled ? 'disabled' : 'enabled'}`}
src={`img/button-md-${!this.state.isRichtextEnabled}.png`} />
<Editor ref="editor" <Editor ref="editor"
placeholder="Type a message…" placeholder="Type a message…"
editorState={this.state.editorState} editorState={this.state.editorState}
@ -654,4 +689,6 @@ MessageComposerInput.propTypes = {
// attempts to confirm currently selected completion, returns whether actually confirmed // attempts to confirm currently selected completion, returns whether actually confirmed
tryComplete: React.PropTypes.func, tryComplete: React.PropTypes.func,
onInputStateChanged: React.PropTypes.func.isRequired,
}; };