import { closestBlock, isBlock } from "./blocks"; import { isContentEditable, isNotEditableNode, isSelfClosingElement, nextLeaf, previousLeaf, } from "./dom_info"; import { isFakeLineBreak } from "./dom_state"; import { closestElement, createDOMPathGenerator } from "./dom_traversal"; import { DIRECTIONS, childNodeIndex, endPos, leftPos, nodeSize, rightPos, startPos, } from "./position"; /** * @typedef { import("./selection_plugin").EditorSelection } EditorSelection */ /** * From selection position, checks if it is left-to-right or right-to-left. * * @param {Node} anchorNode * @param {number} anchorOffset * @param {Node} focusNode * @param {number} focusOffset * @returns {boolean} the direction of the current range if the selection not is collapsed | false */ export function getCursorDirection(anchorNode, anchorOffset, focusNode, focusOffset) { if (anchorNode === focusNode) { if (anchorOffset === focusOffset) { return false; } return anchorOffset < focusOffset ? DIRECTIONS.RIGHT : DIRECTIONS.LEFT; } return anchorNode.compareDocumentPosition(focusNode) & Node.DOCUMENT_POSITION_FOLLOWING ? DIRECTIONS.RIGHT : DIRECTIONS.LEFT; } /** * @param {EditorSelection} selection * @param {string} selector */ export function findInSelection(selection, selector) { const selectorInStartAncestors = closestElement(selection.startContainer, selector); if (selectorInStartAncestors) { return selectorInStartAncestors; } else { const commonElementAncestor = closestElement(selection.commonAncestorContainer); return ( commonElementAncestor && [...commonElementAncestor.querySelectorAll(selector)].find((node) => selection.intersectsNode(node) ) ); } } const leftLeafOnlyInScopeNotBlockEditablePath = createDOMPathGenerator(DIRECTIONS.LEFT, { leafOnly: true, inScope: true, stopTraverseFunction: (node) => isNotEditableNode(node) || isBlock(node), stopFunction: (node) => isNotEditableNode(node) || isBlock(node), }); const rightLeafOnlyInScopeNotBlockEditablePath = createDOMPathGenerator(DIRECTIONS.RIGHT, { leafOnly: true, inScope: true, stopTraverseFunction: (node) => isNotEditableNode(node) || isBlock(node), stopFunction: (node) => isNotEditableNode(node) || isBlock(node), }); export function normalizeSelfClosingElement(node, offset) { if (isSelfClosingElement(node)) { // Cannot put cursor inside those elements, put it after instead. [node, offset] = rightPos(node); } return [node, offset]; } export function normalizeNotEditableNode(node, offset, position = "right") { const editable = closestElement(node, ".odoo-editor-editable"); let closest = closestElement(node); while (closest && closest !== editable && !closest.isContentEditable) { [node, offset] = position === "right" ? rightPos(node) : leftPos(node); closest = node; } return [node, offset]; } export function normalizeCursorPosition(node, offset, position = "right") { [node, offset] = normalizeSelfClosingElement(node, offset); [node, offset] = normalizeNotEditableNode(node, offset, position); // todo @phoenix: we should maybe remove it // // Be permissive about the received offset. // offset = Math.min(Math.max(offset, 0), nodeSize(node)); return [node, offset]; } export function normalizeFakeBR(node, offset) { const prevNode = node.nodeType === Node.ELEMENT_NODE && node.childNodes[offset - 1]; if (prevNode && prevNode.nodeName === "BR" && isFakeLineBreak(prevNode)) { // If trying to put the cursor on the right of a fake line break, put // it before instead. offset--; } return [node, offset]; } /** * From a given position, returns the normalized version. * * E.g. abc[]def -> abc[]def * * @param {Node} node * @param {number} offset * @returns { [Node, number] } */ export function normalizeDeepCursorPosition(node, offset) { // Put the cursor in deepest inline node around the given position if // possible. let el; let elOffset; if (node.nodeType === Node.ELEMENT_NODE) { el = node; elOffset = offset; } else if (node.nodeType === Node.TEXT_NODE) { if (offset === 0) { el = node.parentNode; elOffset = childNodeIndex(node); } else if (offset === node.length) { el = node.parentNode; elOffset = childNodeIndex(node) + 1; } } if (el) { const leftInlineNode = leftLeafOnlyInScopeNotBlockEditablePath(el, elOffset).next().value; let leftVisibleEmpty = false; if (leftInlineNode) { leftVisibleEmpty = isSelfClosingElement(leftInlineNode) || !isContentEditable(leftInlineNode); [node, offset] = leftVisibleEmpty ? rightPos(leftInlineNode) : endPos(leftInlineNode); } if (!leftInlineNode || leftVisibleEmpty) { const rightInlineNode = rightLeafOnlyInScopeNotBlockEditablePath(el, elOffset).next() .value; if (rightInlineNode) { const closest = closestElement(rightInlineNode); const rightVisibleEmpty = isSelfClosingElement(rightInlineNode) || !closest || !closest.isContentEditable; if (!(leftVisibleEmpty && rightVisibleEmpty)) { [node, offset] = rightVisibleEmpty ? leftPos(rightInlineNode) : startPos(rightInlineNode); } } } } return [node, offset]; } function updateCursorBeforeMove(destParent, destIndex, node, cursor) { if (cursor.node === destParent && cursor.offset >= destIndex) { // Update cursor at destination cursor.offset += 1; } else if (cursor.node === node.parentNode) { const childIndex = childNodeIndex(node); // Update cursor at origin if (cursor.offset === childIndex) { // Keep pointing to the moved node [cursor.node, cursor.offset] = [destParent, destIndex]; } else if (cursor.offset > childIndex) { cursor.offset -= 1; } } } function updateCursorBeforeRemove(node, cursor) { if (node.contains(cursor.node)) { [cursor.node, cursor.offset] = [node.parentNode, childNodeIndex(node)]; } else if (cursor.node === node.parentNode && cursor.offset > childNodeIndex(node)) { cursor.offset -= 1; } } function updateCursorBeforeUnwrap(node, cursor) { if (cursor.node === node) { [cursor.node, cursor.offset] = [node.parentNode, cursor.offset + childNodeIndex(node)]; } else if (cursor.node === node.parentNode && cursor.offset > childNodeIndex(node)) { cursor.offset += nodeSize(node) - 1; } } function updateCursorBeforeMergeIntoPreviousSibling(node, cursor) { if (cursor.node === node) { cursor.node = node.previousSibling; cursor.offset += node.previousSibling.childNodes.length; } else if (cursor.node === node.parentNode) { const childIndex = childNodeIndex(node); if (cursor.offset === childIndex) { cursor.node = node.previousSibling; cursor.offset = node.previousSibling.childNodes.length; } else if (cursor.offset > childIndex) { cursor.offset--; } } } /** @typedef {import("@html_editor/core/selection_plugin").Cursor} Cursor */ export const callbacksForCursorUpdate = { /** @type {(node: Node) => (cursor: Cursor) => void} */ remove: (node) => (cursor) => updateCursorBeforeRemove(node, cursor), /** @type {(ref: HTMLElement, node: Node) => (cursor: Cursor) => void} */ before: (ref, node) => (cursor) => updateCursorBeforeMove(ref.parentNode, childNodeIndex(ref), node, cursor), /** @type {(ref: HTMLElement, node: Node) => (cursor: Cursor) => void} */ after: (ref, node) => (cursor) => updateCursorBeforeMove(ref.parentNode, childNodeIndex(ref) + 1, node, cursor), /** @type {(ref: HTMLElement, node: Node) => (cursor: Cursor) => void} */ append: (to, node) => (cursor) => updateCursorBeforeMove(to, to.childNodes.length, node, cursor), /** @type {(ref: HTMLElement, node: Node) => (cursor: Cursor) => void} */ prepend: (to, node) => (cursor) => updateCursorBeforeMove(to, 0, node, cursor), /** @type {(node: HTMLElement) => (cursor: Cursor) => void} */ unwrap: (node) => (cursor) => updateCursorBeforeUnwrap(node, cursor), /** @type {(node: HTMLElement) => (cursor: Cursor) => void} */ merge: (node) => (cursor) => updateCursorBeforeMergeIntoPreviousSibling(node, cursor), }; /** * @param {Selection} selection * @param {"previous"|"next"} side * @param {HTMLElement} editable * @returns {string | undefined} */ export function getAdjacentCharacter(selection, side, editable) { let { focusNode, focusOffset } = selection; const originalBlock = closestBlock(focusNode); let adjacentCharacter; while (!adjacentCharacter && focusNode) { if (side === "previous") { // @todo: this might be wrong in the first time, as focus node might not be a leaf. adjacentCharacter = focusOffset > 0 && focusNode.textContent[focusOffset - 1]; } else { adjacentCharacter = focusNode.textContent[focusOffset]; } if (!adjacentCharacter) { if (side === "previous") { focusNode = previousLeaf(focusNode, editable); focusOffset = focusNode && nodeSize(focusNode); } else { focusNode = nextLeaf(focusNode, editable); focusOffset = 0; } const characterIndex = side === "previous" ? focusOffset - 1 : focusOffset; adjacentCharacter = focusNode && focusNode.textContent[characterIndex]; } } if (!focusNode || !isContentEditable(focusNode) || closestBlock(focusNode) !== originalBlock) { return undefined; } return adjacentCharacter; }