diff --git a/browser/components/extensions/ext-browser.js b/browser/components/extensions/ext-browser.js
--- a/browser/components/extensions/ext-browser.js
+++ b/browser/components/extensions/ext-browser.js
@@ -143,16 +143,24 @@
devtools_panels: {
url: "chrome://browser/content/ext-devtools-panels.js",
schema: "chrome://browser/content/schemas/devtools_panels.json",
scopes: ["devtools_parent"],
paths: [
["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",
scopes: ["addon_parent"],
paths: [
["history"],
],
},
diff --git a/browser/components/extensions/ext-find.js b/browser/components/extensions/ext-find.js
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/ext-find.js
@@ -0,0 +1,98 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+this.find = class extends ExtensionAPI {
+ getAPI(context) {
+ let loadedFrameScripts = {};
+ return {
+ find: {
+ /**
+ * browser.find.search
+ * 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 {integer} tabId optional - Tab to query. Defaults to the active tab.
+ * @param {boolean} caseSensitive optional - Highlight only ranges with case sensitive match.
+ * @param {boolean} entireWord optional - Highlight only ranges that match entire word.
+ * @param {boolean} includeRangeData optional - Whether to return range data.
+ * @param {boolean} includeRectData optional - Whether to return rectangle data.
+ *
+ * @returns a promise that will be resolved when search is completed, that may include:
+ * rangeData - serialized representation of ranges found.
+ * rectData - rect data of ranges found.
+ */
+ search(queryphrase, params) {
+ params.queryphrase = queryphrase;
+ return new Promise(resolve => {
+ this.runFindOperation(params, "CollectSearchResults").then((data) => {
+ resolve(data);
+ });
+ });
+ },
+
+ /**
+ * browser.find.highlightResults
+ * Highlights range(s) found in previous browser.find.search.
+ *
+ * @param {integer} rangeIndex -
+ * Found range to be highlighted held in API's ranges array for the tabId.
+ * Default highlights all ranges.
+ * @param {integer} tabId optional - Tab to highlight. Defaults to the active tab.
+ * @param {boolean} noScroll optional - Don't scroll to highlighted item.
+ *
+ * @returns a value describing the resulting status of the highlighting. This will be one of:
+ * 1 - success.
+ * 2 - index supplied was out of range.
+ * 3 - there were no search results to highlight.
+ */
+ highlightResults(params) {
+ return new Promise(resolve => {
+ this.runFindOperation(params, "HighlightSearchResults").then((data) => {
+ resolve(data);
+ });
+ });
+ },
+
+ /**
+ * browser.find.removeHighlighting
+ * Removes all hightlighting from previous search.
+ *
+ * @param {integer} tabId optional
+ * Tab to clear highlighting. Defaults to the active tab.
+ */
+ removeHighlighting(tabId) {
+ let tab = tabId ? tabTracker.getTab(tabId) : tabTracker.activeTab;
+ tab.linkedBrowser.messageManager.sendAsyncMessage("ext-Finder:clearHighlighting");
+ },
+
+ /**
+ * runFindOperation
+ * Utility for `search` and `highlightResults`.
+ */
+ 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);
+
+ if (!loadedFrameScripts[tabId]) {
+ mm.loadFrameScript("chrome://global/content/find-content.js", true, true);
+ loadedFrameScripts[tabId] = true;
+ }
+
+ return new Promise(resolve => {
+ mm.addMessageListener(`ext-Finder:${message}Finished`, function messageListener(message) {
+ mm.removeMessageListener(`ext-Finder:${message}Finished`, messageListener);
+ message.data.tabId = tabId;
+ resolve(message.data);
+ });
+ mm.sendAsyncMessage(`ext-Finder:${message}`, params);
+ });
+ },
+ },
+ };
+ }
+}
diff --git a/browser/components/extensions/schemas/find.json b/browser/components/extensions/schemas/find.json
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/schemas/find.json
@@ -0,0 +1,120 @@
+// 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
system.",
+ "permissions": ["find"],
+ "functions": [
+ {
+ "name": "search",
+ "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",
+ "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/toolkit/content/find-content.js b/toolkit/content/find-content.js
new file mode 100644
--- /dev/null
+++ b/toolkit/content/find-content.js
@@ -0,0 +1,205 @@
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+const { Finder } = Cu.import("resource://gre/modules/Finder.jsm", {});
+const { FinderHighlighter } = Cu.import("resource://gre/modules/FinderHighlighter.jsm", {});
+const { FinderIterator } = Cu.import("resource://gre/modules/FinderIterator.jsm", {});
+const _finder = new Finder(this.docShell);
+const _iterator = Object.assign({}, FinderIterator);
+const _highlighter = new FinderHighlighter(_finder);
+
+/**
+ * Native FinderIterator._collectFrames skips frames if they are scrolled out
+ * of viewport. Override with method that doesn't do that.
+ */
+_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, ..._iterator._collectFrames(frame));
+ }
+
+ return frames;
+}
+
+/**
+ * findRanges
+ *
+ * Performs a search which will cache found ranges in `_iterator._previousRanges`. Cached
+ * data can then be used by `highlightSearchResults`, `_collectRectData` and `_serializeRangeData`.
+ *
+ * @param {string} word - the text to search for.
+ * @param {boolean} caseSensitive - whether to use case sensitive matches.
+ * @param {boolean} caseSensitive - whether to match entire word.
+ * @param {boolean} includeRangeData - whether to collect and return range data.
+ * @param {boolean} searchString - whether to collect and return rect data.
+ *
+ * @returns a promise that will be resolved when search is completed.
+ */
+function findRanges(params) {
+ let { queryphrase, caseSensitive, entireWord, includeRangeData, includeRectData } = params;
+
+ _iterator.reset();
+
+ // Cast `caseSensitive` and `entireWord` to boolean, otherwise _iterator.start will throw.
+ let iteratorPromise = _iterator.start({ word: queryphrase,
+ caseSensitive: !!caseSensitive,
+ entireWord: !!entireWord,
+ finder: _finder,
+ listener: _finder });
+ iteratorPromise.then(() => {
+ // FinderIterator may have highlighted all results if FinderHighlighter listeners
+ // were still hanging around due to a native "Highlight All" action. Clear those now.
+ _highlighter.highlight(false);
+
+ let rangeData;
+ let rectData;
+ if (includeRangeData) {
+ rangeData = _serializeRangeData();
+ }
+ if (includeRectData) {
+ rectData = _collectRectData();
+ }
+ sendAsyncMessage("ext-Finder:CollectSearchResultsFinished",
+ { count: _iterator._previousRanges.length, rangeData, rectData });
+ });
+}
+
+/**
+ * _serializeRangeData
+ *
+ * Optionally returned by `findRanges`.
+ * Collects DOM data from ranges found on the most recent search made by `getSearchResults`
+ * and encodes it into a serializable form. Useful to extensions for custom UI presentation
+ * of search results, eg, getting surrounding context of search results.
+ *
+ * @returns {array} - serializable range data.
+ */
+function _serializeRangeData() {
+ let ranges = _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) {
+ while(node = walker.nextNode()) {
+ nodeCountWin++;
+ if (node == range.startContainer) {
+ break;
+ }
+ }
+ }
+ data.startTextNodePos = nodeCountWin;
+ data.startOffset = range.startOffset
+
+ if (range.startContainer != range.endContainer) {
+ while(node = walker.nextNode()) {
+ nodeCountWin++;
+ if (node == range.endContainer) {
+ break;
+ }
+ }
+ }
+ 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 `getSearchResults`.
+ * Useful to extensions for custom highlighting of search results.
+ *
+ * @returns {array} rectData - serializable rect data.
+ */
+function _collectRectData() {
+ let rectData = [];
+
+ let ranges = _iterator._previousRanges;
+ for (let range of ranges) {
+ let rectsAndTexts = _highlighter._getRangeRectsAndTexts(range);
+ rectData.push({ text: range.toString(), rectsAndTexts })
+ }
+
+ return rectData;
+}
+
+function highlightSearchResults(params) {
+ let { rangeIndex, noScroll } = params;
+
+ _highlighter.highlight(false);
+ let ranges = _iterator._previousRanges;
+
+ let status = 1;
+
+ if (ranges.length) {
+ if (typeof rangeIndex == "number") {
+ if (rangeIndex < ranges.length) {
+ let foundRange = ranges[rangeIndex];
+ _highlighter.highlightRange(foundRange);
+
+ if (!noScroll) {
+ let node = foundRange.startContainer;
+ let editableNode = _highlighter._getEditableNode(node);
+ let controller = editableNode ? editableNode.editor.selectionController :
+ _finder._getSelectionController(node.ownerDocument.defaultView);
+
+ controller.scrollSelectionIntoView( controller.SELECTION_FIND,
+ controller.SELECTION_ON,
+ controller.SCROLL_CENTER_VERTICALLY);
+ }
+ } else {
+ status = 2;
+ }
+ } else {
+ for (let range of ranges) {
+ _highlighter.highlightRange(range);
+ }
+ }
+ } else {
+ status = 3
+ }
+ sendAsyncMessage("ext-Finder:HighlightSearchResultsFinished", { status });
+}
+
+addMessageListener("ext-Finder:CollectSearchResults", function({ data }) {
+ findRanges(data);
+});
+
+addMessageListener("ext-Finder:HighlightSearchResults", function({ data }) {
+ highlightSearchResults(data);
+});
+
+addMessageListener("ext-Finder:clearHighlighting", function() {
+ _highlighter.highlight(false);
+});