WIP commit, newlines sort of working
							parent
							
								
									9f597c7ec0
								
							
						
					
					
						commit
						7ebb6ce621
					
				|  | @ -56,6 +56,7 @@ export default class MessageEditor extends React.Component { | |||
|         }; | ||||
|         this._editorRef = null; | ||||
|         this._autocompleteRef = null; | ||||
|         // document.execCommand("insertBrOnReturn", undefined, true);
 | ||||
|     } | ||||
| 
 | ||||
|     _updateEditorState = (caret) => { | ||||
|  | @ -72,15 +73,38 @@ export default class MessageEditor extends React.Component { | |||
|                 console.error(err); | ||||
|             } | ||||
|         } | ||||
|         console.log("_updateEditorState", this.state.autoComplete, this.model.autoComplete); | ||||
|         this.setState({autoComplete: this.model.autoComplete}); | ||||
|         const modelOutput = this._editorRef.parentElement.querySelector(".model"); | ||||
|         modelOutput.textContent = JSON.stringify(this.model.serializeParts(), undefined, 2); | ||||
|     } | ||||
| 
 | ||||
|     _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(this._editorRef.textContent, event.inputType, caretOffset); | ||||
|         this.model.update(newValue, event.inputType, caretOffset); | ||||
|     } | ||||
| 
 | ||||
|     _onKeyDown = (event) => { | ||||
|  |  | |||
|  | @ -16,56 +16,67 @@ limitations under the License. | |||
| 
 | ||||
| export function getCaretOffset(editor) { | ||||
|     const sel = document.getSelection(); | ||||
|     const atNodeEnd = sel.focusOffset === sel.focusNode.textContent.length; | ||||
|     let offset = sel.focusOffset; | ||||
|     let node = sel.focusNode; | ||||
| 
 | ||||
|     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
 | ||||
|     if (node === editor) { | ||||
|         let offset = 0; | ||||
|         for (let i = 0; i < sel.focusOffset; ++i) { | ||||
|             const node = editor.childNodes[i]; | ||||
|             if (isVisibleNode(node)) { | ||||
|                 offset += node.textContent.length; | ||||
|             } | ||||
|         } | ||||
|         return {offset, atNodeEnd: false}; | ||||
|     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); | ||||
|     } | ||||
| 
 | ||||
|     // 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 !== editor) { | ||||
|         while (node.previousSibling) { | ||||
|             node = node.previousSibling; | ||||
|             if (isVisibleNode(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; | ||||
|             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});
 | ||||
|     // }
 | ||||
|     return {offset, atNodeEnd}; | ||||
| } | ||||
| 
 | ||||
| function isVisibleNode(node) { | ||||
|     return node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.TEXT_NODE; | ||||
| 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) { | ||||
|  |  | |||
|  | @ -80,7 +80,8 @@ export default class EditorModel { | |||
|     update(newValue, inputType, caret) { | ||||
|         const diff = this._diff(newValue, inputType, caret); | ||||
|         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; | ||||
|         if (diff.removed) { | ||||
|             removedOffsetDecrease = this._removeText(position, diff.removed.length); | ||||
|  |  | |||
|  | @ -95,11 +95,15 @@ class BasePart { | |||
|     get canEdit() { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     toString() { | ||||
|         return `${this.type}(${this.text})`; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class PlainPart extends BasePart { | ||||
|     acceptsInsertion(chr) { | ||||
|         return chr !== "@" && chr !== "#" && chr !== ":"; | ||||
|         return chr !== "@" && chr !== "#" && chr !== ":" && chr !== "\n"; | ||||
|     } | ||||
| 
 | ||||
|     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 { | ||||
|     constructor(displayAlias) { | ||||
|         super(displayAlias, displayAlias); | ||||
|  | @ -228,6 +260,8 @@ export class PartCreator { | |||
|             case "@": | ||||
|             case ":": | ||||
|                 return new PillCandidatePart("", this._autoCompleteCreator); | ||||
|             case "\n": | ||||
|                 return new NewlinePart(); | ||||
|             default: | ||||
|                 return new PlainPart(); | ||||
|         } | ||||
|  |  | |||
|  | @ -18,35 +18,85 @@ export function rerenderModel(editor, model) { | |||
|     while (editor.firstChild) { | ||||
|         editor.removeChild(editor.firstChild); | ||||
|     } | ||||
|     let lineContainer = document.createElement("div"); | ||||
|     editor.appendChild(lineContainer); | ||||
|     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) { | ||||
|     // remove unwanted nodes, like <br>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]; | ||||
|     const lines = model.parts.reduce((lines, part) => { | ||||
|         if (part.type === "newline") { | ||||
|             lines.push([]); | ||||
|         } else { | ||||
|             const lastLine = lines[lines.length - 1]; | ||||
|             lastLine.push(part); | ||||
|         } | ||||
|     } | ||||
|     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); | ||||
|         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]; | ||||
|         } | ||||
|     } | ||||
|     let surplusElementCount = Math.max(0, editor.childNodes.length - model.parts.length); | ||||
|     while (surplusElementCount) { | ||||
|         editor.removeChild(editor.lastChild); | ||||
|         --surplusElementCount; | ||||
|     } | ||||
|         if (!lineContainer) { | ||||
|             lineContainer = document.createElement("div"); | ||||
|             editor.appendChild(lineContainer); | ||||
|         } | ||||
| 
 | ||||
|         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; | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Bruno Windels
						Bruno Windels