Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Initial support for editing messages #2952

Merged
merged 49 commits into from
May 15, 2019
Merged
Changes from 1 commit
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
6599d60
wire up editor component (somewhat hacky)
bwindels May 6, 2019
9f98a6c
add converted prototype code
bwindels May 6, 2019
76bb56a
initial hookup editor code with react component
bwindels May 7, 2019
6be6492
initial parsing of pills for editor
bwindels May 7, 2019
8f0074f
ignore react comment nodes when locating/setting caret
bwindels May 8, 2019
ebdb9fc
don't collapse whitespace in editor
bwindels May 8, 2019
0f38753
some comments
bwindels May 8, 2019
85adc89
remove logging
bwindels May 8, 2019
a2f1f49
update the DOM manually as opposed through react rendering
bwindels May 8, 2019
a765fdf
run autocomplete after mounting
bwindels May 9, 2019
7507d0d
complete proptypes
bwindels May 9, 2019
1330b43
initial support for auto complete in model and parts
bwindels May 9, 2019
317e88b
initial hacky hookup of Autocomplete menu in MessageEditor
bwindels May 9, 2019
bb73521
prefer textContent over innerText as it's faster
bwindels May 9, 2019
4bb8b79
initial auto complete wrapper, make existing autocompleter work w/ model
bwindels May 9, 2019
fc87a27
make editor nicer
bwindels May 9, 2019
5e6367a
basic support for non-editable parts
bwindels May 9, 2019
aa1b4bb
keep auto complete code close to each other
bwindels May 9, 2019
64b1711
rerender through callback instead of after modifying model
bwindels May 9, 2019
ffff66a
handle Escape properly
bwindels May 9, 2019
22587da
close autocomplete on enter
bwindels May 9, 2019
bc14d4f
comment
bwindels May 9, 2019
580a898
fix autocompl. not always appearing/being updated when there is no part
bwindels May 9, 2019
8d97c00
catch this for now as caret behaviour is still a bit flaky
bwindels May 9, 2019
1a577ee
take non-editable parts into account for new caret position
bwindels May 9, 2019
2c3453d
put caret after replaced part if no caretOffset is given by autocomplete
bwindels May 9, 2019
7a85dd4
after completion, set caret in next part at start
bwindels May 10, 2019
9f597c7
no comment nodes without react,so can bring this back to simpler version
bwindels May 10, 2019
7ebb6ce
WIP commit, newlines sort of working
bwindels May 13, 2019
9e0816c
find caret offset and calculate editor text in same tree-walking algo
bwindels May 13, 2019
4ff37ca
don't show model for now
bwindels May 13, 2019
a3b02cf
make logging quiet
bwindels May 13, 2019
c44fed4
even less logging
bwindels May 13, 2019
c98e716
some pill styling
bwindels May 13, 2019
eaf43d7
correctly parse BRs
bwindels May 13, 2019
2fbe73e
draft of formatting
bwindels May 13, 2019
3abdf6b
also serialize to text and method to tell us if we need html for model
bwindels May 14, 2019
34dbe5f
add newline parts for text messages as well
bwindels May 14, 2019
759a4a5
send the actual m.replace event from composer content
bwindels May 14, 2019
036cb02
add feature flag
bwindels May 14, 2019
e2388af
consistent naming between serialize and deserialize modules
bwindels May 14, 2019
15df72e
reload events when event gets replaced in the timeline
bwindels May 14, 2019
45991bc
replace original event if there have been previous edits
bwindels May 14, 2019
0b18ff5
pass feature flag to js-sdk
bwindels May 14, 2019
fd31e79
fix lint
bwindels May 14, 2019
dc21faa
send edit also in n.new_content field
bwindels May 14, 2019
d83e278
PR feedback, cleanup
bwindels May 15, 2019
6b932d5
remove cruft from edit icon
bwindels May 15, 2019
22533ba
use theme var for bg color
bwindels May 15, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
find caret offset and calculate editor text in same tree-walking algo
instead of having the same logic twice
  • Loading branch information
bwindels committed May 14, 2019
commit 9e0816c51c903dede994064c212a2e7e4daf0f63
39 changes: 6 additions & 33 deletions src/components/views/elements/MessageEditor.js
Original file line number Diff line number Diff line change
@@ -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';
bwindels marked this conversation as resolved.
Show resolved Hide resolved
@@ -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) => {
94 changes: 25 additions & 69 deletions src/editor/caret.js
Original file line number Diff line number Diff line change
@@ -14,85 +14,41 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

export function getCaretOffset(editor) {
export function setCaretPosition(editor, model, caretPosition) {
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);
sel.removeAllRanges();
const range = document.createRange();
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;
}
// 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;
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];
}
}
}
}

export function setCaretPosition(editor, caretPosition) {
const sel = document.getSelection();
sel.removeAllRanges();
const range = document.createRange();
let focusNode = editor.childNodes[caretPosition.index];
// 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);
}
83 changes: 83 additions & 0 deletions src/editor/dom.js
Original file line number Diff line number Diff line change
@@ -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};
}