/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* vim: set sts=2 sw=2 et tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { Finder: "resource://gre/modules/Finder.sys.mjs", FinderHighlighter: "resource://gre/modules/FinderHighlighter.sys.mjs", FinderIterator: "resource://gre/modules/FinderIterator.sys.mjs", }); export class FindContent { constructor(docShell) { this.finder = new lazy.Finder(docShell); } get iterator() { if (!this._iterator) { this._iterator = new lazy.FinderIterator(); } return this._iterator; } get highlighter() { if (!this._highlighter) { this._highlighter = new lazy.FinderHighlighter(this.finder, true); } return this._highlighter; } /** * findRanges * * Performs a search which will cache found ranges in `iterator._previousRanges`. Cached * data can then be used by `highlightResults`, `_collectRectData` and `_serializeRangeData`. * * @param {object} params - the params. * @param {string} params.queryphrase - the text to search for. * @param {boolean} params.caseSensitive - whether to use case sensitive matches. * @param {boolean} params.includeRangeData - whether to collect and return range data. * @param {boolean} params.matchDiacritics - whether diacritics must match. * @param {boolean} params.searchString - whether to collect and return rect data. * @param {boolean} params.entireWord - whether to match entire words. * @param {boolean} params.includeRectData - collect and return rect data. * * @returns {object} that includes: * {number} count - number of results found. * {array} rangeData (if opted) - serialized representation of ranges found. * {array} rectData (if opted) - rect data of ranges found. */ findRanges(params) { return new Promise(resolve => { let { queryphrase, caseSensitive, entireWord, includeRangeData, includeRectData, matchDiacritics, } = params; this.iterator.reset(); // Cast `caseSensitive` and `entireWord` to boolean, otherwise _iterator.start will throw. let iteratorPromise = this.iterator.start({ word: queryphrase, caseSensitive: !!caseSensitive, entireWord: !!entireWord, finder: this.finder, listener: this.finder, matchDiacritics: !!matchDiacritics, useSubFrames: false, }); iteratorPromise.then(() => { let rangeData; let rectData; if (includeRangeData) { rangeData = this._serializeRangeData(); } if (includeRectData) { rectData = this._collectRectData(); } resolve({ count: this.iterator._previousRanges.length, rangeData, rectData, }); }); }); } /** * _serializeRangeData * * Optionally returned by `findRanges`. * Collects DOM data from ranges found on the most recent search made by `findRanges` * and encodes it into a serializable form. Useful to extensions for custom UI presentation * of search results, eg, getting surrounding context of results. * * @returns {Array} - serializable range data. */ _serializeRangeData() { let ranges = this.iterator._previousRanges; let rangeData = []; let nodeCountWin = 0; let lastDoc; let walker; let node; for (let range of ranges) { let startContainer = range.startContainer; let doc = startContainer.ownerDocument; if (lastDoc !== doc) { walker = doc.createTreeWalker( doc, doc.defaultView.NodeFilter.SHOW_TEXT, null, false ); // Get first node. node = walker.nextNode(); // Reset node count. nodeCountWin = 0; } lastDoc = doc; // The framePos will be set by the parent process later. let data = { framePos: 0, text: range.toString() }; rangeData.push(data); if (node != range.startContainer) { node = walker.nextNode(); while (node) { nodeCountWin++; if (node == range.startContainer) { break; } node = walker.nextNode(); } } data.startTextNodePos = nodeCountWin; data.startOffset = range.startOffset; if (range.startContainer != range.endContainer) { node = walker.nextNode(); while (node) { nodeCountWin++; if (node == range.endContainer) { break; } node = walker.nextNode(); } } data.endTextNodePos = nodeCountWin; data.endOffset = range.endOffset; } return rangeData; } /** * _collectRectData * * Optionally returned by `findRanges`. * Collects rect data of ranges found by most recent search made by `findRanges`. * Useful to extensions for custom highlighting of search results. * * @returns {Array} rectData - serializable rect data. */ _collectRectData() { let rectData = []; let ranges = this.iterator._previousRanges; for (let range of ranges) { let rectsAndTexts = this.highlighter._getRangeRectsAndTexts(range); rectData.push({ text: range.toString(), rectsAndTexts }); } return rectData; } /** * highlightResults * * Highlights range(s) found in previous browser.find.find. * * @param {object} params - may contain any of the following properties: * all of which are optional: * {number} rangeIndex - * Found range to be highlighted held in API's ranges array for the tabId. * Default highlights all ranges. * {number} tabId - Tab to highlight. Defaults to the active tab. * {boolean} noScroll - Don't scroll to highlighted item. * * @returns {string} - a string describing the resulting status of the highlighting, * which will be used as criteria for resolving or rejecting the promise. * This can be: * "Success" - Highlighting succeeded. * "OutOfRange" - The index supplied was out of range. * "NoResults" - There were no search results to highlight. */ highlightResults(params) { let { rangeIndex, noScroll } = params; this.highlighter.highlight(false); let ranges = this.iterator._previousRanges; let status = "Success"; if (ranges.length) { if (typeof rangeIndex == "number") { if (rangeIndex < ranges.length) { let foundRange = ranges[rangeIndex]; this.highlighter.highlightRange(foundRange); if (!noScroll) { let node = foundRange.startContainer; let editableNode = this.highlighter._getEditableNode(node); let controller = editableNode ? editableNode.editor.selectionController : this.finder._getSelectionController(node.ownerGlobal); controller.scrollSelectionIntoView( controller.SELECTION_FIND, controller.SELECTION_ON, controller.SCROLL_CENTER_VERTICALLY ); } } else { status = "OutOfRange"; } } else { for (let range of ranges) { this.highlighter.highlightRange(range); } } } else { status = "NoResults"; } return status; } }