mirror of
				https://github.com/vector-im/element-web.git
				synced 2025-11-04 02:02:14 +01:00 
			
		
		
		
	insert "caret nodes" where pills don't have an adjacent text node
just empty spans, where the caret can be placed.
This commit is contained in:
		
							parent
							
								
									9591e6b0d3
								
							
						
					
					
						commit
						75fc769742
					
				@ -15,6 +15,133 @@ See the License for the specific language governing permissions and
 | 
				
			|||||||
limitations under the License.
 | 
					limitations under the License.
 | 
				
			||||||
*/
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function needsCaretNodeBefore(part, prevPart) {
 | 
				
			||||||
 | 
					    const isFirst = !prevPart || prevPart.type === "newline";
 | 
				
			||||||
 | 
					    return !part.canEdit && (isFirst || !prevPart.canEdit);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function needsCaretNodeAfter(part, isLastOfLine) {
 | 
				
			||||||
 | 
					    return !part.canEdit && isLastOfLine;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function insertAfter(node, nodeToInsert) {
 | 
				
			||||||
 | 
					    const next = node.nextSibling;
 | 
				
			||||||
 | 
					    if (next) {
 | 
				
			||||||
 | 
					        node.parentElement.insertBefore(nodeToInsert, next);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        node.parentElement.appendChild(nodeToInsert);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// a caret node is an empty node that allows the caret to be place
 | 
				
			||||||
 | 
					// where otherwise it wouldn't be possible
 | 
				
			||||||
 | 
					// (e.g. next to a pill span without adjacent text node)
 | 
				
			||||||
 | 
					function createCaretNode() {
 | 
				
			||||||
 | 
					    const span = document.createElement("span");
 | 
				
			||||||
 | 
					    span.className = "caret";
 | 
				
			||||||
 | 
					    return span;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function updateCaretNode(node) {
 | 
				
			||||||
 | 
					    // ensure the caret node is empty
 | 
				
			||||||
 | 
					    // otherwise they'll break everything
 | 
				
			||||||
 | 
					    // as only things part of the model should have text in them
 | 
				
			||||||
 | 
					    // browsers could end up typing in the caret node for any
 | 
				
			||||||
 | 
					    // number of reasons, so revert this.
 | 
				
			||||||
 | 
					    node.textContent = "";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function isCaretNode(node) {
 | 
				
			||||||
 | 
					    return node && node.tagName === "SPAN" && node.className === "caret";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function removeNextSiblings(node) {
 | 
				
			||||||
 | 
					    if (!node) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    node = node.nextSibling;
 | 
				
			||||||
 | 
					    while (node) {
 | 
				
			||||||
 | 
					        const removeNode = node;
 | 
				
			||||||
 | 
					        node = node.nextSibling;
 | 
				
			||||||
 | 
					        removeNode.remove();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function removeChildren(parent) {
 | 
				
			||||||
 | 
					    const firstChild = parent.firstChild;
 | 
				
			||||||
 | 
					    if (firstChild) {
 | 
				
			||||||
 | 
					        removeNextSiblings(firstChild);
 | 
				
			||||||
 | 
					        firstChild.remove();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function reconcileLine(lineContainer, parts) {
 | 
				
			||||||
 | 
					    let currentNode;
 | 
				
			||||||
 | 
					    let prevPart;
 | 
				
			||||||
 | 
					    const lastPart = parts[parts.length - 1];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const part of parts) {
 | 
				
			||||||
 | 
					        const isFirst = !prevPart;
 | 
				
			||||||
 | 
					        currentNode = isFirst ? lineContainer.firstChild : currentNode.nextSibling;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (needsCaretNodeBefore(part, prevPart)) {
 | 
				
			||||||
 | 
					            if (isCaretNode(currentNode)) {
 | 
				
			||||||
 | 
					                currentNode = currentNode.nextSibling;
 | 
				
			||||||
 | 
					                updateCaretNode(currentNode);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                lineContainer.insertBefore(createCaretNode(), currentNode);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // remove nodes until matching current part
 | 
				
			||||||
 | 
					        while (currentNode && !part.canUpdateDOMNode(currentNode)) {
 | 
				
			||||||
 | 
					            const nextNode = currentNode.nextSibling;
 | 
				
			||||||
 | 
					            lineContainer.removeChild(currentNode);
 | 
				
			||||||
 | 
					            currentNode = nextNode;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // update or insert node for current part
 | 
				
			||||||
 | 
					        if (currentNode && part) {
 | 
				
			||||||
 | 
					            part.updateDOMNode(currentNode);
 | 
				
			||||||
 | 
					        } else if (part) {
 | 
				
			||||||
 | 
					            currentNode = part.toDOMNode();
 | 
				
			||||||
 | 
					            // hooks up nextSibling for next iteration
 | 
				
			||||||
 | 
					            lineContainer.appendChild(currentNode);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (needsCaretNodeAfter(part, part === lastPart)) {
 | 
				
			||||||
 | 
					            if (isCaretNode(currentNode.nextSibling)) {
 | 
				
			||||||
 | 
					                currentNode = currentNode.nextSibling;
 | 
				
			||||||
 | 
					                updateCaretNode(currentNode);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                const caretNode = createCaretNode();
 | 
				
			||||||
 | 
					                insertAfter(currentNode, caretNode);
 | 
				
			||||||
 | 
					                currentNode = caretNode;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        prevPart = part;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    removeNextSiblings(currentNode);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function reconcileEmptyLine(lineContainer) {
 | 
				
			||||||
 | 
					    // empty div needs to have a BR in it to give it height
 | 
				
			||||||
 | 
					    let foundBR = false;
 | 
				
			||||||
 | 
					    let partNode = lineContainer.firstChild;
 | 
				
			||||||
 | 
					    while (partNode) {
 | 
				
			||||||
 | 
					        const nextNode = partNode.nextSibling;
 | 
				
			||||||
 | 
					        if (!foundBR && partNode.tagName === "BR") {
 | 
				
			||||||
 | 
					            foundBR = true;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            partNode.remove();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        partNode = nextNode;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!foundBR) {
 | 
				
			||||||
 | 
					        lineContainer.appendChild(document.createElement("br"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function renderModel(editor, model) {
 | 
					export function renderModel(editor, model) {
 | 
				
			||||||
    const lines = model.parts.reduce((lines, part) => {
 | 
					    const lines = model.parts.reduce((lines, part) => {
 | 
				
			||||||
        if (part.type === "newline") {
 | 
					        if (part.type === "newline") {
 | 
				
			||||||
@ -25,8 +152,9 @@ export function renderModel(editor, model) {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        return lines;
 | 
					        return lines;
 | 
				
			||||||
    }, [[]]);
 | 
					    }, [[]]);
 | 
				
			||||||
    // TODO: refactor this code, DRY it
 | 
					 | 
				
			||||||
    lines.forEach((parts, i) => {
 | 
					    lines.forEach((parts, i) => {
 | 
				
			||||||
 | 
					        // find first (and remove anything else) div without className
 | 
				
			||||||
 | 
					        // (as browsers insert these in contenteditable) line container
 | 
				
			||||||
        let lineContainer = editor.childNodes[i];
 | 
					        let lineContainer = editor.childNodes[i];
 | 
				
			||||||
        while (lineContainer && (lineContainer.tagName !== "DIV" || !!lineContainer.className)) {
 | 
					        while (lineContainer && (lineContainer.tagName !== "DIV" || !!lineContainer.className)) {
 | 
				
			||||||
            editor.removeChild(lineContainer);
 | 
					            editor.removeChild(lineContainer);
 | 
				
			||||||
@ -38,46 +166,14 @@ export function renderModel(editor, model) {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (parts.length) {
 | 
					        if (parts.length) {
 | 
				
			||||||
            parts.forEach((part, j) => {
 | 
					            reconcileLine(lineContainer, parts);
 | 
				
			||||||
                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 {
 | 
					        } else {
 | 
				
			||||||
            // empty div needs to have a BR in it to give it height
 | 
					            reconcileEmptyLine(lineContainer);
 | 
				
			||||||
            let foundBR = false;
 | 
					 | 
				
			||||||
            let partNode = lineContainer.firstChild;
 | 
					 | 
				
			||||||
            while (partNode) {
 | 
					 | 
				
			||||||
                const nextNode = partNode.nextSibling;
 | 
					 | 
				
			||||||
                if (!foundBR && partNode.tagName === "BR") {
 | 
					 | 
				
			||||||
                    foundBR = true;
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    lineContainer.removeChild(partNode);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                partNode = nextNode;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            if (!foundBR) {
 | 
					 | 
				
			||||||
                lineContainer.appendChild(document.createElement("br"));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let surplusElementCount = Math.max(0, editor.childNodes.length - lines.length);
 | 
					 | 
				
			||||||
        while (surplusElementCount) {
 | 
					 | 
				
			||||||
            editor.removeChild(editor.lastChild);
 | 
					 | 
				
			||||||
            --surplusElementCount;
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					    if (lines.length) {
 | 
				
			||||||
 | 
					        removeNextSiblings(editor.children[lines.length]);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        removeChildren(editor);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user