parent
ce40fa1a8f
commit
b62622a814
15 changed files with 407 additions and 194 deletions
|
@ -2,11 +2,17 @@ import React from 'react';
|
|||
import ReactDOM from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
import flatMap from 'lodash/flatMap';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import sdk from '../../../index';
|
||||
import type {Completion, SelectionRange} from '../../../autocomplete/Autocompleter';
|
||||
|
||||
import {getCompletions} from '../../../autocomplete/Autocompleter';
|
||||
|
||||
const COMPOSER_SELECTED = 0;
|
||||
|
||||
export default class Autocomplete extends React.Component {
|
||||
completionPromise: Promise = null;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
@ -19,79 +25,137 @@ export default class Autocomplete extends React.Component {
|
|||
// array of completions, so we can look up current selection by offset quickly
|
||||
completionList: [],
|
||||
|
||||
// how far down the completion list we are
|
||||
selectionOffset: 0,
|
||||
// how far down the completion list we are (THIS IS 1-INDEXED!)
|
||||
selectionOffset: COMPOSER_SELECTED,
|
||||
|
||||
// whether we should show completions if they're available
|
||||
shouldShowCompletions: true,
|
||||
|
||||
hide: false,
|
||||
|
||||
forceComplete: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props, state) {
|
||||
async componentWillReceiveProps(props, state) {
|
||||
if (props.query === this.props.query) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await this.complete(props.query, props.selection);
|
||||
}
|
||||
|
||||
async complete(query, selection) {
|
||||
let forceComplete = this.state.forceComplete;
|
||||
const completionPromise = getCompletions(query, selection, forceComplete);
|
||||
this.completionPromise = completionPromise;
|
||||
const completions = await this.completionPromise;
|
||||
|
||||
// There's a newer completion request, so ignore results.
|
||||
if (completionPromise !== this.completionPromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
getCompletions(props.query, props.selection).forEach(completionResult => {
|
||||
try {
|
||||
completionResult.completions.then(completions => {
|
||||
let i = this.state.completions.findIndex(
|
||||
completion => completion.provider === completionResult.provider
|
||||
);
|
||||
const completionList = flatMap(completions, provider => provider.completions);
|
||||
|
||||
i = i === -1 ? this.state.completions.length : i;
|
||||
let newCompletions = Object.assign([], this.state.completions);
|
||||
completionResult.completions = completions;
|
||||
newCompletions[i] = completionResult;
|
||||
|
||||
this.setState({
|
||||
completions: newCompletions,
|
||||
completionList: flatMap(newCompletions, provider => provider.completions),
|
||||
});
|
||||
}, err => {
|
||||
console.error(err);
|
||||
});
|
||||
} catch (e) {
|
||||
// An error in one provider shouldn't mess up the rest.
|
||||
console.error(e);
|
||||
// Reset selection when completion list becomes empty.
|
||||
let selectionOffset = COMPOSER_SELECTED;
|
||||
if (completionList.length > 0) {
|
||||
/* If the currently selected completion is still in the completion list,
|
||||
try to find it and jump to it. If not, select composer.
|
||||
*/
|
||||
const currentSelection = this.state.selectionOffset === 0 ? null :
|
||||
this.state.completionList[this.state.selectionOffset - 1].completion;
|
||||
selectionOffset = completionList.findIndex(
|
||||
completion => completion.completion === currentSelection);
|
||||
if (selectionOffset === -1) {
|
||||
selectionOffset = COMPOSER_SELECTED;
|
||||
} else {
|
||||
selectionOffset++; // selectionOffset is 1-indexed!
|
||||
}
|
||||
} else {
|
||||
// If no completions were returned, we should turn off force completion.
|
||||
forceComplete = false;
|
||||
}
|
||||
|
||||
let hide = this.state.hide;
|
||||
// These are lists of booleans that indicate whether whether the corresponding provider had a matching pattern
|
||||
const oldMatches = this.state.completions.map(completion => !!completion.command.command),
|
||||
newMatches = completions.map(completion => !!completion.command.command);
|
||||
|
||||
// So, essentially, we re-show autocomplete if any provider finds a new pattern or stops finding an old one
|
||||
if (!isEqual(oldMatches, newMatches)) {
|
||||
hide = false;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
completions,
|
||||
completionList,
|
||||
selectionOffset,
|
||||
hide,
|
||||
forceComplete,
|
||||
});
|
||||
}
|
||||
|
||||
countCompletions(): number {
|
||||
return this.state.completions.map(completionResult => {
|
||||
return completionResult.completions.length;
|
||||
}).reduce((l, r) => l + r);
|
||||
return this.state.completionList.length;
|
||||
}
|
||||
|
||||
// called from MessageComposerInput
|
||||
onUpArrow(): boolean {
|
||||
let completionCount = this.countCompletions(),
|
||||
selectionOffset = (completionCount + this.state.selectionOffset - 1) % completionCount;
|
||||
onUpArrow(): ?Completion {
|
||||
const completionCount = this.countCompletions();
|
||||
// completionCount + 1, since 0 means composer is selected
|
||||
const selectionOffset = (completionCount + 1 + this.state.selectionOffset - 1)
|
||||
% (completionCount + 1);
|
||||
if (!completionCount) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
this.setSelection(selectionOffset);
|
||||
return true;
|
||||
return selectionOffset === COMPOSER_SELECTED ? null : this.state.completionList[selectionOffset - 1];
|
||||
}
|
||||
|
||||
// called from MessageComposerInput
|
||||
onDownArrow(): boolean {
|
||||
let completionCount = this.countCompletions(),
|
||||
selectionOffset = (this.state.selectionOffset + 1) % completionCount;
|
||||
onDownArrow(): ?Completion {
|
||||
const completionCount = this.countCompletions();
|
||||
// completionCount + 1, since 0 means composer is selected
|
||||
const selectionOffset = (this.state.selectionOffset + 1) % (completionCount + 1);
|
||||
if (!completionCount) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
this.setSelection(selectionOffset);
|
||||
return true;
|
||||
return selectionOffset === COMPOSER_SELECTED ? null : this.state.completionList[selectionOffset - 1];
|
||||
}
|
||||
|
||||
onEscape(e): boolean {
|
||||
const completionCount = this.countCompletions();
|
||||
if (completionCount === 0) {
|
||||
// autocomplete is already empty, so don't preventDefault
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
// selectionOffset = 0, so we don't end up completing when autocomplete is hidden
|
||||
this.setState({hide: true, selectionOffset: 0});
|
||||
}
|
||||
|
||||
forceComplete() {
|
||||
this.setState({
|
||||
forceComplete: true,
|
||||
}, () => {
|
||||
this.complete(this.props.query, this.props.selection);
|
||||
});
|
||||
}
|
||||
|
||||
/** called from MessageComposerInput
|
||||
* @returns {boolean} whether confirmation was handled
|
||||
*/
|
||||
onConfirm(): boolean {
|
||||
if (this.countCompletions() === 0) {
|
||||
if (this.countCompletions() === 0 || this.state.selectionOffset === COMPOSER_SELECTED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let selectedCompletion = this.state.completionList[this.state.selectionOffset];
|
||||
let selectedCompletion = this.state.completionList[this.state.selectionOffset - 1];
|
||||
this.props.onConfirm(selectedCompletion.range, selectedCompletion.completion);
|
||||
|
||||
return true;
|
||||
|
@ -117,7 +181,7 @@ export default class Autocomplete extends React.Component {
|
|||
render() {
|
||||
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||
|
||||
let position = 0;
|
||||
let position = 1;
|
||||
let renderedCompletions = this.state.completions.map((completionResult, i) => {
|
||||
let completions = completionResult.completions.map((completion, i) => {
|
||||
|
||||
|
@ -135,7 +199,7 @@ export default class Autocomplete extends React.Component {
|
|||
|
||||
return React.cloneElement(completion.component, {
|
||||
key: i,
|
||||
ref: `completion${i}`,
|
||||
ref: `completion${position - 1}`,
|
||||
className,
|
||||
onMouseOver,
|
||||
onClick,
|
||||
|
@ -151,7 +215,7 @@ export default class Autocomplete extends React.Component {
|
|||
) : null;
|
||||
}).filter(completion => !!completion);
|
||||
|
||||
return renderedCompletions.length > 0 ? (
|
||||
return !this.state.hide && renderedCompletions.length > 0 ? (
|
||||
<div className="mx_Autocomplete" ref={(e) => this.container = e}>
|
||||
{renderedCompletions}
|
||||
</div>
|
||||
|
|
|
@ -166,7 +166,7 @@ export default class MessageComposer extends React.Component {
|
|||
|
||||
_onAutocompleteConfirm(range, completion) {
|
||||
if (this.messageComposerInput) {
|
||||
this.messageComposerInput.onConfirmAutocompletion(range, completion);
|
||||
this.messageComposerInput.setDisplayedCompletion(range, completion);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -313,7 +313,6 @@ export default class MessageComposer extends React.Component {
|
|||
|
||||
return (
|
||||
<div className="mx_MessageComposer mx_fadable" style={{ opacity: this.props.opacity }}>
|
||||
{autoComplete}
|
||||
<div className="mx_MessageComposer_wrapper">
|
||||
<div className="mx_MessageComposer_row">
|
||||
{controls}
|
||||
|
|
|
@ -34,6 +34,7 @@ import {Editor, EditorState, RichUtils, CompositeDecorator,
|
|||
import {stateToMarkdown as __stateToMarkdown} from 'draft-js-export-markdown';
|
||||
import classNames from 'classnames';
|
||||
import escape from 'lodash/escape';
|
||||
import Q from 'q';
|
||||
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
|
||||
|
@ -46,6 +47,8 @@ import KeyCode from '../../../KeyCode';
|
|||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
|
||||
import * as RichText from '../../../RichText';
|
||||
import Autocomplete from './Autocomplete';
|
||||
import {Completion} from "../../../autocomplete/Autocompleter";
|
||||
|
||||
const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
|
||||
|
||||
|
@ -88,34 +91,52 @@ export default class MessageComposerInput extends React.Component {
|
|||
return getDefaultKeyBinding(e);
|
||||
}
|
||||
|
||||
static getBlockStyle(block: ContentBlock): ?string {
|
||||
if (block.getType() === 'strikethrough') {
|
||||
return 'mx_Markdown_STRIKETHROUGH';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
client: MatrixClient;
|
||||
autocomplete: Autocomplete;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.onAction = this.onAction.bind(this);
|
||||
this.handleReturn = this.handleReturn.bind(this);
|
||||
this.handleKeyCommand = this.handleKeyCommand.bind(this);
|
||||
this.onEditorContentChanged = this.onEditorContentChanged.bind(this);
|
||||
this.setEditorState = this.setEditorState.bind(this);
|
||||
this.onUpArrow = this.onUpArrow.bind(this);
|
||||
this.onDownArrow = this.onDownArrow.bind(this);
|
||||
this.onTab = this.onTab.bind(this);
|
||||
this.onConfirmAutocompletion = this.onConfirmAutocompletion.bind(this);
|
||||
this.onEscape = this.onEscape.bind(this);
|
||||
this.setDisplayedCompletion = this.setDisplayedCompletion.bind(this);
|
||||
this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this);
|
||||
|
||||
const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', true);
|
||||
|
||||
this.state = {
|
||||
// whether we're in rich text or markdown mode
|
||||
isRichtextEnabled,
|
||||
|
||||
// the currently displayed editor state (note: this is always what is modified on input)
|
||||
editorState: null,
|
||||
|
||||
// the original editor state, before we started tabbing through completions
|
||||
originalEditorState: null,
|
||||
};
|
||||
|
||||
// bit of a hack, but we need to do this here since createEditorState needs isRichtextEnabled
|
||||
/* eslint react/no-direct-mutation-state:0 */
|
||||
this.state.editorState = this.createEditorState();
|
||||
|
||||
this.client = MatrixClientPeg.get();
|
||||
}
|
||||
|
||||
/**
|
||||
/*
|
||||
* "Does the right thing" to create an EditorState, based on:
|
||||
* - whether we've got rich text mode enabled
|
||||
* - contentState was passed in
|
||||
|
@ -234,10 +255,6 @@ export default class MessageComposerInput extends React.Component {
|
|||
this.refs.editor,
|
||||
this.props.room.roomId
|
||||
);
|
||||
// this is disabled for now, since https://github.com/matrix-org/matrix-react-sdk/pull/296 will land soon
|
||||
// if (this.props.tabComplete) {
|
||||
// this.props.tabComplete.setEditor(this.refs.editor);
|
||||
// }
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -273,7 +290,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
);
|
||||
let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
|
||||
editorState = EditorState.forceSelection(editorState, contentState.getSelectionAfter());
|
||||
this.setEditorState(editorState);
|
||||
this.onEditorContentChanged(editorState);
|
||||
editor.focus();
|
||||
}
|
||||
break;
|
||||
|
@ -295,10 +312,11 @@ export default class MessageComposerInput extends React.Component {
|
|||
startSelection,
|
||||
blockMap);
|
||||
startSelection = SelectionState.createEmpty(contentState.getFirstBlock().getKey());
|
||||
if (this.state.isRichtextEnabled)
|
||||
if (this.state.isRichtextEnabled) {
|
||||
contentState = Modifier.setBlockType(contentState, startSelection, 'blockquote');
|
||||
}
|
||||
let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
|
||||
this.setEditorState(editorState);
|
||||
this.onEditorContentChanged(editorState);
|
||||
editor.focus();
|
||||
}
|
||||
}
|
||||
|
@ -372,10 +390,16 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
setEditorState(editorState: EditorState, cb = () => null) {
|
||||
// Called by Draft to change editor contents, and by setEditorState
|
||||
onEditorContentChanged(editorState: EditorState, didRespondToUserInput: boolean = true) {
|
||||
editorState = RichText.attachImmutableEntitiesToEmoji(editorState);
|
||||
this.setState({editorState}, cb);
|
||||
|
||||
const setPromise = Q.defer();
|
||||
/* If a modification was made, set originalEditorState to null, since newState is now our original */
|
||||
this.setState({
|
||||
editorState,
|
||||
originalEditorState: didRespondToUserInput ? null : this.state.originalEditorState,
|
||||
}, () => setPromise.resolve());
|
||||
|
||||
if (editorState.getCurrentContent().hasText()) {
|
||||
this.onTypingActivity();
|
||||
|
@ -390,6 +414,11 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
this.props.onContentChanged(textContent, selection);
|
||||
}
|
||||
return setPromise;
|
||||
}
|
||||
|
||||
setEditorState(editorState: EditorState) {
|
||||
this.onEditorContentChanged(editorState, false);
|
||||
}
|
||||
|
||||
enableRichtext(enabled: boolean) {
|
||||
|
@ -470,7 +499,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
handleReturn(ev) {
|
||||
if (ev.shiftKey) {
|
||||
this.setEditorState(RichUtils.insertSoftNewline(this.state.editorState));
|
||||
this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -547,41 +576,68 @@ export default class MessageComposerInput extends React.Component {
|
|||
return true;
|
||||
}
|
||||
|
||||
onUpArrow(e) {
|
||||
if (this.props.onUpArrow && this.props.onUpArrow()) {
|
||||
async onUpArrow(e) {
|
||||
const completion = this.autocomplete.onUpArrow();
|
||||
if (completion != null) {
|
||||
e.preventDefault();
|
||||
}
|
||||
return await this.setDisplayedCompletion(completion);
|
||||
}
|
||||
|
||||
async onDownArrow(e) {
|
||||
const completion = this.autocomplete.onDownArrow();
|
||||
e.preventDefault();
|
||||
return await this.setDisplayedCompletion(completion);
|
||||
}
|
||||
|
||||
// tab and shift-tab are mapped to down and up arrow respectively
|
||||
async onTab(e) {
|
||||
e.preventDefault(); // we *never* want tab's default to happen, but we do want up/down sometimes
|
||||
const didTab = await (e.shiftKey ? this.onUpArrow : this.onDownArrow)(e);
|
||||
if (!didTab && this.autocomplete) {
|
||||
this.autocomplete.forceComplete();
|
||||
}
|
||||
}
|
||||
|
||||
onDownArrow(e) {
|
||||
if (this.props.onDownArrow && this.props.onDownArrow()) {
|
||||
e.preventDefault();
|
||||
onEscape(e) {
|
||||
e.preventDefault();
|
||||
if (this.autocomplete) {
|
||||
this.autocomplete.onEscape(e);
|
||||
}
|
||||
this.setDisplayedCompletion(null); // restore originalEditorState
|
||||
}
|
||||
|
||||
onTab(e) {
|
||||
if (this.props.tryComplete) {
|
||||
if (this.props.tryComplete()) {
|
||||
e.preventDefault();
|
||||
/* If passed null, restores the original editor content from state.originalEditorState.
|
||||
* If passed a non-null displayedCompletion, modifies state.originalEditorState to compute new state.editorState.
|
||||
*/
|
||||
async setDisplayedCompletion(displayedCompletion: ?Completion): boolean {
|
||||
const activeEditorState = this.state.originalEditorState || this.state.editorState;
|
||||
|
||||
if (displayedCompletion == null) {
|
||||
if (this.state.originalEditorState) {
|
||||
this.setEditorState(this.state.originalEditorState);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
onConfirmAutocompletion(range, content: string) {
|
||||
const {range = {}, completion = ''} = displayedCompletion;
|
||||
|
||||
let contentState = Modifier.replaceText(
|
||||
this.state.editorState.getCurrentContent(),
|
||||
RichText.textOffsetsToSelectionState(range, this.state.editorState.getCurrentContent().getBlocksAsArray()),
|
||||
content
|
||||
activeEditorState.getCurrentContent(),
|
||||
RichText.textOffsetsToSelectionState(range, activeEditorState.getCurrentContent().getBlocksAsArray()),
|
||||
completion
|
||||
);
|
||||
|
||||
let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
|
||||
|
||||
let editorState = EditorState.push(activeEditorState, contentState, 'insert-characters');
|
||||
editorState = EditorState.forceSelection(editorState, contentState.getSelectionAfter());
|
||||
const originalEditorState = activeEditorState;
|
||||
|
||||
this.setEditorState(editorState);
|
||||
await this.setEditorState(editorState);
|
||||
this.setState({originalEditorState});
|
||||
|
||||
// for some reason, doing this right away does not update the editor :(
|
||||
setTimeout(() => this.refs.editor.focus(), 50);
|
||||
return true;
|
||||
}
|
||||
|
||||
onFormatButtonClicked(name: "bold" | "italic" | "strike" | "code" | "underline" | "quote" | "bullet" | "numbullet", e) {
|
||||
|
@ -632,22 +688,14 @@ export default class MessageComposerInput extends React.Component {
|
|||
this.handleKeyCommand('toggle-mode');
|
||||
}
|
||||
|
||||
getBlockStyle(block: ContentBlock): ?string {
|
||||
if (block.getType() === 'strikethrough') {
|
||||
return 'mx_Markdown_STRIKETHROUGH';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {editorState} = this.state;
|
||||
const activeEditorState = this.state.originalEditorState || this.state.editorState;
|
||||
|
||||
// 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();
|
||||
const contentState = activeEditorState.getCurrentContent();
|
||||
if (!contentState.hasText()) {
|
||||
if (contentState.getBlockMap().first().getType() !== 'unstyled') {
|
||||
hidePlaceholder = true;
|
||||
|
@ -655,28 +703,43 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
|
||||
const className = classNames('mx_MessageComposer_input', {
|
||||
mx_MessageComposer_input_empty: hidePlaceholder,
|
||||
mx_MessageComposer_input_empty: hidePlaceholder,
|
||||
});
|
||||
|
||||
const content = activeEditorState.getCurrentContent();
|
||||
const contentText = content.getPlainText();
|
||||
const selection = RichText.selectionStateToTextOffsets(activeEditorState.getSelection(),
|
||||
activeEditorState.getCurrentContent().getBlocksAsArray());
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<img className="mx_MessageComposer_input_markdownIndicator"
|
||||
onMouseDown={this.onMarkdownToggleClicked}
|
||||
title={`Markdown is ${this.state.isRichtextEnabled ? 'disabled' : 'enabled'}`}
|
||||
src={`img/button-md-${!this.state.isRichtextEnabled}.png`} />
|
||||
<Editor ref="editor"
|
||||
placeholder="Type a message…"
|
||||
editorState={this.state.editorState}
|
||||
onChange={this.setEditorState}
|
||||
blockStyleFn={this.getBlockStyle}
|
||||
keyBindingFn={MessageComposerInput.getKeyBinding}
|
||||
handleKeyCommand={this.handleKeyCommand}
|
||||
handleReturn={this.handleReturn}
|
||||
stripPastedStyles={!this.state.isRichtextEnabled}
|
||||
onTab={this.onTab}
|
||||
onUpArrow={this.onUpArrow}
|
||||
onDownArrow={this.onDownArrow}
|
||||
spellCheck={true} />
|
||||
<div className="mx_MessageComposer_input_wrapper">
|
||||
<div className="mx_MessageComposer_autocomplete_wrapper">
|
||||
<Autocomplete
|
||||
ref={(e) => this.autocomplete = e}
|
||||
onConfirm={this.setDisplayedCompletion}
|
||||
query={contentText}
|
||||
selection={selection} />
|
||||
</div>
|
||||
<div className={className}>
|
||||
<img className="mx_MessageComposer_input_markdownIndicator"
|
||||
onMouseDown={this.onMarkdownToggleClicked}
|
||||
title={`Markdown is ${this.state.isRichtextEnabled ? 'disabled' : 'enabled'}`}
|
||||
src={`img/button-md-${!this.state.isRichtextEnabled}.png`} />
|
||||
<Editor ref="editor"
|
||||
placeholder="Type a message…"
|
||||
editorState={this.state.editorState}
|
||||
onChange={this.onEditorContentChanged}
|
||||
blockStyleFn={MessageComposerInput.getBlockStyle}
|
||||
keyBindingFn={MessageComposerInput.getKeyBinding}
|
||||
handleKeyCommand={this.handleKeyCommand}
|
||||
handleReturn={this.handleReturn}
|
||||
stripPastedStyles={!this.state.isRichtextEnabled}
|
||||
onTab={this.onTab}
|
||||
onUpArrow={this.onUpArrow}
|
||||
onDownArrow={this.onDownArrow}
|
||||
onEscape={this.onEscape}
|
||||
spellCheck={true} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue