import { Optional } from '@ephox/katamari';
import * as NodeType from '../../dom/NodeType';
import { TextWalker } from '../../dom/TextWalker';
import DOMUtils from './DOMUtils';
type TextProcessCallback = (node: Text, offset: number, text: string) => number;
interface Spot {
container: Text;
offset: number;
}
interface TextSeeker {
backwards: (node: Node, offset: number, process: TextProcessCallback, root?: Node) => Spot | null;
forwards: (node: Node, offset: number, process: TextProcessCallback, root?: Node) => Spot | null;
}
/**
* The TextSeeker class enables you to seek for a specific point in text across the DOM.
*
* @class tinymce.dom.TextSeeker
* @example
* const seeker = tinymce.dom.TextSeeker(editor.dom);
* const startOfWord = seeker.backwards(startNode, startOffset, (textNode, offset, text) => {
* const lastSpaceCharIndex = text.lastIndexOf(' ');
* if (lastSpaceCharIndex !== -1) {
* return lastSpaceCharIndex + 1;
* } else {
* // No space found so continue searching
* return -1;
* }
* });
*/
/**
* Constructs a new TextSeeker instance.
*
* @constructor
* @param {tinymce.dom.DOMUtils} dom DOMUtils object reference.
* @param {Function} isBoundary Optional function to determine if the seeker should continue to walk past the node provided. The default is to search until a block or br element is found.
*/
const TextSeeker = (dom: DOMUtils, isBoundary?: (node: Node) => boolean): TextSeeker => {
const isBlockBoundary = isBoundary ? isBoundary : (node: Node) => dom.isBlock(node) || NodeType.isBr(node) || NodeType.isContentEditableFalse(node);
const walk = (node: Node, offset: number, walker: () => Optional, process: TextProcessCallback): Optional => {
if (NodeType.isText(node)) {
const newOffset = process(node, offset, node.data);
if (newOffset !== -1) {
return Optional.some({ container: node, offset: newOffset });
}
}
return walker().bind((next) => walk(next.container, next.offset, walker, process));
};
/**
* Search backwards through text nodes until a match, boundary, or root node has been found.
*
* @method backwards
* @param {Node} node The node to start searching from.
* @param {Number} offset The offset of the node to start searching from.
* @param {Function} process A function that's passed the current text node, the current offset and the text content of the node. It should return the offset of the match or -1 to continue searching.
* @param {Node} root An optional root node to constrain the search to.
* @return {Object} An object containing the matched text node and offset. If no match is found, null will be returned.
*/
const backwards = (node: Node, offset: number, process: TextProcessCallback, root?: Node) => {
const walker = TextWalker(node, root ?? dom.getRoot(), isBlockBoundary);
return walk(node, offset, () => walker.prev().map((prev) => ({ container: prev, offset: prev.length })), process).getOrNull();
};
/**
* Search forwards through text nodes until a match, boundary, or root node has been found.
*
* @method forwards
* @param {Node} node The node to start searching from.
* @param {Number} offset The offset of the node to start searching from.
* @param {Function} process A function that's passed the current text node, the current offset and the text content of the node. It should return the offset of the match or -1 to continue searching.
* @param {Node} root An optional root node to constrain the search to.
* @return {Object} An object containing the matched text node and offset. If no match is found, null will be returned.
*/
const forwards = (node: Node, offset: number, process: TextProcessCallback, root?: Node) => {
const walker = TextWalker(node, root ?? dom.getRoot(), isBlockBoundary);
return walk(node, offset, () => walker.next().map((next) => ({ container: next, offset: 0 })), process).getOrNull();
};
return {
backwards,
forwards
};
};
export default TextSeeker;