From cccc58b47f77f0eec644bc2455916496ed468318 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Sun, 3 Jul 2016 22:15:13 +0530 Subject: [PATCH] feat: implement autocomplete replacement --- package.json | 1 + src/RichText.js | 51 ++++++++++++++--- src/autocomplete/AutocompleteProvider.js | 29 +++++++--- src/autocomplete/Autocompleter.js | 4 +- src/autocomplete/CommandProvider.js | 43 ++++++++------ src/autocomplete/Components.js | 14 +++-- src/autocomplete/DuckDuckGoProvider.js | 57 +++++++++++++------ src/autocomplete/EmojiProvider.js | 14 +++-- src/autocomplete/RoomProvider.js | 24 +++++--- src/autocomplete/UserProvider.js | 23 +++++--- src/components/views/rooms/Autocomplete.js | 51 ++++++++++++----- src/components/views/rooms/MessageComposer.js | 34 ++++++----- .../views/rooms/MessageComposerInput.js | 47 +++++++++++---- 13 files changed, 271 insertions(+), 121 deletions(-) diff --git a/package.json b/package.json index fc3b1c8f24..13cabf32d9 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "glob": "^5.0.14", "highlight.js": "^8.9.1", "linkifyjs": "^2.0.0-beta.4", + "lodash": "^4.13.1", "marked": "^0.3.5", "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", "optimist": "^0.6.1", diff --git a/src/RichText.js b/src/RichText.js index f4fa4883cb..abbe860863 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -1,12 +1,14 @@ +import React from 'react'; import { Editor, Modifier, ContentState, + ContentBlock, convertFromHTML, DefaultDraftBlockRenderMap, DefaultDraftInlineStyle, CompositeDecorator, - SelectionState + SelectionState, } from 'draft-js'; import * as sdk from './index'; import * as emojione from 'emojione'; @@ -25,7 +27,7 @@ const STYLES = { CODE: 'code', ITALIC: 'em', STRIKETHROUGH: 's', - UNDERLINE: 'u' + UNDERLINE: 'u', }; const MARKDOWN_REGEX = { @@ -168,7 +170,7 @@ export function modifyText(contentState: ContentState, rangeToReplace: Selection text = ""; - for(let currentKey = startKey; + for (let currentKey = startKey; currentKey && currentKey !== endKey; currentKey = contentState.getKeyAfter(currentKey)) { let blockText = getText(currentKey); @@ -189,14 +191,14 @@ export function modifyText(contentState: ContentState, rangeToReplace: Selection * Note that this inherently means we make assumptions about what that means (no separator between ContentBlocks, etc) * Used by autocomplete to show completions when the current selection lies within, or at the edges of a command. */ -export function getTextSelectionOffsets(selectionState: SelectionState, - contentBlocks: Array): {start: number, end: number} { +export function selectionStateToTextOffsets(selectionState: SelectionState, + contentBlocks: Array): {start: number, end: number} { let offset = 0, start = 0, end = 0; for(let block of contentBlocks) { - if (selectionState.getStartKey() == block.getKey()) { + if (selectionState.getStartKey() === block.getKey()) { start = offset + selectionState.getStartOffset(); } - if (selectionState.getEndKey() == block.getKey()) { + if (selectionState.getEndKey() === block.getKey()) { end = offset + selectionState.getEndOffset(); break; } @@ -205,6 +207,37 @@ export function getTextSelectionOffsets(selectionState: SelectionState, return { start, - end - } + end, + }; +} + +export function textOffsetsToSelectionState({start, end}: {start: number, end: number}, + contentBlocks: Array): SelectionState { + let selectionState = SelectionState.createEmpty(); + + for (let block of contentBlocks) { + let blockLength = block.getLength(); + + if (start !== -1 && start < blockLength) { + selectionState = selectionState.merge({ + anchorKey: block.getKey(), + anchorOffset: start, + }); + start = -1; + } else { + start -= blockLength; + } + + if (end !== -1 && end <= blockLength) { + selectionState = selectionState.merge({ + focusKey: block.getKey(), + focusOffset: end, + }); + end = -1; + } else { + end -= blockLength; + } + } + + return selectionState; } diff --git a/src/autocomplete/AutocompleteProvider.js b/src/autocomplete/AutocompleteProvider.js index f741e085b0..05bbeacfab 100644 --- a/src/autocomplete/AutocompleteProvider.js +++ b/src/autocomplete/AutocompleteProvider.js @@ -14,25 +14,36 @@ export default class AutocompleteProvider { * Of the matched commands in the query, returns the first that contains or is contained by the selection, or null. */ getCurrentCommand(query: string, selection: {start: number, end: number}): ?Array { - if(this.commandRegex == null) + if (this.commandRegex == null) { return null; + } - let match = null; - while((match = this.commandRegex.exec(query)) != null) { + let match; + while ((match = this.commandRegex.exec(query)) != null) { let matchStart = match.index, matchEnd = matchStart + match[0].length; - - console.log(match); - if(selection.start <= matchEnd && selection.end >= matchStart) { - return match; + if (selection.start <= matchEnd && selection.end >= matchStart) { + return { + command: match, + range: { + start: matchStart, + end: matchEnd, + }, + }; } } this.commandRegex.lastIndex = 0; - return null; + return { + command: null, + range: { + start: -1, + end: -1, + }, + }; } - getCompletions(query: String, selection: {start: number, end: number}) { + getCompletions(query: string, selection: {start: number, end: number}) { return Q.when([]); } diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index 6b66d2fbdc..7f32e0ca40 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -9,14 +9,14 @@ const PROVIDERS = [ CommandProvider, DuckDuckGoProvider, RoomProvider, - EmojiProvider + EmojiProvider, ].map(completer => completer.getInstance()); export function getCompletions(query: string, selection: {start: number, end: number}) { return PROVIDERS.map(provider => { return { completions: provider.getCompletions(query, selection), - provider + provider, }; }); } diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index 30b448d7f2..19a366ac63 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -1,42 +1,45 @@ +import React from 'react'; import AutocompleteProvider from './AutocompleteProvider'; import Q from 'q'; import Fuse from 'fuse.js'; +import {TextualCompletion} from './Components'; const COMMANDS = [ { command: '/me', args: '', - description: 'Displays action' + description: 'Displays action', }, { command: '/ban', args: ' [reason]', - description: 'Bans user with given id' + description: 'Bans user with given id', }, { - command: '/deop' + command: '/deop', + args: '', + description: 'Deops user with given id', }, { - command: '/encrypt' - }, - { - command: '/invite' + command: '/invite', + args: '', + description: 'Invites user with given id to current room' }, { command: '/join', args: '', - description: 'Joins room with given alias' + description: 'Joins room with given alias', }, { command: '/kick', args: ' [reason]', - description: 'Kicks user with given id' + description: 'Kicks user with given id', }, { command: '/nick', args: '', - description: 'Changes your display nickname' - } + description: 'Changes your display nickname', + }, ]; let COMMAND_RE = /(^\/\w*)/g; @@ -47,19 +50,23 @@ export default class CommandProvider extends AutocompleteProvider { constructor() { super(COMMAND_RE); this.fuse = new Fuse(COMMANDS, { - keys: ['command', 'args', 'description'] + keys: ['command', 'args', 'description'], }); } getCompletions(query: string, selection: {start: number, end: number}) { let completions = []; - const command = this.getCurrentCommand(query, selection); - if(command) { + let {command, range} = this.getCurrentCommand(query, selection); + if (command) { completions = this.fuse.search(command[0]).map(result => { return { - title: result.command, - subtitle: result.args, - description: result.description + completion: result.command + ' ', + component: (), + range, }; }); } @@ -71,7 +78,7 @@ export default class CommandProvider extends AutocompleteProvider { } static getInstance(): CommandProvider { - if(instance == null) + if (instance == null) instance = new CommandProvider(); return instance; diff --git a/src/autocomplete/Components.js b/src/autocomplete/Components.js index cb7d56f9bf..d9d1c7b3ff 100644 --- a/src/autocomplete/Components.js +++ b/src/autocomplete/Components.js @@ -1,13 +1,19 @@ -export function TextualCompletion(props: { +import React from 'react'; + +export function TextualCompletion({ + title, + subtitle, + description, +}: { title: ?string, subtitle: ?string, description: ?string }) { return (
- {completion.title} - {completion.subtitle} - {completion.description} + {title} + {subtitle} + {description}
); } diff --git a/src/autocomplete/DuckDuckGoProvider.js b/src/autocomplete/DuckDuckGoProvider.js index b2bf27a21a..cfd3cb2ff6 100644 --- a/src/autocomplete/DuckDuckGoProvider.js +++ b/src/autocomplete/DuckDuckGoProvider.js @@ -1,9 +1,12 @@ +import React from 'react'; import AutocompleteProvider from './AutocompleteProvider'; import Q from 'q'; import 'whatwg-fetch'; +import {TextualCompletion} from './Components'; + const DDG_REGEX = /\/ddg\s+(.+)$/g; -const REFERER = 'vector'; +const REFERRER = 'vector'; let instance = null; @@ -14,42 +17,62 @@ export default class DuckDuckGoProvider extends AutocompleteProvider { static getQueryUri(query: String) { return `http://api.duckduckgo.com/?q=${encodeURIComponent(query)}` - + `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERER)}`; + + `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERRER)}`; } getCompletions(query: string, selection: {start: number, end: number}) { - let command = this.getCurrentCommand(query, selection); - if(!query || !command) + let {command, range} = this.getCurrentCommand(query, selection); + if (!query || !command) { return Q.when([]); + } return fetch(DuckDuckGoProvider.getQueryUri(command[1]), { - method: 'GET' + method: 'GET', }) .then(response => response.json()) .then(json => { let results = json.Results.map(result => { return { - title: result.Text, - description: result.Result + completion: result.Text, + component: ( + + ), + range, }; }); - if(json.Answer) { + if (json.Answer) { results.unshift({ - title: json.Answer, - description: json.AnswerType + completion: json.Answer, + component: ( + + ), + range, }); } - if(json.RelatedTopics && json.RelatedTopics.length > 0) { + if (json.RelatedTopics && json.RelatedTopics.length > 0) { results.unshift({ - title: json.RelatedTopics[0].Text + completion: json.RelatedTopics[0].Text, + component: ( + + ), + range, }); } - if(json.AbstractText) { + if (json.AbstractText) { results.unshift({ - title: json.AbstractText + completion: json.AbstractText, + component: ( + + ), + range, }); } - // console.log(results); return results; }); } @@ -59,9 +82,9 @@ export default class DuckDuckGoProvider extends AutocompleteProvider { } static getInstance(): DuckDuckGoProvider { - if(instance == null) + if (instance == null) { instance = new DuckDuckGoProvider(); - + } return instance; } } diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index e1b5f3ea38..574144e95b 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -1,6 +1,7 @@ +import React from 'react'; import AutocompleteProvider from './AutocompleteProvider'; import Q from 'q'; -import {emojioneList, shortnameToImage} from 'emojione'; +import {emojioneList, shortnameToImage, shortnameToUnicode} from 'emojione'; import Fuse from 'fuse.js'; const EMOJI_REGEX = /:\w*:?/g; @@ -16,18 +17,19 @@ export default class EmojiProvider extends AutocompleteProvider { getCompletions(query: string, selection: {start: number, end: number}) { let completions = []; - let command = this.getCurrentCommand(query, selection); - if(command) { + let {command, range} = this.getCurrentCommand(query, selection); + if (command) { completions = this.fuse.search(command[0]).map(result => { let shortname = EMOJI_SHORTNAMES[result]; let imageHTML = shortnameToImage(shortname); return { - title: shortname, + completion: shortnameToUnicode(shortname), component: (
{shortname}
- ) + ), + range, }; }).slice(0, 4); } @@ -39,7 +41,7 @@ export default class EmojiProvider extends AutocompleteProvider { } static getInstance() { - if(instance == null) + if (instance == null) instance = new EmojiProvider(); return instance; } diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index 8b5650e7a7..e38be65987 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -1,7 +1,9 @@ +import React from 'react'; import AutocompleteProvider from './AutocompleteProvider'; import Q from 'q'; import MatrixClientPeg from '../MatrixClientPeg'; import Fuse from 'fuse.js'; +import {TextualCompletion} from './Components'; const ROOM_REGEX = /(?=#)([^\s]*)/g; @@ -10,32 +12,35 @@ let instance = null; export default class RoomProvider extends AutocompleteProvider { constructor() { super(ROOM_REGEX, { - keys: ['displayName', 'userId'] + keys: ['displayName', 'userId'], }); this.fuse = new Fuse([], { - keys: ['name', 'roomId', 'aliases'] + keys: ['name', 'roomId', 'aliases'], }); } getCompletions(query: string, selection: {start: number, end: number}) { let client = MatrixClientPeg.get(); let completions = []; - const command = this.getCurrentCommand(query, selection); - if(command) { + const {command, range} = this.getCurrentCommand(query, selection); + if (command) { // the only reason we need to do this is because Fuse only matches on properties this.fuse.set(client.getRooms().filter(room => !!room).map(room => { return { name: room.name, roomId: room.roomId, - aliases: room.getAliases() + aliases: room.getAliases(), }; })); completions = this.fuse.search(command[0]).map(room => { return { - title: room.name, - subtitle: room.roomId + completion: room.roomId, + component: ( + + ), + range, }; - }).slice(0, 4);; + }).slice(0, 4); } return Q.when(completions); } @@ -45,8 +50,9 @@ export default class RoomProvider extends AutocompleteProvider { } static getInstance() { - if(instance == null) + if (instance == null) { instance = new RoomProvider(); + } return instance; } diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 3edb2bf00c..3e65a65676 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -1,6 +1,8 @@ +import React from 'react'; import AutocompleteProvider from './AutocompleteProvider'; import Q from 'q'; import Fuse from 'fuse.js'; +import {TextualCompletion} from './Components'; const USER_REGEX = /@[^\s]*/g; @@ -9,23 +11,27 @@ let instance = null; export default class UserProvider extends AutocompleteProvider { constructor() { super(USER_REGEX, { - keys: ['displayName', 'userId'] + keys: ['displayName', 'userId'], }); this.users = []; this.fuse = new Fuse([], { - keys: ['displayName', 'userId'] - }) + keys: ['displayName', 'userId'], + }); } getCompletions(query: string, selection: {start: number, end: number}) { let completions = []; - let command = this.getCurrentCommand(query, selection); - if(command) { + let {command, range} = this.getCurrentCommand(query, selection); + if (command) { this.fuse.set(this.users); completions = this.fuse.search(command[0]).map(user => { return { - title: user.displayName || user.userId, - description: user.userId + completion: user.userId, + component: ( + + ), }; }).slice(0, 4); } @@ -41,8 +47,9 @@ export default class UserProvider extends AutocompleteProvider { } static getInstance(): UserProvider { - if(instance == null) + if (instance == null) { instance = new UserProvider(); + } return instance; } } diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index 414b0f1ebb..dfeda96845 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -1,15 +1,23 @@ import React from 'react'; import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; import classNames from 'classnames'; +import _ from 'lodash'; import {getCompletions} from '../../../autocomplete/Autocompleter'; export default class Autocomplete extends React.Component { constructor(props) { super(props); + + this.onConfirm = this.onConfirm.bind(this); + this.state = { + // list of completionResults, each containing completions completions: [], + // array of completions, so we can look up current selection by offset quickly + completionList: [], + // how far down the completion list we are selectionOffset: 0, }; @@ -31,8 +39,10 @@ export default class Autocomplete extends React.Component { 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); @@ -54,7 +64,7 @@ export default class Autocomplete extends React.Component { onUpArrow(): boolean { let completionCount = this.countCompletions(), selectionOffset = (completionCount + this.state.selectionOffset - 1) % completionCount; - this.setState({selectionOffset}); + this.setSelection(selectionOffset); return true; } @@ -62,34 +72,49 @@ export default class Autocomplete extends React.Component { onDownArrow(): boolean { let completionCount = this.countCompletions(), selectionOffset = (this.state.selectionOffset + 1) % completionCount; - this.setState({selectionOffset}); + this.setSelection(selectionOffset); return true; } + /** called from MessageComposerInput + * @returns {boolean} whether confirmation was handled + */ + onConfirm(): boolean { + if (this.countCompletions() === 0) + return false; + + let selectedCompletion = this.state.completionList[this.state.selectionOffset]; + this.props.onConfirm(selectedCompletion.range, selectedCompletion.completion); + + return true; + } + + setSelection(selectionOffset: number) { + this.setState({selectionOffset}); + } + render() { let position = 0; let renderedCompletions = this.state.completions.map((completionResult, i) => { let completions = completionResult.completions.map((completion, i) => { - let Component = completion.component; let className = classNames('mx_Autocomplete_Completion', { 'selected': position === this.state.selectionOffset, }); let componentPosition = position; position++; - if (Component) { - return Component; - } - let onMouseOver = () => this.setState({selectionOffset: componentPosition}); - + let onMouseOver = () => this.setSelection(componentPosition), + onClick = () => { + this.setSelection(componentPosition); + this.onConfirm(); + }; + return (
- {completion.title} - {completion.subtitle} - - {completion.description} + onMouseOver={onMouseOver} + onClick={onClick}> + {completion.component}
); }); diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 24d0bd2510..4dc28e73c5 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -40,16 +40,17 @@ export default class MessageComposer extends React.Component { this.state = { autocompleteQuery: '', - selection: null + selection: null, }; + } onUploadClick(ev) { if (MatrixClientPeg.get().isGuest()) { - var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); + let NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); Modal.createDialog(NeedToRegisterDialog, { title: "Please Register", - description: "Guest users can't upload files. Please register to upload." + description: "Guest users can't upload files. Please register to upload.", }); return; } @@ -58,13 +59,13 @@ export default class MessageComposer extends React.Component { } onUploadFileSelected(ev) { - var files = ev.target.files; + let files = ev.target.files; - var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - var TintableSvg = sdk.getComponent("elements.TintableSvg"); + let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + let TintableSvg = sdk.getComponent("elements.TintableSvg"); - var fileList = []; - for(var i=0; i {files[i].name} ); @@ -91,7 +92,7 @@ export default class MessageComposer extends React.Component { } this.refs.uploadInput.value = null; - } + }, }); } @@ -105,7 +106,7 @@ export default class MessageComposer extends React.Component { action: 'hangup', // hangup the call for this room, which may not be the room in props // (e.g. conferences which will hangup the 1:1 room instead) - room_id: call.roomId + room_id: call.roomId, }); } @@ -113,7 +114,7 @@ export default class MessageComposer extends React.Component { dis.dispatch({ action: 'place_call', type: ev.shiftKey ? "screensharing" : "video", - room_id: this.props.room.roomId + room_id: this.props.room.roomId, }); } @@ -121,14 +122,14 @@ export default class MessageComposer extends React.Component { dis.dispatch({ action: 'place_call', type: 'voice', - room_id: this.props.room.roomId + room_id: this.props.room.roomId, }); } onInputContentChanged(content: string, selection: {start: number, end: number}) { this.setState({ autocompleteQuery: content, - selection + selection, }); } @@ -171,11 +172,11 @@ export default class MessageComposer extends React.Component { callButton =
-
+ ; videoCallButton =
-
+ ; } var canSendMessages = this.props.room.currentState.maySendMessage( @@ -198,9 +199,11 @@ export default class MessageComposer extends React.Component { controls.push( this.messageComposerInput = c} key="controls_input" onResize={this.props.onResize} room={this.props.room} + tryComplete={this.refs.autocomplete && this.refs.autocomplete.onConfirm} onUpArrow={this.onUpArrow} onDownArrow={this.onDownArrow} onTab={this.onTab} @@ -223,6 +226,7 @@ export default class MessageComposer extends React.Component {
diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 313216d54c..46abc20ed6 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -76,6 +76,7 @@ export default class MessageComposerInput extends React.Component { this.onUpArrow = this.onUpArrow.bind(this); this.onDownArrow = this.onDownArrow.bind(this); this.onTab = this.onTab.bind(this); + this.onConfirmAutocompletion = this.onConfirmAutocompletion.bind(this); let isRichtextEnabled = window.localStorage.getItem('mx_editor_rte_enabled'); if(isRichtextEnabled == null) { @@ -85,7 +86,7 @@ export default class MessageComposerInput extends React.Component { this.state = { isRichtextEnabled: isRichtextEnabled, - editorState: null + editorState: null, }; // bit of a hack, but we need to do this here since createEditorState needs isRichtextEnabled @@ -96,7 +97,7 @@ export default class MessageComposerInput extends React.Component { static getKeyBinding(e: SyntheticKeyboardEvent): string { // C-m => Toggles between rich text and markdown modes - if(e.keyCode == KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) { + if (e.keyCode === KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) { return 'toggle-mode'; } @@ -212,7 +213,7 @@ export default class MessageComposerInput extends React.Component { let content = convertFromRaw(JSON.parse(contentJSON)); component.setEditorState(component.createEditorState(component.state.isRichtextEnabled, content)); } - } + }, }; } @@ -234,7 +235,7 @@ export default class MessageComposerInput extends React.Component { } onAction(payload) { - var editor = this.refs.editor; + let editor = this.refs.editor; switch (payload.action) { case 'focus_composer': @@ -252,7 +253,7 @@ export default class MessageComposerInput extends React.Component { payload.displayname ); this.setState({ - editorState: EditorState.push(this.state.editorState, contentState, 'insert-characters') + editorState: EditorState.push(this.state.editorState, contentState, 'insert-characters'), }); editor.focus(); } @@ -356,7 +357,7 @@ export default class MessageComposerInput extends React.Component { if(this.props.onContentChanged) { this.props.onContentChanged(editorState.getCurrentContent().getPlainText(), - RichText.getTextSelectionOffsets(editorState.getSelection(), + RichText.selectionStateToTextOffsets(editorState.getSelection(), editorState.getCurrentContent().getBlocksAsArray())); } } @@ -418,12 +419,21 @@ export default class MessageComposerInput extends React.Component { } handleReturn(ev) { - if(ev.shiftKey) + if (ev.shiftKey) { return false; + } + + if(this.props.tryComplete) { + if(this.props.tryComplete()) { + return true; + } + } const contentState = this.state.editorState.getCurrentContent(); - if(!contentState.hasText()) + if (!contentState.hasText()) { return true; + } + let contentText = contentState.getPlainText(), contentHTML; @@ -509,17 +519,32 @@ export default class MessageComposerInput extends React.Component { } onTab(e) { - if(this.props.onTab) { - if(this.props.onTab()) { + if (this.props.onTab) { + if (this.props.onTab()) { e.preventDefault(); } } } + onConfirmAutocompletion(range, content: string) { + let contentState = Modifier.replaceText( + this.state.editorState.getCurrentContent(), + RichText.textOffsetsToSelectionState(range, this.state.editorState.getCurrentContent().getBlocksAsArray()), + content + ); + + this.setState({ + editorState: EditorState.push(this.state.editorState, contentState, 'insert-characters'), + }); + + // for some reason, doing this right away does not update the editor :( + setTimeout(() => this.refs.editor.focus(), 50); + } + render() { let className = "mx_MessageComposer_input"; - if(this.state.isRichtextEnabled) { + if (this.state.isRichtextEnabled) { className += " mx_MessageComposer_input_rte"; // placeholder indicator for RTE mode }