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