Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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_VERTICAL_CENTER
);
}
} else {
status = "OutOfRange";
}
} else {
for (let range of ranges) {
this.highlighter.highlightRange(range);
}
}
} else {
status = "NoResults";
}
return status;
}
}