diff --git a/src/components/views/elements/MessageEditor.js b/src/components/views/elements/MessageEditor.js index 32171391f9..b047e210e0 100644 --- a/src/components/views/elements/MessageEditor.js +++ b/src/components/views/elements/MessageEditor.js @@ -56,6 +56,7 @@ export default class MessageEditor extends React.Component { }; this._editorRef = null; this._autocompleteRef = null; + // document.execCommand("insertBrOnReturn", undefined, true); } _updateEditorState = (caret) => { @@ -72,15 +73,38 @@ export default class MessageEditor extends React.Component { console.error(err); } } - console.log("_updateEditorState", this.state.autoComplete, this.model.autoComplete); this.setState({autoComplete: this.model.autoComplete}); const modelOutput = this._editorRef.parentElement.querySelector(".model"); modelOutput.textContent = JSON.stringify(this.model.serializeParts(), undefined, 2); } _onInput = (event) => { + console.log("finding newValue", this._editorRef.innerHTML); + let newValue = ""; + let node = this._editorRef.firstChild; + while (node && node !== this._editorRef) { + if (node.nodeType === Node.TEXT_NODE) { + newValue += node.nodeValue; + } + + if (node.firstChild) { + node = node.firstChild; + } else if (node.nextSibling) { + node = node.nextSibling; + } else { + while (!node.nextSibling && node !== this._editorRef) { + node = node.parentElement; + if (node.tagName === "DIV" && node.nextSibling && node.nextSibling.tagName === "DIV" && node !== this._editorRef) { + newValue += "\n"; + } + } + if (node !== this._editorRef) { + node = node.nextSibling; + } + } + } const caretOffset = getCaretOffset(this._editorRef); - this.model.update(this._editorRef.textContent, event.inputType, caretOffset); + this.model.update(newValue, event.inputType, caretOffset); } _onKeyDown = (event) => { diff --git a/src/editor/caret.js b/src/editor/caret.js index f0359bed2d..e9081ee05d 100644 --- a/src/editor/caret.js +++ b/src/editor/caret.js @@ -16,56 +16,67 @@ limitations under the License. export function getCaretOffset(editor) { const sel = document.getSelection(); - const atNodeEnd = sel.focusOffset === sel.focusNode.textContent.length; - let offset = sel.focusOffset; - let node = sel.focusNode; - + console.info("getCaretOffset", sel.focusNode, sel.focusOffset); // when deleting the last character of a node, // the caret gets reported as being after the focusOffset-th node, // with the focusNode being the editor - if (node === editor) { - let offset = 0; - for (let i = 0; i < sel.focusOffset; ++i) { - const node = editor.childNodes[i]; - if (isVisibleNode(node)) { - offset += node.textContent.length; - } - } - return {offset, atNodeEnd: false}; + let offset = 0; + let node; + let atNodeEnd = true; + if (sel.focusNode.nodeType === Node.TEXT_NODE) { + node = sel.focusNode; + offset = sel.focusOffset; + atNodeEnd = sel.focusOffset === sel.focusNode.textContent.length; + } else if (sel.focusNode.nodeType === Node.ELEMENT_NODE) { + node = sel.focusNode.childNodes[sel.focusOffset]; + offset = nodeLength(node); } - // first make sure we're at the level of a direct child of editor - if (node.parentElement !== editor) { - // include all preceding siblings of the non-direct editor children + while (node !== editor) { while (node.previousSibling) { node = node.previousSibling; - if (isVisibleNode(node)) { - offset += node.textContent.length; - } - } - // then move up - // I guess technically there could be preceding text nodes in the parents here as well, - // but we're assuming there are no mixed text and element nodes - while (node.parentElement !== editor) { - node = node.parentElement; - } - } - // now include the text length of all preceding direct editor children - while (node.previousSibling) { - node = node.previousSibling; - if (isVisibleNode(node)) { - offset += node.textContent.length; + offset += nodeLength(node); } + // then 1 move up + node = node.parentElement; } + + return {offset, atNodeEnd}; + + + // // first make sure we're at the level of a direct child of editor + // if (node.parentElement !== editor) { + // // include all preceding siblings of the non-direct editor children + // while (node.previousSibling) { + // node = node.previousSibling; + // offset += nodeLength(node); + // } + // // then move up + // // I guess technically there could be preceding text nodes in the parents here as well, + // // but we're assuming there are no mixed text and element nodes + // while (node.parentElement !== editor) { + // node = node.parentElement; + // } + // } + // // now include the text length of all preceding direct editor children + // while (node.previousSibling) { + // node = node.previousSibling; + // offset += nodeLength(node); + // } // { // const {focusOffset, focusNode} = sel; // console.log("selection", {focusOffset, focusNode, position, atNodeEnd}); // } - return {offset, atNodeEnd}; } -function isVisibleNode(node) { - return node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.TEXT_NODE; +function nodeLength(node) { + if (node.nodeType === Node.ELEMENT_NODE) { + const isBlock = node.tagName === "DIV"; + const isLastDiv = !node.nextSibling || node.nextSibling.tagName !== "DIV"; + return node.textContent.length + ((isBlock && !isLastDiv) ? 1 : 0); + } else { + return node.textContent.length; + } } export function setCaretPosition(editor, caretPosition) { diff --git a/src/editor/model.js b/src/editor/model.js index fb3658a0be..e7184ad3d3 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -80,7 +80,8 @@ export default class EditorModel { update(newValue, inputType, caret) { const diff = this._diff(newValue, inputType, caret); const position = this._positionForOffset(diff.at, caret.atNodeEnd); - console.log("update at", {position, diff, newValue, prevValue: this.parts.reduce((text, p) => text + p.text, "")}); + const valueWithCaret = newValue.slice(0, caret.offset) + "|" + newValue.slice(caret.offset); + console.log("update at", {diff, valueWithCaret}); let removedOffsetDecrease = 0; if (diff.removed) { removedOffsetDecrease = this._removeText(position, diff.removed.length); diff --git a/src/editor/parts.js b/src/editor/parts.js index 619bdfba9b..a20b857fee 100644 --- a/src/editor/parts.js +++ b/src/editor/parts.js @@ -95,11 +95,15 @@ class BasePart { get canEdit() { return true; } + + toString() { + return `${this.type}(${this.text})`; + } } export class PlainPart extends BasePart { acceptsInsertion(chr) { - return chr !== "@" && chr !== "#" && chr !== ":"; + return chr !== "@" && chr !== "#" && chr !== ":" && chr !== "\n"; } toDOMNode() { @@ -175,6 +179,34 @@ class PillPart extends BasePart { } } +export class NewlinePart extends BasePart { + acceptsInsertion(chr) { + return this.text.length === 0 && chr === "\n"; + } + + acceptsRemoval(position, chr) { + return true; + } + + toDOMNode() { + return document.createElement("br"); + } + + merge() { + return false; + } + + updateDOMNode() {} + + canUpdateDOMNode(node) { + return node.tagName === "BR"; + } + + get type() { + return "newline"; + } +} + export class RoomPillPart extends PillPart { constructor(displayAlias) { super(displayAlias, displayAlias); @@ -228,6 +260,8 @@ export class PartCreator { case "@": case ":": return new PillCandidatePart("", this._autoCompleteCreator); + case "\n": + return new NewlinePart(); default: return new PlainPart(); } diff --git a/src/editor/render.js b/src/editor/render.js index f7eb5d5c2b..ae39f62c41 100644 --- a/src/editor/render.js +++ b/src/editor/render.js @@ -18,35 +18,85 @@ export function rerenderModel(editor, model) { while (editor.firstChild) { editor.removeChild(editor.firstChild); } + let lineContainer = document.createElement("div"); + editor.appendChild(lineContainer); for (const part of model.parts) { - editor.appendChild(part.toDOMNode()); + if (part.type === "newline") { + lineContainer = document.createElement("div"); + editor.appendChild(lineContainer); + } else { + lineContainer.appendChild(part.toDOMNode()); + } } } export function renderModel(editor, model) { - // remove unwanted nodes, like
s - for (let i = 0; i < model.parts.length; ++i) { - const part = model.parts[i]; - let node = editor.childNodes[i]; - while (node && !part.canUpdateDOMNode(node)) { - editor.removeChild(node); - node = editor.childNodes[i]; + const lines = model.parts.reduce((lines, part) => { + if (part.type === "newline") { + lines.push([]); + } else { + const lastLine = lines[lines.length - 1]; + lastLine.push(part); } - } - for (let i = 0; i < model.parts.length; ++i) { - const part = model.parts[i]; - const node = editor.childNodes[i]; - if (node && part) { - part.updateDOMNode(node); - } else if (part) { - editor.appendChild(part.toDOMNode()); - } else if (node) { - editor.removeChild(node); + return lines; + }, [[]]); + + console.log(lines.map(parts => parts.map(p => p.toString()))); + + lines.forEach((parts, i) => { + let lineContainer = editor.childNodes[i]; + while (lineContainer && (lineContainer.tagName !== "DIV" || !!lineContainer.className)) { + editor.removeChild(lineContainer); + lineContainer = editor.childNodes[i]; } - } - let surplusElementCount = Math.max(0, editor.childNodes.length - model.parts.length); - while (surplusElementCount) { - editor.removeChild(editor.lastChild); - --surplusElementCount; - } + if (!lineContainer) { + lineContainer = document.createElement("div"); + editor.appendChild(lineContainer); + } + + if (parts.length) { + parts.forEach((part, j) => { + let partNode = lineContainer.childNodes[j]; + while (partNode && !part.canUpdateDOMNode(partNode)) { + lineContainer.removeChild(partNode); + partNode = lineContainer.childNodes[j]; + } + if (partNode && part) { + part.updateDOMNode(partNode); + } else if (part) { + lineContainer.appendChild(part.toDOMNode()); + } + }); + + let surplusElementCount = Math.max(0, lineContainer.childNodes.length - parts.length); + while (surplusElementCount) { + lineContainer.removeChild(lineContainer.lastChild); + --surplusElementCount; + } + } else { + // empty div needs to have a BR in it + let foundBR = false; + let partNode = lineContainer.firstChild; + console.log("partNode", partNode, editor.innerHTML); + while (partNode) { + console.log("partNode(in loop)", partNode); + if (!foundBR && partNode.tagName === "BR") { + foundBR = true; + } else { + lineContainer.removeChild(partNode); + } + partNode = partNode.nextSibling; + } + if (!foundBR) { + console.log("adding a BR in an empty div because there was none already"); + lineContainer.appendChild(document.createElement("br")); + } + } + + let surplusElementCount = Math.max(0, editor.childNodes.length - lines.length); + while (surplusElementCount) { + editor.removeChild(editor.lastChild); + --surplusElementCount; + } + }); }