mirror of https://github.com/vector-im/riot-web
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.pull/21833/head
parent
71286b5610
commit
c135cd60d2
|
@ -28,6 +28,28 @@ import {Room} from 'matrix-js-sdk';
|
||||||
|
|
||||||
const IS_MAC = navigator.platform.indexOf("Mac") !== -1;
|
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 {
|
export default class BasicMessageEditor extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
model: PropTypes.instanceOf(EditorModel).isRequired,
|
model: PropTypes.instanceOf(EditorModel).isRequired,
|
||||||
|
@ -74,6 +96,7 @@ export default class BasicMessageEditor extends React.Component {
|
||||||
this._modifiedFlag = true;
|
this._modifiedFlag = true;
|
||||||
const sel = document.getSelection();
|
const sel = document.getSelection();
|
||||||
const {caret, text} = getCaretOffsetAndText(this._editorRef, sel);
|
const {caret, text} = getCaretOffsetAndText(this._editorRef, sel);
|
||||||
|
this._setLastCaret(caret, text, sel);
|
||||||
this.props.model.update(text, event.inputType, caret);
|
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);
|
this.props.model.update(newText, inputType, caret);
|
||||||
}
|
}
|
||||||
|
|
||||||
_isCaretAtStart() {
|
// this is used later to see if we need to recalculate the caret
|
||||||
const {caret} = getCaretOffsetAndText(this._editorRef, document.getSelection());
|
// on selectionchange. If it is just a consequence of typing
|
||||||
return caret.offset === 0;
|
// 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() {
|
_refreshLastCaretIfNeeded() {
|
||||||
const {caret, text} = getCaretOffsetAndText(this._editorRef, document.getSelection());
|
// TODO: needed when going up and down in editing messages ... not sure why yet
|
||||||
return caret.offset === text.length;
|
// 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) => {
|
_onKeyDown = (event) => {
|
||||||
|
@ -202,17 +270,6 @@ export default class BasicMessageEditor extends React.Component {
|
||||||
return caretPosition;
|
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() {
|
render() {
|
||||||
let autoComplete;
|
let autoComplete;
|
||||||
if (this.state.autoComplete) {
|
if (this.state.autoComplete) {
|
||||||
|
@ -235,6 +292,8 @@ export default class BasicMessageEditor extends React.Component {
|
||||||
className="mx_BasicMessageComposer_input"
|
className="mx_BasicMessageComposer_input"
|
||||||
contentEditable="true"
|
contentEditable="true"
|
||||||
tabIndex="1"
|
tabIndex="1"
|
||||||
|
onBlur={this._onBlur}
|
||||||
|
onFocus={this._onFocus}
|
||||||
onKeyDown={this._onKeyDown}
|
onKeyDown={this._onKeyDown}
|
||||||
ref={ref => this._editorRef = ref}
|
ref={ref => this._editorRef = ref}
|
||||||
aria-label={this.props.label}
|
aria-label={this.props.label}
|
||||||
|
|
|
@ -97,6 +97,15 @@ export default class SendMessageComposer extends React.Component {
|
||||||
case 'focus_composer':
|
case 'focus_composer':
|
||||||
this._editorRef.focus();
|
this._editorRef.focus();
|
||||||
break;
|
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);
|
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) {
|
update(newValue, inputType, caret) {
|
||||||
const diff = this._diff(newValue, inputType, caret);
|
const diff = this._diff(newValue, inputType, caret);
|
||||||
const position = this.positionForOffset(diff.at, caret.atNodeEnd);
|
const position = this.positionForOffset(diff.at, caret.atNodeEnd);
|
||||||
|
@ -232,6 +241,23 @@ export default class EditorModel {
|
||||||
}
|
}
|
||||||
return removedOffsetDecrease;
|
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`.
|
* inserts `str` into the model at `pos`.
|
||||||
|
|
Loading…
Reference in New Issue