import { Arr, Fun } from '@ephox/katamari'; import * as NodeType from '../dom/NodeType'; import * as ArrUtils from '../util/ArrUtils'; import * as CaretCandidate from './CaretCandidate'; import CaretPosition from './CaretPosition'; import { findNode, isBackwards, isForwards } from './CaretUtils'; export interface CaretWalker { next: (caretPosition: CaretPosition | null) => CaretPosition | null; prev: (caretPosition: CaretPosition | null) => CaretPosition | null; } /** * This module contains logic for moving around a virtual caret in logical order within a DOM element. * * It ignores the most obvious invalid caret locations such as within a script element or within a * contentEditable=false element but it will return locations that isn't possible to render visually. * * @private * @class tinymce.caret.CaretWalker * @example * const caretWalker = CaretWalker(rootElm); * * const prevLogicalCaretPosition = caretWalker.prev(CaretPosition.fromRangeStart(range)); * const nextLogicalCaretPosition = caretWalker.next(CaretPosition.fromRangeEnd(range)); */ export enum HDirection { Backwards = -1, Forwards = 1 } const isContentEditableFalse = NodeType.isContentEditableFalse; const isText = NodeType.isText; const isElement = NodeType.isElement; const isBr = NodeType.isBr; const isCaretCandidate = CaretCandidate.isCaretCandidate; const isAtomic = CaretCandidate.isAtomic; const isEditableCaretCandidate = CaretCandidate.isEditableCaretCandidate; const getParents = (node: Node, root: Node): Node[] => { const parents: Node[] = []; let tempNode: Node | null = node; while (tempNode && tempNode !== root) { parents.push(tempNode); tempNode = tempNode.parentNode; } return parents; }; const nodeAtIndex = (container: Node, offset: number): Node | null => { if (container.hasChildNodes() && offset < container.childNodes.length) { return container.childNodes[offset]; } return null; }; const getCaretCandidatePosition = (direction: HDirection, node: Node): CaretPosition => { if (isForwards(direction)) { if (isCaretCandidate(node.previousSibling) && !isText(node.previousSibling)) { return CaretPosition.before(node); } if (isText(node)) { return CaretPosition(node, 0); } } if (isBackwards(direction)) { if (isCaretCandidate(node.nextSibling) && !isText(node.nextSibling)) { return CaretPosition.after(node); } if (isText(node)) { return CaretPosition(node, node.data.length); } } if (isBackwards(direction)) { if (isBr(node)) { return CaretPosition.before(node); } return CaretPosition.after(node); } return CaretPosition.before(node); }; const moveForwardFromBr = (root: Element, nextNode: Node): CaretPosition | null => { const nextSibling = nextNode.nextSibling; if (nextSibling && isCaretCandidate(nextSibling)) { if (isText(nextSibling)) { return CaretPosition(nextSibling, 0); } else { return CaretPosition.before(nextSibling); } } else { return findCaretPosition(HDirection.Forwards, CaretPosition.after(nextNode), root); } }; const findCaretPosition = (direction: HDirection, startPos: CaretPosition | null, root: Node): CaretPosition | null => { let node: Node | null | undefined; let nextNode: Node | null | undefined; let innerNode: Node | null | undefined; let caretPosition: CaretPosition; if (!isElement(root) || !startPos) { return null; } if (startPos.isEqual(CaretPosition.after(root)) && root.lastChild) { caretPosition = CaretPosition.after(root.lastChild); if (isBackwards(direction) && isCaretCandidate(root.lastChild) && isElement(root.lastChild)) { return isBr(root.lastChild) ? CaretPosition.before(root.lastChild) : caretPosition; } } else { caretPosition = startPos; } const container = caretPosition.container(); let offset = caretPosition.offset(); if (isText(container)) { if (isBackwards(direction) && offset > 0) { return CaretPosition(container, --offset); } if (isForwards(direction) && offset < container.length) { return CaretPosition(container, ++offset); } node = container; } else { if (isBackwards(direction) && offset > 0) { nextNode = nodeAtIndex(container, offset - 1); if (isCaretCandidate(nextNode)) { if (!isAtomic(nextNode)) { innerNode = findNode(nextNode, direction, isEditableCaretCandidate, nextNode); if (innerNode) { if (isText(innerNode)) { return CaretPosition(innerNode, innerNode.data.length); } return CaretPosition.after(innerNode); } } if (isText(nextNode)) { return CaretPosition(nextNode, nextNode.data.length); } return CaretPosition.before(nextNode); } } if (isForwards(direction) && offset < container.childNodes.length) { nextNode = nodeAtIndex(container, offset); if (isCaretCandidate(nextNode)) { if (isBr(nextNode)) { return moveForwardFromBr(root, nextNode); } if (!isAtomic(nextNode)) { innerNode = findNode(nextNode, direction, isEditableCaretCandidate, nextNode); if (innerNode) { if (isText(innerNode)) { return CaretPosition(innerNode, 0); } return CaretPosition.before(innerNode); } } if (isText(nextNode)) { return CaretPosition(nextNode, 0); } return CaretPosition.after(nextNode); } } node = nextNode ? nextNode : caretPosition.getNode(); } if (node && ((isForwards(direction) && caretPosition.isAtEnd()) || (isBackwards(direction) && caretPosition.isAtStart()))) { node = findNode(node, direction, Fun.always, root, true); if (isEditableCaretCandidate(node, root)) { return getCaretCandidatePosition(direction, node); } } nextNode = node ? findNode(node, direction, isEditableCaretCandidate, root) : node; const rootContentEditableFalseElm = ArrUtils.last(Arr.filter(getParents(container, root), isContentEditableFalse)); if (rootContentEditableFalseElm && (!nextNode || !rootContentEditableFalseElm.contains(nextNode))) { if (isForwards(direction)) { caretPosition = CaretPosition.after(rootContentEditableFalseElm); } else { caretPosition = CaretPosition.before(rootContentEditableFalseElm); } return caretPosition; } if (nextNode) { return getCaretCandidatePosition(direction, nextNode); } return null; }; export const CaretWalker = (root: Node): CaretWalker => ({ /** * Returns the next logical caret position from the specified input * caretPosition or null if there isn't any more positions left for example * at the end specified root element. * * @method next * @param {tinymce.caret.CaretPosition} caretPosition Caret position to start from. * @return {tinymce.caret.CaretPosition} CaretPosition or null if no position was found. */ next: (caretPosition: CaretPosition | null): CaretPosition | null => { return findCaretPosition(HDirection.Forwards, caretPosition, root); }, /** * Returns the previous logical caret position from the specified input * caretPosition or null if there isn't any more positions left for example * at the end specified root element. * * @method prev * @param {tinymce.caret.CaretPosition} caretPosition Caret position to start from. * @return {tinymce.caret.CaretPosition} CaretPosition or null if no position was found. */ prev: (caretPosition: CaretPosition | null): CaretPosition | null => { return findCaretPosition(HDirection.Backwards, caretPosition, root); } });