restore insert mention

for this, we need to store the last caret in the editor,
to know where to insert the user pill.

Because clicking on a member blurs the editor, and the
selection is moved away from the editor.

For this reason, we keep as cache of the last caretOffset object,
invalidated by a selection with different values.

The selection needs to be cloned because apparently the browser
mutates the object instead of returning a new one.
This commit is contained in:
Bruno Windels 2019-08-07 17:44:49 +02:00
parent 71286b5610
commit c135cd60d2
3 changed files with 111 additions and 17 deletions

View file

@ -28,6 +28,28 @@ import {Room} from 'matrix-js-sdk';
const IS_MAC = navigator.platform.indexOf("Mac") !== -1;
function cloneSelection(selection) {
return {
anchorNode: selection.anchorNode,
anchorOffset: selection.anchorOffset,
focusNode: selection.focusNode,
focusOffset: selection.focusOffset,
isCollapsed: selection.isCollapsed,
rangeCount: selection.rangeCount,
type: selection.type,
};
}
function selectionEquals(a: Selection, b: Selection): boolean {
return a.anchorNode === b.anchorNode &&
a.anchorOffset === b.anchorOffset &&
a.focusNode === b.focusNode &&
a.focusOffset === b.focusOffset &&
a.isCollapsed === b.isCollapsed &&
a.rangeCount === b.rangeCount &&
a.type === b.type;
}
export default class BasicMessageEditor extends React.Component {
static propTypes = {
model: PropTypes.instanceOf(EditorModel).isRequired,
@ -74,6 +96,7 @@ export default class BasicMessageEditor extends React.Component {
this._modifiedFlag = true;
const sel = document.getSelection();
const {caret, text} = getCaretOffsetAndText(this._editorRef, sel);
this._setLastCaret(caret, text, sel);
this.props.model.update(text, event.inputType, caret);
}
@ -85,14 +108,59 @@ export default class BasicMessageEditor extends React.Component {
this.props.model.update(newText, inputType, caret);
}
_isCaretAtStart() {
const {caret} = getCaretOffsetAndText(this._editorRef, document.getSelection());
return caret.offset === 0;
// this is used later to see if we need to recalculate the caret
// on selectionchange. If it is just a consequence of typing
// we don't need to. But if the user is navigating the caret without input
// we need to recalculate it, to be able to know where to insert content after
// losing focus
_setLastCaret(caret, text, selection) {
this._lastSelection = cloneSelection(selection);
this._lastCaret = caret;
this._lastTextLength = text.length;
}
_isCaretAtEnd() {
const {caret, text} = getCaretOffsetAndText(this._editorRef, document.getSelection());
return caret.offset === text.length;
_refreshLastCaretIfNeeded() {
// TODO: needed when going up and down in editing messages ... not sure why yet
// because the editors should stop doing this when when blurred ...
// maybe it's on focus and the _editorRef isn't available yet or something.
if (!this._editorRef) {
return;
}
const selection = document.getSelection();
if (!this._lastSelection || !selectionEquals(this._lastSelection, selection)) {
this._lastSelection = cloneSelection(selection);
const {caret, text} = getCaretOffsetAndText(this._editorRef, selection);
this._lastCaret = caret;
this._lastTextLength = text.length;
}
return this._lastCaret;
}
getCaret() {
return this._lastCaret;
}
isCaretAtStart() {
return this.getCaret().offset === 0;
}
isCaretAtEnd() {
return this.getCaret().offset === this._lastTextLength;
}
_onBlur = () => {
document.removeEventListener("selectionchange", this._onSelectionChange);
}
_onFocus = () => {
document.addEventListener("selectionchange", this._onSelectionChange);
// force to recalculate
this._lastSelection = null;
this._refreshLastCaretIfNeeded();
}
_onSelectionChange = () => {
this._refreshLastCaretIfNeeded();
}
_onKeyDown = (event) => {
@ -202,17 +270,6 @@ export default class BasicMessageEditor extends React.Component {
return caretPosition;
}
isCaretAtStart() {
const {caret} = getCaretOffsetAndText(this._editorRef, document.getSelection());
return caret.offset === 0;
}
isCaretAtEnd() {
const {caret, text} = getCaretOffsetAndText(this._editorRef, document.getSelection());
return caret.offset === text.length;
}
render() {
let autoComplete;
if (this.state.autoComplete) {
@ -235,6 +292,8 @@ export default class BasicMessageEditor extends React.Component {
className="mx_BasicMessageComposer_input"
contentEditable="true"
tabIndex="1"
onBlur={this._onBlur}
onFocus={this._onFocus}
onKeyDown={this._onKeyDown}
ref={ref => this._editorRef = ref}
aria-label={this.props.label}

View file

@ -97,6 +97,15 @@ export default class SendMessageComposer extends React.Component {
case 'focus_composer':
this._editorRef.focus();
break;
case 'insert_mention': {
const userId = payload.user_id;
const member = this.props.room.getMember(userId);
const displayName = member ?
member.rawDisplayName : payload.user_id;
const userPillPart = this.model.partCreator.userPill(displayName, userId);
this.model.insertPartAt(userPillPart, this._editorRef.getCaret());
break;
}
}
};

View file

@ -108,6 +108,15 @@ export default class EditorModel {
this._updateCallback(caret, inputType);
}
insertPartAt(part, caret) {
const position = this.positionForOffset(caret.offset, caret.atNodeEnd);
const insertIndex = this._splitAt(position);
this._insertPart(insertIndex, part);
// want to put caret after new part?
const newPosition = new DocumentPosition(insertIndex, part.text.length);
this._updateCallback(newPosition);
}
update(newValue, inputType, caret) {
const diff = this._diff(newValue, inputType, caret);
const position = this.positionForOffset(diff.at, caret.atNodeEnd);
@ -232,6 +241,23 @@ export default class EditorModel {
}
return removedOffsetDecrease;
}
// return part index where insertion will insert between at offset
_splitAt(pos) {
if (pos.index === -1) {
return 0;
}
if (pos.offset === 0) {
return pos.index;
}
const part = this._parts[pos.index];
if (pos.offset >= part.text.length) {
return pos.index + 1;
}
const secondPart = part.split(pos.offset);
this._insertPart(pos.index + 1, secondPart);
return pos.index + 1;
}
/**
* inserts `str` into the model at `pos`.