WIP commit, newlines sort of working

This commit is contained in:
Bruno Windels 2019-05-13 15:21:57 +01:00
parent 9f597c7ec0
commit 7ebb6ce621
5 changed files with 183 additions and 63 deletions

View file

@ -56,6 +56,7 @@ export default class MessageEditor extends React.Component {
}; };
this._editorRef = null; this._editorRef = null;
this._autocompleteRef = null; this._autocompleteRef = null;
// document.execCommand("insertBrOnReturn", undefined, true);
} }
_updateEditorState = (caret) => { _updateEditorState = (caret) => {
@ -72,15 +73,38 @@ export default class MessageEditor extends React.Component {
console.error(err); console.error(err);
} }
} }
console.log("_updateEditorState", this.state.autoComplete, this.model.autoComplete);
this.setState({autoComplete: this.model.autoComplete}); this.setState({autoComplete: this.model.autoComplete});
const modelOutput = this._editorRef.parentElement.querySelector(".model"); const modelOutput = this._editorRef.parentElement.querySelector(".model");
modelOutput.textContent = JSON.stringify(this.model.serializeParts(), undefined, 2); modelOutput.textContent = JSON.stringify(this.model.serializeParts(), undefined, 2);
} }
_onInput = (event) => { _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); const caretOffset = getCaretOffset(this._editorRef);
this.model.update(this._editorRef.textContent, event.inputType, caretOffset); this.model.update(newValue, event.inputType, caretOffset);
} }
_onKeyDown = (event) => { _onKeyDown = (event) => {

View file

@ -16,56 +16,67 @@ limitations under the License.
export function getCaretOffset(editor) { export function getCaretOffset(editor) {
const sel = document.getSelection(); const sel = document.getSelection();
const atNodeEnd = sel.focusOffset === sel.focusNode.textContent.length; console.info("getCaretOffset", sel.focusNode, sel.focusOffset);
let offset = sel.focusOffset;
let node = sel.focusNode;
// when deleting the last character of a node, // when deleting the last character of a node,
// the caret gets reported as being after the focusOffset-th node, // the caret gets reported as being after the focusOffset-th node,
// with the focusNode being the editor // with the focusNode being the editor
if (node === editor) { let offset = 0;
let offset = 0; let node;
for (let i = 0; i < sel.focusOffset; ++i) { let atNodeEnd = true;
const node = editor.childNodes[i]; if (sel.focusNode.nodeType === Node.TEXT_NODE) {
if (isVisibleNode(node)) { node = sel.focusNode;
offset += node.textContent.length; offset = sel.focusOffset;
} atNodeEnd = sel.focusOffset === sel.focusNode.textContent.length;
} } else if (sel.focusNode.nodeType === Node.ELEMENT_NODE) {
return {offset, atNodeEnd: false}; node = sel.focusNode.childNodes[sel.focusOffset];
offset = nodeLength(node);
} }
// first make sure we're at the level of a direct child of editor while (node !== editor) {
if (node.parentElement !== editor) {
// include all preceding siblings of the non-direct editor children
while (node.previousSibling) { while (node.previousSibling) {
node = node.previousSibling; node = node.previousSibling;
if (isVisibleNode(node)) { offset += nodeLength(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;
} }
// 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; // const {focusOffset, focusNode} = sel;
// console.log("selection", {focusOffset, focusNode, position, atNodeEnd}); // console.log("selection", {focusOffset, focusNode, position, atNodeEnd});
// } // }
return {offset, atNodeEnd};
} }
function isVisibleNode(node) { function nodeLength(node) {
return node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.TEXT_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, caretPosition) {

View file

@ -80,7 +80,8 @@ export default class EditorModel {
update(newValue, inputType, caret) { update(newValue, inputType, caret) {
const diff = this._diff(newValue, inputType, caret); const diff = this._diff(newValue, inputType, caret);
const position = this._positionForOffset(diff.at, caret.atNodeEnd); 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; let removedOffsetDecrease = 0;
if (diff.removed) { if (diff.removed) {
removedOffsetDecrease = this._removeText(position, diff.removed.length); removedOffsetDecrease = this._removeText(position, diff.removed.length);

View file

@ -95,11 +95,15 @@ class BasePart {
get canEdit() { get canEdit() {
return true; return true;
} }
toString() {
return `${this.type}(${this.text})`;
}
} }
export class PlainPart extends BasePart { export class PlainPart extends BasePart {
acceptsInsertion(chr) { acceptsInsertion(chr) {
return chr !== "@" && chr !== "#" && chr !== ":"; return chr !== "@" && chr !== "#" && chr !== ":" && chr !== "\n";
} }
toDOMNode() { 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 { export class RoomPillPart extends PillPart {
constructor(displayAlias) { constructor(displayAlias) {
super(displayAlias, displayAlias); super(displayAlias, displayAlias);
@ -228,6 +260,8 @@ export class PartCreator {
case "@": case "@":
case ":": case ":":
return new PillCandidatePart("", this._autoCompleteCreator); return new PillCandidatePart("", this._autoCompleteCreator);
case "\n":
return new NewlinePart();
default: default:
return new PlainPart(); return new PlainPart();
} }

View file

@ -18,35 +18,85 @@ export function rerenderModel(editor, model) {
while (editor.firstChild) { while (editor.firstChild) {
editor.removeChild(editor.firstChild); editor.removeChild(editor.firstChild);
} }
let lineContainer = document.createElement("div");
editor.appendChild(lineContainer);
for (const part of model.parts) { 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) { export function renderModel(editor, model) {
// remove unwanted nodes, like <br>s const lines = model.parts.reduce((lines, part) => {
for (let i = 0; i < model.parts.length; ++i) { if (part.type === "newline") {
const part = model.parts[i]; lines.push([]);
let node = editor.childNodes[i]; } else {
while (node && !part.canUpdateDOMNode(node)) { const lastLine = lines[lines.length - 1];
editor.removeChild(node); lastLine.push(part);
node = editor.childNodes[i];
} }
} return lines;
for (let i = 0; i < model.parts.length; ++i) { }, [[]]);
const part = model.parts[i];
const node = editor.childNodes[i]; console.log(lines.map(parts => parts.map(p => p.toString())));
if (node && part) {
part.updateDOMNode(node); lines.forEach((parts, i) => {
} else if (part) { let lineContainer = editor.childNodes[i];
editor.appendChild(part.toDOMNode()); while (lineContainer && (lineContainer.tagName !== "DIV" || !!lineContainer.className)) {
} else if (node) { editor.removeChild(lineContainer);
editor.removeChild(node); lineContainer = editor.childNodes[i];
} }
} if (!lineContainer) {
let surplusElementCount = Math.max(0, editor.childNodes.length - model.parts.length); lineContainer = document.createElement("div");
while (surplusElementCount) { editor.appendChild(lineContainer);
editor.removeChild(editor.lastChild); }
--surplusElementCount;
} 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;
}
});
} }