From a2f1f49972dddf268dd24f9d8c3fb8753a4ffbd6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 8 May 2019 14:31:43 +0200 Subject: [PATCH] update the DOM manually as opposed through react rendering react messes up the DOM sometimes because of, I assume, not being aware of the changes to the real DOM by contenteditable. --- .../views/elements/MessageEditor.js | 73 ++++++++++++------- src/editor/render.js | 52 +++++++++++++ 2 files changed, 99 insertions(+), 26 deletions(-) create mode 100644 src/editor/render.js diff --git a/src/components/views/elements/MessageEditor.js b/src/components/views/elements/MessageEditor.js index 104e805c05..be44d4ffa8 100644 --- a/src/components/views/elements/MessageEditor.js +++ b/src/components/views/elements/MessageEditor.js @@ -21,6 +21,7 @@ import dis from '../../../dispatcher'; import EditorModel from '../../../editor/model'; import {getCaretOffset, setCaretPosition} from '../../../editor/caret'; import parseEvent from '../../../editor/parse-event'; +import {renderModel, rerenderModel} from '../../../editor/render'; import {MatrixEvent, MatrixClient} from 'matrix-js-sdk'; export default class MessageEditor extends React.Component { @@ -38,46 +39,66 @@ export default class MessageEditor extends React.Component { constructor(props, context) { super(props, context); this.model = new EditorModel(parseEvent(this.props.event)); - this.state = { - parts: this.model.serializeParts(), - }; - this._onCancelClicked = this._onCancelClicked.bind(this); - this._onInput = this._onInput.bind(this); + this.state = {}; + this._editorRef = null; } - _onInput(event) { - const editor = event.target; - const caretOffset = getCaretOffset(editor); - const caret = this.model.update(editor.textContent, event.inputType, caretOffset); - const parts = this.model.serializeParts(); - this.setState({parts}, () => { - setCaretPosition(editor, caret); - }); + _onInput = (event) => { + const caretOffset = getCaretOffset(this._editorRef); + const caret = this.model.update(this._editorRef.textContent, event.inputType, caretOffset); + // const parts = this.model.serializeParts(); + const shouldRerender = event.inputType === "insertFromDrop" || event.inputType === "insertFromPaste"; + if (shouldRerender) { + rerenderModel(this._editorRef, this.model); + } else { + renderModel(this._editorRef, this.model); + } + setCaretPosition(this._editorRef, caret); + + const modelOutput = this._editorRef.parentElement.querySelector(".model"); + modelOutput.textContent = JSON.stringify(this.model.serializeParts(), undefined, 2); } - _onCancelClicked() { + _onCancelClicked = () => { dis.dispatch({action: "edit_event", event: null}); } + _collectEditorRef = (ref) => { + this._editorRef = ref; + } + + componentDidMount() { + const editor = this._editorRef; + rerenderModel(editor, this.model); + const modelOutput = this._editorRef.parentElement.querySelector(".model"); + modelOutput.textContent = JSON.stringify(this.model.serializeParts(), undefined, 2); + } + render() { - const parts = this.state.parts.map((p, i) => { - const key = `${i}-${p.type}`; - switch (p.type) { - case "plain": return p.text; - case "room-pill": return ({p.text}); - case "user-pill": return ({p.text}); - } - }); - const modelOutput = JSON.stringify(this.state.parts, undefined, 2); + // const parts = this.state.parts.map((p, i) => { + // const key = `${i}-${p.type}`; + // switch (p.type) { + // case "plain": return p.text; + // case "room-pill": return ({p.text}); + // case "user-pill": return ({p.text}); + // } + // }); + // const modelOutput = JSON.stringify(this.state.parts, undefined, 2); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); return
-
- {parts} +
{_t("Cancel")}
- {modelOutput} +
; } } diff --git a/src/editor/render.js b/src/editor/render.js new file mode 100644 index 0000000000..f7eb5d5c2b --- /dev/null +++ b/src/editor/render.js @@ -0,0 +1,52 @@ +/* +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. +*/ + +export function rerenderModel(editor, model) { + while (editor.firstChild) { + editor.removeChild(editor.firstChild); + } + for (const part of model.parts) { + editor.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]; + } + } + 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); + } + } + let surplusElementCount = Math.max(0, editor.childNodes.length - model.parts.length); + while (surplusElementCount) { + editor.removeChild(editor.lastChild); + --surplusElementCount; + } +}