WIP commit, newlines sort of working
This commit is contained in:
parent
9f597c7ec0
commit
7ebb6ce621
5 changed files with 183 additions and 63 deletions
|
@ -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) => {
|
||||||
|
|
|
@ -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;
|
||||||
for (let i = 0; i < sel.focusOffset; ++i) {
|
let node;
|
||||||
const node = editor.childNodes[i];
|
let atNodeEnd = true;
|
||||||
if (isVisibleNode(node)) {
|
if (sel.focusNode.nodeType === Node.TEXT_NODE) {
|
||||||
offset += node.textContent.length;
|
node = sel.focusNode;
|
||||||
}
|
offset = sel.focusOffset;
|
||||||
}
|
atNodeEnd = sel.focusOffset === sel.focusNode.textContent.length;
|
||||||
return {offset, atNodeEnd: false};
|
} 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
|
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 1 move up
|
||||||
// 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;
|
node = node.parentElement;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// now include the text length of all preceding direct editor children
|
return {offset, atNodeEnd};
|
||||||
while (node.previousSibling) {
|
|
||||||
node = node.previousSibling;
|
|
||||||
if (isVisibleNode(node)) {
|
// // first make sure we're at the level of a direct child of editor
|
||||||
offset += node.textContent.length;
|
// 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) {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}, [[]]);
|
||||||
|
|
||||||
|
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];
|
||||||
}
|
}
|
||||||
for (let i = 0; i < model.parts.length; ++i) {
|
if (!lineContainer) {
|
||||||
const part = model.parts[i];
|
lineContainer = document.createElement("div");
|
||||||
const node = editor.childNodes[i];
|
editor.appendChild(lineContainer);
|
||||||
if (node && part) {
|
}
|
||||||
part.updateDOMNode(node);
|
|
||||||
|
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) {
|
} else if (part) {
|
||||||
editor.appendChild(part.toDOMNode());
|
lineContainer.appendChild(part.toDOMNode());
|
||||||
} else if (node) {
|
}
|
||||||
editor.removeChild(node);
|
});
|
||||||
|
|
||||||
|
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 - model.parts.length);
|
|
||||||
|
let surplusElementCount = Math.max(0, editor.childNodes.length - lines.length);
|
||||||
while (surplusElementCount) {
|
while (surplusElementCount) {
|
||||||
editor.removeChild(editor.lastChild);
|
editor.removeChild(editor.lastChild);
|
||||||
--surplusElementCount;
|
--surplusElementCount;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue