From 9e0816c51c903dede994064c212a2e7e4daf0f63 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 13 May 2019 16:42:00 +0100 Subject: [PATCH] find caret offset and calculate editor text in same tree-walking algo instead of having the same logic twice --- .../views/elements/MessageEditor.js | 39 ++------ src/editor/caret.js | 96 +++++-------------- src/editor/dom.js | 83 ++++++++++++++++ 3 files changed, 115 insertions(+), 103 deletions(-) create mode 100644 src/editor/dom.js diff --git a/src/components/views/elements/MessageEditor.js b/src/components/views/elements/MessageEditor.js index b047e210e0..803b06455f 100644 --- a/src/components/views/elements/MessageEditor.js +++ b/src/components/views/elements/MessageEditor.js @@ -19,7 +19,8 @@ import {_t} from '../../../languageHandler'; import PropTypes from 'prop-types'; import dis from '../../../dispatcher'; import EditorModel from '../../../editor/model'; -import {getCaretOffset, setCaretPosition} from '../../../editor/caret'; +import {setCaretPosition} from '../../../editor/caret'; +import {getCaretOffsetAndText} from '../../../editor/dom'; import parseEvent from '../../../editor/parse-event'; import Autocomplete from '../rooms/Autocomplete'; // import AutocompleteModel from '../../../editor/autocomplete'; @@ -60,15 +61,10 @@ export default class MessageEditor extends React.Component { } _updateEditorState = (caret) => { - const shouldRerender = false; //event.inputType === "insertFromDrop" || event.inputType === "insertFromPaste"; - if (shouldRerender) { - rerenderModel(this._editorRef, this.model); - } else { - renderModel(this._editorRef, this.model); - } + renderModel(this._editorRef, this.model); if (caret) { try { - setCaretPosition(this._editorRef, caret); + setCaretPosition(this._editorRef, this.model, caret); } catch (err) { console.error(err); } @@ -80,31 +76,8 @@ export default class MessageEditor extends React.Component { _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(newValue, event.inputType, caretOffset); + const {caret, text} = getCaretOffsetAndText(this._editorRef, document.getSelection()); + this.model.update(text, event.inputType, caret); } _onKeyDown = (event) => { diff --git a/src/editor/caret.js b/src/editor/caret.js index e9081ee05d..3a784aa8eb 100644 --- a/src/editor/caret.js +++ b/src/editor/caret.js @@ -14,85 +14,41 @@ See the License for the specific language governing permissions and limitations under the License. */ -export function getCaretOffset(editor) { - const sel = document.getSelection(); - 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 - 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); - } - - while (node !== editor) { - while (node.previousSibling) { - node = node.previousSibling; - 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}); - // } -} - -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) { +export function setCaretPosition(editor, model, caretPosition) { const sel = document.getSelection(); sel.removeAllRanges(); const range = document.createRange(); - let focusNode = editor.childNodes[caretPosition.index]; + const {parts} = model; + let lineIndex = 0; + let nodeIndex = -1; + for (let i = 0; i <= caretPosition.index; ++i) { + const part = parts[i]; + if (part && part.type === "newline") { + lineIndex += 1; + nodeIndex = -1; + } else { + nodeIndex += 1; + } + } + let focusNode; + const lineNode = editor.childNodes[lineIndex]; + if (lineNode) { + if (lineNode.childNodes.length === 0 && caretPosition.offset === 0) { + focusNode = lineNode; + } else { + focusNode = lineNode.childNodes[nodeIndex]; + + if (focusNode && focusNode.nodeType === Node.ELEMENT_NODE) { + focusNode = focusNode.childNodes[0]; + } + } + } // node not found, set caret at end if (!focusNode) { range.selectNodeContents(editor); range.collapse(false); } else { // make sure we have a text node - if (focusNode.nodeType === Node.ELEMENT_NODE) { - focusNode = focusNode.childNodes[0]; - } range.setStart(focusNode, caretPosition.offset); range.collapse(true); } diff --git a/src/editor/dom.js b/src/editor/dom.js new file mode 100644 index 0000000000..fd46c0820a --- /dev/null +++ b/src/editor/dom.js @@ -0,0 +1,83 @@ +/* +Copyright 2019 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +function walkDOMDepthFirst(editor, enterNodeCallback, leaveNodeCallback) { + let node = editor.firstChild; + while (node && node !== editor) { + enterNodeCallback(node); + if (node.firstChild) { + node = node.firstChild; + } else if (node.nextSibling) { + node = node.nextSibling; + } else { + while (!node.nextSibling && node !== editor) { + node = node.parentElement; + if (node !== editor) { + leaveNodeCallback(node); + } + } + if (node !== editor) { + node = node.nextSibling; + } + } + } +} + +export function getCaretOffsetAndText(editor, sel) { + let {focusOffset, focusNode} = sel; + let caretOffset = focusOffset; + let foundCaret = false; + let text = ""; + + if (focusNode.nodeType === Node.ELEMENT_NODE && focusOffset !== 0) { + focusNode = focusNode.childNodes[focusOffset - 1]; + caretOffset = focusNode.textContent.length; + } + + function enterNodeCallback(node) { + const nodeText = node.nodeType === Node.TEXT_NODE && node.nodeValue; + if (!foundCaret) { + if (node === focusNode) { + foundCaret = true; + } + } + if (nodeText) { + if (!foundCaret) { + caretOffset += nodeText.length; + } + text += nodeText; + } + } + + function leaveNodeCallback(node) { + // if this is not the last DIV (which are only used as line containers atm) + // we don't just check if there is a nextSibling because sometimes the caret ends up + // after the last DIV and it creates a newline if you type then, + // whereas you just want it to be appended to the current line + if (node.tagName === "DIV" && node.nextSibling && node.nextSibling.tagName === "DIV") { + text += "\n"; + if (!foundCaret) { + caretOffset += 1; + } + } + } + + walkDOMDepthFirst(editor, enterNodeCallback, leaveNodeCallback); + + const atNodeEnd = sel.focusOffset === sel.focusNode.textContent.length; + const caret = {atNodeEnd, offset: caretOffset}; + return {caret, text}; +}