зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1332144 - Add browser.find extension API. r=mikedeboer, r=mixedpuppy
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
This commit is contained in:
Родитель
034b4fdfa6
Коммит
841456a51c
|
@ -72,6 +72,14 @@
|
|||
["devtools", "panels"]
|
||||
]
|
||||
},
|
||||
"find": {
|
||||
"url": "chrome://browser/content/ext-find.js",
|
||||
"schema": "chrome://browser/content/schemas/find.json",
|
||||
"scopes": ["addon_parent"],
|
||||
"paths": [
|
||||
["find"]
|
||||
]
|
||||
},
|
||||
"history": {
|
||||
"url": "chrome://browser/content/ext-history.js",
|
||||
"schema": "chrome://browser/content/schemas/history.json",
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
/* global tabTracker */
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* runFindOperation
|
||||
* Utility for `find` and `highlightResults`.
|
||||
*
|
||||
* @param {object} params - params to pass to message sender.
|
||||
* @param {string} message - identifying component of message name.
|
||||
*
|
||||
* @returns {Promise} a promise that will be resolved or rejected based on the
|
||||
* data received by the message listener.
|
||||
*/
|
||||
function runFindOperation(params, message) {
|
||||
let {tabId} = params;
|
||||
let tab = tabId ? tabTracker.getTab(tabId) : tabTracker.activeTab;
|
||||
let browser = tab.linkedBrowser;
|
||||
let mm = browser.messageManager;
|
||||
tabId = tabId || tabTracker.getId(tab);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
mm.addMessageListener(`ext-Finder:${message}Finished`, function messageListener(message) {
|
||||
mm.removeMessageListener(`ext-Finder:${message}Finished`, messageListener);
|
||||
switch (message.data) {
|
||||
case "Success":
|
||||
resolve();
|
||||
break;
|
||||
case "OutOfRange":
|
||||
reject({message: "index supplied was out of range"});
|
||||
break;
|
||||
case "NoResults":
|
||||
reject({message: "no search results to highlight"});
|
||||
break;
|
||||
}
|
||||
resolve(message.data);
|
||||
});
|
||||
mm.sendAsyncMessage(`ext-Finder:${message}`, params);
|
||||
});
|
||||
};
|
||||
|
||||
this.find = class extends ExtensionAPI {
|
||||
getAPI(context) {
|
||||
return {
|
||||
find: {
|
||||
/**
|
||||
* browser.find.find
|
||||
* Searches document and its frames for a given queryphrase and stores all found
|
||||
* Range objects in an array accessible by other browser.find methods.
|
||||
*
|
||||
* @param {string} queryphrase - The string to search for.
|
||||
* @param {object} params optional - may contain any of the following properties,
|
||||
* all of which are optional:
|
||||
* {number} tabId - Tab to query. Defaults to the active tab.
|
||||
* {boolean} caseSensitive - Highlight only ranges with case sensitive match.
|
||||
* {boolean} entireWord - Highlight only ranges that match entire word.
|
||||
* {boolean} includeRangeData - Whether to return range data.
|
||||
* {boolean} includeRectData - Whether to return rectangle data.
|
||||
*
|
||||
* @returns {object} data received by the message listener 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.
|
||||
*/
|
||||
find(queryphrase, params) {
|
||||
params = params || {};
|
||||
params.queryphrase = queryphrase;
|
||||
return runFindOperation(params, "CollectResults");
|
||||
},
|
||||
|
||||
/**
|
||||
* browser.find.highlightResults
|
||||
* Highlights range(s) found in previous browser.find.find.
|
||||
*
|
||||
* @param {object} params optional - may contain any of the following properties,
|
||||
* all of which are optional:
|
||||
* {number} rangeIndex - Found range to be highlighted. Default highlights all ranges.
|
||||
* {number} tabId - Tab to highlight. Defaults to the active tab.
|
||||
* {boolean} noScroll - Don't scroll to highlighted item.
|
||||
*
|
||||
* @returns {string} - data received by the message listener that may be:
|
||||
* "Success" - Highlighting succeeded.
|
||||
* "OutOfRange" - The index supplied was out of range.
|
||||
* "NoResults" - There were no search results to highlight.
|
||||
*/
|
||||
highlightResults(params) {
|
||||
params = params || {};
|
||||
return runFindOperation(params, "HighlightResults");
|
||||
},
|
||||
|
||||
/**
|
||||
* browser.find.removeHighlighting
|
||||
* Removes all hightlighting from previous search.
|
||||
*
|
||||
* @param {number} tabId optional
|
||||
* Tab to clear highlighting in. Defaults to the active tab.
|
||||
*/
|
||||
removeHighlighting(tabId) {
|
||||
let tab = tabId ? tabTracker.getTab(tabId) : tabTracker.activeTab;
|
||||
tab.linkedBrowser.messageManager.sendAsyncMessage("ext-Finder:clearHighlighting");
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
|
@ -23,6 +23,7 @@ browser.jar:
|
|||
content/browser/ext-devtools-inspectedWindow.js
|
||||
content/browser/ext-devtools-network.js
|
||||
content/browser/ext-devtools-panels.js
|
||||
content/browser/ext-find.js
|
||||
content/browser/ext-geckoProfiler.js
|
||||
content/browser/ext-history.js
|
||||
content/browser/ext-menus.js
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
[
|
||||
{
|
||||
"namespace": "manifest",
|
||||
"types": [
|
||||
{
|
||||
"$extend": "OptionalPermission",
|
||||
"choices": [{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"find"
|
||||
]
|
||||
}]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"namespace": "find",
|
||||
"description": "Use the <code>browser.find</code> API to interact with the browser's <code>Find</code> interface.",
|
||||
"permissions": ["find"],
|
||||
"functions": [
|
||||
{
|
||||
"name": "find",
|
||||
"type": "function",
|
||||
"async": true,
|
||||
"description": "Search for text in document and store found ranges in array, in document order.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "queryphrase",
|
||||
"type": "string",
|
||||
"description": "The string to search for."
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"type": "object",
|
||||
"description": "Search parameters.",
|
||||
"optional": true,
|
||||
"properties": {
|
||||
"tabId": {
|
||||
"type": "integer",
|
||||
"description": "Tab to query. Defaults to the active tab.",
|
||||
"optional": true,
|
||||
"minimum": 0
|
||||
},
|
||||
"caseSensitive": {
|
||||
"type": "boolean",
|
||||
"description": "Find only ranges with case sensitive match.",
|
||||
"optional": true
|
||||
},
|
||||
"entireWord": {
|
||||
"type": "boolean",
|
||||
"description": "Find only ranges that match entire word.",
|
||||
"optional": true
|
||||
},
|
||||
"includeRectData": {
|
||||
"description": "Return rectangle data which describes visual position of search results.",
|
||||
"type": "boolean",
|
||||
"optional": true
|
||||
},
|
||||
"includeRangeData": {
|
||||
"description": "Return range data which provides range data in a serializable form.",
|
||||
"type": "boolean",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "highlightResults",
|
||||
"type": "function",
|
||||
"async": true,
|
||||
"description": "Highlight a range",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "params",
|
||||
"type": "object",
|
||||
"description": "highlightResults parameters",
|
||||
"optional": true,
|
||||
"properties": {
|
||||
"rangeIndex": {
|
||||
"type": "integer",
|
||||
"description": "Found range to be highlighted. Default highlights all ranges.",
|
||||
"minimum": 0,
|
||||
"optional": true
|
||||
},
|
||||
"tabId": {
|
||||
"type": "integer",
|
||||
"description": "Tab to highlight. Defaults to the active tab.",
|
||||
"minimum": 0,
|
||||
"optional": true
|
||||
},
|
||||
"noScroll": {
|
||||
"type": "boolean",
|
||||
"description": "Don't scroll to highlighted item.",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "removeHighlighting",
|
||||
"type": "function",
|
||||
"async": true,
|
||||
"description": "Remove all highlighting from previous searches.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "tabId",
|
||||
"type": "integer",
|
||||
"description": "Tab to highlight. Defaults to the active tab.",
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -12,6 +12,7 @@ browser.jar:
|
|||
content/browser/schemas/devtools_inspected_window.json
|
||||
content/browser/schemas/devtools_network.json
|
||||
content/browser/schemas/devtools_panels.json
|
||||
content/browser/schemas/find.json
|
||||
content/browser/schemas/geckoProfiler.json
|
||||
content/browser/schemas/history.json
|
||||
content/browser/schemas/menus.json
|
||||
|
|
|
@ -10,6 +10,7 @@ support-files =
|
|||
context_tabs_onUpdated_page.html
|
||||
context_tabs_onUpdated_iframe.html
|
||||
file_clearplugindata.html
|
||||
file_find_frames.html
|
||||
file_popup_api_injection_a.html
|
||||
file_popup_api_injection_b.html
|
||||
file_iframe_document.html
|
||||
|
@ -73,6 +74,7 @@ skip-if = (os == 'win' && !debug) # bug 1352668
|
|||
[browser_ext_devtools_page.js]
|
||||
[browser_ext_devtools_panel.js]
|
||||
[browser_ext_devtools_panels_elements.js]
|
||||
[browser_ext_find.js]
|
||||
[browser_ext_geckoProfiler_symbolicate.js]
|
||||
[browser_ext_getViews.js]
|
||||
[browser_ext_identity_indication.js]
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
/* global browser */
|
||||
"use strict";
|
||||
|
||||
function frameScript() {
|
||||
function getSelectedText() {
|
||||
let frame = this.content.frames[0].frames[1];
|
||||
let Ci = Components.interfaces;
|
||||
let docShell = frame.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIWebNavigation)
|
||||
.QueryInterface(Ci.nsIDocShell);
|
||||
let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsISelectionDisplay)
|
||||
.QueryInterface(Ci.nsISelectionController);
|
||||
let selection = controller.getSelection(controller.SELECTION_FIND);
|
||||
let range = selection.getRangeAt(0);
|
||||
let r1 = frame.parent.frameElement.getBoundingClientRect();
|
||||
let r2 = frame.frameElement.getBoundingClientRect();
|
||||
let r3 = range.getBoundingClientRect();
|
||||
let rect = {top: (r1.top + r2.top + r3.top), left: (r1.left + r2.left + r3.left)};
|
||||
this.sendAsyncMessage("test:find:selectionTest", {text: selection.toString(), rect});
|
||||
}
|
||||
getSelectedText();
|
||||
}
|
||||
|
||||
function waitForMessage(messageManager, topic) {
|
||||
return new Promise(resolve => {
|
||||
messageManager.addMessageListener(topic, function messageListener(message) {
|
||||
messageManager.removeMessageListener(topic, messageListener);
|
||||
resolve(message);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
add_task(async function testDuplicatePinnedTab() {
|
||||
async function background() {
|
||||
function awaitLoad(tabId) {
|
||||
return new Promise(resolve => {
|
||||
browser.tabs.onUpdated.addListener(function listener(tabId_, changed, tab) {
|
||||
if (tabId == tabId_ && changed.status == "complete") {
|
||||
browser.tabs.onUpdated.removeListener(listener);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let url = "http://example.com/browser/browser/components/extensions/test/browser/file_find_frames.html";
|
||||
let tab = await browser.tabs.update({url});
|
||||
await awaitLoad(tab.id);
|
||||
|
||||
let data = await browser.find.find("banana", {includeRangeData: true});
|
||||
let rangeData = data.rangeData;
|
||||
|
||||
browser.test.log("Test that `data.count` is the expected value.");
|
||||
browser.test.assertEq(6, data.count, "The value returned from `data.count`");
|
||||
|
||||
browser.test.log("Test that `rangeData` has the proper number of values.");
|
||||
browser.test.assertEq(6, rangeData.length, "The number of values held in `rangeData`");
|
||||
|
||||
browser.test.log("Test that the text found in the top window and nested frames corresponds to the proper position.");
|
||||
let terms = ["Banana", "bAnana", "baNana", "banAna", "banaNa", "bananA"];
|
||||
for (let i = 0; i < terms.length; i++) {
|
||||
browser.test.assertEq(terms[i], rangeData[i].text, `The text at range position ${i}:`);
|
||||
}
|
||||
|
||||
browser.test.log("Test that case sensitive match works properly.");
|
||||
data = await browser.find.find("baNana", {caseSensitive: true, includeRangeData: true});
|
||||
browser.test.assertEq(1, data.count, "The number of matches found:");
|
||||
browser.test.assertEq("baNana", data.rangeData[0].text, "The text found:");
|
||||
|
||||
browser.test.log("Test that case insensitive match works properly.");
|
||||
data = await browser.find.find("banana", {caseSensitive: false});
|
||||
browser.test.assertEq(6, data.count, "The number of matches found:");
|
||||
|
||||
browser.test.log("Test that entire word match works properly.");
|
||||
data = await browser.find.find("banana", {entireWord: true});
|
||||
browser.test.assertEq(4, data.count, "The number of matches found, should skip 2 matches, \"banaNaland\" and \"bananAland\":");
|
||||
|
||||
browser.test.log("Test that `rangeData` is not returned if `includeRangeData` is false.");
|
||||
data = await browser.find.find("banana", {caseSensitive: false, includeRangeData: false});
|
||||
browser.test.assertEq(false, !!data.rangeData, "The boolean cast value of `rangeData`:");
|
||||
|
||||
browser.test.log("Test that `rectData` is not returned if `includeRectData` is false.");
|
||||
data = await browser.find.find("banana", {caseSensitive: false, includeRectData: false});
|
||||
browser.test.assertEq(false, !!data.rectData, "The boolean cast value of `rectData`:");
|
||||
|
||||
browser.test.log("Test that text spanning multiple inline elements is found.");
|
||||
data = await browser.find.find("fruitcake");
|
||||
browser.test.assertEq(1, data.count, "The number of matches found:");
|
||||
|
||||
browser.test.log("Test that text spanning multiple block elements is not found.");
|
||||
data = await browser.find.find("angelfood");
|
||||
browser.test.assertEq(0, data.count, "The number of matches found:");
|
||||
|
||||
browser.test.log("Test that `highlightResults` returns proper status code.");
|
||||
await browser.find.find("banana");
|
||||
|
||||
await browser.test.assertRejects(browser.find.highlightResults({rangeIndex: 6}),
|
||||
/index supplied was out of range/,
|
||||
"rejected Promise should pass the expected error");
|
||||
|
||||
data = await browser.find.find("xyz");
|
||||
await browser.test.assertRejects(browser.find.highlightResults({rangeIndex: 0}),
|
||||
/no search results to highlight/,
|
||||
"rejected Promise should pass the expected error");
|
||||
|
||||
data = await browser.find.find("banana", {includeRectData: true});
|
||||
await browser.find.highlightResults({rangeIndex: 5});
|
||||
|
||||
browser.test.sendMessage("test:find:WebExtensionFinished", data.rectData);
|
||||
}
|
||||
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
"permissions": ["find", "tabs"],
|
||||
},
|
||||
background,
|
||||
});
|
||||
|
||||
await extension.startup();
|
||||
let rectData = await extension.awaitMessage("test:find:WebExtensionFinished");
|
||||
let {top, left} = rectData[5].rectsAndTexts.rectList[0];
|
||||
await extension.unload();
|
||||
|
||||
let {selectedBrowser} = gBrowser;
|
||||
|
||||
let frameScriptUrl = `data:,(${frameScript})()`;
|
||||
selectedBrowser.messageManager.loadFrameScript(frameScriptUrl, false);
|
||||
let message = await waitForMessage(selectedBrowser.messageManager, "test:find:selectionTest");
|
||||
|
||||
info("Test that text was highlighted properly.");
|
||||
is(message.data.text, "bananA", `The text that was highlighted: - Expected: bananA, Actual: ${message.data.text}`);
|
||||
|
||||
info("Test that rectangle data returned from the search matches the highlighted result.");
|
||||
is(message.data.rect.top, top, `rect.top: - Expected: ${message.data.rect.top}, Actual: ${top}`);
|
||||
is(message.data.rect.left, left, `rect.left: - Expected: ${message.data.rect.left}, Actual: ${left}`);
|
||||
});
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
|
||||
<p>Banana 0</p>
|
||||
<iframe src="data:text/html,<p>baNana 2</p>
|
||||
<iframe src='data:text/html,banaNaland 4' height='50' width='100%'></iframe>
|
||||
<iframe src='data:text/html,bananAland 5' height='50' width='100%'></iframe>
|
||||
<p>banAna 3</p>" height="250" width="100%"></iframe>
|
||||
<p>bAnana 1</p>
|
||||
<p><b>fru</b>it<i><b>ca</b>ke</i></p>
|
||||
<p>ang<div>elf</div>ood</p>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -100,6 +100,7 @@ webextPerms.description.browserSettings=Read and modify browser settings
|
|||
webextPerms.description.clipboardRead=Get data from the clipboard
|
||||
webextPerms.description.clipboardWrite=Input data to the clipboard
|
||||
webextPerms.description.downloads=Download files and read and modify the browser’s download history
|
||||
webextPerms.description.find=Read the Web page text of all open tabs
|
||||
webextPerms.description.geolocation=Access your location
|
||||
webextPerms.description.history=Access browsing history
|
||||
webextPerms.description.management=Monitor extension usage and manage themes
|
||||
|
|
|
@ -0,0 +1,248 @@
|
|||
/* 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ EXTRA_JS_MODULES += [
|
|||
'ExtensionStorage.jsm',
|
||||
'ExtensionStorageSync.jsm',
|
||||
'ExtensionUtils.jsm',
|
||||
'FindContent.jsm',
|
||||
'LegacyExtensionsUtils.jsm',
|
||||
'MessageChannel.jsm',
|
||||
'NativeMessaging.jsm',
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/* eslint-env mozilla/frame-script */
|
||||
/* global sendAsyncMessage */
|
||||
|
||||
var Cc = Components.classes;
|
||||
var Ci = Components.interfaces;
|
||||
|
@ -19,6 +20,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
|
|||
"resource://gre/modules/BrowserUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "SelectContentHelper",
|
||||
"resource://gre/modules/SelectContentHelper.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "FindContent",
|
||||
"resource://gre/modules/FindContent.jsm");
|
||||
|
||||
var global = this;
|
||||
|
||||
|
@ -1847,3 +1850,38 @@ addEventListener("mozshowdropdown-sourcetouch", event => {
|
|||
new SelectContentHelper(event.target, {isOpenedViaTouch: true}, this);
|
||||
}
|
||||
});
|
||||
|
||||
let ExtFind = {
|
||||
init() {
|
||||
addMessageListener("ext-Finder:CollectResults", this);
|
||||
addMessageListener("ext-Finder:HighlightResults", this);
|
||||
addMessageListener("ext-Finder:clearHighlighting", this);
|
||||
},
|
||||
|
||||
_findContent: null,
|
||||
|
||||
async receiveMessage(message) {
|
||||
if (!this._findContent) {
|
||||
this._findContent = new FindContent(docShell);
|
||||
}
|
||||
|
||||
let data;
|
||||
switch (message.name) {
|
||||
case "ext-Finder:CollectResults":
|
||||
this.finderInited = true;
|
||||
data = await this._findContent.findRanges(message.data);
|
||||
sendAsyncMessage("ext-Finder:CollectResultsFinished", data);
|
||||
break;
|
||||
case "ext-Finder:HighlightResults":
|
||||
data = this._findContent.highlightResults(message.data);
|
||||
sendAsyncMessage("ext-Finder:HighlightResultsFinished", data);
|
||||
break;
|
||||
case "ext-Finder:clearHighlighting":
|
||||
this._findContent.highlighter.highlight(false);
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
ExtFind.init();
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче