diff --git a/browser/components/extensions/ext-browser.json b/browser/components/extensions/ext-browser.json index f62a842d3ea2..e3e0c43d343d 100644 --- a/browser/components/extensions/ext-browser.json +++ b/browser/components/extensions/ext-browser.json @@ -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", diff --git a/browser/components/extensions/ext-find.js b/browser/components/extensions/ext-find.js new file mode 100644 index 000000000000..e26cdb290226 --- /dev/null +++ b/browser/components/extensions/ext-find.js @@ -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"); + }, + }, + }; + } +}; diff --git a/browser/components/extensions/jar.mn b/browser/components/extensions/jar.mn index f6b356c46efa..8532aa8d0292 100644 --- a/browser/components/extensions/jar.mn +++ b/browser/components/extensions/jar.mn @@ -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 diff --git a/browser/components/extensions/schemas/find.json b/browser/components/extensions/schemas/find.json new file mode 100644 index 000000000000..a365a7436224 --- /dev/null +++ b/browser/components/extensions/schemas/find.json @@ -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 browser.find API to interact with the browser's Find 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 + } + ] + } + ] + } +] diff --git a/browser/components/extensions/schemas/jar.mn b/browser/components/extensions/schemas/jar.mn index 02bc61ec2e23..8ff07a716138 100644 --- a/browser/components/extensions/schemas/jar.mn +++ b/browser/components/extensions/schemas/jar.mn @@ -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 diff --git a/browser/components/extensions/test/browser/browser-common.ini b/browser/components/extensions/test/browser/browser-common.ini index a0279e2d1b3f..c4d3446651bc 100644 --- a/browser/components/extensions/test/browser/browser-common.ini +++ b/browser/components/extensions/test/browser/browser-common.ini @@ -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] diff --git a/browser/components/extensions/test/browser/browser_ext_find.js b/browser/components/extensions/test/browser/browser_ext_find.js new file mode 100644 index 000000000000..f7bae8dc2102 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_find.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}`); +}); + diff --git a/browser/components/extensions/test/browser/file_find_frames.html b/browser/components/extensions/test/browser/file_find_frames.html new file mode 100644 index 000000000000..0d8a7a7694f1 --- /dev/null +++ b/browser/components/extensions/test/browser/file_find_frames.html @@ -0,0 +1,15 @@ + + + + +

Banana 0

+ +

bAnana 1

+

fruitcake

+

ang

elf
ood

+ + + diff --git a/browser/locales/en-US/chrome/browser/browser.properties b/browser/locales/en-US/chrome/browser/browser.properties index 596765297f5e..08ea03991a89 100644 --- a/browser/locales/en-US/chrome/browser/browser.properties +++ b/browser/locales/en-US/chrome/browser/browser.properties @@ -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 diff --git a/toolkit/components/extensions/FindContent.jsm b/toolkit/components/extensions/FindContent.jsm new file mode 100644 index 000000000000..3aa9b5305b5d --- /dev/null +++ b/toolkit/components/extensions/FindContent.jsm @@ -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; + } +} + diff --git a/toolkit/components/extensions/moz.build b/toolkit/components/extensions/moz.build index a6d3923e7da4..b41a89f94982 100644 --- a/toolkit/components/extensions/moz.build +++ b/toolkit/components/extensions/moz.build @@ -21,6 +21,7 @@ EXTRA_JS_MODULES += [ 'ExtensionStorage.jsm', 'ExtensionStorageSync.jsm', 'ExtensionUtils.jsm', + 'FindContent.jsm', 'LegacyExtensionsUtils.jsm', 'MessageChannel.jsm', 'NativeMessaging.jsm', diff --git a/toolkit/content/browser-content.js b/toolkit/content/browser-content.js index d7f86f816449..4d2a8fca47a5 100644 --- a/toolkit/content/browser-content.js +++ b/toolkit/content/browser-content.js @@ -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(); +