Merge pull request #3356 from matrix-org/bwindels/cider-paste-newlines
New composer: handle newlines properly when pasting
This commit is contained in:
commit
c1d7ad40c5
3 changed files with 35 additions and 38 deletions
|
@ -84,6 +84,14 @@ function getTextAndFocusNodeOffset(editor, focusNode, focusOffset) {
|
||||||
foundCaret = true;
|
foundCaret = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// usually newlines are entered as new DIV elements,
|
||||||
|
// but for example while pasting in some browsers, they are still
|
||||||
|
// converted to BRs, so also take these into account when they
|
||||||
|
// are not the last element in the DIV.
|
||||||
|
if (node.tagName === "BR" && node.nextSibling) {
|
||||||
|
text += "\n";
|
||||||
|
focusNodeOffset += 1;
|
||||||
|
}
|
||||||
const nodeText = node.nodeType === Node.TEXT_NODE && getTextNodeValue(node);
|
const nodeText = node.nodeType === Node.TEXT_NODE && getTextNodeValue(node);
|
||||||
if (nodeText) {
|
if (nodeText) {
|
||||||
if (!foundCaret) {
|
if (!foundCaret) {
|
||||||
|
|
|
@ -183,15 +183,14 @@ export default class EditorModel {
|
||||||
if (diff.removed) {
|
if (diff.removed) {
|
||||||
removedOffsetDecrease = this.removeText(position, diff.removed.length);
|
removedOffsetDecrease = this.removeText(position, diff.removed.length);
|
||||||
}
|
}
|
||||||
const canOpenAutoComplete = inputType !== "insertFromPaste" && inputType !== "insertFromDrop";
|
|
||||||
let addedLen = 0;
|
let addedLen = 0;
|
||||||
if (diff.added) {
|
if (diff.added) {
|
||||||
// these shouldn't trigger auto-complete, you just want to append a piece of text
|
addedLen = this._addText(position, diff.added, inputType);
|
||||||
addedLen = this._addText(position, diff.added, {validate: canOpenAutoComplete});
|
|
||||||
}
|
}
|
||||||
this._mergeAdjacentParts();
|
this._mergeAdjacentParts();
|
||||||
const caretOffset = diff.at - removedOffsetDecrease + addedLen;
|
const caretOffset = diff.at - removedOffsetDecrease + addedLen;
|
||||||
let newPosition = this.positionForOffset(caretOffset, true);
|
let newPosition = this.positionForOffset(caretOffset, true);
|
||||||
|
const canOpenAutoComplete = inputType !== "insertFromPaste" && inputType !== "insertFromDrop";
|
||||||
const acPromise = this._setActivePart(newPosition, canOpenAutoComplete);
|
const acPromise = this._setActivePart(newPosition, canOpenAutoComplete);
|
||||||
if (this._transformCallback) {
|
if (this._transformCallback) {
|
||||||
const transformAddedLen = this._transform(newPosition, inputType, diff);
|
const transformAddedLen = this._transform(newPosition, inputType, diff);
|
||||||
|
@ -333,22 +332,20 @@ export default class EditorModel {
|
||||||
* inserts `str` into the model at `pos`.
|
* inserts `str` into the model at `pos`.
|
||||||
* @param {Object} pos
|
* @param {Object} pos
|
||||||
* @param {string} str
|
* @param {string} str
|
||||||
* @param {Object} options
|
* @param {string} inputType the source of the input, see html InputEvent.inputType
|
||||||
* @param {bool} options.validate Whether characters will be validated by the part.
|
* @param {bool} options.validate Whether characters will be validated by the part.
|
||||||
* Validating allows the inserted text to be parsed according to the part rules.
|
* Validating allows the inserted text to be parsed according to the part rules.
|
||||||
* @return {Number} how far from position (in characters) the insertion ended.
|
* @return {Number} how far from position (in characters) the insertion ended.
|
||||||
* This can be more than the length of `str` when crossing non-editable parts, which are skipped.
|
* This can be more than the length of `str` when crossing non-editable parts, which are skipped.
|
||||||
*/
|
*/
|
||||||
_addText(pos, str, {validate=true}) {
|
_addText(pos, str, inputType) {
|
||||||
let {index} = pos;
|
let {index} = pos;
|
||||||
const {offset} = pos;
|
const {offset} = pos;
|
||||||
let addLen = str.length;
|
let addLen = str.length;
|
||||||
const part = this._parts[index];
|
const part = this._parts[index];
|
||||||
if (part) {
|
if (part) {
|
||||||
if (part.canEdit) {
|
if (part.canEdit) {
|
||||||
if (validate && part.validateAndInsert(offset, str)) {
|
if (part.validateAndInsert(offset, str, inputType)) {
|
||||||
str = null;
|
|
||||||
} else if (!validate && part.insert(offset, str)) {
|
|
||||||
str = null;
|
str = null;
|
||||||
} else {
|
} else {
|
||||||
const splitPart = part.split(offset);
|
const splitPart = part.split(offset);
|
||||||
|
@ -367,13 +364,8 @@ export default class EditorModel {
|
||||||
index = 0;
|
index = 0;
|
||||||
}
|
}
|
||||||
while (str) {
|
while (str) {
|
||||||
const newPart = this._partCreator.createPartForInput(str, index);
|
const newPart = this._partCreator.createPartForInput(str, index, inputType);
|
||||||
if (validate) {
|
str = newPart.appendUntilRejected(str, inputType);
|
||||||
str = newPart.appendUntilRejected(str);
|
|
||||||
} else {
|
|
||||||
newPart.insert(0, str);
|
|
||||||
str = null;
|
|
||||||
}
|
|
||||||
this._insertPart(index, newPart);
|
this._insertPart(index, newPart);
|
||||||
index += 1;
|
index += 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ class BasePart {
|
||||||
this._text = text;
|
this._text = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
acceptsInsertion(chr) {
|
acceptsInsertion(chr, offset, inputType) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,10 +56,11 @@ class BasePart {
|
||||||
}
|
}
|
||||||
|
|
||||||
// append str, returns the remaining string if a character was rejected.
|
// append str, returns the remaining string if a character was rejected.
|
||||||
appendUntilRejected(str) {
|
appendUntilRejected(str, inputType) {
|
||||||
|
const offset = this.text.length;
|
||||||
for (let i = 0; i < str.length; ++i) {
|
for (let i = 0; i < str.length; ++i) {
|
||||||
const chr = str.charAt(i);
|
const chr = str.charAt(i);
|
||||||
if (!this.acceptsInsertion(chr, i)) {
|
if (!this.acceptsInsertion(chr, offset + i, inputType)) {
|
||||||
this._text = this._text + str.substr(0, i);
|
this._text = this._text + str.substr(0, i);
|
||||||
return str.substr(i);
|
return str.substr(i);
|
||||||
}
|
}
|
||||||
|
@ -69,10 +70,10 @@ class BasePart {
|
||||||
|
|
||||||
// inserts str at offset if all the characters in str were accepted, otherwise don't do anything
|
// inserts str at offset if all the characters in str were accepted, otherwise don't do anything
|
||||||
// return whether the str was accepted or not.
|
// return whether the str was accepted or not.
|
||||||
validateAndInsert(offset, str) {
|
validateAndInsert(offset, str, inputType) {
|
||||||
for (let i = 0; i < str.length; ++i) {
|
for (let i = 0; i < str.length; ++i) {
|
||||||
const chr = str.charAt(i);
|
const chr = str.charAt(i);
|
||||||
if (!this.acceptsInsertion(chr)) {
|
if (!this.acceptsInsertion(chr, offset + i, inputType)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,16 +83,6 @@ class BasePart {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
insert(offset, str) {
|
|
||||||
if (this.canEdit) {
|
|
||||||
const beforeInsert = this._text.substr(0, offset);
|
|
||||||
const afterInsert = this._text.substr(offset);
|
|
||||||
this._text = beforeInsert + str + afterInsert;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
createAutoComplete() {}
|
createAutoComplete() {}
|
||||||
|
|
||||||
trim(len) {
|
trim(len) {
|
||||||
|
@ -119,8 +110,15 @@ class BasePart {
|
||||||
|
|
||||||
// exported for unit tests, should otherwise only be used through PartCreator
|
// exported for unit tests, should otherwise only be used through PartCreator
|
||||||
export class PlainPart extends BasePart {
|
export class PlainPart extends BasePart {
|
||||||
acceptsInsertion(chr) {
|
acceptsInsertion(chr, offset, inputType) {
|
||||||
return chr !== "@" && chr !== "#" && chr !== ":" && chr !== "\n";
|
if (chr === "\n") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// when not pasting or dropping text, reject characters that should start a pill candidate
|
||||||
|
if (inputType !== "insertFromPaste" && inputType !== "insertFromDrop") {
|
||||||
|
return chr !== "@" && chr !== "#" && chr !== ":";
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
toDOMNode() {
|
toDOMNode() {
|
||||||
|
@ -141,7 +139,6 @@ export class PlainPart extends BasePart {
|
||||||
|
|
||||||
updateDOMNode(node) {
|
updateDOMNode(node) {
|
||||||
if (node.textContent !== this.text) {
|
if (node.textContent !== this.text) {
|
||||||
// console.log("changing plain text from", node.textContent, "to", this.text);
|
|
||||||
node.textContent = this.text;
|
node.textContent = this.text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -211,8 +208,8 @@ class PillPart extends BasePart {
|
||||||
}
|
}
|
||||||
|
|
||||||
class NewlinePart extends BasePart {
|
class NewlinePart extends BasePart {
|
||||||
acceptsInsertion(chr, i) {
|
acceptsInsertion(chr, offset) {
|
||||||
return (this.text.length + i) === 0 && chr === "\n";
|
return offset === 0 && chr === "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
acceptsRemoval(position, chr) {
|
acceptsRemoval(position, chr) {
|
||||||
|
@ -331,11 +328,11 @@ class PillCandidatePart extends PlainPart {
|
||||||
return this._autoCompleteCreator.create(updateCallback);
|
return this._autoCompleteCreator.create(updateCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
acceptsInsertion(chr, i) {
|
acceptsInsertion(chr, offset, inputType) {
|
||||||
if ((this.text.length + i) === 0) {
|
if (offset === 0) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return super.acceptsInsertion(chr, i);
|
return super.acceptsInsertion(chr, offset, inputType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue