Bug 1838629 - Part 7: Inline nursery cell allocation method in Allocator.h r=sfink
[gecko.git] / toolkit / modules / FinderHighlighter.sys.mjs
blob41a9db805222823bf2d5f7e3ee66410c0146c839
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
7 const lazy = {};
9 ChromeUtils.defineESModuleGetters(lazy, {
10   Color: "resource://gre/modules/Color.sys.mjs",
11   Rect: "resource://gre/modules/Geometry.sys.mjs",
12 });
13 XPCOMUtils.defineLazyGetter(lazy, "kDebug", () => {
14   const kDebugPref = "findbar.modalHighlight.debug";
15   return (
16     Services.prefs.getPrefType(kDebugPref) &&
17     Services.prefs.getBoolPref(kDebugPref)
18   );
19 });
21 const kContentChangeThresholdPx = 5;
22 const kBrightTextSampleSize = 5;
23 // This limit is arbitrary and doesn't scale for low-powered machines or
24 // high-powered machines. Netbooks will probably need a much lower limit, for
25 // example. Though getting something out there is better than nothing.
26 const kPageIsTooBigPx = 500000;
27 const kModalHighlightRepaintLoFreqMs = 100;
28 const kModalHighlightRepaintHiFreqMs = 16;
29 const kHighlightAllPref = "findbar.highlightAll";
30 const kModalHighlightPref = "findbar.modalHighlight";
31 const kFontPropsCSS = [
32   "color",
33   "font-family",
34   "font-kerning",
35   "font-size",
36   "font-size-adjust",
37   "font-stretch",
38   "font-variant",
39   "font-weight",
40   "line-height",
41   "letter-spacing",
42   "text-emphasis",
43   "text-orientation",
44   "text-transform",
45   "word-spacing",
47 const kFontPropsCamelCase = kFontPropsCSS.map(prop => {
48   let parts = prop.split("-");
49   return (
50     parts.shift() +
51     parts.map(part => part.charAt(0).toUpperCase() + part.slice(1)).join("")
52   );
53 });
54 const kRGBRE = /^rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*/i;
55 // This uuid is used to prefix HTML element IDs in order to make them unique and
56 // hard to clash with IDs content authors come up with.
57 const kModalIdPrefix = "cedee4d0-74c5-4f2d-ab43-4d37c0f9d463";
58 const kModalOutlineId = kModalIdPrefix + "-findbar-modalHighlight-outline";
59 const kOutlineBoxColor = "255,197,53";
60 const kOutlineBoxBorderSize = 1;
61 const kOutlineBoxBorderRadius = 2;
62 const kModalStyles = {
63   outlineNode: [
64     ["background-color", `rgb(${kOutlineBoxColor})`],
65     ["background-clip", "padding-box"],
66     ["border", `${kOutlineBoxBorderSize}px solid rgba(${kOutlineBoxColor},.7)`],
67     ["border-radius", `${kOutlineBoxBorderRadius}px`],
68     ["box-shadow", `0 2px 0 0 rgba(0,0,0,.1)`],
69     ["color", "#000"],
70     ["display", "flex"],
71     [
72       "margin",
73       `-${kOutlineBoxBorderSize}px 0 0 -${kOutlineBoxBorderSize}px !important`,
74     ],
75     ["overflow", "hidden"],
76     ["pointer-events", "none"],
77     ["position", "absolute"],
78     ["white-space", "nowrap"],
79     ["will-change", "transform"],
80     ["z-index", 2],
81   ],
82   outlineNodeDebug: [["z-index", 2147483647]],
83   outlineText: [
84     ["margin", "0 !important"],
85     ["padding", "0 !important"],
86     ["vertical-align", "top !important"],
87   ],
88   maskNode: [
89     ["background", "rgba(0,0,0,.25)"],
90     ["pointer-events", "none"],
91     ["position", "absolute"],
92     ["z-index", 1],
93   ],
94   maskNodeTransition: [["transition", "background .2s ease-in"]],
95   maskNodeDebug: [
96     ["z-index", 2147483646],
97     ["top", 0],
98     ["left", 0],
99   ],
100   maskNodeBrightText: [["background", "rgba(255,255,255,.25)"]],
102 const kModalOutlineAnim = {
103   keyframes: [
104     { transform: "scaleX(1) scaleY(1)" },
105     { transform: "scaleX(1.5) scaleY(1.5)", offset: 0.5, easing: "ease-in" },
106     { transform: "scaleX(1) scaleY(1)" },
107   ],
108   duration: 50,
110 const kNSHTML = "http://www.w3.org/1999/xhtml";
111 const kRepaintSchedulerStopped = 1;
112 const kRepaintSchedulerPaused = 2;
113 const kRepaintSchedulerRunning = 3;
115 function mockAnonymousContentNode(domNode) {
116   return {
117     setTextContentForElement(id, text) {
118       (domNode.querySelector("#" + id) || domNode).textContent = text;
119     },
120     getAttributeForElement(id, attrName) {
121       let node = domNode.querySelector("#" + id) || domNode;
122       if (!node.hasAttribute(attrName)) {
123         return undefined;
124       }
125       return node.getAttribute(attrName);
126     },
127     setAttributeForElement(id, attrName, attrValue) {
128       (domNode.querySelector("#" + id) || domNode).setAttribute(
129         attrName,
130         attrValue
131       );
132     },
133     removeAttributeForElement(id, attrName) {
134       let node = domNode.querySelector("#" + id) || domNode;
135       if (!node.hasAttribute(attrName)) {
136         return;
137       }
138       node.removeAttribute(attrName);
139     },
140     remove() {
141       try {
142         domNode.remove();
143       } catch (ex) {}
144     },
145     setAnimationForElement(id, keyframes, duration) {
146       return (domNode.querySelector("#" + id) || domNode).animate(
147         keyframes,
148         duration
149       );
150     },
151     setCutoutRectsForElement(id, rects) {
152       // no-op for now.
153     },
154   };
157 let gWindows = new WeakMap();
160  * FinderHighlighter class that is used by Finder.sys.mjs to take care of the
161  * 'Highlight All' feature, which can highlight all find occurrences in a page.
163  * @param {Finder} finder Finder.sys.mjs instance
164  * @param {boolean} useTop check and use top-level windows for rectangle
165  *                         computation, if possible.
166  */
167 export function FinderHighlighter(finder, useTop = false) {
168   this._highlightAll = Services.prefs.getBoolPref(kHighlightAllPref);
169   this._modal = Services.prefs.getBoolPref(kModalHighlightPref);
170   this._useSubFrames = false;
171   this._useTop = useTop;
172   this._marksListener = null;
173   this._testing = false;
174   this.finder = finder;
177 FinderHighlighter.prototype = {
178   get iterator() {
179     return this.finder.iterator;
180   },
182   enableTesting(enable) {
183     this._testing = enable;
184   },
186   // Get the top-most window when allowed. When out-of-process frames are used,
187   // this will usually be the same as the passed-in window. The checkUseTop
188   // argument can be used to instead check the _useTop flag which can be used
189   // to enable rectangle coordinate checks.
190   getTopWindow(window, checkUseTop) {
191     if (this._useSubFrames || (checkUseTop && this._useTop)) {
192       try {
193         return window.top;
194       } catch (ex) {}
195     }
197     return window;
198   },
200   useModal() {
201     // Modal highlighting is currently only enabled when there are no
202     // out-of-process subframes.
203     return this._modal && this._useSubFrames;
204   },
206   /**
207    * Each window is unique, globally, and the relation between an active
208    * highlighting session and a window is 1:1.
209    * For each window we track a number of properties which _at least_ consist of
210    *  - {Boolean} detectedGeometryChange Whether the geometry of the found ranges'
211    *                                     rectangles has changed substantially
212    *  - {Set}     dynamicRangesSet       Set of ranges that may move around, depending
213    *                                     on page layout changes and user input
214    *  - {Map}     frames                 Collection of frames that were encountered
215    *                                     when inspecting the found ranges
216    *  - {Map}     modalHighlightRectsMap Collection of ranges and their corresponding
217    *                                     Rects and texts
218    *
219    * @param  {nsIDOMWindow} window
220    * @return {Object}
221    */
222   getForWindow(window, propName = null) {
223     if (!gWindows.has(window)) {
224       gWindows.set(window, {
225         detectedGeometryChange: false,
226         dynamicRangesSet: new Set(),
227         frames: new Map(),
228         lastWindowDimensions: { width: 0, height: 0 },
229         modalHighlightRectsMap: new Map(),
230         previousRangeRectsAndTexts: { rectList: [], textList: [] },
231         repaintSchedulerState: kRepaintSchedulerStopped,
232       });
233     }
234     return gWindows.get(window);
235   },
237   /**
238    * Notify all registered listeners that the 'Highlight All' operation finished.
239    *
240    * @param {Boolean} highlight Whether highlighting was turned on
241    */
242   notifyFinished(highlight) {
243     for (let l of this.finder._listeners) {
244       try {
245         l.onHighlightFinished(highlight);
246       } catch (ex) {}
247     }
248   },
250   /**
251    * Toggle highlighting all occurrences of a word in a page. This method will
252    * be called recursively for each (i)frame inside a page.
253    *
254    * @param {Booolean} highlight    Whether highlighting should be turned on
255    * @param {String}   word         Needle to search for and highlight when found
256    * @param {Boolean}  linksOnly    Only consider nodes that are links for the search
257    * @param {Boolean}  drawOutline  Whether found links should be outlined.
258    * @param {Boolean}  useSubFrames Whether to iterate over subframes.
259    * @yield {Promise}  that resolves once the operation has finished
260    */
261   async highlight(highlight, word, linksOnly, drawOutline, useSubFrames) {
262     let window = this.finder._getWindow();
263     let dict = this.getForWindow(window);
264     let controller = this.finder._getSelectionController(window);
265     let doc = window.document;
266     this._found = false;
267     this._useSubFrames = useSubFrames;
269     let result = { searchString: word, highlight, found: false };
271     if (!controller || !doc || !doc.documentElement) {
272       // Without the selection controller,
273       // we are unable to (un)highlight any matches
274       return result;
275     }
277     if (highlight) {
278       let params = {
279         allowDistance: 1,
280         caseSensitive: this.finder._fastFind.caseSensitive,
281         entireWord: this.finder._fastFind.entireWord,
282         linksOnly,
283         word,
284         finder: this.finder,
285         listener: this,
286         matchDiacritics: this.finder._fastFind.matchDiacritics,
287         useCache: true,
288         useSubFrames,
289         window,
290       };
291       if (
292         this.iterator.isAlreadyRunning(params) ||
293         (this.useModal() &&
294           this.iterator._areParamsEqual(params, dict.lastIteratorParams))
295       ) {
296         return result;
297       }
299       if (!this.useModal()) {
300         dict.visible = true;
301       }
302       await this.iterator.start(params);
303       if (this._found) {
304         this.finder._outlineLink(drawOutline);
305       }
306     } else {
307       this.hide(window);
309       // Removing the highlighting always succeeds, so return true.
310       this._found = true;
311     }
313     result.found = this._found;
314     this.notifyFinished(result);
315     return result;
316   },
318   // FinderIterator listener implementation
320   onIteratorRangeFound(range) {
321     this.highlightRange(range);
322     this._found = true;
323   },
325   onIteratorReset() {},
327   onIteratorRestart() {
328     this.clear(this.finder._getWindow());
329   },
331   onIteratorStart(params) {
332     let window = this.finder._getWindow();
333     let dict = this.getForWindow(window);
334     // Save a clean params set for use later in the `update()` method.
335     dict.lastIteratorParams = params;
336     if (!this.useModal()) {
337       this.hide(window, this.finder._fastFind.getFoundRange());
338     }
339     this.clear(window);
340   },
342   /**
343    * Add a range to the find selection, i.e. highlight it, and if it's inside an
344    * editable node, track it.
345    *
346    * @param {Range} range Range object to be highlighted
347    */
348   highlightRange(range) {
349     let node = range.startContainer;
350     let editableNode = this._getEditableNode(node);
351     let window = node.ownerGlobal;
352     let controller = this.finder._getSelectionController(window);
353     if (editableNode) {
354       controller = editableNode.editor.selectionController;
355     }
357     if (this.useModal()) {
358       this._modalHighlight(range, controller, window);
359     } else {
360       let findSelection = controller.getSelection(
361         Ci.nsISelectionController.SELECTION_FIND
362       );
363       findSelection.addRange(range);
364       // Check if the range is inside an (i)frame.
365       if (window != this.getTopWindow(window)) {
366         let dict = this.getForWindow(this.getTopWindow(window));
367         // Add this frame to the list, so that we'll be able to find it later
368         // when we need to clear its selection(s).
369         dict.frames.set(window, {});
370       }
371     }
373     if (editableNode) {
374       // Highlighting added, so cache this editor, and hook up listeners
375       // to ensure we deal properly with edits within the highlighting
376       this._addEditorListeners(editableNode.editor);
377     }
378   },
380   /**
381    * If modal highlighting is enabled, show the dimmed background that will overlay
382    * the page.
383    *
384    * @param {nsIDOMWindow} window The dimmed background will overlay this window.
385    *                              Optional, defaults to the finder window.
386    */
387   show(window = null) {
388     window = this.getTopWindow(window || this.finder._getWindow());
389     let dict = this.getForWindow(window);
390     if (!this.useModal() || dict.visible) {
391       return;
392     }
394     dict.visible = true;
396     this._maybeCreateModalHighlightNodes(window);
397     this._addModalHighlightListeners(window);
398   },
400   /**
401    * Clear all highlighted matches. If modal highlighting is enabled and
402    * the outline + dimmed background is currently visible, both will be hidden.
403    *
404    * @param {nsIDOMWindow} window    The dimmed background will overlay this window.
405    *                                 Optional, defaults to the finder window.
406    * @param {Range}        skipRange A range that should not be removed from the
407    *                                 find selection.
408    * @param {Event}        event     When called from an event handler, this will
409    *                                 be the triggering event.
410    */
411   hide(window, skipRange = null, event = null) {
412     try {
413       window = this.getTopWindow(window);
414     } catch (ex) {
415       console.error(ex);
416       return;
417     }
418     let dict = this.getForWindow(window);
420     let isBusySelecting = dict.busySelecting;
421     dict.busySelecting = false;
422     // Do not hide on anything but a left-click.
423     if (
424       event &&
425       event.type == "click" &&
426       (event.button !== 0 ||
427         event.altKey ||
428         event.ctrlKey ||
429         event.metaKey ||
430         event.shiftKey ||
431         event.relatedTarget ||
432         isBusySelecting ||
433         (event.target.localName == "a" && event.target.href))
434     ) {
435       return;
436     }
438     this._clearSelection(
439       this.finder._getSelectionController(window),
440       skipRange
441     );
442     for (let frame of dict.frames.keys()) {
443       this._clearSelection(
444         this.finder._getSelectionController(frame),
445         skipRange
446       );
447     }
449     // Next, check our editor cache, for editors belonging to this
450     // document
451     if (this._editors) {
452       let doc = window.document;
453       for (let x = this._editors.length - 1; x >= 0; --x) {
454         if (this._editors[x].document == doc) {
455           this._clearSelection(this._editors[x].selectionController, skipRange);
456           // We don't need to listen to this editor any more
457           this._unhookListenersAtIndex(x);
458         }
459       }
460     }
462     if (dict.modalRepaintScheduler) {
463       window.clearTimeout(dict.modalRepaintScheduler);
464       dict.modalRepaintScheduler = null;
465       dict.repaintSchedulerState = kRepaintSchedulerStopped;
466     }
467     dict.lastWindowDimensions = { width: 0, height: 0 };
469     this._removeRangeOutline(window);
470     this._removeHighlightAllMask(window);
471     this._removeModalHighlightListeners(window);
473     dict.visible = false;
474   },
476   /**
477    * Called by the Finder after a find result comes in; update the position and
478    * content of the outline to the newly found occurrence.
479    * To make sure that the outline covers the found range completely, all the
480    * CSS styles that influence the text are copied and applied to the outline.
481    *
482    * @param {Object} data Dictionary coming from Finder that contains the
483    *                      following properties:
484    *   {Number}  result        One of the nsITypeAheadFind.FIND_* constants
485    *                           indicating the result of a search operation.
486    *   {Boolean} findBackwards If TRUE, the search was performed backwards,
487    *                           FALSE if forwards.
488    *   {Boolean} findAgain     If TRUE, the search was performed using the same
489    *                           search string as before.
490    *   {String}  linkURL       If a link was hit, this will contain a URL string.
491    *   {Rect}    rect          An object with top, left, width and height
492    *                           coordinates of the current selection.
493    *   {String}  searchString  The string the search was performed with.
494    *   {Boolean} storeResult   Indicator if the search string should be stored
495    *                           by the consumer of the Finder.
496    */
497   async update(data, foundInThisFrame) {
498     let window = this.finder._getWindow();
499     let dict = this.getForWindow(window);
500     let foundRange = this.finder._fastFind.getFoundRange();
502     if (
503       data.result == Ci.nsITypeAheadFind.FIND_NOTFOUND ||
504       !data.searchString ||
505       (foundInThisFrame && !foundRange)
506     ) {
507       this.hide(window);
508       return;
509     }
511     this._useSubFrames = data.useSubFrames;
512     if (!this.useModal()) {
513       if (this._highlightAll) {
514         dict.previousFoundRange = dict.currentFoundRange;
515         dict.currentFoundRange = foundRange;
516         let params = this.iterator.params;
517         if (
518           dict.visible &&
519           this.iterator._areParamsEqual(params, dict.lastIteratorParams)
520         ) {
521           return;
522         }
523         if (!dict.visible && !params) {
524           params = { word: data.searchString, linksOnly: data.linksOnly };
525         }
526         if (params) {
527           await this.highlight(
528             true,
529             params.word,
530             params.linksOnly,
531             params.drawOutline,
532             data.useSubFrames
533           );
534         }
535       }
536       return;
537     }
539     dict.animateOutline = true;
540     // Immediately finish running animations, if any.
541     this._finishOutlineAnimations(dict);
543     if (foundRange !== dict.currentFoundRange || data.findAgain) {
544       dict.previousFoundRange = dict.currentFoundRange;
545       dict.currentFoundRange = foundRange;
547       if (!dict.visible) {
548         this.show(window);
549       } else {
550         this._maybeCreateModalHighlightNodes(window);
551       }
552     }
554     if (this._highlightAll) {
555       await this.highlight(
556         true,
557         data.searchString,
558         data.linksOnly,
559         data.drawOutline,
560         data.useSubFrames
561       );
562     }
563   },
565   /**
566    * Invalidates the list by clearing the map of highlighted ranges that we
567    * keep to build the mask for.
568    */
569   clear(window = null) {
570     if (!window || !this.getTopWindow(window)) {
571       return;
572     }
574     let dict = this.getForWindow(this.getTopWindow(window));
575     this._finishOutlineAnimations(dict);
576     dict.dynamicRangesSet.clear();
577     dict.frames.clear();
578     dict.modalHighlightRectsMap.clear();
579     dict.brightText = null;
580   },
582   /**
583    * Removes the outline from a single window. This is done when
584    * switching the current search to a new frame.
585    */
586   clearCurrentOutline(window = null) {
587     let dict = this.getForWindow(this.getTopWindow(window));
588     this._finishOutlineAnimations(dict);
589     this._removeRangeOutline(window);
590   },
592   // Update the tick marks that should appear on the page's scrollbar(s).
593   updateScrollMarks() {
594     // Only show scrollbar marks when normal highlighting is enabled.
595     if (this.useModal() || !this._highlightAll) {
596       this.removeScrollMarks();
597       return;
598     }
600     let marks = new Set(); // Use a set so duplicate values are removed.
601     let window = this.finder._getWindow();
602     // Show the marks on the horizontal scrollbar for vertical writing modes.
603     let onHorizontalScrollbar = !window
604       .getComputedStyle(window.document.body || window.document.documentElement)
605       .writingMode.startsWith("horizontal");
606     let yStart = window.scrollY - window.scrollMinY;
607     let xStart = window.scrollX - window.scrollMinX;
609     let hasRanges = false;
610     if (window) {
611       let controllers = [this.finder._getSelectionController(window)];
612       let editors = this.editors;
613       if (editors) {
614         // Add the selection controllers from any input fields.
615         controllers.push(...editors.map(editor => editor.selectionController));
616       }
618       for (let controller of controllers) {
619         let findSelection = controller.getSelection(
620           Ci.nsISelectionController.SELECTION_FIND
621         );
623         let rangeCount = findSelection.rangeCount;
624         if (rangeCount > 0) {
625           hasRanges = true;
626         }
628         // No need to calculate the mark positions if there is no visible scrollbar.
629         if (window.scrollMaxY > window.scrollMinY && !onHorizontalScrollbar) {
630           // Use the body's scrollHeight if available.
631           let scrollHeight =
632             window.document.body?.scrollHeight ||
633             window.document.documentElement.scrollHeight;
634           let yAdj = (window.scrollMaxY - window.scrollMinY) / scrollHeight;
636           for (let r = 0; r < rangeCount; r++) {
637             let rect = findSelection.getRangeAt(r).getBoundingClientRect();
638             let yPos = Math.round((yStart + rect.y + rect.height / 2) * yAdj); // use the midpoint
639             marks.add(yPos);
640           }
641         } else if (
642           window.scrollMaxX > window.scrollMinX &&
643           onHorizontalScrollbar
644         ) {
645           // Use the body's scrollWidth if available.
646           let scrollWidth =
647             window.document.body?.scrollWidth ||
648             window.document.documentElement.scrollWidth;
649           let xAdj = (window.scrollMaxX - window.scrollMinX) / scrollWidth;
651           for (let r = 0; r < rangeCount; r++) {
652             let rect = findSelection.getRangeAt(r).getBoundingClientRect();
653             let xPos = Math.round((xStart + rect.x + rect.width / 2) * xAdj);
654             marks.add(xPos);
655           }
656         }
657       }
658     }
660     if (hasRanges) {
661       // Assign the marks to the window and add a listener for the MozScrolledAreaChanged
662       // event which fires whenever the scrollable area's size is updated.
663       this.setScrollMarks(window, Array.from(marks), onHorizontalScrollbar);
665       if (!this._marksListener) {
666         this._marksListener = event => {
667           this.updateScrollMarks();
668         };
670         window.addEventListener(
671           "MozScrolledAreaChanged",
672           this._marksListener,
673           true
674         );
675         window.addEventListener("resize", this._marksListener);
676       }
677     } else if (this._marksListener) {
678       // No results were found so remove any existing ones and the MozScrolledAreaChanged listener.
679       this.removeScrollMarks();
680     }
681   },
683   removeScrollMarks() {
684     let window;
685     try {
686       window = this.finder._getWindow();
687     } catch (ex) {
688       // An exception can happen after changing remoteness but this
689       // would have deleted the marks anyway.
690       return;
691     }
693     if (this._marksListener) {
694       window.removeEventListener(
695         "MozScrolledAreaChanged",
696         this._marksListener,
697         true
698       );
699       window.removeEventListener("resize", this._marksListener);
700       this._marksListener = null;
701     }
702     this.setScrollMarks(window, []);
703   },
705   /**
706    * Set the scrollbar marks for a current search. If testing mode is enabled, fire a
707    * find-scrollmarks-changed event at the window.
708    *
709    * @param window window to set the scrollbar marks on
710    * @param marks array of integer scrollbar mark positions
711    * @param onHorizontalScrollbar whether to display the marks on the horizontal scrollbar
712    */
713   setScrollMarks(window, marks, onHorizontalScrollbar = false) {
714     window.setScrollMarks(marks, onHorizontalScrollbar);
716     // Fire an event containing the found mark values if testing mode is enabled.
717     if (this._testing) {
718       window.dispatchEvent(
719         new CustomEvent("find-scrollmarks-changed", {
720           detail: {
721             marks: Array.from(marks),
722             onHorizontalScrollbar,
723           },
724         })
725       );
726     }
727   },
729   /**
730    * When the current page is refreshed or navigated away from, the CanvasFrame
731    * contents is not valid anymore, i.e. all anonymous content is destroyed.
732    * We need to clear the references we keep, which'll make sure we redraw
733    * everything when the user starts to find in page again.
734    */
735   onLocationChange() {
736     let window = this.finder._getWindow();
737     if (!window || !this.getTopWindow(window)) {
738       return;
739     }
740     this.hide(window);
741     this.clear(window);
742     this._removeRangeOutline(window);
744     gWindows.delete(this.getTopWindow(window));
745   },
747   /**
748    * When `kModalHighlightPref` pref changed during a session, this callback is
749    * invoked. When modal highlighting is turned off, we hide the CanvasFrame
750    * contents.
751    *
752    * @param {Boolean} useModalHighlight
753    */
754   onModalHighlightChange(useModalHighlight) {
755     let window = this.finder._getWindow();
756     if (window && this.useModal() && !useModalHighlight) {
757       this.hide(window);
758       this.clear(window);
759     }
760     this._modal = useModalHighlight;
761     this.updateScrollMarks();
762   },
764   /**
765    * When 'Highlight All' is toggled during a session, this callback is invoked
766    * and when it's turned off, the found occurrences will be removed from the mask.
767    *
768    * @param {Boolean} highlightAll
769    */
770   onHighlightAllChange(highlightAll) {
771     this._highlightAll = highlightAll;
772     if (!highlightAll) {
773       let window = this.finder._getWindow();
774       if (!this.useModal()) {
775         this.hide(window);
776       }
777       this.clear(window);
778       this._scheduleRepaintOfMask(window);
779     }
781     this.updateScrollMarks();
782   },
784   /**
785    * Utility; removes all ranges from the find selection that belongs to a
786    * controller. Optionally skips a specific range.
787    *
788    * @param  {nsISelectionController} controller
789    * @param  {Range}                  restoreRange
790    */
791   _clearSelection(controller, restoreRange = null) {
792     if (!controller) {
793       return;
794     }
795     let sel = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND);
796     sel.removeAllRanges();
797     if (restoreRange) {
798       sel = controller.getSelection(Ci.nsISelectionController.SELECTION_NORMAL);
799       sel.addRange(restoreRange);
800       controller.setDisplaySelection(
801         Ci.nsISelectionController.SELECTION_ATTENTION
802       );
803       controller.repaintSelection(Ci.nsISelectionController.SELECTION_NORMAL);
804     }
805   },
807   /**
808    * Utility; get the nsIDOMWindowUtils for a window.
809    *
810    * @param  {nsIDOMWindow} window Optional, defaults to the finder window.
811    * @return {nsIDOMWindowUtils}
812    */
813   _getDWU(window = null) {
814     return (window || this.finder._getWindow()).windowUtils;
815   },
817   /**
818    * Utility; returns the bounds of the page relative to the viewport.
819    * If the pages is part of a frameset or inside an iframe of any kind, its
820    * offset is accounted for.
821    * Geometry.sys.mjs takes care of the DOMRect calculations.
822    *
823    * @param  {nsIDOMWindow} window          Window to read the boundary rect from
824    * @param  {Boolean}      [includeScroll] Whether to ignore the scroll offset,
825    *                                        which is useful for comparing DOMRects.
826    *                                        Optional, defaults to `true`
827    * @return {Rect}
828    */
829   _getRootBounds(window, includeScroll = true) {
830     let dwu = this._getDWU(this.getTopWindow(window, true));
831     let cssPageRect = lazy.Rect.fromRect(dwu.getRootBounds());
832     let scrollX = {};
833     let scrollY = {};
834     if (includeScroll && window == this.getTopWindow(window, true)) {
835       dwu.getScrollXY(false, scrollX, scrollY);
836       cssPageRect.translate(scrollX.value, scrollY.value);
837     }
839     // If we're in a frame, update the position of the rect (top/ left).
840     let currWin = window;
841     while (currWin != this.getTopWindow(window, true)) {
842       let frameOffsets = this._getFrameElementOffsets(currWin);
843       cssPageRect.translate(frameOffsets.x, frameOffsets.y);
845       // Since the frame is an element inside a parent window, we'd like to
846       // learn its position relative to it.
847       let el = currWin.browsingContext.embedderElement;
848       currWin = currWin.parent;
849       dwu = this._getDWU(currWin);
850       let parentRect = lazy.Rect.fromRect(dwu.getBoundsWithoutFlushing(el));
852       if (includeScroll) {
853         dwu.getScrollXY(false, scrollX, scrollY);
854         parentRect.translate(scrollX.value, scrollY.value);
855         // If the current window is an iframe with scrolling="no" and its parent
856         // is also an iframe the scroll offsets from the parents' documentElement
857         // (inverse scroll position) needs to be subtracted from the parent
858         // window rect.
859         if (
860           el.getAttribute("scrolling") == "no" &&
861           currWin != this.getTopWindow(window, true)
862         ) {
863           let docEl = currWin.document.documentElement;
864           parentRect.translate(-docEl.scrollLeft, -docEl.scrollTop);
865         }
866       }
868       cssPageRect.translate(parentRect.left, parentRect.top);
869     }
870     let frameOffsets = this._getFrameElementOffsets(currWin);
871     cssPageRect.translate(frameOffsets.x, frameOffsets.y);
873     return cssPageRect;
874   },
876   /**
877    * (I)Frame elements may have a border and/ or padding set, which is not
878    * included in the bounds returned by nsDOMWindowUtils#getRootBounds() for the
879    * window it hosts.
880    * This method fetches this offset of the frame element to the respective window.
881    *
882    * @param  {nsIDOMWindow} window          Window to read the boundary rect from
883    * @return {Object}       Simple object that contains the following two properties:
884    *                        - {Number} x Offset along the horizontal axis.
885    *                        - {Number} y Offset along the vertical axis.
886    */
887   _getFrameElementOffsets(window) {
888     let frame = window.frameElement;
889     if (!frame) {
890       return { x: 0, y: 0 };
891     }
893     // Getting style info is super expensive, causing reflows, so let's cache
894     // frame border widths and padding values aggressively.
895     let dict = this.getForWindow(this.getTopWindow(window, true));
896     let frameData = dict.frames.get(window);
897     if (!frameData) {
898       dict.frames.set(window, (frameData = {}));
899     }
900     if (frameData.offset) {
901       return frameData.offset;
902     }
904     let style = frame.ownerGlobal.getComputedStyle(frame);
905     // We only need to left sides, because ranges are offset from point 0,0 in
906     // the top-left corner.
907     let borderOffset = [
908       parseInt(style.borderLeftWidth, 10) || 0,
909       parseInt(style.borderTopWidth, 10) || 0,
910     ];
911     let paddingOffset = [
912       parseInt(style.paddingLeft, 10) || 0,
913       parseInt(style.paddingTop, 10) || 0,
914     ];
915     return (frameData.offset = {
916       x: borderOffset[0] + paddingOffset[0],
917       y: borderOffset[1] + paddingOffset[1],
918     });
919   },
921   /**
922    * Utility; fetch the full width and height of the current window, excluding
923    * scrollbars.
924    *
925    * @param  {nsiDOMWindow} window The current finder window.
926    * @return {Object} The current full page dimensions with `width` and `height`
927    *                  properties
928    */
929   _getWindowDimensions(window) {
930     // First we'll try without flushing layout, because it's way faster.
931     let dwu = this._getDWU(window);
932     let { width, height } = dwu.getRootBounds();
934     if (!width || !height) {
935       // We need a flush after all :'(
936       width = window.innerWidth + window.scrollMaxX - window.scrollMinX;
937       height = window.innerHeight + window.scrollMaxY - window.scrollMinY;
939       let scrollbarHeight = {};
940       let scrollbarWidth = {};
941       dwu.getScrollbarSize(false, scrollbarWidth, scrollbarHeight);
942       width -= scrollbarWidth.value;
943       height -= scrollbarHeight.value;
944     }
946     return { width, height };
947   },
949   /**
950    * Utility; get all available font styles as applied to the content of a given
951    * range. The CSS properties we look for can be found in `kFontPropsCSS`.
952    *
953    * @param  {Range} range Range to fetch style info from.
954    * @return {Object} Dictionary consisting of the styles that were found.
955    */
956   _getRangeFontStyle(range) {
957     let node = range.startContainer;
958     while (node.nodeType != 1) {
959       node = node.parentNode;
960     }
961     let style = node.ownerGlobal.getComputedStyle(node);
962     let props = {};
963     for (let prop of kFontPropsCamelCase) {
964       if (prop in style && style[prop]) {
965         props[prop] = style[prop];
966       }
967     }
968     return props;
969   },
971   /**
972    * Utility; transform a dictionary object as returned by `_getRangeFontStyle`
973    * above into a HTML style attribute value.
974    *
975    * @param  {Object} fontStyle
976    * @return {String}
977    */
978   _getHTMLFontStyle(fontStyle) {
979     let style = [];
980     for (let prop of Object.getOwnPropertyNames(fontStyle)) {
981       let idx = kFontPropsCamelCase.indexOf(prop);
982       if (idx == -1) {
983         continue;
984       }
985       style.push(`${kFontPropsCSS[idx]}: ${fontStyle[prop]}`);
986     }
987     return style.join("; ");
988   },
990   /**
991    * Transform a style definition array as defined in `kModalStyles` into a CSS
992    * string that can be used to set the 'style' property of a DOM node.
993    *
994    * @param  {Array}    stylePairs         Two-dimensional array of style pairs
995    * @param  {...Array} [additionalStyles] Optional set of style pairs that will
996    *                                       augment or override the styles defined
997    *                                       by `stylePairs`
998    * @return {String}
999    */
1000   _getStyleString(stylePairs, ...additionalStyles) {
1001     let baseStyle = new Map(stylePairs);
1002     for (let additionalStyle of additionalStyles) {
1003       for (let [prop, value] of additionalStyle) {
1004         baseStyle.set(prop, value);
1005       }
1006     }
1007     return [...baseStyle]
1008       .map(([cssProp, cssVal]) => `${cssProp}: ${cssVal}`)
1009       .join("; ");
1010   },
1012   /**
1013    * Checks whether a CSS RGB color value can be classified as being 'bright'.
1014    *
1015    * @param  {String} cssColor RGB color value in the default format rgb[a](r,g,b)
1016    * @return {Boolean}
1017    */
1018   _isColorBright(cssColor) {
1019     cssColor = cssColor.match(kRGBRE);
1020     if (!cssColor || !cssColor.length) {
1021       return false;
1022     }
1023     cssColor.shift();
1024     return !new lazy.Color(...cssColor).useBrightText;
1025   },
1027   /**
1028    * Detects if the overall text color in the page can be described as bright.
1029    * This is done according to the following algorithm:
1030    *  1. With the entire set of ranges that we have found thusfar;
1031    *  2. Get an odd-numbered `sampleSize`, with a maximum of `kBrightTextSampleSize`
1032    *     ranges,
1033    *  3. Slice the set of ranges into `sampleSize` number of equal parts,
1034    *  4. Grab the first range for each slice and inspect the brightness of the
1035    *     color of its text content.
1036    *  5. When the majority of ranges are counted as contain bright colored text,
1037    *     the page is considered to contain bright text overall.
1038    *
1039    * @param {Object} dict Dictionary of properties belonging to the
1040    *                      currently active window. The page text color property
1041    *                      will be recorded in `dict.brightText` as `true` or `false`.
1042    */
1043   _detectBrightText(dict) {
1044     let sampleSize = Math.min(
1045       dict.modalHighlightRectsMap.size,
1046       kBrightTextSampleSize
1047     );
1048     let ranges = [...dict.modalHighlightRectsMap.keys()];
1049     let rangesCount = ranges.length;
1050     // Make sure the sample size is an odd number.
1051     if (sampleSize % 2 == 0) {
1052       // Make the previously or currently found range weigh heavier.
1053       if (dict.previousFoundRange || dict.currentFoundRange) {
1054         ranges.push(dict.previousFoundRange || dict.currentFoundRange);
1055         ++sampleSize;
1056         ++rangesCount;
1057       } else {
1058         --sampleSize;
1059       }
1060     }
1061     let brightCount = 0;
1062     for (let i = 0; i < sampleSize; ++i) {
1063       let range = ranges[Math.floor((rangesCount / sampleSize) * i)];
1064       let fontStyle = this._getRangeFontStyle(range);
1065       if (this._isColorBright(fontStyle.color)) {
1066         ++brightCount;
1067       }
1068     }
1070     dict.brightText = brightCount >= Math.ceil(sampleSize / 2);
1071   },
1073   /**
1074    * Checks if a range is inside a DOM node that's positioned in a way that it
1075    * doesn't scroll along when the document is scrolled and/ or zoomed. This
1076    * is the case for 'fixed' and 'sticky' positioned elements, elements inside
1077    * (i)frames and elements that have their overflow styles set to 'auto' or
1078    * 'scroll'.
1079    *
1080    * @param  {Range} range Range that be enclosed in a dynamic container
1081    * @return {Boolean}
1082    */
1083   _isInDynamicContainer(range) {
1084     const kFixed = new Set(["fixed", "sticky", "scroll", "auto"]);
1085     let node = range.startContainer;
1086     while (node.nodeType != 1) {
1087       node = node.parentNode;
1088     }
1089     let document = node.ownerDocument;
1090     let window = document.defaultView;
1091     let dict = this.getForWindow(this.getTopWindow(window));
1093     // Check if we're in a frameset (including iframes).
1094     if (window != this.getTopWindow(window)) {
1095       if (!dict.frames.has(window)) {
1096         dict.frames.set(window, {});
1097       }
1098       return true;
1099     }
1101     do {
1102       let style = window.getComputedStyle(node);
1103       if (
1104         kFixed.has(style.position) ||
1105         kFixed.has(style.overflow) ||
1106         kFixed.has(style.overflowX) ||
1107         kFixed.has(style.overflowY)
1108       ) {
1109         return true;
1110       }
1111       node = node.parentNode;
1112     } while (node && node != document.documentElement);
1114     return false;
1115   },
1117   /**
1118    * Read and store the rectangles that encompass the entire region of a range
1119    * for use by the drawing function of the highlighter.
1120    *
1121    * @param  {Range}  range  Range to fetch the rectangles from
1122    * @param  {Object} [dict] Dictionary of properties belonging to
1123    *                         the currently active window
1124    * @return {Set}    Set of rects that were found for the range
1125    */
1126   _getRangeRectsAndTexts(range, dict = null) {
1127     let window = range.startContainer.ownerGlobal;
1128     let bounds;
1129     // If the window is part of a frameset, try to cache the bounds query.
1130     if (dict && dict.frames.has(window)) {
1131       let frameData = dict.frames.get(window);
1132       bounds = frameData.bounds;
1133       if (!bounds) {
1134         bounds = frameData.bounds = this._getRootBounds(window);
1135       }
1136     } else {
1137       bounds = this._getRootBounds(window);
1138     }
1140     let topBounds = this._getRootBounds(this.getTopWindow(window, true), false);
1141     let rects = [];
1142     // A range may consist of multiple rectangles, we can also do these kind of
1143     // precise cut-outs. range.getBoundingClientRect() returns the fully
1144     // encompassing rectangle, which is too much for our purpose here.
1145     let { rectList, textList } = range.getClientRectsAndTexts();
1146     for (let rect of rectList) {
1147       rect = lazy.Rect.fromRect(rect);
1148       rect.x += bounds.x;
1149       rect.y += bounds.y;
1150       // If the rect is not even visible from the top document, we can ignore it.
1151       if (rect.intersects(topBounds)) {
1152         rects.push(rect);
1153       }
1154     }
1155     return { rectList: rects, textList };
1156   },
1158   /**
1159    * Read and store the rectangles that encompass the entire region of a range
1160    * for use by the drawing function of the highlighter and store them in the
1161    * cache.
1162    *
1163    * @param  {Range}   range            Range to fetch the rectangles from
1164    * @param  {Boolean} [checkIfDynamic] Whether we should check if the range
1165    *                                    is dynamic as per the rules in
1166    *                                    `_isInDynamicContainer()`. Optional,
1167    *                                    defaults to `true`
1168    * @param  {Object}  [dict]           Dictionary of properties belonging to
1169    *                                    the currently active window
1170    * @return {Set}     Set of rects that were found for the range
1171    */
1172   _updateRangeRects(range, checkIfDynamic = true, dict = null) {
1173     let window = range.startContainer.ownerGlobal;
1174     let rectsAndTexts = this._getRangeRectsAndTexts(range, dict);
1176     // Only fetch the rect at this point, if not passed in as argument.
1177     dict = dict || this.getForWindow(this.getTopWindow(window));
1178     let oldRectsAndTexts = dict.modalHighlightRectsMap.get(range);
1179     dict.modalHighlightRectsMap.set(range, rectsAndTexts);
1180     // Check here if we suddenly went down to zero rects from more than zero before,
1181     // which indicates that we should re-iterate the document.
1182     if (
1183       oldRectsAndTexts &&
1184       oldRectsAndTexts.rectList.length &&
1185       !rectsAndTexts.rectList.length
1186     ) {
1187       dict.detectedGeometryChange = true;
1188     }
1189     if (checkIfDynamic && this._isInDynamicContainer(range)) {
1190       dict.dynamicRangesSet.add(range);
1191     }
1192     return rectsAndTexts;
1193   },
1195   /**
1196    * Re-read the rectangles of the ranges that we keep track of separately,
1197    * because they're enclosed by a position: fixed container DOM node or (i)frame.
1198    *
1199    * @param {Object} dict Dictionary of properties belonging to the currently
1200    *                      active window
1201    */
1202   _updateDynamicRangesRects(dict) {
1203     // Reset the frame bounds cache.
1204     for (let frameData of dict.frames.values()) {
1205       frameData.bounds = null;
1206     }
1207     for (let range of dict.dynamicRangesSet) {
1208       this._updateRangeRects(range, false, dict);
1209     }
1210   },
1212   /**
1213    * Update the content, position and style of the yellow current found range
1214    * outline that floats atop the mask with the dimmed background.
1215    * Rebuild it, if necessary, This will deactivate the animation between
1216    * occurrences.
1217    *
1218    * @param {Object} dict Dictionary of properties belonging to the currently
1219    *                      active window
1220    */
1221   _updateRangeOutline(dict) {
1222     let range = dict.currentFoundRange;
1223     if (!range) {
1224       return;
1225     }
1227     let fontStyle = this._getRangeFontStyle(range);
1228     // Text color in the outline is determined by kModalStyles.
1229     delete fontStyle.color;
1231     let rectsAndTexts = this._updateRangeRects(range, true, dict);
1232     let outlineAnonNode = dict.modalHighlightOutline;
1233     let rectCount = rectsAndTexts.rectList.length;
1234     let previousRectCount = dict.previousRangeRectsAndTexts.rectList.length;
1235     // (re-)Building the outline is conditional and happens when one of the
1236     // following conditions is met:
1237     // 1. No outline nodes were built before, or
1238     // 2. When the amount of rectangles to draw is different from before, or
1239     // 3. When there's more than one rectangle to draw, because it's impossible
1240     //    to animate that consistently with AnonymousContent nodes.
1241     let rebuildOutline =
1242       !outlineAnonNode || rectCount !== previousRectCount || rectCount != 1;
1243     dict.previousRangeRectsAndTexts = rectsAndTexts;
1245     let window = this.getTopWindow(range.startContainer.ownerGlobal);
1246     let document = window.document;
1247     // First see if we need to and can remove the previous outline nodes.
1248     if (rebuildOutline) {
1249       this._removeRangeOutline(window);
1250     }
1252     // Abort when there's no text to highlight OR when it's the exact same range
1253     // as the previous call and isn't inside a dynamic container.
1254     if (
1255       !rectsAndTexts.textList.length ||
1256       (!rebuildOutline &&
1257         dict.previousUpdatedRange == range &&
1258         !dict.dynamicRangesSet.has(range))
1259     ) {
1260       return;
1261     }
1263     let outlineBox;
1264     if (rebuildOutline) {
1265       // Create the main (yellow) highlight outline box.
1266       outlineBox = document.createElementNS(kNSHTML, "div");
1267       outlineBox.setAttribute("id", kModalOutlineId);
1268     }
1270     const kModalOutlineTextId = kModalOutlineId + "-text";
1271     let i = 0;
1272     for (let rect of rectsAndTexts.rectList) {
1273       let text = rectsAndTexts.textList[i];
1275       // Next up is to check of the outline box' borders will not overlap with
1276       // rects that we drew before or will draw after this one.
1277       // We're taking the width of the border into account, which is
1278       // `kOutlineBoxBorderSize` pixels.
1279       // When left and/ or right sides will overlap with the current, previous
1280       // or next rect, make sure to make the necessary adjustments to the style.
1281       // These adjustments will override the styles as defined in `kModalStyles.outlineNode`.
1282       let intersectingSides = new Set();
1283       let previous = rectsAndTexts.rectList[i - 1];
1284       if (previous && rect.left - previous.right <= 2 * kOutlineBoxBorderSize) {
1285         intersectingSides.add("left");
1286       }
1287       let next = rectsAndTexts.rectList[i + 1];
1288       if (next && next.left - rect.right <= 2 * kOutlineBoxBorderSize) {
1289         intersectingSides.add("right");
1290       }
1291       let borderStyles = [...intersectingSides].map(side => [
1292         "border-" + side,
1293         0,
1294       ]);
1295       if (intersectingSides.size) {
1296         borderStyles.push([
1297           "margin",
1298           `-${kOutlineBoxBorderSize}px 0 0 ${
1299             intersectingSides.has("left") ? 0 : -kOutlineBoxBorderSize
1300           }px !important`,
1301         ]);
1302         borderStyles.push([
1303           "border-radius",
1304           (intersectingSides.has("left") ? 0 : kOutlineBoxBorderRadius) +
1305             "px " +
1306             (intersectingSides.has("right") ? 0 : kOutlineBoxBorderRadius) +
1307             "px " +
1308             (intersectingSides.has("right") ? 0 : kOutlineBoxBorderRadius) +
1309             "px " +
1310             (intersectingSides.has("left") ? 0 : kOutlineBoxBorderRadius) +
1311             "px",
1312         ]);
1313       }
1315       let outlineStyle = this._getStyleString(
1316         kModalStyles.outlineNode,
1317         [
1318           ["top", rect.top + "px"],
1319           ["left", rect.left + "px"],
1320           ["height", rect.height + "px"],
1321           ["width", rect.width + "px"],
1322         ],
1323         borderStyles,
1324         lazy.kDebug ? kModalStyles.outlineNodeDebug : []
1325       );
1326       fontStyle.lineHeight = rect.height + "px";
1327       let textStyle =
1328         this._getStyleString(kModalStyles.outlineText) +
1329         "; " +
1330         this._getHTMLFontStyle(fontStyle);
1332       if (rebuildOutline) {
1333         let textBoxParent = outlineBox.appendChild(
1334           document.createElementNS(kNSHTML, "div")
1335         );
1336         textBoxParent.setAttribute("id", kModalOutlineId + i);
1337         textBoxParent.setAttribute("style", outlineStyle);
1339         let textBox = document.createElementNS(kNSHTML, "span");
1340         textBox.setAttribute("id", kModalOutlineTextId + i);
1341         textBox.setAttribute("style", textStyle);
1342         textBox.textContent = text;
1343         textBoxParent.appendChild(textBox);
1344       } else {
1345         // Set the appropriate properties on the existing nodes, which will also
1346         // activate the transitions.
1347         outlineAnonNode.setAttributeForElement(
1348           kModalOutlineId + i,
1349           "style",
1350           outlineStyle
1351         );
1352         outlineAnonNode.setAttributeForElement(
1353           kModalOutlineTextId + i,
1354           "style",
1355           textStyle
1356         );
1357         outlineAnonNode.setTextContentForElement(kModalOutlineTextId + i, text);
1358       }
1360       ++i;
1361     }
1363     if (rebuildOutline) {
1364       dict.modalHighlightOutline = lazy.kDebug
1365         ? mockAnonymousContentNode(
1366             (document.body || document.documentElement).appendChild(outlineBox)
1367           )
1368         : document.insertAnonymousContent(outlineBox);
1369     }
1371     if (dict.animateOutline && !this._isPageTooBig(dict)) {
1372       let animation;
1373       dict.animations = new Set();
1374       for (let i = rectsAndTexts.rectList.length - 1; i >= 0; --i) {
1375         animation = dict.modalHighlightOutline.setAnimationForElement(
1376           kModalOutlineId + i,
1377           Cu.cloneInto(kModalOutlineAnim.keyframes, window),
1378           kModalOutlineAnim.duration
1379         );
1380         animation.onfinish = function () {
1381           dict.animations.delete(this);
1382         };
1383         dict.animations.add(animation);
1384       }
1385     }
1386     dict.animateOutline = false;
1387     dict.ignoreNextContentChange = true;
1389     dict.previousUpdatedRange = range;
1390   },
1392   /**
1393    * Finish any currently playing animations on the found range outline node.
1394    *
1395    * @param {Object} dict Dictionary of properties belonging to the currently
1396    *                      active window
1397    */
1398   _finishOutlineAnimations(dict) {
1399     if (!dict.animations) {
1400       return;
1401     }
1402     for (let animation of dict.animations) {
1403       animation.finish();
1404     }
1405   },
1407   /**
1408    * Safely remove the outline AnoymousContent node from the CanvasFrame.
1409    *
1410    * @param {nsIDOMWindow} window
1411    */
1412   _removeRangeOutline(window) {
1413     let dict = this.getForWindow(window);
1414     if (!dict.modalHighlightOutline) {
1415       return;
1416     }
1418     if (lazy.kDebug) {
1419       dict.modalHighlightOutline.remove();
1420     } else {
1421       try {
1422         window.document.removeAnonymousContent(dict.modalHighlightOutline);
1423       } catch (ex) {}
1424     }
1426     dict.modalHighlightOutline = null;
1427   },
1429   /**
1430    * Add a range to the list of ranges to highlight on, or cut out of, the dimmed
1431    * background.
1432    *
1433    * @param {Range}        range  Range object that should be inspected
1434    * @param {nsIDOMWindow} window Window object, whose DOM tree is being traversed
1435    */
1436   _modalHighlight(range, controller, window) {
1437     this._updateRangeRects(range);
1439     this.show(window);
1440     // We don't repaint the mask right away, but pass it off to a render loop of
1441     // sorts.
1442     this._scheduleRepaintOfMask(window);
1443   },
1445   /**
1446    * Lazily insert the nodes we need as anonymous content into the CanvasFrame
1447    * of a window.
1448    *
1449    * @param {nsIDOMWindow} window Window to draw in.
1450    */
1451   _maybeCreateModalHighlightNodes(window) {
1452     window = this.getTopWindow(window);
1453     let dict = this.getForWindow(window);
1454     if (dict.modalHighlightOutline) {
1455       if (!dict.modalHighlightAllMask) {
1456         // Make sure to at least show the dimmed background.
1457         this._repaintHighlightAllMask(window, false);
1458         this._scheduleRepaintOfMask(window);
1459       } else {
1460         this._scheduleRepaintOfMask(window, { contentChanged: true });
1461       }
1462       return;
1463     }
1465     let document = window.document;
1466     // A hidden document doesn't accept insertAnonymousContent calls yet.
1467     if (document.hidden) {
1468       let onVisibilityChange = () => {
1469         document.removeEventListener("visibilitychange", onVisibilityChange);
1470         this._maybeCreateModalHighlightNodes(window);
1471       };
1472       document.addEventListener("visibilitychange", onVisibilityChange);
1473       return;
1474     }
1476     // Make sure to at least show the dimmed background.
1477     this._repaintHighlightAllMask(window, false);
1478   },
1480   /**
1481    * Build and draw the mask that takes care of the dimmed background that
1482    * overlays the current page and the mask that cuts out all the rectangles of
1483    * the ranges that were found.
1484    *
1485    * @param {nsIDOMWindow} window Window to draw in.
1486    * @param {Boolean} [paintContent]
1487    */
1488   _repaintHighlightAllMask(window, paintContent = true) {
1489     window = this.getTopWindow(window);
1490     let dict = this.getForWindow(window);
1492     const kMaskId = kModalIdPrefix + "-findbar-modalHighlight-outlineMask";
1493     if (!dict.modalHighlightAllMask) {
1494       let document = window.document;
1495       let maskNode = document.createElementNS(kNSHTML, "div");
1496       maskNode.setAttribute("id", kMaskId);
1497       dict.modalHighlightAllMask = lazy.kDebug
1498         ? mockAnonymousContentNode(
1499             (document.body || document.documentElement).appendChild(maskNode)
1500           )
1501         : document.insertAnonymousContent(maskNode);
1502     }
1504     // Make sure the dimmed mask node takes the full width and height that's available.
1505     let { width, height } = (dict.lastWindowDimensions =
1506       this._getWindowDimensions(window));
1507     if (typeof dict.brightText != "boolean" || dict.updateAllRanges) {
1508       this._detectBrightText(dict);
1509     }
1510     let maskStyle = this._getStyleString(
1511       kModalStyles.maskNode,
1512       [
1513         ["width", width + "px"],
1514         ["height", height + "px"],
1515       ],
1516       dict.brightText ? kModalStyles.maskNodeBrightText : [],
1517       paintContent ? kModalStyles.maskNodeTransition : [],
1518       lazy.kDebug ? kModalStyles.maskNodeDebug : []
1519     );
1520     dict.modalHighlightAllMask.setAttributeForElement(
1521       kMaskId,
1522       "style",
1523       maskStyle
1524     );
1526     this._updateRangeOutline(dict);
1528     let allRects = [];
1529     // When the user's busy scrolling the document, don't bother cutting out rectangles,
1530     // because they're not going to keep up with scrolling speed anyway.
1531     if (!dict.busyScrolling && (paintContent || dict.modalHighlightAllMask)) {
1532       // No need to update dynamic ranges separately when we already about to
1533       // update all of them anyway.
1534       if (!dict.updateAllRanges) {
1535         this._updateDynamicRangesRects(dict);
1536       }
1538       let DOMRect = window.DOMRect;
1539       for (let [range, rectsAndTexts] of dict.modalHighlightRectsMap) {
1540         if (!this.finder._fastFind.isRangeVisible(range, false)) {
1541           continue;
1542         }
1544         if (dict.updateAllRanges) {
1545           rectsAndTexts = this._updateRangeRects(range);
1546         }
1548         // If a geometry change was detected, we bail out right away here, because
1549         // the current set of ranges has been invalidated.
1550         if (dict.detectedGeometryChange) {
1551           return;
1552         }
1554         for (let rect of rectsAndTexts.rectList) {
1555           allRects.push(new DOMRect(rect.x, rect.y, rect.width, rect.height));
1556         }
1557       }
1558       dict.updateAllRanges = false;
1559     }
1561     // We may also want to cut out zero rects, which effectively clears out the mask.
1562     dict.modalHighlightAllMask.setCutoutRectsForElement(kMaskId, allRects);
1564     // The reflow observer may ignore the reflow we cause ourselves here.
1565     dict.ignoreNextContentChange = true;
1566   },
1568   /**
1569    * Safely remove the mask AnoymousContent node from the CanvasFrame.
1570    *
1571    * @param {nsIDOMWindow} window
1572    */
1573   _removeHighlightAllMask(window) {
1574     window = this.getTopWindow(window);
1575     let dict = this.getForWindow(window);
1576     if (!dict.modalHighlightAllMask) {
1577       return;
1578     }
1580     // If the current window isn't the one the content was inserted into, this
1581     // will fail, but that's fine.
1582     if (lazy.kDebug) {
1583       dict.modalHighlightAllMask.remove();
1584     } else {
1585       try {
1586         window.document.removeAnonymousContent(dict.modalHighlightAllMask);
1587       } catch (ex) {}
1588     }
1589     dict.modalHighlightAllMask = null;
1590   },
1592   /**
1593    * Check if the width or height of the current document is too big to handle
1594    * for certain operations. This allows us to degrade gracefully when we expect
1595    * the performance to be negatively impacted due to drawing-intensive operations.
1596    *
1597    * @param  {Object} dict Dictionary of properties belonging to the currently
1598    *                       active window
1599    * @return {Boolean}
1600    */
1601   _isPageTooBig(dict) {
1602     let { height, width } = dict.lastWindowDimensions;
1603     return height >= kPageIsTooBigPx || width >= kPageIsTooBigPx;
1604   },
1606   /**
1607    * Doing a full repaint each time a range is delivered by the highlight iterator
1608    * is way too costly, thus we pipe the frequency down to every
1609    * `kModalHighlightRepaintLoFreqMs` milliseconds. If there are dynamic ranges
1610    * found (see `_isInDynamicContainer()` for the definition), the frequency
1611    * will be upscaled to `kModalHighlightRepaintHiFreqMs`.
1612    *
1613    * @param {nsIDOMWindow} window
1614    * @param {Object}       options Dictionary of painter hints that contains the
1615    *                               following properties:
1616    *   {Boolean} contentChanged  Whether the documents' content changed in the
1617    *                             meantime. This happens when the DOM is updated
1618    *                             whilst the page is loaded.
1619    *   {Boolean} scrollOnly      TRUE when the page has scrolled in the meantime,
1620    *                             which means that the dynamically positioned
1621    *                             elements need to be repainted.
1622    *   {Boolean} updateAllRanges Whether to recalculate the rects of all ranges
1623    *                             that were found up until now.
1624    */
1625   _scheduleRepaintOfMask(
1626     window,
1627     { contentChanged = false, scrollOnly = false, updateAllRanges = false } = {}
1628   ) {
1629     if (!this.useModal()) {
1630       return;
1631     }
1633     window = this.getTopWindow(window);
1634     let dict = this.getForWindow(window);
1635     // Bail out early if the repaint scheduler is paused or when we're supposed
1636     // to ignore the next paint (i.e. content change).
1637     if (
1638       dict.repaintSchedulerState == kRepaintSchedulerPaused ||
1639       (contentChanged && dict.ignoreNextContentChange)
1640     ) {
1641       dict.ignoreNextContentChange = false;
1642       return;
1643     }
1645     let hasDynamicRanges = !!dict.dynamicRangesSet.size;
1646     let pageIsTooBig = this._isPageTooBig(dict);
1647     let repaintDynamicRanges =
1648       (scrollOnly || contentChanged) && hasDynamicRanges && !pageIsTooBig;
1650     // Determine scroll behavior and keep that state around.
1651     let startedScrolling = !dict.busyScrolling && scrollOnly;
1652     // When the user started scrolling the document, hide the other highlights.
1653     if (startedScrolling) {
1654       dict.busyScrolling = startedScrolling;
1655       this._repaintHighlightAllMask(window);
1656     }
1657     // Whilst scrolling, suspend the repaint scheduler, but only when the page is
1658     // too big or the find results contains ranges that are inside dynamic
1659     // containers.
1660     if (dict.busyScrolling && (pageIsTooBig || hasDynamicRanges)) {
1661       dict.ignoreNextContentChange = true;
1662       this._updateRangeOutline(dict);
1663       // NB: we're not using `kRepaintSchedulerPaused` on purpose here, otherwise
1664       // we'd break the `busyScrolling` detection (re-)using the timer.
1665       if (dict.modalRepaintScheduler) {
1666         window.clearTimeout(dict.modalRepaintScheduler);
1667         dict.modalRepaintScheduler = null;
1668       }
1669     }
1671     // When we request to repaint unconditionally, we mean to call
1672     // `_repaintHighlightAllMask()` right after the timeout.
1673     if (!dict.unconditionalRepaintRequested) {
1674       dict.unconditionalRepaintRequested =
1675         !contentChanged || repaintDynamicRanges;
1676     }
1677     // Some events, like a resize, call for recalculation of all the rects of all ranges.
1678     if (!dict.updateAllRanges) {
1679       dict.updateAllRanges = updateAllRanges;
1680     }
1682     if (dict.modalRepaintScheduler) {
1683       return;
1684     }
1686     let timeoutMs =
1687       hasDynamicRanges && !dict.busyScrolling
1688         ? kModalHighlightRepaintHiFreqMs
1689         : kModalHighlightRepaintLoFreqMs;
1690     dict.modalRepaintScheduler = window.setTimeout(() => {
1691       dict.modalRepaintScheduler = null;
1692       dict.repaintSchedulerState = kRepaintSchedulerStopped;
1693       dict.busyScrolling = false;
1695       let pageContentChanged = dict.detectedGeometryChange;
1696       if (!pageContentChanged && !pageIsTooBig) {
1697         let { width: previousWidth, height: previousHeight } =
1698           dict.lastWindowDimensions;
1699         let { width, height } = (dict.lastWindowDimensions =
1700           this._getWindowDimensions(window));
1701         pageContentChanged =
1702           dict.detectedGeometryChange ||
1703           Math.abs(previousWidth - width) > kContentChangeThresholdPx ||
1704           Math.abs(previousHeight - height) > kContentChangeThresholdPx;
1705       }
1706       dict.detectedGeometryChange = false;
1707       // When the page has changed significantly enough in size, we'll restart
1708       // the iterator with the same parameters as before to find us new ranges.
1709       if (pageContentChanged && !pageIsTooBig) {
1710         this.iterator.restart(this.finder);
1711       }
1713       if (
1714         dict.unconditionalRepaintRequested ||
1715         (dict.modalHighlightRectsMap.size && pageContentChanged)
1716       ) {
1717         dict.unconditionalRepaintRequested = false;
1718         this._repaintHighlightAllMask(window);
1719       }
1720     }, timeoutMs);
1721     dict.repaintSchedulerState = kRepaintSchedulerRunning;
1722   },
1724   /**
1725    * Add event listeners to the content which will cause the modal highlight
1726    * AnonymousContent to be re-painted or hidden.
1727    *
1728    * @param {nsIDOMWindow} window
1729    */
1730   _addModalHighlightListeners(window) {
1731     window = this.getTopWindow(window);
1732     let dict = this.getForWindow(window);
1733     if (dict.highlightListeners) {
1734       return;
1735     }
1737     dict.highlightListeners = [
1738       this._scheduleRepaintOfMask.bind(this, window, { contentChanged: true }),
1739       this._scheduleRepaintOfMask.bind(this, window, { updateAllRanges: true }),
1740       this._scheduleRepaintOfMask.bind(this, window, { scrollOnly: true }),
1741       this.hide.bind(this, window, null),
1742       () => (dict.busySelecting = true),
1743       () => {
1744         if (window.document.hidden) {
1745           dict.repaintSchedulerState = kRepaintSchedulerPaused;
1746         } else if (dict.repaintSchedulerState == kRepaintSchedulerPaused) {
1747           dict.repaintSchedulerState = kRepaintSchedulerRunning;
1748           this._scheduleRepaintOfMask(window);
1749         }
1750       },
1751     ];
1752     let target = this.iterator._getDocShell(window).chromeEventHandler;
1753     target.addEventListener("MozAfterPaint", dict.highlightListeners[0]);
1754     target.addEventListener("resize", dict.highlightListeners[1]);
1755     target.addEventListener("scroll", dict.highlightListeners[2], {
1756       capture: true,
1757       passive: true,
1758     });
1759     target.addEventListener("click", dict.highlightListeners[3]);
1760     target.addEventListener("selectstart", dict.highlightListeners[4]);
1761     window.document.addEventListener(
1762       "visibilitychange",
1763       dict.highlightListeners[5]
1764     );
1765   },
1767   /**
1768    * Remove event listeners from content.
1769    *
1770    * @param {nsIDOMWindow} window
1771    */
1772   _removeModalHighlightListeners(window) {
1773     window = this.getTopWindow(window);
1774     let dict = this.getForWindow(window);
1775     if (!dict.highlightListeners) {
1776       return;
1777     }
1779     let target = this.iterator._getDocShell(window).chromeEventHandler;
1780     target.removeEventListener("MozAfterPaint", dict.highlightListeners[0]);
1781     target.removeEventListener("resize", dict.highlightListeners[1]);
1782     target.removeEventListener("scroll", dict.highlightListeners[2], {
1783       capture: true,
1784       passive: true,
1785     });
1786     target.removeEventListener("click", dict.highlightListeners[3]);
1787     target.removeEventListener("selectstart", dict.highlightListeners[4]);
1788     window.document.removeEventListener(
1789       "visibilitychange",
1790       dict.highlightListeners[5]
1791     );
1793     dict.highlightListeners = null;
1794   },
1796   /**
1797    * For a given node returns its editable parent or null if there is none.
1798    * It's enough to check if node is a text node and its parent's parent is
1799    * an input or textarea.
1800    *
1801    * @param node the node we want to check
1802    * @returns the first node in the parent chain that is editable,
1803    *          null if there is no such node
1804    */
1805   _getEditableNode(node) {
1806     if (
1807       node.nodeType === node.TEXT_NODE &&
1808       node.parentNode &&
1809       node.parentNode.parentNode &&
1810       (ChromeUtils.getClassName(node.parentNode.parentNode) ===
1811         "HTMLInputElement" ||
1812         ChromeUtils.getClassName(node.parentNode.parentNode) ===
1813           "HTMLTextAreaElement")
1814     ) {
1815       return node.parentNode.parentNode;
1816     }
1817     return null;
1818   },
1820   /**
1821    * Add ourselves as an nsIEditActionListener and nsIDocumentStateListener for
1822    * a given editor
1823    *
1824    * @param editor the editor we'd like to listen to
1825    */
1826   _addEditorListeners(editor) {
1827     if (!this._editors) {
1828       this._editors = [];
1829       this._stateListeners = [];
1830     }
1832     let existingIndex = this._editors.indexOf(editor);
1833     if (existingIndex == -1) {
1834       let x = this._editors.length;
1835       this._editors[x] = editor;
1836       this._stateListeners[x] = this._createStateListener();
1837       this._editors[x].addEditActionListener(this);
1838       this._editors[x].addDocumentStateListener(this._stateListeners[x]);
1839     }
1840   },
1842   /**
1843    * Helper method to unhook listeners, remove cached editors
1844    * and keep the relevant arrays in sync
1845    *
1846    * @param idx the index into the array of editors/state listeners
1847    *        we wish to remove
1848    */
1849   _unhookListenersAtIndex(idx) {
1850     this._editors[idx].removeEditActionListener(this);
1851     this._editors[idx].removeDocumentStateListener(this._stateListeners[idx]);
1852     this._editors.splice(idx, 1);
1853     this._stateListeners.splice(idx, 1);
1854     if (!this._editors.length) {
1855       delete this._editors;
1856       delete this._stateListeners;
1857     }
1858   },
1860   /**
1861    * Remove ourselves as an nsIEditActionListener and
1862    * nsIDocumentStateListener from a given cached editor
1863    *
1864    * @param editor the editor we no longer wish to listen to
1865    */
1866   _removeEditorListeners(editor) {
1867     // editor is an editor that we listen to, so therefore must be
1868     // cached. Find the index of this editor
1869     let idx = this._editors.indexOf(editor);
1870     if (idx == -1) {
1871       return;
1872     }
1873     // Now unhook ourselves, and remove our cached copy
1874     this._unhookListenersAtIndex(idx);
1875   },
1877   /*
1878    * nsIEditActionListener logic follows
1879    *
1880    * We implement this interface to allow us to catch the case where
1881    * the findbar found a match in a HTML <input> or <textarea>. If the
1882    * user adjusts the text in some way, it will no longer match, so we
1883    * want to remove the highlight, rather than have it expand/contract
1884    * when letters are added or removed.
1885    */
1887   /**
1888    * Helper method used to check whether a selection intersects with
1889    * some highlighting
1890    *
1891    * @param selectionRange the range from the selection to check
1892    * @param findRange the highlighted range to check against
1893    * @returns true if they intersect, false otherwise
1894    */
1895   _checkOverlap(selectionRange, findRange) {
1896     if (!selectionRange || !findRange) {
1897       return false;
1898     }
1899     // The ranges overlap if one of the following is true:
1900     // 1) At least one of the endpoints of the deleted selection
1901     //    is in the find selection
1902     // 2) At least one of the endpoints of the find selection
1903     //    is in the deleted selection
1904     if (
1905       findRange.isPointInRange(
1906         selectionRange.startContainer,
1907         selectionRange.startOffset
1908       )
1909     ) {
1910       return true;
1911     }
1912     if (
1913       findRange.isPointInRange(
1914         selectionRange.endContainer,
1915         selectionRange.endOffset
1916       )
1917     ) {
1918       return true;
1919     }
1920     if (
1921       selectionRange.isPointInRange(
1922         findRange.startContainer,
1923         findRange.startOffset
1924       )
1925     ) {
1926       return true;
1927     }
1928     if (
1929       selectionRange.isPointInRange(findRange.endContainer, findRange.endOffset)
1930     ) {
1931       return true;
1932     }
1934     return false;
1935   },
1937   /**
1938    * Helper method to determine if an edit occurred within a highlight
1939    *
1940    * @param selection the selection we wish to check
1941    * @param node the node we want to check is contained in selection
1942    * @param offset the offset into node that we want to check
1943    * @returns the range containing (node, offset) or null if no ranges
1944    *          in the selection contain it
1945    */
1946   _findRange(selection, node, offset) {
1947     let rangeCount = selection.rangeCount;
1948     let rangeidx = 0;
1949     let foundContainingRange = false;
1950     let range = null;
1952     // Check to see if this node is inside one of the selection's ranges
1953     while (!foundContainingRange && rangeidx < rangeCount) {
1954       range = selection.getRangeAt(rangeidx);
1955       if (range.isPointInRange(node, offset)) {
1956         foundContainingRange = true;
1957         break;
1958       }
1959       rangeidx++;
1960     }
1962     if (foundContainingRange) {
1963       return range;
1964     }
1966     return null;
1967   },
1969   // Start of nsIEditActionListener implementations
1971   WillDeleteText(textNode, offset, length) {
1972     let editor = this._getEditableNode(textNode).editor;
1973     let controller = editor.selectionController;
1974     let fSelection = controller.getSelection(
1975       Ci.nsISelectionController.SELECTION_FIND
1976     );
1977     let range = this._findRange(fSelection, textNode, offset);
1979     if (range) {
1980       // Don't remove the highlighting if the deleted text is at the
1981       // end of the range
1982       if (textNode != range.endContainer || offset != range.endOffset) {
1983         // Text within the highlight is being removed - the text can
1984         // no longer be a match, so remove the highlighting
1985         fSelection.removeRange(range);
1986         if (fSelection.rangeCount == 0) {
1987           this._removeEditorListeners(editor);
1988         }
1989       }
1990     }
1991   },
1993   DidInsertText(textNode, offset, aString) {
1994     let editor = this._getEditableNode(textNode).editor;
1995     let controller = editor.selectionController;
1996     let fSelection = controller.getSelection(
1997       Ci.nsISelectionController.SELECTION_FIND
1998     );
1999     let range = this._findRange(fSelection, textNode, offset);
2001     if (range) {
2002       // If the text was inserted before the highlight
2003       // adjust the highlight's bounds accordingly
2004       if (textNode == range.startContainer && offset == range.startOffset) {
2005         range.setStart(
2006           range.startContainer,
2007           range.startOffset + aString.length
2008         );
2009       } else if (textNode != range.endContainer || offset != range.endOffset) {
2010         // The edit occurred within the highlight - any addition of text
2011         // will result in the text no longer being a match,
2012         // so remove the highlighting
2013         fSelection.removeRange(range);
2014         if (fSelection.rangeCount == 0) {
2015           this._removeEditorListeners(editor);
2016         }
2017       }
2018     }
2019   },
2021   WillDeleteRanges(rangesToDelete) {
2022     let { editor } = this._getEditableNode(rangesToDelete[0].startContainer);
2023     let controller = editor.selectionController;
2024     let fSelection = controller.getSelection(
2025       Ci.nsISelectionController.SELECTION_FIND
2026     );
2028     let shouldDelete = {};
2029     let numberOfDeletedSelections = 0;
2030     let numberOfMatches = fSelection.rangeCount;
2032     // We need to test if any ranges to be deleted
2033     // are in any of the ranges of the find selection
2034     // Usually both selections will only contain one range, however
2035     // either may contain more than one.
2037     for (let fIndex = 0; fIndex < numberOfMatches; fIndex++) {
2038       shouldDelete[fIndex] = false;
2039       let fRange = fSelection.getRangeAt(fIndex);
2041       for (let selRange of rangesToDelete) {
2042         if (shouldDelete[fIndex]) {
2043           continue;
2044         }
2046         let doesOverlap = this._checkOverlap(selRange, fRange);
2047         if (doesOverlap) {
2048           shouldDelete[fIndex] = true;
2049           numberOfDeletedSelections++;
2050         }
2051       }
2052     }
2054     // OK, so now we know what matches (if any) are in the selection
2055     // that is being deleted. Time to remove them.
2056     if (!numberOfDeletedSelections) {
2057       return;
2058     }
2060     for (let i = numberOfMatches - 1; i >= 0; i--) {
2061       if (shouldDelete[i]) {
2062         fSelection.removeRange(fSelection.getRangeAt(i));
2063       }
2064     }
2066     // Remove listeners if no more highlights left
2067     if (!fSelection.rangeCount) {
2068       this._removeEditorListeners(editor);
2069     }
2070   },
2072   /*
2073    * nsIDocumentStateListener logic follows
2074    *
2075    * When attaching nsIEditActionListeners, there are no guarantees
2076    * as to whether the findbar or the documents in the browser will get
2077    * destructed first. This leads to the potential to either leak, or to
2078    * hold on to a reference an editable element's editor for too long,
2079    * preventing it from being destructed.
2080    *
2081    * However, when an editor's owning node is being destroyed, the editor
2082    * sends out a DocumentWillBeDestroyed notification. We can use this to
2083    * clean up our references to the object, to allow it to be destroyed in a
2084    * timely fashion.
2085    */
2087   /**
2088    * Unhook ourselves when one of our state listeners has been called.
2089    * This can happen in 4 cases:
2090    *  1) The document the editor belongs to is navigated away from, and
2091    *     the document is not being cached
2092    *
2093    *  2) The document the editor belongs to is expired from the cache
2094    *
2095    *  3) The tab containing the owning document is closed
2096    *
2097    *  4) The <input> or <textarea> that owns the editor is explicitly
2098    *     removed from the DOM
2099    *
2100    * @param the listener that was invoked
2101    */
2102   _onEditorDestruction(aListener) {
2103     // First find the index of the editor the given listener listens to.
2104     // The listeners and editors arrays must always be in sync.
2105     // The listener will be in our array of cached listeners, as this
2106     // method could not have been called otherwise.
2107     let idx = 0;
2108     while (this._stateListeners[idx] != aListener) {
2109       idx++;
2110     }
2112     // Unhook both listeners
2113     this._unhookListenersAtIndex(idx);
2114   },
2116   /**
2117    * Creates a unique document state listener for an editor.
2118    *
2119    * It is not possible to simply have the findbar implement the
2120    * listener interface itself, as it wouldn't have sufficient information
2121    * to work out which editor was being destroyed. Therefore, we create new
2122    * listeners on the fly, and cache them in sync with the editors they
2123    * listen to.
2124    */
2125   _createStateListener() {
2126     return {
2127       findbar: this,
2129       QueryInterface: ChromeUtils.generateQI(["nsIDocumentStateListener"]),
2131       NotifyDocumentWillBeDestroyed() {
2132         this.findbar._onEditorDestruction(this);
2133       },
2135       // Unimplemented
2136       notifyDocumentStateChanged(aDirty) {},
2137     };
2138   },