1/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
2/* vim: set sts=2 sw=2 et tw=80: */
3/* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6"use strict";
7
8var EXPORTED_SYMBOLS = ["FindContent"];
9
10/* exported FindContent */
11
12ChromeUtils.defineModuleGetter(
13  this,
14  "FinderIterator",
15  "resource://gre/modules/FinderIterator.jsm"
16);
17
18ChromeUtils.defineModuleGetter(
19  this,
20  "FinderHighlighter",
21  "resource://gre/modules/FinderHighlighter.jsm"
22);
23
24class FindContent {
25  constructor(docShell) {
26    const { Finder } = ChromeUtils.import("resource://gre/modules/Finder.jsm");
27    this.finder = new Finder(docShell);
28  }
29
30  get iterator() {
31    if (!this._iterator) {
32      this._iterator = new FinderIterator();
33    }
34    return this._iterator;
35  }
36
37  get highlighter() {
38    if (!this._highlighter) {
39      this._highlighter = new FinderHighlighter(this.finder, true);
40    }
41    return this._highlighter;
42  }
43
44  /**
45   * findRanges
46   *
47   * Performs a search which will cache found ranges in `iterator._previousRanges`.  Cached
48   * data can then be used by `highlightResults`, `_collectRectData` and `_serializeRangeData`.
49   *
50   * @param {object} params - the params.
51   * @param {string} queryphrase - the text to search for.
52   * @param {boolean} caseSensitive - whether to use case sensitive matches.
53   * @param {boolean} includeRangeData - whether to collect and return range data.
54   * @param {boolean} matchDiacritics - whether diacritics must match.
55   * @param {boolean} searchString - whether to collect and return rect data.
56   *
57   * @returns {object} that includes:
58   *   {number} count - number of results found.
59   *   {array} rangeData (if opted) - serialized representation of ranges found.
60   *   {array} rectData (if opted) - rect data of ranges found.
61   */
62  findRanges(params) {
63    return new Promise(resolve => {
64      let {
65        queryphrase,
66        caseSensitive,
67        entireWord,
68        includeRangeData,
69        includeRectData,
70        matchDiacritics,
71      } = params;
72
73      this.iterator.reset();
74
75      // Cast `caseSensitive` and `entireWord` to boolean, otherwise _iterator.start will throw.
76      let iteratorPromise = this.iterator.start({
77        word: queryphrase,
78        caseSensitive: !!caseSensitive,
79        entireWord: !!entireWord,
80        finder: this.finder,
81        listener: this.finder,
82        matchDiacritics: !!matchDiacritics,
83        useSubFrames: false,
84      });
85
86      iteratorPromise.then(() => {
87        let rangeData;
88        let rectData;
89        if (includeRangeData) {
90          rangeData = this._serializeRangeData();
91        }
92        if (includeRectData) {
93          rectData = this._collectRectData();
94        }
95
96        resolve({
97          count: this.iterator._previousRanges.length,
98          rangeData,
99          rectData,
100        });
101      });
102    });
103  }
104
105  /**
106   * _serializeRangeData
107   *
108   * Optionally returned by `findRanges`.
109   * Collects DOM data from ranges found on the most recent search made by `findRanges`
110   * and encodes it into a serializable form.  Useful to extensions for custom UI presentation
111   * of search results, eg, getting surrounding context of results.
112   *
113   * @returns {array} - serializable range data.
114   */
115  _serializeRangeData() {
116    let ranges = this.iterator._previousRanges;
117
118    let rangeData = [];
119    let nodeCountWin = 0;
120    let lastDoc;
121    let walker;
122    let node;
123
124    for (let range of ranges) {
125      let startContainer = range.startContainer;
126      let doc = startContainer.ownerDocument;
127
128      if (lastDoc !== doc) {
129        walker = doc.createTreeWalker(
130          doc,
131          doc.defaultView.NodeFilter.SHOW_TEXT,
132          null,
133          false
134        );
135        // Get first node.
136        node = walker.nextNode();
137        // Reset node count.
138        nodeCountWin = 0;
139      }
140      lastDoc = doc;
141
142      // The framePos will be set by the parent process later.
143      let data = { framePos: 0, text: range.toString() };
144      rangeData.push(data);
145
146      if (node != range.startContainer) {
147        node = walker.nextNode();
148        while (node) {
149          nodeCountWin++;
150          if (node == range.startContainer) {
151            break;
152          }
153          node = walker.nextNode();
154        }
155      }
156      data.startTextNodePos = nodeCountWin;
157      data.startOffset = range.startOffset;
158
159      if (range.startContainer != range.endContainer) {
160        node = walker.nextNode();
161        while (node) {
162          nodeCountWin++;
163          if (node == range.endContainer) {
164            break;
165          }
166          node = walker.nextNode();
167        }
168      }
169      data.endTextNodePos = nodeCountWin;
170      data.endOffset = range.endOffset;
171    }
172
173    return rangeData;
174  }
175
176  /**
177   * _collectRectData
178   *
179   * Optionally returned by `findRanges`.
180   * Collects rect data of ranges found by most recent search made by `findRanges`.
181   * Useful to extensions for custom highlighting of search results.
182   *
183   * @returns {array} rectData - serializable rect data.
184   */
185  _collectRectData() {
186    let rectData = [];
187
188    let ranges = this.iterator._previousRanges;
189    for (let range of ranges) {
190      let rectsAndTexts = this.highlighter._getRangeRectsAndTexts(range);
191      rectData.push({ text: range.toString(), rectsAndTexts });
192    }
193
194    return rectData;
195  }
196
197  /**
198   * highlightResults
199   *
200   * Highlights range(s) found in previous browser.find.find.
201   *
202   * @param {object} params - may contain any of the following properties:
203   *   all of which are optional:
204   *   {number} rangeIndex -
205   *            Found range to be highlighted held in API's ranges array for the tabId.
206   *            Default highlights all ranges.
207   *   {number} tabId - Tab to highlight.  Defaults to the active tab.
208   *   {boolean} noScroll - Don't scroll to highlighted item.
209   *
210   * @returns {string} - a string describing the resulting status of the highlighting,
211   *   which will be used as criteria for resolving or rejecting the promise.
212   *   This can be:
213   *   "Success" - Highlighting succeeded.
214   *   "OutOfRange" - The index supplied was out of range.
215   *   "NoResults" - There were no search results to highlight.
216   */
217  highlightResults(params) {
218    let { rangeIndex, noScroll } = params;
219
220    this.highlighter.highlight(false);
221    let ranges = this.iterator._previousRanges;
222
223    let status = "Success";
224
225    if (ranges.length) {
226      if (typeof rangeIndex == "number") {
227        if (rangeIndex < ranges.length) {
228          let foundRange = ranges[rangeIndex];
229          this.highlighter.highlightRange(foundRange);
230
231          if (!noScroll) {
232            let node = foundRange.startContainer;
233            let editableNode = this.highlighter._getEditableNode(node);
234            let controller = editableNode
235              ? editableNode.editor.selectionController
236              : this.finder._getSelectionController(node.ownerGlobal);
237
238            controller.scrollSelectionIntoView(
239              controller.SELECTION_FIND,
240              controller.SELECTION_ON,
241              controller.SCROLL_CENTER_VERTICALLY
242            );
243          }
244        } else {
245          status = "OutOfRange";
246        }
247      } else {
248        for (let range of ranges) {
249          this.highlighter.highlightRange(range);
250        }
251      }
252    } else {
253      status = "NoResults";
254    }
255
256    return status;
257  }
258}
259