diff --git a/src/editor/dom.js b/src/editor/dom.js index 1b683c2c5e..4f15a57303 100644 --- a/src/editor/dom.js +++ b/src/editor/dom.js @@ -84,6 +84,14 @@ function getTextAndFocusNodeOffset(editor, focusNode, focusOffset) { 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); if (nodeText) { if (!foundCaret) { diff --git a/src/editor/model.js b/src/editor/model.js index 59371cc3e6..0fbaa4bb3c 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -183,15 +183,14 @@ export default class EditorModel { if (diff.removed) { removedOffsetDecrease = this.removeText(position, diff.removed.length); } - const canOpenAutoComplete = inputType !== "insertFromPaste" && inputType !== "insertFromDrop"; let addedLen = 0; if (diff.added) { - // these shouldn't trigger auto-complete, you just want to append a piece of text - addedLen = this._addText(position, diff.added, {validate: canOpenAutoComplete}); + addedLen = this._addText(position, diff.added, inputType); } this._mergeAdjacentParts(); const caretOffset = diff.at - removedOffsetDecrease + addedLen; let newPosition = this.positionForOffset(caretOffset, true); + const canOpenAutoComplete = inputType !== "insertFromPaste" && inputType !== "insertFromDrop"; const acPromise = this._setActivePart(newPosition, canOpenAutoComplete); if (this._transformCallback) { const transformAddedLen = this._transform(newPosition, inputType, diff); @@ -333,22 +332,20 @@ export default class EditorModel { * inserts `str` into the model at `pos`. * @param {Object} pos * @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. * Validating allows the inserted text to be parsed according to the part rules. * @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. */ - _addText(pos, str, {validate=true}) { + _addText(pos, str, inputType) { let {index} = pos; const {offset} = pos; let addLen = str.length; const part = this._parts[index]; if (part) { if (part.canEdit) { - if (validate && part.validateAndInsert(offset, str)) { - str = null; - } else if (!validate && part.insert(offset, str)) { + if (part.validateAndInsert(offset, str, inputType)) { str = null; } else { const splitPart = part.split(offset); @@ -367,13 +364,8 @@ export default class EditorModel { index = 0; } while (str) { - const newPart = this._partCreator.createPartForInput(str, index); - if (validate) { - str = newPart.appendUntilRejected(str); - } else { - newPart.insert(0, str); - str = null; - } + const newPart = this._partCreator.createPartForInput(str, index, inputType); + str = newPart.appendUntilRejected(str, inputType); this._insertPart(index, newPart); index += 1; } diff --git a/src/editor/parts.js b/src/editor/parts.js index 8d0fe36c28..d14fcf98a2 100644 --- a/src/editor/parts.js +++ b/src/editor/parts.js @@ -23,7 +23,7 @@ class BasePart { this._text = text; } - acceptsInsertion(chr) { + acceptsInsertion(chr, offset, inputType) { return true; } @@ -56,10 +56,11 @@ class BasePart { } // 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) { 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); 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 // return whether the str was accepted or not. - validateAndInsert(offset, str) { + validateAndInsert(offset, str, inputType) { for (let i = 0; i < str.length; ++i) { const chr = str.charAt(i); - if (!this.acceptsInsertion(chr)) { + if (!this.acceptsInsertion(chr, offset + i, inputType)) { return false; } } @@ -82,16 +83,6 @@ class BasePart { 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() {} trim(len) { @@ -119,8 +110,15 @@ class BasePart { // exported for unit tests, should otherwise only be used through PartCreator export class PlainPart extends BasePart { - acceptsInsertion(chr) { - return chr !== "@" && chr !== "#" && chr !== ":" && chr !== "\n"; + acceptsInsertion(chr, offset, inputType) { + 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() { @@ -141,7 +139,6 @@ export class PlainPart extends BasePart { updateDOMNode(node) { if (node.textContent !== this.text) { - // console.log("changing plain text from", node.textContent, "to", this.text); node.textContent = this.text; } } @@ -211,8 +208,8 @@ class PillPart extends BasePart { } class NewlinePart extends BasePart { - acceptsInsertion(chr, i) { - return (this.text.length + i) === 0 && chr === "\n"; + acceptsInsertion(chr, offset) { + return offset === 0 && chr === "\n"; } acceptsRemoval(position, chr) { @@ -331,11 +328,11 @@ class PillCandidatePart extends PlainPart { return this._autoCompleteCreator.create(updateCallback); } - acceptsInsertion(chr, i) { - if ((this.text.length + i) === 0) { + acceptsInsertion(chr, offset, inputType) { + if (offset === 0) { return true; } else { - return super.acceptsInsertion(chr, i); + return super.acceptsInsertion(chr, offset, inputType); } }