mirror of
https://github.com/element-hq/element-web
synced 2024-11-28 12:28:50 +03:00
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:
parent
71286b5610
commit
c135cd60d2
3 changed files with 111 additions and 17 deletions
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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`.
|
||||
|
|
Loading…
Reference in a new issue