mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-26 19:55:39 +00:00
841456a51c
Provides access to the browser's internal Find APIs. Can search, get range data and rect data on found results, and highlight results. --HG-- extra : amend_source : dfa2b36794543378db58e411ca4e317a64921831
249 lines
7.6 KiB
JavaScript
249 lines
7.6 KiB
JavaScript
/* 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/. */
|
|
|
|
"use strict";
|
|
|
|
this.EXPORTED_SYMBOLS = ["FindContent"];
|
|
|
|
/* exported FindContent */
|
|
|
|
const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
|
|
|
|
class FindContent {
|
|
constructor(docShell) {
|
|
const {Finder} = Cu.import("resource://gre/modules/Finder.jsm", {});
|
|
this.finder = new Finder(docShell);
|
|
}
|
|
|
|
get iterator() {
|
|
if (!this._iterator) {
|
|
const {FinderIterator} = Cu.import("resource://gre/modules/FinderIterator.jsm", {});
|
|
this._iterator = Object.assign({}, FinderIterator);
|
|
|
|
// Native FinderIterator._collectFrames skips frames if they are scrolled out
|
|
// of viewport. Override with method that doesn't do that.
|
|
this._iterator._collectFrames = (window) => {
|
|
let frames = [];
|
|
if (!("frames" in window) || !window.frames.length) {
|
|
return frames;
|
|
}
|
|
|
|
for (let i = 0, l = window.frames.length; i < l; ++i) {
|
|
let frame = window.frames[i];
|
|
if (!frame || !frame.frameElement) {
|
|
continue;
|
|
}
|
|
frames.push(frame, ...this._iterator._collectFrames(frame));
|
|
}
|
|
|
|
return frames;
|
|
};
|
|
}
|
|
return this._iterator;
|
|
}
|
|
|
|
get highlighter() {
|
|
if (!this._highlighter) {
|
|
const {FinderHighlighter} = Cu.import("resource://gre/modules/FinderHighlighter.jsm", {});
|
|
this._highlighter = new FinderHighlighter(this.finder);
|
|
}
|
|
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} queryphrase - the text to search for.
|
|
* @param {boolean} caseSensitive - whether to use case sensitive matches.
|
|
* @param {boolean} includeRangeData - whether to collect and return range data.
|
|
* @param {boolean} searchString - whether to 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} = 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,
|
|
});
|
|
|
|
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 framePos = -1;
|
|
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;
|
|
framePos++;
|
|
}
|
|
lastDoc = doc;
|
|
|
|
let data = {framePos, text: range.toString()};
|
|
rangeData.push(data);
|
|
|
|
if (node != range.startContainer) {
|
|
let 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) {
|
|
let 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;
|
|
}
|
|
}
|
|
|