зеркало из 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"]
|
["devtools", "panels"]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"find": {
|
||||||
|
"url": "chrome://browser/content/ext-find.js",
|
||||||
|
"schema": "chrome://browser/content/schemas/find.json",
|
||||||
|
"scopes": ["addon_parent"],
|
||||||
|
"paths": [
|
||||||
|
["find"]
|
||||||
|
]
|
||||||
|
},
|
||||||
"history": {
|
"history": {
|
||||||
"url": "chrome://browser/content/ext-history.js",
|
"url": "chrome://browser/content/ext-history.js",
|
||||||
"schema": "chrome://browser/content/schemas/history.json",
|
"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-inspectedWindow.js
|
||||||
content/browser/ext-devtools-network.js
|
content/browser/ext-devtools-network.js
|
||||||
content/browser/ext-devtools-panels.js
|
content/browser/ext-devtools-panels.js
|
||||||
|
content/browser/ext-find.js
|
||||||
content/browser/ext-geckoProfiler.js
|
content/browser/ext-geckoProfiler.js
|
||||||
content/browser/ext-history.js
|
content/browser/ext-history.js
|
||||||
content/browser/ext-menus.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_inspected_window.json
|
||||||
content/browser/schemas/devtools_network.json
|
content/browser/schemas/devtools_network.json
|
||||||
content/browser/schemas/devtools_panels.json
|
content/browser/schemas/devtools_panels.json
|
||||||
|
content/browser/schemas/find.json
|
||||||
content/browser/schemas/geckoProfiler.json
|
content/browser/schemas/geckoProfiler.json
|
||||||
content/browser/schemas/history.json
|
content/browser/schemas/history.json
|
||||||
content/browser/schemas/menus.json
|
content/browser/schemas/menus.json
|
||||||
|
|
|
@ -10,6 +10,7 @@ support-files =
|
||||||
context_tabs_onUpdated_page.html
|
context_tabs_onUpdated_page.html
|
||||||
context_tabs_onUpdated_iframe.html
|
context_tabs_onUpdated_iframe.html
|
||||||
file_clearplugindata.html
|
file_clearplugindata.html
|
||||||
|
file_find_frames.html
|
||||||
file_popup_api_injection_a.html
|
file_popup_api_injection_a.html
|
||||||
file_popup_api_injection_b.html
|
file_popup_api_injection_b.html
|
||||||
file_iframe_document.html
|
file_iframe_document.html
|
||||||
|
@ -73,6 +74,7 @@ skip-if = (os == 'win' && !debug) # bug 1352668
|
||||||
[browser_ext_devtools_page.js]
|
[browser_ext_devtools_page.js]
|
||||||
[browser_ext_devtools_panel.js]
|
[browser_ext_devtools_panel.js]
|
||||||
[browser_ext_devtools_panels_elements.js]
|
[browser_ext_devtools_panels_elements.js]
|
||||||
|
[browser_ext_find.js]
|
||||||
[browser_ext_geckoProfiler_symbolicate.js]
|
[browser_ext_geckoProfiler_symbolicate.js]
|
||||||
[browser_ext_getViews.js]
|
[browser_ext_getViews.js]
|
||||||
[browser_ext_identity_indication.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.clipboardRead=Get data from the clipboard
|
||||||
webextPerms.description.clipboardWrite=Input data to 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.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.geolocation=Access your location
|
||||||
webextPerms.description.history=Access browsing history
|
webextPerms.description.history=Access browsing history
|
||||||
webextPerms.description.management=Monitor extension usage and manage themes
|
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',
|
'ExtensionStorage.jsm',
|
||||||
'ExtensionStorageSync.jsm',
|
'ExtensionStorageSync.jsm',
|
||||||
'ExtensionUtils.jsm',
|
'ExtensionUtils.jsm',
|
||||||
|
'FindContent.jsm',
|
||||||
'LegacyExtensionsUtils.jsm',
|
'LegacyExtensionsUtils.jsm',
|
||||||
'MessageChannel.jsm',
|
'MessageChannel.jsm',
|
||||||
'NativeMessaging.jsm',
|
'NativeMessaging.jsm',
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
/* eslint-env mozilla/frame-script */
|
/* eslint-env mozilla/frame-script */
|
||||||
|
/* global sendAsyncMessage */
|
||||||
|
|
||||||
var Cc = Components.classes;
|
var Cc = Components.classes;
|
||||||
var Ci = Components.interfaces;
|
var Ci = Components.interfaces;
|
||||||
|
@ -19,6 +20,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
|
||||||
"resource://gre/modules/BrowserUtils.jsm");
|
"resource://gre/modules/BrowserUtils.jsm");
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "SelectContentHelper",
|
XPCOMUtils.defineLazyModuleGetter(this, "SelectContentHelper",
|
||||||
"resource://gre/modules/SelectContentHelper.jsm");
|
"resource://gre/modules/SelectContentHelper.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "FindContent",
|
||||||
|
"resource://gre/modules/FindContent.jsm");
|
||||||
|
|
||||||
var global = this;
|
var global = this;
|
||||||
|
|
||||||
|
@ -1847,3 +1850,38 @@ addEventListener("mozshowdropdown-sourcetouch", event => {
|
||||||
new SelectContentHelper(event.target, {isOpenedViaTouch: true}, this);
|
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();
|
||||||
|
|
||||||
|
|
Загрузка…
Ссылка в новой задаче