From c5cd8b943aa20471c58f15ec4ce86f30354343e9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 21 Aug 2019 15:27:50 +0200 Subject: [PATCH] support auto complete for /commands --- .../views/rooms/SendMessageComposer.js | 10 ++----- src/editor/model.js | 2 +- src/editor/parts.js | 30 +++++++++++++++++++ src/editor/serialize.js | 8 ++++- 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index bd5b5b9102..da858e9029 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -20,7 +20,7 @@ import dis from '../../../dispatcher'; import EditorModel from '../../../editor/model'; import {getCaretOffsetAndText} from '../../../editor/dom'; import {htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand} from '../../../editor/serialize'; -import {PartCreator} from '../../../editor/parts'; +import {CommandPartCreator} from '../../../editor/parts'; import {MatrixClient} from 'matrix-js-sdk'; import BasicMessageComposer from "./BasicMessageComposer"; import ReplyPreview from "./ReplyPreview"; @@ -164,7 +164,7 @@ export default class SendMessageComposer extends React.Component { _isSlashCommand() { const parts = this.model.parts; const isPlain = parts.reduce((isPlain, part) => { - return isPlain && (part.type === "plain" || part.type === "newline"); + return isPlain && (part.type === "command" || part.type === "plain" || part.type === "newline"); }, true); return isPlain && parts.length > 0 && parts[0].text.startsWith("/"); } @@ -227,15 +227,11 @@ export default class SendMessageComposer extends React.Component { } componentWillUnmount() { - const sel = document.getSelection(); - const {caret} = getCaretOffsetAndText(this._editorRef, sel); - const parts = this.model.serializeParts(); - this.props.editState.setEditorState(caret, parts); dis.unregister(this.dispatcherRef); } componentWillMount() { - const partCreator = new PartCreator(this.props.room, this.context.matrixClient); + const partCreator = new CommandPartCreator(this.props.room, this.context.matrixClient); this.model = new EditorModel([], partCreator); this.dispatcherRef = dis.register(this.onAction); this.sendHistoryManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_'); diff --git a/src/editor/model.js b/src/editor/model.js index 99d590bf26..91b724cf9e 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -303,7 +303,7 @@ export default class EditorModel { index = 0; } while (str) { - const newPart = this._partCreator.createPartForInput(str); + const newPart = this._partCreator.createPartForInput(str, index); if (validate) { str = newPart.appendUntilRejected(str); } else { diff --git a/src/editor/parts.js b/src/editor/parts.js index cf11dc74dc..f9b4243de4 100644 --- a/src/editor/parts.js +++ b/src/editor/parts.js @@ -441,3 +441,33 @@ export class PartCreator { } } +// part creator that support auto complete for /commands, +// used in SendMessageComposer +export class CommandPartCreator extends PartCreator { + createPartForInput(text, partIndex) { + // at beginning and starts with /? create + if (partIndex === 0 && text[0] === "/") { + return new CommandPart("", this._autoCompleteCreator); + } else { + return super.createPartForInput(text, partIndex); + } + } + + deserializePart(part) { + if (part.type === "command") { + return new CommandPart(part.text, this._autoCompleteCreator); + } else { + return super.deserializePart(part); + } + } +} + +class CommandPart extends PillCandidatePart { + acceptsInsertion(chr, i) { + return PlainPart.prototype.acceptsInsertion.call(this, chr, i); + } + + get type() { + return "command"; + } +} diff --git a/src/editor/serialize.js b/src/editor/serialize.js index 756a27dd03..5a1a941309 100644 --- a/src/editor/serialize.js +++ b/src/editor/serialize.js @@ -23,6 +23,7 @@ export function mdSerialize(model) { case "newline": return html + "\n"; case "plain": + case "command": case "pill-candidate": case "at-room-pill": return html + part.text; @@ -47,6 +48,7 @@ export function textSerialize(model) { case "newline": return text + "\n"; case "plain": + case "command": case "pill-candidate": case "at-room-pill": return text + part.text; @@ -59,7 +61,11 @@ export function textSerialize(model) { export function containsEmote(model) { const firstPart = model.parts[0]; - return firstPart && firstPart.type === "plain" && firstPart.text.startsWith("/me "); + // part type will be "plain" while editing, + // and "command" while composing a message. + return firstPart && + (firstPart.type === "plain" || firstPart.type === "command") && + firstPart.text.startsWith("/me "); } export function stripEmoteCommand(model) {