forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			2137 lines
		
	
	
	
		
			71 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			2137 lines
		
	
	
	
		
			71 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* This Source Code Form is subject to the terms of the Mozilla Public
 | 
						|
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
						|
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | 
						|
 | 
						|
const lazy = {};
 | 
						|
 | 
						|
ChromeUtils.defineESModuleGetters(lazy, {
 | 
						|
  Color: "resource://gre/modules/Color.sys.mjs",
 | 
						|
  Rect: "resource://gre/modules/Geometry.sys.mjs",
 | 
						|
});
 | 
						|
ChromeUtils.defineLazyGetter(lazy, "kDebug", () => {
 | 
						|
  const kDebugPref = "findbar.modalHighlight.debug";
 | 
						|
  return (
 | 
						|
    Services.prefs.getPrefType(kDebugPref) &&
 | 
						|
    Services.prefs.getBoolPref(kDebugPref)
 | 
						|
  );
 | 
						|
});
 | 
						|
 | 
						|
const kContentChangeThresholdPx = 5;
 | 
						|
const kBrightTextSampleSize = 5;
 | 
						|
// This limit is arbitrary and doesn't scale for low-powered machines or
 | 
						|
// high-powered machines. Netbooks will probably need a much lower limit, for
 | 
						|
// example. Though getting something out there is better than nothing.
 | 
						|
const kPageIsTooBigPx = 500000;
 | 
						|
const kModalHighlightRepaintLoFreqMs = 100;
 | 
						|
const kModalHighlightRepaintHiFreqMs = 16;
 | 
						|
const kHighlightAllPref = "findbar.highlightAll";
 | 
						|
const kModalHighlightPref = "findbar.modalHighlight";
 | 
						|
const kFontPropsCSS = [
 | 
						|
  "color",
 | 
						|
  "font-family",
 | 
						|
  "font-kerning",
 | 
						|
  "font-size",
 | 
						|
  "font-size-adjust",
 | 
						|
  "font-stretch",
 | 
						|
  "font-variant",
 | 
						|
  "font-weight",
 | 
						|
  "line-height",
 | 
						|
  "letter-spacing",
 | 
						|
  "text-emphasis",
 | 
						|
  "text-orientation",
 | 
						|
  "text-transform",
 | 
						|
  "word-spacing",
 | 
						|
];
 | 
						|
const kFontPropsCamelCase = kFontPropsCSS.map(prop => {
 | 
						|
  let parts = prop.split("-");
 | 
						|
  return (
 | 
						|
    parts.shift() +
 | 
						|
    parts.map(part => part.charAt(0).toUpperCase() + part.slice(1)).join("")
 | 
						|
  );
 | 
						|
});
 | 
						|
const kRGBRE = /^rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*/i;
 | 
						|
// This uuid is used to prefix HTML element IDs in order to make them unique and
 | 
						|
// hard to clash with IDs content authors come up with.
 | 
						|
const kModalIdPrefix = "cedee4d0-74c5-4f2d-ab43-4d37c0f9d463";
 | 
						|
const kModalOutlineId = kModalIdPrefix + "-findbar-modalHighlight-outline";
 | 
						|
const kOutlineBoxColor = "255,197,53";
 | 
						|
const kOutlineBoxBorderSize = 1;
 | 
						|
const kOutlineBoxBorderRadius = 2;
 | 
						|
const kModalStyles = {
 | 
						|
  outlineNode: [
 | 
						|
    ["background-color", `rgb(${kOutlineBoxColor})`],
 | 
						|
    ["background-clip", "padding-box"],
 | 
						|
    ["border", `${kOutlineBoxBorderSize}px solid rgba(${kOutlineBoxColor},.7)`],
 | 
						|
    ["border-radius", `${kOutlineBoxBorderRadius}px`],
 | 
						|
    ["box-shadow", `0 2px 0 0 rgba(0,0,0,.1)`],
 | 
						|
    ["color", "#000"],
 | 
						|
    ["display", "flex"],
 | 
						|
    [
 | 
						|
      "margin",
 | 
						|
      `-${kOutlineBoxBorderSize}px 0 0 -${kOutlineBoxBorderSize}px !important`,
 | 
						|
    ],
 | 
						|
    ["overflow", "hidden"],
 | 
						|
    ["pointer-events", "none"],
 | 
						|
    ["position", "absolute"],
 | 
						|
    ["white-space", "nowrap"],
 | 
						|
    ["will-change", "transform"],
 | 
						|
    ["z-index", 2],
 | 
						|
  ],
 | 
						|
  outlineNodeDebug: [["z-index", 2147483647]],
 | 
						|
  outlineText: [
 | 
						|
    ["margin", "0 !important"],
 | 
						|
    ["padding", "0 !important"],
 | 
						|
    ["vertical-align", "top !important"],
 | 
						|
  ],
 | 
						|
  maskNode: [
 | 
						|
    ["background", "rgba(0,0,0,.25)"],
 | 
						|
    ["pointer-events", "none"],
 | 
						|
    ["position", "absolute"],
 | 
						|
    ["z-index", 1],
 | 
						|
  ],
 | 
						|
  maskNodeTransition: [["transition", "background .2s ease-in"]],
 | 
						|
  maskNodeDebug: [
 | 
						|
    ["z-index", 2147483646],
 | 
						|
    ["top", 0],
 | 
						|
    ["left", 0],
 | 
						|
  ],
 | 
						|
  maskNodeBrightText: [["background", "rgba(255,255,255,.25)"]],
 | 
						|
};
 | 
						|
const kModalOutlineAnim = {
 | 
						|
  keyframes: [
 | 
						|
    { transform: "scaleX(1) scaleY(1)" },
 | 
						|
    { transform: "scaleX(1.5) scaleY(1.5)", offset: 0.5, easing: "ease-in" },
 | 
						|
    { transform: "scaleX(1) scaleY(1)" },
 | 
						|
  ],
 | 
						|
  duration: 50,
 | 
						|
};
 | 
						|
const kNSHTML = "http://www.w3.org/1999/xhtml";
 | 
						|
const kRepaintSchedulerStopped = 1;
 | 
						|
const kRepaintSchedulerPaused = 2;
 | 
						|
const kRepaintSchedulerRunning = 3;
 | 
						|
 | 
						|
function mockAnonymousContentNode(domNode) {
 | 
						|
  return {
 | 
						|
    setTextContentForElement(id, text) {
 | 
						|
      (domNode.querySelector("#" + id) || domNode).textContent = text;
 | 
						|
    },
 | 
						|
    getAttributeForElement(id, attrName) {
 | 
						|
      let node = domNode.querySelector("#" + id) || domNode;
 | 
						|
      if (!node.hasAttribute(attrName)) {
 | 
						|
        return undefined;
 | 
						|
      }
 | 
						|
      return node.getAttribute(attrName);
 | 
						|
    },
 | 
						|
    setAttributeForElement(id, attrName, attrValue) {
 | 
						|
      (domNode.querySelector("#" + id) || domNode).setAttribute(
 | 
						|
        attrName,
 | 
						|
        attrValue
 | 
						|
      );
 | 
						|
    },
 | 
						|
    removeAttributeForElement(id, attrName) {
 | 
						|
      let node = domNode.querySelector("#" + id) || domNode;
 | 
						|
      if (!node.hasAttribute(attrName)) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      node.removeAttribute(attrName);
 | 
						|
    },
 | 
						|
    remove() {
 | 
						|
      try {
 | 
						|
        domNode.remove();
 | 
						|
      } catch (ex) {}
 | 
						|
    },
 | 
						|
    setAnimationForElement(id, keyframes, duration) {
 | 
						|
      return (domNode.querySelector("#" + id) || domNode).animate(
 | 
						|
        keyframes,
 | 
						|
        duration
 | 
						|
      );
 | 
						|
    },
 | 
						|
    setCutoutRectsForElement(id, rects) {
 | 
						|
      // no-op for now.
 | 
						|
    },
 | 
						|
  };
 | 
						|
}
 | 
						|
 | 
						|
let gWindows = new WeakMap();
 | 
						|
 | 
						|
/**
 | 
						|
 * FinderHighlighter class that is used by Finder.sys.mjs to take care of the
 | 
						|
 * 'Highlight All' feature, which can highlight all find occurrences in a page.
 | 
						|
 *
 | 
						|
 * @param {Finder} finder Finder.sys.mjs instance
 | 
						|
 * @param {boolean} useTop check and use top-level windows for rectangle
 | 
						|
 *                         computation, if possible.
 | 
						|
 */
 | 
						|
export function FinderHighlighter(finder, useTop = false) {
 | 
						|
  this._highlightAll = Services.prefs.getBoolPref(kHighlightAllPref);
 | 
						|
  this._modal = Services.prefs.getBoolPref(kModalHighlightPref);
 | 
						|
  this._useSubFrames = false;
 | 
						|
  this._useTop = useTop;
 | 
						|
  this._marksListener = null;
 | 
						|
  this._testing = false;
 | 
						|
  this.finder = finder;
 | 
						|
}
 | 
						|
 | 
						|
FinderHighlighter.prototype = {
 | 
						|
  get iterator() {
 | 
						|
    return this.finder.iterator;
 | 
						|
  },
 | 
						|
 | 
						|
  enableTesting(enable) {
 | 
						|
    this._testing = enable;
 | 
						|
  },
 | 
						|
 | 
						|
  // Get the top-most window when allowed. When out-of-process frames are used,
 | 
						|
  // this will usually be the same as the passed-in window. The checkUseTop
 | 
						|
  // argument can be used to instead check the _useTop flag which can be used
 | 
						|
  // to enable rectangle coordinate checks.
 | 
						|
  getTopWindow(window, checkUseTop) {
 | 
						|
    if (this._useSubFrames || (checkUseTop && this._useTop)) {
 | 
						|
      try {
 | 
						|
        return window.top;
 | 
						|
      } catch (ex) {}
 | 
						|
    }
 | 
						|
 | 
						|
    return window;
 | 
						|
  },
 | 
						|
 | 
						|
  useModal() {
 | 
						|
    // Modal highlighting is currently only enabled when there are no
 | 
						|
    // out-of-process subframes.
 | 
						|
    return this._modal && this._useSubFrames;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Each window is unique, globally, and the relation between an active
 | 
						|
   * highlighting session and a window is 1:1.
 | 
						|
   * For each window we track a number of properties which _at least_ consist of
 | 
						|
   *  - {Boolean} detectedGeometryChange Whether the geometry of the found ranges'
 | 
						|
   *                                     rectangles has changed substantially
 | 
						|
   *  - {Set}     dynamicRangesSet       Set of ranges that may move around, depending
 | 
						|
   *                                     on page layout changes and user input
 | 
						|
   *  - {Map}     frames                 Collection of frames that were encountered
 | 
						|
   *                                     when inspecting the found ranges
 | 
						|
   *  - {Map}     modalHighlightRectsMap Collection of ranges and their corresponding
 | 
						|
   *                                     Rects and texts
 | 
						|
   *
 | 
						|
   * @param  {nsIDOMWindow} window
 | 
						|
   * @return {Object}
 | 
						|
   */
 | 
						|
  getForWindow(window, propName = null) {
 | 
						|
    if (!gWindows.has(window)) {
 | 
						|
      gWindows.set(window, {
 | 
						|
        detectedGeometryChange: false,
 | 
						|
        dynamicRangesSet: new Set(),
 | 
						|
        frames: new Map(),
 | 
						|
        lastWindowDimensions: { width: 0, height: 0 },
 | 
						|
        modalHighlightRectsMap: new Map(),
 | 
						|
        previousRangeRectsAndTexts: { rectList: [], textList: [] },
 | 
						|
        repaintSchedulerState: kRepaintSchedulerStopped,
 | 
						|
      });
 | 
						|
    }
 | 
						|
    return gWindows.get(window);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Notify all registered listeners that the 'Highlight All' operation finished.
 | 
						|
   *
 | 
						|
   * @param {Boolean} highlight Whether highlighting was turned on
 | 
						|
   */
 | 
						|
  notifyFinished(highlight) {
 | 
						|
    for (let l of this.finder._listeners) {
 | 
						|
      try {
 | 
						|
        l.onHighlightFinished(highlight);
 | 
						|
      } catch (ex) {}
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Toggle highlighting all occurrences of a word in a page. This method will
 | 
						|
   * be called recursively for each (i)frame inside a page.
 | 
						|
   *
 | 
						|
   * @param {Booolean} highlight    Whether highlighting should be turned on
 | 
						|
   * @param {String}   word         Needle to search for and highlight when found
 | 
						|
   * @param {Boolean}  linksOnly    Only consider nodes that are links for the search
 | 
						|
   * @param {Boolean}  drawOutline  Whether found links should be outlined.
 | 
						|
   * @param {Boolean}  useSubFrames Whether to iterate over subframes.
 | 
						|
   * @yield {Promise}  that resolves once the operation has finished
 | 
						|
   */
 | 
						|
  async highlight(highlight, word, linksOnly, drawOutline, useSubFrames) {
 | 
						|
    let window = this.finder._getWindow();
 | 
						|
    let dict = this.getForWindow(window);
 | 
						|
    let controller = this.finder._getSelectionController(window);
 | 
						|
    let doc = window.document;
 | 
						|
    this._found = false;
 | 
						|
    this._useSubFrames = useSubFrames;
 | 
						|
 | 
						|
    let result = { searchString: word, highlight, found: false };
 | 
						|
 | 
						|
    if (!controller || !doc || !doc.documentElement) {
 | 
						|
      // Without the selection controller,
 | 
						|
      // we are unable to (un)highlight any matches
 | 
						|
      return result;
 | 
						|
    }
 | 
						|
 | 
						|
    if (highlight) {
 | 
						|
      let params = {
 | 
						|
        allowDistance: 1,
 | 
						|
        caseSensitive: this.finder._fastFind.caseSensitive,
 | 
						|
        entireWord: this.finder._fastFind.entireWord,
 | 
						|
        linksOnly,
 | 
						|
        word,
 | 
						|
        finder: this.finder,
 | 
						|
        listener: this,
 | 
						|
        matchDiacritics: this.finder._fastFind.matchDiacritics,
 | 
						|
        useCache: true,
 | 
						|
        useSubFrames,
 | 
						|
        window,
 | 
						|
      };
 | 
						|
      if (
 | 
						|
        this.iterator.isAlreadyRunning(params) ||
 | 
						|
        (this.useModal() &&
 | 
						|
          this.iterator._areParamsEqual(params, dict.lastIteratorParams))
 | 
						|
      ) {
 | 
						|
        return result;
 | 
						|
      }
 | 
						|
 | 
						|
      if (!this.useModal()) {
 | 
						|
        dict.visible = true;
 | 
						|
      }
 | 
						|
      await this.iterator.start(params);
 | 
						|
      if (this._found) {
 | 
						|
        this.finder._outlineLink(drawOutline);
 | 
						|
      }
 | 
						|
    } else {
 | 
						|
      this.hide(window);
 | 
						|
 | 
						|
      // Removing the highlighting always succeeds, so return true.
 | 
						|
      this._found = true;
 | 
						|
    }
 | 
						|
 | 
						|
    result.found = this._found;
 | 
						|
    this.notifyFinished(result);
 | 
						|
    return result;
 | 
						|
  },
 | 
						|
 | 
						|
  // FinderIterator listener implementation
 | 
						|
 | 
						|
  onIteratorRangeFound(range) {
 | 
						|
    this.highlightRange(range);
 | 
						|
    this._found = true;
 | 
						|
  },
 | 
						|
 | 
						|
  onIteratorReset() {},
 | 
						|
 | 
						|
  onIteratorRestart() {
 | 
						|
    this.clear(this.finder._getWindow());
 | 
						|
  },
 | 
						|
 | 
						|
  onIteratorStart(params) {
 | 
						|
    let window = this.finder._getWindow();
 | 
						|
    let dict = this.getForWindow(window);
 | 
						|
    // Save a clean params set for use later in the `update()` method.
 | 
						|
    dict.lastIteratorParams = params;
 | 
						|
    if (!this.useModal()) {
 | 
						|
      this.hide(window, this.finder._fastFind.getFoundRange());
 | 
						|
    }
 | 
						|
    this.clear(window);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Add a range to the find selection, i.e. highlight it, and if it's inside an
 | 
						|
   * editable node, track it.
 | 
						|
   *
 | 
						|
   * @param {Range} range Range object to be highlighted
 | 
						|
   */
 | 
						|
  highlightRange(range) {
 | 
						|
    let node = range.startContainer;
 | 
						|
    let editableNode = this._getEditableNode(node);
 | 
						|
    let window = node.ownerGlobal;
 | 
						|
    let controller = this.finder._getSelectionController(window);
 | 
						|
    if (editableNode) {
 | 
						|
      controller = editableNode.editor.selectionController;
 | 
						|
    }
 | 
						|
 | 
						|
    if (this.useModal()) {
 | 
						|
      this._modalHighlight(range, controller, window);
 | 
						|
    } else {
 | 
						|
      let findSelection = controller.getSelection(
 | 
						|
        Ci.nsISelectionController.SELECTION_FIND
 | 
						|
      );
 | 
						|
      findSelection.addRange(range);
 | 
						|
      // Check if the range is inside an (i)frame.
 | 
						|
      if (window != this.getTopWindow(window)) {
 | 
						|
        let dict = this.getForWindow(this.getTopWindow(window));
 | 
						|
        // Add this frame to the list, so that we'll be able to find it later
 | 
						|
        // when we need to clear its selection(s).
 | 
						|
        dict.frames.set(window, {});
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (editableNode) {
 | 
						|
      // Highlighting added, so cache this editor, and hook up listeners
 | 
						|
      // to ensure we deal properly with edits within the highlighting
 | 
						|
      this._addEditorListeners(editableNode.editor);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * If modal highlighting is enabled, show the dimmed background that will overlay
 | 
						|
   * the page.
 | 
						|
   *
 | 
						|
   * @param {nsIDOMWindow} window The dimmed background will overlay this window.
 | 
						|
   *                              Optional, defaults to the finder window.
 | 
						|
   */
 | 
						|
  show(window = null) {
 | 
						|
    window = this.getTopWindow(window || this.finder._getWindow());
 | 
						|
    let dict = this.getForWindow(window);
 | 
						|
    if (!this.useModal() || dict.visible) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    dict.visible = true;
 | 
						|
 | 
						|
    this._maybeCreateModalHighlightNodes(window);
 | 
						|
    this._addModalHighlightListeners(window);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Clear all highlighted matches. If modal highlighting is enabled and
 | 
						|
   * the outline + dimmed background is currently visible, both will be hidden.
 | 
						|
   *
 | 
						|
   * @param {nsIDOMWindow} window    The dimmed background will overlay this window.
 | 
						|
   *                                 Optional, defaults to the finder window.
 | 
						|
   * @param {Range}        skipRange A range that should not be removed from the
 | 
						|
   *                                 find selection.
 | 
						|
   * @param {Event}        event     When called from an event handler, this will
 | 
						|
   *                                 be the triggering event.
 | 
						|
   */
 | 
						|
  hide(window, skipRange = null, event = null) {
 | 
						|
    try {
 | 
						|
      window = this.getTopWindow(window);
 | 
						|
    } catch (ex) {
 | 
						|
      console.error(ex);
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    let dict = this.getForWindow(window);
 | 
						|
 | 
						|
    let isBusySelecting = dict.busySelecting;
 | 
						|
    dict.busySelecting = false;
 | 
						|
    // Do not hide on anything but a left-click.
 | 
						|
    if (
 | 
						|
      event &&
 | 
						|
      event.type == "click" &&
 | 
						|
      (event.button !== 0 ||
 | 
						|
        event.altKey ||
 | 
						|
        event.ctrlKey ||
 | 
						|
        event.metaKey ||
 | 
						|
        event.shiftKey ||
 | 
						|
        event.relatedTarget ||
 | 
						|
        isBusySelecting ||
 | 
						|
        (event.target.localName == "a" && event.target.href))
 | 
						|
    ) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    this._clearSelection(
 | 
						|
      this.finder._getSelectionController(window),
 | 
						|
      skipRange
 | 
						|
    );
 | 
						|
    for (let frame of dict.frames.keys()) {
 | 
						|
      this._clearSelection(
 | 
						|
        this.finder._getSelectionController(frame),
 | 
						|
        skipRange
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    // Next, check our editor cache, for editors belonging to this
 | 
						|
    // document
 | 
						|
    if (this._editors) {
 | 
						|
      let doc = window.document;
 | 
						|
      for (let x = this._editors.length - 1; x >= 0; --x) {
 | 
						|
        if (this._editors[x].document == doc) {
 | 
						|
          this._clearSelection(this._editors[x].selectionController, skipRange);
 | 
						|
          // We don't need to listen to this editor any more
 | 
						|
          this._unhookListenersAtIndex(x);
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (dict.modalRepaintScheduler) {
 | 
						|
      window.clearTimeout(dict.modalRepaintScheduler);
 | 
						|
      dict.modalRepaintScheduler = null;
 | 
						|
      dict.repaintSchedulerState = kRepaintSchedulerStopped;
 | 
						|
    }
 | 
						|
    dict.lastWindowDimensions = { width: 0, height: 0 };
 | 
						|
 | 
						|
    this._removeRangeOutline(window);
 | 
						|
    this._removeHighlightAllMask(window);
 | 
						|
    this._removeModalHighlightListeners(window);
 | 
						|
 | 
						|
    dict.visible = false;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Called by the Finder after a find result comes in; update the position and
 | 
						|
   * content of the outline to the newly found occurrence.
 | 
						|
   * To make sure that the outline covers the found range completely, all the
 | 
						|
   * CSS styles that influence the text are copied and applied to the outline.
 | 
						|
   *
 | 
						|
   * @param {Object} data Dictionary coming from Finder that contains the
 | 
						|
   *                      following properties:
 | 
						|
   *   {Number}  result        One of the nsITypeAheadFind.FIND_* constants
 | 
						|
   *                           indicating the result of a search operation.
 | 
						|
   *   {Boolean} findBackwards If TRUE, the search was performed backwards,
 | 
						|
   *                           FALSE if forwards.
 | 
						|
   *   {Boolean} findAgain     If TRUE, the search was performed using the same
 | 
						|
   *                           search string as before.
 | 
						|
   *   {String}  linkURL       If a link was hit, this will contain a URL string.
 | 
						|
   *   {Rect}    rect          An object with top, left, width and height
 | 
						|
   *                           coordinates of the current selection.
 | 
						|
   *   {String}  searchString  The string the search was performed with.
 | 
						|
   *   {Boolean} storeResult   Indicator if the search string should be stored
 | 
						|
   *                           by the consumer of the Finder.
 | 
						|
   */
 | 
						|
  async update(data, foundInThisFrame) {
 | 
						|
    let window = this.finder._getWindow();
 | 
						|
    let dict = this.getForWindow(window);
 | 
						|
    let foundRange = this.finder._fastFind.getFoundRange();
 | 
						|
 | 
						|
    if (
 | 
						|
      data.result == Ci.nsITypeAheadFind.FIND_NOTFOUND ||
 | 
						|
      !data.searchString ||
 | 
						|
      (foundInThisFrame && !foundRange)
 | 
						|
    ) {
 | 
						|
      this.hide(window);
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    this._useSubFrames = data.useSubFrames;
 | 
						|
    if (!this.useModal()) {
 | 
						|
      if (this._highlightAll) {
 | 
						|
        dict.previousFoundRange = dict.currentFoundRange;
 | 
						|
        dict.currentFoundRange = foundRange;
 | 
						|
        let params = this.iterator.params;
 | 
						|
        if (
 | 
						|
          dict.visible &&
 | 
						|
          this.iterator._areParamsEqual(params, dict.lastIteratorParams)
 | 
						|
        ) {
 | 
						|
          return;
 | 
						|
        }
 | 
						|
        if (!dict.visible && !params) {
 | 
						|
          params = { word: data.searchString, linksOnly: data.linksOnly };
 | 
						|
        }
 | 
						|
        if (params) {
 | 
						|
          await this.highlight(
 | 
						|
            true,
 | 
						|
            params.word,
 | 
						|
            params.linksOnly,
 | 
						|
            params.drawOutline,
 | 
						|
            data.useSubFrames
 | 
						|
          );
 | 
						|
        }
 | 
						|
      }
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    dict.animateOutline = true;
 | 
						|
    // Immediately finish running animations, if any.
 | 
						|
    this._finishOutlineAnimations(dict);
 | 
						|
 | 
						|
    if (foundRange !== dict.currentFoundRange || data.findAgain) {
 | 
						|
      dict.previousFoundRange = dict.currentFoundRange;
 | 
						|
      dict.currentFoundRange = foundRange;
 | 
						|
 | 
						|
      if (!dict.visible) {
 | 
						|
        this.show(window);
 | 
						|
      } else {
 | 
						|
        this._maybeCreateModalHighlightNodes(window);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (this._highlightAll) {
 | 
						|
      await this.highlight(
 | 
						|
        true,
 | 
						|
        data.searchString,
 | 
						|
        data.linksOnly,
 | 
						|
        data.drawOutline,
 | 
						|
        data.useSubFrames
 | 
						|
      );
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Invalidates the list by clearing the map of highlighted ranges that we
 | 
						|
   * keep to build the mask for.
 | 
						|
   */
 | 
						|
  clear(window = null) {
 | 
						|
    if (!window || !this.getTopWindow(window)) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    let dict = this.getForWindow(this.getTopWindow(window));
 | 
						|
    this._finishOutlineAnimations(dict);
 | 
						|
    dict.dynamicRangesSet.clear();
 | 
						|
    dict.frames.clear();
 | 
						|
    dict.modalHighlightRectsMap.clear();
 | 
						|
    dict.brightText = null;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Removes the outline from a single window. This is done when
 | 
						|
   * switching the current search to a new frame.
 | 
						|
   */
 | 
						|
  clearCurrentOutline(window = null) {
 | 
						|
    let dict = this.getForWindow(this.getTopWindow(window));
 | 
						|
    this._finishOutlineAnimations(dict);
 | 
						|
    this._removeRangeOutline(window);
 | 
						|
  },
 | 
						|
 | 
						|
  // Update the tick marks that should appear on the page's scrollbar(s).
 | 
						|
  updateScrollMarks() {
 | 
						|
    // Only show scrollbar marks when normal highlighting is enabled.
 | 
						|
    if (this.useModal() || !this._highlightAll) {
 | 
						|
      this.removeScrollMarks();
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    let marks = new Set(); // Use a set so duplicate values are removed.
 | 
						|
    let window = this.finder._getWindow();
 | 
						|
    // Show the marks on the horizontal scrollbar for vertical writing modes.
 | 
						|
    let onHorizontalScrollbar = !window
 | 
						|
      .getComputedStyle(window.document.body || window.document.documentElement)
 | 
						|
      .writingMode.startsWith("horizontal");
 | 
						|
    let yStart = window.scrollY - window.scrollMinY;
 | 
						|
    let xStart = window.scrollX - window.scrollMinX;
 | 
						|
 | 
						|
    let hasRanges = false;
 | 
						|
    if (window) {
 | 
						|
      let controllers = [this.finder._getSelectionController(window)];
 | 
						|
      let editors = this.editors;
 | 
						|
      if (editors) {
 | 
						|
        // Add the selection controllers from any input fields.
 | 
						|
        controllers.push(...editors.map(editor => editor.selectionController));
 | 
						|
      }
 | 
						|
 | 
						|
      for (let controller of controllers) {
 | 
						|
        let findSelection = controller.getSelection(
 | 
						|
          Ci.nsISelectionController.SELECTION_FIND
 | 
						|
        );
 | 
						|
 | 
						|
        let rangeCount = findSelection.rangeCount;
 | 
						|
        if (rangeCount > 0) {
 | 
						|
          hasRanges = true;
 | 
						|
        }
 | 
						|
 | 
						|
        // No need to calculate the mark positions if there is no visible scrollbar.
 | 
						|
        if (window.scrollMaxY > window.scrollMinY && !onHorizontalScrollbar) {
 | 
						|
          // Use the body's scrollHeight if available.
 | 
						|
          let scrollHeight =
 | 
						|
            window.document.body?.scrollHeight ||
 | 
						|
            window.document.documentElement.scrollHeight;
 | 
						|
          let yAdj = (window.scrollMaxY - window.scrollMinY) / scrollHeight;
 | 
						|
 | 
						|
          for (let r = 0; r < rangeCount; r++) {
 | 
						|
            let rect = findSelection.getRangeAt(r).getBoundingClientRect();
 | 
						|
            let yPos = Math.round((yStart + rect.y + rect.height / 2) * yAdj); // use the midpoint
 | 
						|
            marks.add(yPos);
 | 
						|
          }
 | 
						|
        } else if (
 | 
						|
          window.scrollMaxX > window.scrollMinX &&
 | 
						|
          onHorizontalScrollbar
 | 
						|
        ) {
 | 
						|
          // Use the body's scrollWidth if available.
 | 
						|
          let scrollWidth =
 | 
						|
            window.document.body?.scrollWidth ||
 | 
						|
            window.document.documentElement.scrollWidth;
 | 
						|
          let xAdj = (window.scrollMaxX - window.scrollMinX) / scrollWidth;
 | 
						|
 | 
						|
          for (let r = 0; r < rangeCount; r++) {
 | 
						|
            let rect = findSelection.getRangeAt(r).getBoundingClientRect();
 | 
						|
            let xPos = Math.round((xStart + rect.x + rect.width / 2) * xAdj);
 | 
						|
            marks.add(xPos);
 | 
						|
          }
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (hasRanges) {
 | 
						|
      // Assign the marks to the window and add a listener for the MozScrolledAreaChanged
 | 
						|
      // event which fires whenever the scrollable area's size is updated.
 | 
						|
      this.setScrollMarks(window, Array.from(marks), onHorizontalScrollbar);
 | 
						|
 | 
						|
      if (!this._marksListener) {
 | 
						|
        this._marksListener = event => {
 | 
						|
          this.updateScrollMarks();
 | 
						|
        };
 | 
						|
 | 
						|
        window.addEventListener(
 | 
						|
          "MozScrolledAreaChanged",
 | 
						|
          this._marksListener,
 | 
						|
          true
 | 
						|
        );
 | 
						|
        window.addEventListener("resize", this._marksListener);
 | 
						|
      }
 | 
						|
    } else if (this._marksListener) {
 | 
						|
      // No results were found so remove any existing ones and the MozScrolledAreaChanged listener.
 | 
						|
      this.removeScrollMarks();
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  removeScrollMarks() {
 | 
						|
    let window;
 | 
						|
    try {
 | 
						|
      window = this.finder._getWindow();
 | 
						|
    } catch (ex) {
 | 
						|
      // An exception can happen after changing remoteness but this
 | 
						|
      // would have deleted the marks anyway.
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (this._marksListener) {
 | 
						|
      window.removeEventListener(
 | 
						|
        "MozScrolledAreaChanged",
 | 
						|
        this._marksListener,
 | 
						|
        true
 | 
						|
      );
 | 
						|
      window.removeEventListener("resize", this._marksListener);
 | 
						|
      this._marksListener = null;
 | 
						|
    }
 | 
						|
    this.setScrollMarks(window, []);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Set the scrollbar marks for a current search. If testing mode is enabled, fire a
 | 
						|
   * find-scrollmarks-changed event at the window.
 | 
						|
   *
 | 
						|
   * @param window window to set the scrollbar marks on
 | 
						|
   * @param marks array of integer scrollbar mark positions
 | 
						|
   * @param onHorizontalScrollbar whether to display the marks on the horizontal scrollbar
 | 
						|
   */
 | 
						|
  setScrollMarks(window, marks, onHorizontalScrollbar = false) {
 | 
						|
    window.setScrollMarks(marks, onHorizontalScrollbar);
 | 
						|
 | 
						|
    // Fire an event containing the found mark values if testing mode is enabled.
 | 
						|
    if (this._testing) {
 | 
						|
      window.dispatchEvent(
 | 
						|
        new CustomEvent("find-scrollmarks-changed", {
 | 
						|
          detail: {
 | 
						|
            marks: Array.from(marks),
 | 
						|
            onHorizontalScrollbar,
 | 
						|
          },
 | 
						|
        })
 | 
						|
      );
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * When the current page is refreshed or navigated away from, the CanvasFrame
 | 
						|
   * contents is not valid anymore, i.e. all anonymous content is destroyed.
 | 
						|
   * We need to clear the references we keep, which'll make sure we redraw
 | 
						|
   * everything when the user starts to find in page again.
 | 
						|
   */
 | 
						|
  onLocationChange() {
 | 
						|
    let window = this.finder._getWindow();
 | 
						|
    if (!window || !this.getTopWindow(window)) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    this.hide(window);
 | 
						|
    this.clear(window);
 | 
						|
    this._removeRangeOutline(window);
 | 
						|
 | 
						|
    gWindows.delete(this.getTopWindow(window));
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * When `kModalHighlightPref` pref changed during a session, this callback is
 | 
						|
   * invoked. When modal highlighting is turned off, we hide the CanvasFrame
 | 
						|
   * contents.
 | 
						|
   *
 | 
						|
   * @param {Boolean} useModalHighlight
 | 
						|
   */
 | 
						|
  onModalHighlightChange(useModalHighlight) {
 | 
						|
    let window = this.finder._getWindow();
 | 
						|
    if (window && this.useModal() && !useModalHighlight) {
 | 
						|
      this.hide(window);
 | 
						|
      this.clear(window);
 | 
						|
    }
 | 
						|
    this._modal = useModalHighlight;
 | 
						|
    this.updateScrollMarks();
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * When 'Highlight All' is toggled during a session, this callback is invoked
 | 
						|
   * and when it's turned off, the found occurrences will be removed from the mask.
 | 
						|
   *
 | 
						|
   * @param {Boolean} highlightAll
 | 
						|
   */
 | 
						|
  onHighlightAllChange(highlightAll) {
 | 
						|
    this._highlightAll = highlightAll;
 | 
						|
    if (!highlightAll) {
 | 
						|
      let window = this.finder._getWindow();
 | 
						|
      if (!this.useModal()) {
 | 
						|
        this.hide(window);
 | 
						|
      }
 | 
						|
      this.clear(window);
 | 
						|
      this._scheduleRepaintOfMask(window);
 | 
						|
    }
 | 
						|
 | 
						|
    this.updateScrollMarks();
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Utility; removes all ranges from the find selection that belongs to a
 | 
						|
   * controller. Optionally skips a specific range.
 | 
						|
   *
 | 
						|
   * @param  {nsISelectionController} controller
 | 
						|
   * @param  {Range}                  restoreRange
 | 
						|
   */
 | 
						|
  _clearSelection(controller, restoreRange = null) {
 | 
						|
    if (!controller) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    let sel = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND);
 | 
						|
    sel.removeAllRanges();
 | 
						|
    if (restoreRange) {
 | 
						|
      sel = controller.getSelection(Ci.nsISelectionController.SELECTION_NORMAL);
 | 
						|
      sel.addRange(restoreRange);
 | 
						|
      controller.setDisplaySelection(
 | 
						|
        Ci.nsISelectionController.SELECTION_ATTENTION
 | 
						|
      );
 | 
						|
      controller.repaintSelection(Ci.nsISelectionController.SELECTION_NORMAL);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Utility; get the nsIDOMWindowUtils for a window.
 | 
						|
   *
 | 
						|
   * @param  {nsIDOMWindow} window Optional, defaults to the finder window.
 | 
						|
   * @return {nsIDOMWindowUtils}
 | 
						|
   */
 | 
						|
  _getDWU(window = null) {
 | 
						|
    return (window || this.finder._getWindow()).windowUtils;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Utility; returns the bounds of the page relative to the viewport.
 | 
						|
   * If the pages is part of a frameset or inside an iframe of any kind, its
 | 
						|
   * offset is accounted for.
 | 
						|
   * Geometry.sys.mjs takes care of the DOMRect calculations.
 | 
						|
   *
 | 
						|
   * @param  {nsIDOMWindow} window          Window to read the boundary rect from
 | 
						|
   * @param  {Boolean}      [includeScroll] Whether to ignore the scroll offset,
 | 
						|
   *                                        which is useful for comparing DOMRects.
 | 
						|
   *                                        Optional, defaults to `true`
 | 
						|
   * @return {Rect}
 | 
						|
   */
 | 
						|
  _getRootBounds(window, includeScroll = true) {
 | 
						|
    let dwu = this._getDWU(this.getTopWindow(window, true));
 | 
						|
    let cssPageRect = lazy.Rect.fromRect(dwu.getRootBounds());
 | 
						|
    let scrollX = {};
 | 
						|
    let scrollY = {};
 | 
						|
    if (includeScroll && window == this.getTopWindow(window, true)) {
 | 
						|
      dwu.getScrollXY(false, scrollX, scrollY);
 | 
						|
      cssPageRect.translate(scrollX.value, scrollY.value);
 | 
						|
    }
 | 
						|
 | 
						|
    // If we're in a frame, update the position of the rect (top/ left).
 | 
						|
    let currWin = window;
 | 
						|
    while (currWin != this.getTopWindow(window, true)) {
 | 
						|
      let frameOffsets = this._getFrameElementOffsets(currWin);
 | 
						|
      cssPageRect.translate(frameOffsets.x, frameOffsets.y);
 | 
						|
 | 
						|
      // Since the frame is an element inside a parent window, we'd like to
 | 
						|
      // learn its position relative to it.
 | 
						|
      let el = currWin.browsingContext.embedderElement;
 | 
						|
      currWin = currWin.parent;
 | 
						|
      dwu = this._getDWU(currWin);
 | 
						|
      let parentRect = lazy.Rect.fromRect(dwu.getBoundsWithoutFlushing(el));
 | 
						|
 | 
						|
      if (includeScroll) {
 | 
						|
        dwu.getScrollXY(false, scrollX, scrollY);
 | 
						|
        parentRect.translate(scrollX.value, scrollY.value);
 | 
						|
        // If the current window is an iframe with scrolling="no" and its parent
 | 
						|
        // is also an iframe the scroll offsets from the parents' documentElement
 | 
						|
        // (inverse scroll position) needs to be subtracted from the parent
 | 
						|
        // window rect.
 | 
						|
        if (
 | 
						|
          el.getAttribute("scrolling") == "no" &&
 | 
						|
          currWin != this.getTopWindow(window, true)
 | 
						|
        ) {
 | 
						|
          let docEl = currWin.document.documentElement;
 | 
						|
          parentRect.translate(-docEl.scrollLeft, -docEl.scrollTop);
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      cssPageRect.translate(parentRect.left, parentRect.top);
 | 
						|
    }
 | 
						|
    let frameOffsets = this._getFrameElementOffsets(currWin);
 | 
						|
    cssPageRect.translate(frameOffsets.x, frameOffsets.y);
 | 
						|
 | 
						|
    return cssPageRect;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * (I)Frame elements may have a border and/ or padding set, which is not
 | 
						|
   * included in the bounds returned by nsDOMWindowUtils#getRootBounds() for the
 | 
						|
   * window it hosts.
 | 
						|
   * This method fetches this offset of the frame element to the respective window.
 | 
						|
   *
 | 
						|
   * @param  {nsIDOMWindow} window          Window to read the boundary rect from
 | 
						|
   * @return {Object}       Simple object that contains the following two properties:
 | 
						|
   *                        - {Number} x Offset along the horizontal axis.
 | 
						|
   *                        - {Number} y Offset along the vertical axis.
 | 
						|
   */
 | 
						|
  _getFrameElementOffsets(window) {
 | 
						|
    let frame = window.frameElement;
 | 
						|
    if (!frame) {
 | 
						|
      return { x: 0, y: 0 };
 | 
						|
    }
 | 
						|
 | 
						|
    // Getting style info is super expensive, causing reflows, so let's cache
 | 
						|
    // frame border widths and padding values aggressively.
 | 
						|
    let dict = this.getForWindow(this.getTopWindow(window, true));
 | 
						|
    let frameData = dict.frames.get(window);
 | 
						|
    if (!frameData) {
 | 
						|
      dict.frames.set(window, (frameData = {}));
 | 
						|
    }
 | 
						|
    if (frameData.offset) {
 | 
						|
      return frameData.offset;
 | 
						|
    }
 | 
						|
 | 
						|
    let style = frame.ownerGlobal.getComputedStyle(frame);
 | 
						|
    // We only need to left sides, because ranges are offset from point 0,0 in
 | 
						|
    // the top-left corner.
 | 
						|
    let borderOffset = [
 | 
						|
      parseInt(style.borderLeftWidth, 10) || 0,
 | 
						|
      parseInt(style.borderTopWidth, 10) || 0,
 | 
						|
    ];
 | 
						|
    let paddingOffset = [
 | 
						|
      parseInt(style.paddingLeft, 10) || 0,
 | 
						|
      parseInt(style.paddingTop, 10) || 0,
 | 
						|
    ];
 | 
						|
    return (frameData.offset = {
 | 
						|
      x: borderOffset[0] + paddingOffset[0],
 | 
						|
      y: borderOffset[1] + paddingOffset[1],
 | 
						|
    });
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Utility; fetch the full width and height of the current window, excluding
 | 
						|
   * scrollbars.
 | 
						|
   *
 | 
						|
   * @param  {nsiDOMWindow} window The current finder window.
 | 
						|
   * @return {Object} The current full page dimensions with `width` and `height`
 | 
						|
   *                  properties
 | 
						|
   */
 | 
						|
  _getWindowDimensions(window) {
 | 
						|
    // First we'll try without flushing layout, because it's way faster.
 | 
						|
    let dwu = this._getDWU(window);
 | 
						|
    let { width, height } = dwu.getRootBounds();
 | 
						|
 | 
						|
    if (!width || !height) {
 | 
						|
      // We need a flush after all :'(
 | 
						|
      width = window.innerWidth + window.scrollMaxX - window.scrollMinX;
 | 
						|
      height = window.innerHeight + window.scrollMaxY - window.scrollMinY;
 | 
						|
 | 
						|
      let scrollbarHeight = {};
 | 
						|
      let scrollbarWidth = {};
 | 
						|
      dwu.getScrollbarSize(false, scrollbarWidth, scrollbarHeight);
 | 
						|
      width -= scrollbarWidth.value;
 | 
						|
      height -= scrollbarHeight.value;
 | 
						|
    }
 | 
						|
 | 
						|
    return { width, height };
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Utility; get all available font styles as applied to the content of a given
 | 
						|
   * range. The CSS properties we look for can be found in `kFontPropsCSS`.
 | 
						|
   *
 | 
						|
   * @param  {Range} range Range to fetch style info from.
 | 
						|
   * @return {Object} Dictionary consisting of the styles that were found.
 | 
						|
   */
 | 
						|
  _getRangeFontStyle(range) {
 | 
						|
    let node = range.startContainer;
 | 
						|
    while (node.nodeType != 1) {
 | 
						|
      node = node.parentNode;
 | 
						|
    }
 | 
						|
    let style = node.ownerGlobal.getComputedStyle(node);
 | 
						|
    let props = {};
 | 
						|
    for (let prop of kFontPropsCamelCase) {
 | 
						|
      if (prop in style && style[prop]) {
 | 
						|
        props[prop] = style[prop];
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return props;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Utility; transform a dictionary object as returned by `_getRangeFontStyle`
 | 
						|
   * above into a HTML style attribute value.
 | 
						|
   *
 | 
						|
   * @param  {Object} fontStyle
 | 
						|
   * @return {String}
 | 
						|
   */
 | 
						|
  _getHTMLFontStyle(fontStyle) {
 | 
						|
    let style = [];
 | 
						|
    for (let prop of Object.getOwnPropertyNames(fontStyle)) {
 | 
						|
      let idx = kFontPropsCamelCase.indexOf(prop);
 | 
						|
      if (idx == -1) {
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
      style.push(`${kFontPropsCSS[idx]}: ${fontStyle[prop]}`);
 | 
						|
    }
 | 
						|
    return style.join("; ");
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Transform a style definition array as defined in `kModalStyles` into a CSS
 | 
						|
   * string that can be used to set the 'style' property of a DOM node.
 | 
						|
   *
 | 
						|
   * @param  {Array}    stylePairs         Two-dimensional array of style pairs
 | 
						|
   * @param  {...Array} [additionalStyles] Optional set of style pairs that will
 | 
						|
   *                                       augment or override the styles defined
 | 
						|
   *                                       by `stylePairs`
 | 
						|
   * @return {String}
 | 
						|
   */
 | 
						|
  _getStyleString(stylePairs, ...additionalStyles) {
 | 
						|
    let baseStyle = new Map(stylePairs);
 | 
						|
    for (let additionalStyle of additionalStyles) {
 | 
						|
      for (let [prop, value] of additionalStyle) {
 | 
						|
        baseStyle.set(prop, value);
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return [...baseStyle]
 | 
						|
      .map(([cssProp, cssVal]) => `${cssProp}: ${cssVal}`)
 | 
						|
      .join("; ");
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Checks whether a CSS RGB color value can be classified as being 'bright'.
 | 
						|
   *
 | 
						|
   * @param  {String} cssColor RGB color value in the default format rgb[a](r,g,b)
 | 
						|
   * @return {Boolean}
 | 
						|
   */
 | 
						|
  _isColorBright(cssColor) {
 | 
						|
    cssColor = cssColor.match(kRGBRE);
 | 
						|
    if (!cssColor || !cssColor.length) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    cssColor.shift();
 | 
						|
    return !new lazy.Color(...cssColor).useBrightText;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Detects if the overall text color in the page can be described as bright.
 | 
						|
   * This is done according to the following algorithm:
 | 
						|
   *  1. With the entire set of ranges that we have found thusfar;
 | 
						|
   *  2. Get an odd-numbered `sampleSize`, with a maximum of `kBrightTextSampleSize`
 | 
						|
   *     ranges,
 | 
						|
   *  3. Slice the set of ranges into `sampleSize` number of equal parts,
 | 
						|
   *  4. Grab the first range for each slice and inspect the brightness of the
 | 
						|
   *     color of its text content.
 | 
						|
   *  5. When the majority of ranges are counted as contain bright colored text,
 | 
						|
   *     the page is considered to contain bright text overall.
 | 
						|
   *
 | 
						|
   * @param {Object} dict Dictionary of properties belonging to the
 | 
						|
   *                      currently active window. The page text color property
 | 
						|
   *                      will be recorded in `dict.brightText` as `true` or `false`.
 | 
						|
   */
 | 
						|
  _detectBrightText(dict) {
 | 
						|
    let sampleSize = Math.min(
 | 
						|
      dict.modalHighlightRectsMap.size,
 | 
						|
      kBrightTextSampleSize
 | 
						|
    );
 | 
						|
    let ranges = [...dict.modalHighlightRectsMap.keys()];
 | 
						|
    let rangesCount = ranges.length;
 | 
						|
    // Make sure the sample size is an odd number.
 | 
						|
    if (sampleSize % 2 == 0) {
 | 
						|
      // Make the previously or currently found range weigh heavier.
 | 
						|
      if (dict.previousFoundRange || dict.currentFoundRange) {
 | 
						|
        ranges.push(dict.previousFoundRange || dict.currentFoundRange);
 | 
						|
        ++sampleSize;
 | 
						|
        ++rangesCount;
 | 
						|
      } else {
 | 
						|
        --sampleSize;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    let brightCount = 0;
 | 
						|
    for (let i = 0; i < sampleSize; ++i) {
 | 
						|
      let range = ranges[Math.floor((rangesCount / sampleSize) * i)];
 | 
						|
      let fontStyle = this._getRangeFontStyle(range);
 | 
						|
      if (this._isColorBright(fontStyle.color)) {
 | 
						|
        ++brightCount;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    dict.brightText = brightCount >= Math.ceil(sampleSize / 2);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Checks if a range is inside a DOM node that's positioned in a way that it
 | 
						|
   * doesn't scroll along when the document is scrolled and/ or zoomed. This
 | 
						|
   * is the case for 'fixed' and 'sticky' positioned elements, elements inside
 | 
						|
   * (i)frames and elements that have their overflow styles set to 'auto' or
 | 
						|
   * 'scroll'.
 | 
						|
   *
 | 
						|
   * @param  {Range} range Range that be enclosed in a dynamic container
 | 
						|
   * @return {Boolean}
 | 
						|
   */
 | 
						|
  _isInDynamicContainer(range) {
 | 
						|
    const kFixed = new Set(["fixed", "sticky", "scroll", "auto"]);
 | 
						|
    let node = range.startContainer;
 | 
						|
    while (node.nodeType != 1) {
 | 
						|
      node = node.parentNode;
 | 
						|
    }
 | 
						|
    let document = node.ownerDocument;
 | 
						|
    let window = document.defaultView;
 | 
						|
    let dict = this.getForWindow(this.getTopWindow(window));
 | 
						|
 | 
						|
    // Check if we're in a frameset (including iframes).
 | 
						|
    if (window != this.getTopWindow(window)) {
 | 
						|
      if (!dict.frames.has(window)) {
 | 
						|
        dict.frames.set(window, {});
 | 
						|
      }
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
 | 
						|
    do {
 | 
						|
      let style = window.getComputedStyle(node);
 | 
						|
      if (
 | 
						|
        kFixed.has(style.position) ||
 | 
						|
        kFixed.has(style.overflow) ||
 | 
						|
        kFixed.has(style.overflowX) ||
 | 
						|
        kFixed.has(style.overflowY)
 | 
						|
      ) {
 | 
						|
        return true;
 | 
						|
      }
 | 
						|
      node = node.parentNode;
 | 
						|
    } while (node && node != document.documentElement);
 | 
						|
 | 
						|
    return false;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Read and store the rectangles that encompass the entire region of a range
 | 
						|
   * for use by the drawing function of the highlighter.
 | 
						|
   *
 | 
						|
   * @param  {Range}  range  Range to fetch the rectangles from
 | 
						|
   * @param  {Object} [dict] Dictionary of properties belonging to
 | 
						|
   *                         the currently active window
 | 
						|
   * @return {Set}    Set of rects that were found for the range
 | 
						|
   */
 | 
						|
  _getRangeRectsAndTexts(range, dict = null) {
 | 
						|
    let window = range.startContainer.ownerGlobal;
 | 
						|
    let bounds;
 | 
						|
    // If the window is part of a frameset, try to cache the bounds query.
 | 
						|
    if (dict && dict.frames.has(window)) {
 | 
						|
      let frameData = dict.frames.get(window);
 | 
						|
      bounds = frameData.bounds;
 | 
						|
      if (!bounds) {
 | 
						|
        bounds = frameData.bounds = this._getRootBounds(window);
 | 
						|
      }
 | 
						|
    } else {
 | 
						|
      bounds = this._getRootBounds(window);
 | 
						|
    }
 | 
						|
 | 
						|
    let topBounds = this._getRootBounds(this.getTopWindow(window, true), false);
 | 
						|
    let rects = [];
 | 
						|
    // A range may consist of multiple rectangles, we can also do these kind of
 | 
						|
    // precise cut-outs. range.getBoundingClientRect() returns the fully
 | 
						|
    // encompassing rectangle, which is too much for our purpose here.
 | 
						|
    let { rectList, textList } = range.getClientRectsAndTexts();
 | 
						|
    for (let rect of rectList) {
 | 
						|
      rect = lazy.Rect.fromRect(rect);
 | 
						|
      rect.x += bounds.x;
 | 
						|
      rect.y += bounds.y;
 | 
						|
      // If the rect is not even visible from the top document, we can ignore it.
 | 
						|
      if (rect.intersects(topBounds)) {
 | 
						|
        rects.push(rect);
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return { rectList: rects, textList };
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Read and store the rectangles that encompass the entire region of a range
 | 
						|
   * for use by the drawing function of the highlighter and store them in the
 | 
						|
   * cache.
 | 
						|
   *
 | 
						|
   * @param  {Range}   range            Range to fetch the rectangles from
 | 
						|
   * @param  {Boolean} [checkIfDynamic] Whether we should check if the range
 | 
						|
   *                                    is dynamic as per the rules in
 | 
						|
   *                                    `_isInDynamicContainer()`. Optional,
 | 
						|
   *                                    defaults to `true`
 | 
						|
   * @param  {Object}  [dict]           Dictionary of properties belonging to
 | 
						|
   *                                    the currently active window
 | 
						|
   * @return {Set}     Set of rects that were found for the range
 | 
						|
   */
 | 
						|
  _updateRangeRects(range, checkIfDynamic = true, dict = null) {
 | 
						|
    let window = range.startContainer.ownerGlobal;
 | 
						|
    let rectsAndTexts = this._getRangeRectsAndTexts(range, dict);
 | 
						|
 | 
						|
    // Only fetch the rect at this point, if not passed in as argument.
 | 
						|
    dict = dict || this.getForWindow(this.getTopWindow(window));
 | 
						|
    let oldRectsAndTexts = dict.modalHighlightRectsMap.get(range);
 | 
						|
    dict.modalHighlightRectsMap.set(range, rectsAndTexts);
 | 
						|
    // Check here if we suddenly went down to zero rects from more than zero before,
 | 
						|
    // which indicates that we should re-iterate the document.
 | 
						|
    if (
 | 
						|
      oldRectsAndTexts &&
 | 
						|
      oldRectsAndTexts.rectList.length &&
 | 
						|
      !rectsAndTexts.rectList.length
 | 
						|
    ) {
 | 
						|
      dict.detectedGeometryChange = true;
 | 
						|
    }
 | 
						|
    if (checkIfDynamic && this._isInDynamicContainer(range)) {
 | 
						|
      dict.dynamicRangesSet.add(range);
 | 
						|
    }
 | 
						|
    return rectsAndTexts;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Re-read the rectangles of the ranges that we keep track of separately,
 | 
						|
   * because they're enclosed by a position: fixed container DOM node or (i)frame.
 | 
						|
   *
 | 
						|
   * @param {Object} dict Dictionary of properties belonging to the currently
 | 
						|
   *                      active window
 | 
						|
   */
 | 
						|
  _updateDynamicRangesRects(dict) {
 | 
						|
    // Reset the frame bounds cache.
 | 
						|
    for (let frameData of dict.frames.values()) {
 | 
						|
      frameData.bounds = null;
 | 
						|
    }
 | 
						|
    for (let range of dict.dynamicRangesSet) {
 | 
						|
      this._updateRangeRects(range, false, dict);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Update the content, position and style of the yellow current found range
 | 
						|
   * outline that floats atop the mask with the dimmed background.
 | 
						|
   * Rebuild it, if necessary, This will deactivate the animation between
 | 
						|
   * occurrences.
 | 
						|
   *
 | 
						|
   * @param {Object} dict Dictionary of properties belonging to the currently
 | 
						|
   *                      active window
 | 
						|
   */
 | 
						|
  _updateRangeOutline(dict) {
 | 
						|
    let range = dict.currentFoundRange;
 | 
						|
    if (!range) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    let fontStyle = this._getRangeFontStyle(range);
 | 
						|
    // Text color in the outline is determined by kModalStyles.
 | 
						|
    delete fontStyle.color;
 | 
						|
 | 
						|
    let rectsAndTexts = this._updateRangeRects(range, true, dict);
 | 
						|
    let outlineAnonNode = dict.modalHighlightOutline;
 | 
						|
    let rectCount = rectsAndTexts.rectList.length;
 | 
						|
    let previousRectCount = dict.previousRangeRectsAndTexts.rectList.length;
 | 
						|
    // (re-)Building the outline is conditional and happens when one of the
 | 
						|
    // following conditions is met:
 | 
						|
    // 1. No outline nodes were built before, or
 | 
						|
    // 2. When the amount of rectangles to draw is different from before, or
 | 
						|
    // 3. When there's more than one rectangle to draw, because it's impossible
 | 
						|
    //    to animate that consistently with AnonymousContent nodes.
 | 
						|
    let rebuildOutline =
 | 
						|
      !outlineAnonNode || rectCount !== previousRectCount || rectCount != 1;
 | 
						|
    dict.previousRangeRectsAndTexts = rectsAndTexts;
 | 
						|
 | 
						|
    let window = this.getTopWindow(range.startContainer.ownerGlobal);
 | 
						|
    let document = window.document;
 | 
						|
    // First see if we need to and can remove the previous outline nodes.
 | 
						|
    if (rebuildOutline) {
 | 
						|
      this._removeRangeOutline(window);
 | 
						|
    }
 | 
						|
 | 
						|
    // Abort when there's no text to highlight OR when it's the exact same range
 | 
						|
    // as the previous call and isn't inside a dynamic container.
 | 
						|
    if (
 | 
						|
      !rectsAndTexts.textList.length ||
 | 
						|
      (!rebuildOutline &&
 | 
						|
        dict.previousUpdatedRange == range &&
 | 
						|
        !dict.dynamicRangesSet.has(range))
 | 
						|
    ) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    let outlineBox;
 | 
						|
    if (rebuildOutline) {
 | 
						|
      // Create the main (yellow) highlight outline box.
 | 
						|
      outlineBox = document.createElementNS(kNSHTML, "div");
 | 
						|
      outlineBox.setAttribute("id", kModalOutlineId);
 | 
						|
    }
 | 
						|
 | 
						|
    const kModalOutlineTextId = kModalOutlineId + "-text";
 | 
						|
    let i = 0;
 | 
						|
    for (let rect of rectsAndTexts.rectList) {
 | 
						|
      let text = rectsAndTexts.textList[i];
 | 
						|
 | 
						|
      // Next up is to check of the outline box' borders will not overlap with
 | 
						|
      // rects that we drew before or will draw after this one.
 | 
						|
      // We're taking the width of the border into account, which is
 | 
						|
      // `kOutlineBoxBorderSize` pixels.
 | 
						|
      // When left and/ or right sides will overlap with the current, previous
 | 
						|
      // or next rect, make sure to make the necessary adjustments to the style.
 | 
						|
      // These adjustments will override the styles as defined in `kModalStyles.outlineNode`.
 | 
						|
      let intersectingSides = new Set();
 | 
						|
      let previous = rectsAndTexts.rectList[i - 1];
 | 
						|
      if (previous && rect.left - previous.right <= 2 * kOutlineBoxBorderSize) {
 | 
						|
        intersectingSides.add("left");
 | 
						|
      }
 | 
						|
      let next = rectsAndTexts.rectList[i + 1];
 | 
						|
      if (next && next.left - rect.right <= 2 * kOutlineBoxBorderSize) {
 | 
						|
        intersectingSides.add("right");
 | 
						|
      }
 | 
						|
      let borderStyles = [...intersectingSides].map(side => [
 | 
						|
        "border-" + side,
 | 
						|
        0,
 | 
						|
      ]);
 | 
						|
      if (intersectingSides.size) {
 | 
						|
        borderStyles.push([
 | 
						|
          "margin",
 | 
						|
          `-${kOutlineBoxBorderSize}px 0 0 ${
 | 
						|
            intersectingSides.has("left") ? 0 : -kOutlineBoxBorderSize
 | 
						|
          }px !important`,
 | 
						|
        ]);
 | 
						|
        borderStyles.push([
 | 
						|
          "border-radius",
 | 
						|
          (intersectingSides.has("left") ? 0 : kOutlineBoxBorderRadius) +
 | 
						|
            "px " +
 | 
						|
            (intersectingSides.has("right") ? 0 : kOutlineBoxBorderRadius) +
 | 
						|
            "px " +
 | 
						|
            (intersectingSides.has("right") ? 0 : kOutlineBoxBorderRadius) +
 | 
						|
            "px " +
 | 
						|
            (intersectingSides.has("left") ? 0 : kOutlineBoxBorderRadius) +
 | 
						|
            "px",
 | 
						|
        ]);
 | 
						|
      }
 | 
						|
 | 
						|
      let outlineStyle = this._getStyleString(
 | 
						|
        kModalStyles.outlineNode,
 | 
						|
        [
 | 
						|
          ["top", rect.top + "px"],
 | 
						|
          ["left", rect.left + "px"],
 | 
						|
          ["height", rect.height + "px"],
 | 
						|
          ["width", rect.width + "px"],
 | 
						|
        ],
 | 
						|
        borderStyles,
 | 
						|
        lazy.kDebug ? kModalStyles.outlineNodeDebug : []
 | 
						|
      );
 | 
						|
      fontStyle.lineHeight = rect.height + "px";
 | 
						|
      let textStyle =
 | 
						|
        this._getStyleString(kModalStyles.outlineText) +
 | 
						|
        "; " +
 | 
						|
        this._getHTMLFontStyle(fontStyle);
 | 
						|
 | 
						|
      if (rebuildOutline) {
 | 
						|
        let textBoxParent = outlineBox.appendChild(
 | 
						|
          document.createElementNS(kNSHTML, "div")
 | 
						|
        );
 | 
						|
        textBoxParent.setAttribute("id", kModalOutlineId + i);
 | 
						|
        textBoxParent.setAttribute("style", outlineStyle);
 | 
						|
 | 
						|
        let textBox = document.createElementNS(kNSHTML, "span");
 | 
						|
        textBox.setAttribute("id", kModalOutlineTextId + i);
 | 
						|
        textBox.setAttribute("style", textStyle);
 | 
						|
        textBox.textContent = text;
 | 
						|
        textBoxParent.appendChild(textBox);
 | 
						|
      } else {
 | 
						|
        // Set the appropriate properties on the existing nodes, which will also
 | 
						|
        // activate the transitions.
 | 
						|
        outlineAnonNode.setAttributeForElement(
 | 
						|
          kModalOutlineId + i,
 | 
						|
          "style",
 | 
						|
          outlineStyle
 | 
						|
        );
 | 
						|
        outlineAnonNode.setAttributeForElement(
 | 
						|
          kModalOutlineTextId + i,
 | 
						|
          "style",
 | 
						|
          textStyle
 | 
						|
        );
 | 
						|
        outlineAnonNode.setTextContentForElement(kModalOutlineTextId + i, text);
 | 
						|
      }
 | 
						|
 | 
						|
      ++i;
 | 
						|
    }
 | 
						|
 | 
						|
    if (rebuildOutline) {
 | 
						|
      dict.modalHighlightOutline = lazy.kDebug
 | 
						|
        ? mockAnonymousContentNode(
 | 
						|
            (document.body || document.documentElement).appendChild(outlineBox)
 | 
						|
          )
 | 
						|
        : document.insertAnonymousContent(outlineBox);
 | 
						|
    }
 | 
						|
 | 
						|
    if (dict.animateOutline && !this._isPageTooBig(dict)) {
 | 
						|
      let animation;
 | 
						|
      dict.animations = new Set();
 | 
						|
      for (let i = rectsAndTexts.rectList.length - 1; i >= 0; --i) {
 | 
						|
        animation = dict.modalHighlightOutline.setAnimationForElement(
 | 
						|
          kModalOutlineId + i,
 | 
						|
          Cu.cloneInto(kModalOutlineAnim.keyframes, window),
 | 
						|
          kModalOutlineAnim.duration
 | 
						|
        );
 | 
						|
        animation.onfinish = function () {
 | 
						|
          dict.animations.delete(this);
 | 
						|
        };
 | 
						|
        dict.animations.add(animation);
 | 
						|
      }
 | 
						|
    }
 | 
						|
    dict.animateOutline = false;
 | 
						|
    dict.ignoreNextContentChange = true;
 | 
						|
 | 
						|
    dict.previousUpdatedRange = range;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Finish any currently playing animations on the found range outline node.
 | 
						|
   *
 | 
						|
   * @param {Object} dict Dictionary of properties belonging to the currently
 | 
						|
   *                      active window
 | 
						|
   */
 | 
						|
  _finishOutlineAnimations(dict) {
 | 
						|
    if (!dict.animations) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    for (let animation of dict.animations) {
 | 
						|
      animation.finish();
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Safely remove the outline AnoymousContent node from the CanvasFrame.
 | 
						|
   *
 | 
						|
   * @param {nsIDOMWindow} window
 | 
						|
   */
 | 
						|
  _removeRangeOutline(window) {
 | 
						|
    let dict = this.getForWindow(window);
 | 
						|
    if (!dict.modalHighlightOutline) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (lazy.kDebug) {
 | 
						|
      dict.modalHighlightOutline.remove();
 | 
						|
    } else {
 | 
						|
      try {
 | 
						|
        window.document.removeAnonymousContent(dict.modalHighlightOutline);
 | 
						|
      } catch (ex) {}
 | 
						|
    }
 | 
						|
 | 
						|
    dict.modalHighlightOutline = null;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Add a range to the list of ranges to highlight on, or cut out of, the dimmed
 | 
						|
   * background.
 | 
						|
   *
 | 
						|
   * @param {Range}        range  Range object that should be inspected
 | 
						|
   * @param {nsIDOMWindow} window Window object, whose DOM tree is being traversed
 | 
						|
   */
 | 
						|
  _modalHighlight(range, controller, window) {
 | 
						|
    this._updateRangeRects(range);
 | 
						|
 | 
						|
    this.show(window);
 | 
						|
    // We don't repaint the mask right away, but pass it off to a render loop of
 | 
						|
    // sorts.
 | 
						|
    this._scheduleRepaintOfMask(window);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Lazily insert the nodes we need as anonymous content into the CanvasFrame
 | 
						|
   * of a window.
 | 
						|
   *
 | 
						|
   * @param {nsIDOMWindow} window Window to draw in.
 | 
						|
   */
 | 
						|
  _maybeCreateModalHighlightNodes(window) {
 | 
						|
    window = this.getTopWindow(window);
 | 
						|
    let dict = this.getForWindow(window);
 | 
						|
    if (dict.modalHighlightOutline) {
 | 
						|
      if (!dict.modalHighlightAllMask) {
 | 
						|
        // Make sure to at least show the dimmed background.
 | 
						|
        this._repaintHighlightAllMask(window, false);
 | 
						|
        this._scheduleRepaintOfMask(window);
 | 
						|
      } else {
 | 
						|
        this._scheduleRepaintOfMask(window, { contentChanged: true });
 | 
						|
      }
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    let document = window.document;
 | 
						|
    // A hidden document doesn't accept insertAnonymousContent calls yet.
 | 
						|
    if (document.hidden) {
 | 
						|
      let onVisibilityChange = () => {
 | 
						|
        document.removeEventListener("visibilitychange", onVisibilityChange);
 | 
						|
        this._maybeCreateModalHighlightNodes(window);
 | 
						|
      };
 | 
						|
      document.addEventListener("visibilitychange", onVisibilityChange);
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // Make sure to at least show the dimmed background.
 | 
						|
    this._repaintHighlightAllMask(window, false);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Build and draw the mask that takes care of the dimmed background that
 | 
						|
   * overlays the current page and the mask that cuts out all the rectangles of
 | 
						|
   * the ranges that were found.
 | 
						|
   *
 | 
						|
   * @param {nsIDOMWindow} window Window to draw in.
 | 
						|
   * @param {Boolean} [paintContent]
 | 
						|
   */
 | 
						|
  _repaintHighlightAllMask(window, paintContent = true) {
 | 
						|
    window = this.getTopWindow(window);
 | 
						|
    let dict = this.getForWindow(window);
 | 
						|
 | 
						|
    const kMaskId = kModalIdPrefix + "-findbar-modalHighlight-outlineMask";
 | 
						|
    if (!dict.modalHighlightAllMask) {
 | 
						|
      let document = window.document;
 | 
						|
      let maskNode = document.createElementNS(kNSHTML, "div");
 | 
						|
      maskNode.setAttribute("id", kMaskId);
 | 
						|
      dict.modalHighlightAllMask = lazy.kDebug
 | 
						|
        ? mockAnonymousContentNode(
 | 
						|
            (document.body || document.documentElement).appendChild(maskNode)
 | 
						|
          )
 | 
						|
        : document.insertAnonymousContent(maskNode);
 | 
						|
    }
 | 
						|
 | 
						|
    // Make sure the dimmed mask node takes the full width and height that's available.
 | 
						|
    let { width, height } = (dict.lastWindowDimensions =
 | 
						|
      this._getWindowDimensions(window));
 | 
						|
    if (typeof dict.brightText != "boolean" || dict.updateAllRanges) {
 | 
						|
      this._detectBrightText(dict);
 | 
						|
    }
 | 
						|
    let maskStyle = this._getStyleString(
 | 
						|
      kModalStyles.maskNode,
 | 
						|
      [
 | 
						|
        ["width", width + "px"],
 | 
						|
        ["height", height + "px"],
 | 
						|
      ],
 | 
						|
      dict.brightText ? kModalStyles.maskNodeBrightText : [],
 | 
						|
      paintContent ? kModalStyles.maskNodeTransition : [],
 | 
						|
      lazy.kDebug ? kModalStyles.maskNodeDebug : []
 | 
						|
    );
 | 
						|
    dict.modalHighlightAllMask.setAttributeForElement(
 | 
						|
      kMaskId,
 | 
						|
      "style",
 | 
						|
      maskStyle
 | 
						|
    );
 | 
						|
 | 
						|
    this._updateRangeOutline(dict);
 | 
						|
 | 
						|
    let allRects = [];
 | 
						|
    // When the user's busy scrolling the document, don't bother cutting out rectangles,
 | 
						|
    // because they're not going to keep up with scrolling speed anyway.
 | 
						|
    if (!dict.busyScrolling && (paintContent || dict.modalHighlightAllMask)) {
 | 
						|
      // No need to update dynamic ranges separately when we already about to
 | 
						|
      // update all of them anyway.
 | 
						|
      if (!dict.updateAllRanges) {
 | 
						|
        this._updateDynamicRangesRects(dict);
 | 
						|
      }
 | 
						|
 | 
						|
      let DOMRect = window.DOMRect;
 | 
						|
      for (let [range, rectsAndTexts] of dict.modalHighlightRectsMap) {
 | 
						|
        if (!this.finder._fastFind.isRangeVisible(range, false)) {
 | 
						|
          continue;
 | 
						|
        }
 | 
						|
 | 
						|
        if (dict.updateAllRanges) {
 | 
						|
          rectsAndTexts = this._updateRangeRects(range);
 | 
						|
        }
 | 
						|
 | 
						|
        // If a geometry change was detected, we bail out right away here, because
 | 
						|
        // the current set of ranges has been invalidated.
 | 
						|
        if (dict.detectedGeometryChange) {
 | 
						|
          return;
 | 
						|
        }
 | 
						|
 | 
						|
        for (let rect of rectsAndTexts.rectList) {
 | 
						|
          allRects.push(new DOMRect(rect.x, rect.y, rect.width, rect.height));
 | 
						|
        }
 | 
						|
      }
 | 
						|
      dict.updateAllRanges = false;
 | 
						|
    }
 | 
						|
 | 
						|
    // We may also want to cut out zero rects, which effectively clears out the mask.
 | 
						|
    dict.modalHighlightAllMask.setCutoutRectsForElement(kMaskId, allRects);
 | 
						|
 | 
						|
    // The reflow observer may ignore the reflow we cause ourselves here.
 | 
						|
    dict.ignoreNextContentChange = true;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Safely remove the mask AnoymousContent node from the CanvasFrame.
 | 
						|
   *
 | 
						|
   * @param {nsIDOMWindow} window
 | 
						|
   */
 | 
						|
  _removeHighlightAllMask(window) {
 | 
						|
    window = this.getTopWindow(window);
 | 
						|
    let dict = this.getForWindow(window);
 | 
						|
    if (!dict.modalHighlightAllMask) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // If the current window isn't the one the content was inserted into, this
 | 
						|
    // will fail, but that's fine.
 | 
						|
    if (lazy.kDebug) {
 | 
						|
      dict.modalHighlightAllMask.remove();
 | 
						|
    } else {
 | 
						|
      try {
 | 
						|
        window.document.removeAnonymousContent(dict.modalHighlightAllMask);
 | 
						|
      } catch (ex) {}
 | 
						|
    }
 | 
						|
    dict.modalHighlightAllMask = null;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Check if the width or height of the current document is too big to handle
 | 
						|
   * for certain operations. This allows us to degrade gracefully when we expect
 | 
						|
   * the performance to be negatively impacted due to drawing-intensive operations.
 | 
						|
   *
 | 
						|
   * @param  {Object} dict Dictionary of properties belonging to the currently
 | 
						|
   *                       active window
 | 
						|
   * @return {Boolean}
 | 
						|
   */
 | 
						|
  _isPageTooBig(dict) {
 | 
						|
    let { height, width } = dict.lastWindowDimensions;
 | 
						|
    return height >= kPageIsTooBigPx || width >= kPageIsTooBigPx;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Doing a full repaint each time a range is delivered by the highlight iterator
 | 
						|
   * is way too costly, thus we pipe the frequency down to every
 | 
						|
   * `kModalHighlightRepaintLoFreqMs` milliseconds. If there are dynamic ranges
 | 
						|
   * found (see `_isInDynamicContainer()` for the definition), the frequency
 | 
						|
   * will be upscaled to `kModalHighlightRepaintHiFreqMs`.
 | 
						|
   *
 | 
						|
   * @param {nsIDOMWindow} window
 | 
						|
   * @param {Object}       options Dictionary of painter hints that contains the
 | 
						|
   *                               following properties:
 | 
						|
   *   {Boolean} contentChanged  Whether the documents' content changed in the
 | 
						|
   *                             meantime. This happens when the DOM is updated
 | 
						|
   *                             whilst the page is loaded.
 | 
						|
   *   {Boolean} scrollOnly      TRUE when the page has scrolled in the meantime,
 | 
						|
   *                             which means that the dynamically positioned
 | 
						|
   *                             elements need to be repainted.
 | 
						|
   *   {Boolean} updateAllRanges Whether to recalculate the rects of all ranges
 | 
						|
   *                             that were found up until now.
 | 
						|
   */
 | 
						|
  _scheduleRepaintOfMask(
 | 
						|
    window,
 | 
						|
    { contentChanged = false, scrollOnly = false, updateAllRanges = false } = {}
 | 
						|
  ) {
 | 
						|
    if (!this.useModal()) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    window = this.getTopWindow(window);
 | 
						|
    let dict = this.getForWindow(window);
 | 
						|
    // Bail out early if the repaint scheduler is paused or when we're supposed
 | 
						|
    // to ignore the next paint (i.e. content change).
 | 
						|
    if (
 | 
						|
      dict.repaintSchedulerState == kRepaintSchedulerPaused ||
 | 
						|
      (contentChanged && dict.ignoreNextContentChange)
 | 
						|
    ) {
 | 
						|
      dict.ignoreNextContentChange = false;
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    let hasDynamicRanges = !!dict.dynamicRangesSet.size;
 | 
						|
    let pageIsTooBig = this._isPageTooBig(dict);
 | 
						|
    let repaintDynamicRanges =
 | 
						|
      (scrollOnly || contentChanged) && hasDynamicRanges && !pageIsTooBig;
 | 
						|
 | 
						|
    // Determine scroll behavior and keep that state around.
 | 
						|
    let startedScrolling = !dict.busyScrolling && scrollOnly;
 | 
						|
    // When the user started scrolling the document, hide the other highlights.
 | 
						|
    if (startedScrolling) {
 | 
						|
      dict.busyScrolling = startedScrolling;
 | 
						|
      this._repaintHighlightAllMask(window);
 | 
						|
    }
 | 
						|
    // Whilst scrolling, suspend the repaint scheduler, but only when the page is
 | 
						|
    // too big or the find results contains ranges that are inside dynamic
 | 
						|
    // containers.
 | 
						|
    if (dict.busyScrolling && (pageIsTooBig || hasDynamicRanges)) {
 | 
						|
      dict.ignoreNextContentChange = true;
 | 
						|
      this._updateRangeOutline(dict);
 | 
						|
      // NB: we're not using `kRepaintSchedulerPaused` on purpose here, otherwise
 | 
						|
      // we'd break the `busyScrolling` detection (re-)using the timer.
 | 
						|
      if (dict.modalRepaintScheduler) {
 | 
						|
        window.clearTimeout(dict.modalRepaintScheduler);
 | 
						|
        dict.modalRepaintScheduler = null;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // When we request to repaint unconditionally, we mean to call
 | 
						|
    // `_repaintHighlightAllMask()` right after the timeout.
 | 
						|
    if (!dict.unconditionalRepaintRequested) {
 | 
						|
      dict.unconditionalRepaintRequested =
 | 
						|
        !contentChanged || repaintDynamicRanges;
 | 
						|
    }
 | 
						|
    // Some events, like a resize, call for recalculation of all the rects of all ranges.
 | 
						|
    if (!dict.updateAllRanges) {
 | 
						|
      dict.updateAllRanges = updateAllRanges;
 | 
						|
    }
 | 
						|
 | 
						|
    if (dict.modalRepaintScheduler) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    let timeoutMs =
 | 
						|
      hasDynamicRanges && !dict.busyScrolling
 | 
						|
        ? kModalHighlightRepaintHiFreqMs
 | 
						|
        : kModalHighlightRepaintLoFreqMs;
 | 
						|
    dict.modalRepaintScheduler = window.setTimeout(() => {
 | 
						|
      dict.modalRepaintScheduler = null;
 | 
						|
      dict.repaintSchedulerState = kRepaintSchedulerStopped;
 | 
						|
      dict.busyScrolling = false;
 | 
						|
 | 
						|
      let pageContentChanged = dict.detectedGeometryChange;
 | 
						|
      if (!pageContentChanged && !pageIsTooBig) {
 | 
						|
        let { width: previousWidth, height: previousHeight } =
 | 
						|
          dict.lastWindowDimensions;
 | 
						|
        let { width, height } = (dict.lastWindowDimensions =
 | 
						|
          this._getWindowDimensions(window));
 | 
						|
        pageContentChanged =
 | 
						|
          dict.detectedGeometryChange ||
 | 
						|
          Math.abs(previousWidth - width) > kContentChangeThresholdPx ||
 | 
						|
          Math.abs(previousHeight - height) > kContentChangeThresholdPx;
 | 
						|
      }
 | 
						|
      dict.detectedGeometryChange = false;
 | 
						|
      // When the page has changed significantly enough in size, we'll restart
 | 
						|
      // the iterator with the same parameters as before to find us new ranges.
 | 
						|
      if (pageContentChanged && !pageIsTooBig) {
 | 
						|
        this.iterator.restart(this.finder);
 | 
						|
      }
 | 
						|
 | 
						|
      if (
 | 
						|
        dict.unconditionalRepaintRequested ||
 | 
						|
        (dict.modalHighlightRectsMap.size && pageContentChanged)
 | 
						|
      ) {
 | 
						|
        dict.unconditionalRepaintRequested = false;
 | 
						|
        this._repaintHighlightAllMask(window);
 | 
						|
      }
 | 
						|
    }, timeoutMs);
 | 
						|
    dict.repaintSchedulerState = kRepaintSchedulerRunning;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Add event listeners to the content which will cause the modal highlight
 | 
						|
   * AnonymousContent to be re-painted or hidden.
 | 
						|
   *
 | 
						|
   * @param {nsIDOMWindow} window
 | 
						|
   */
 | 
						|
  _addModalHighlightListeners(window) {
 | 
						|
    window = this.getTopWindow(window);
 | 
						|
    let dict = this.getForWindow(window);
 | 
						|
    if (dict.highlightListeners) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    dict.highlightListeners = [
 | 
						|
      this._scheduleRepaintOfMask.bind(this, window, { contentChanged: true }),
 | 
						|
      this._scheduleRepaintOfMask.bind(this, window, { updateAllRanges: true }),
 | 
						|
      this._scheduleRepaintOfMask.bind(this, window, { scrollOnly: true }),
 | 
						|
      this.hide.bind(this, window, null),
 | 
						|
      () => (dict.busySelecting = true),
 | 
						|
      () => {
 | 
						|
        if (window.document.hidden) {
 | 
						|
          dict.repaintSchedulerState = kRepaintSchedulerPaused;
 | 
						|
        } else if (dict.repaintSchedulerState == kRepaintSchedulerPaused) {
 | 
						|
          dict.repaintSchedulerState = kRepaintSchedulerRunning;
 | 
						|
          this._scheduleRepaintOfMask(window);
 | 
						|
        }
 | 
						|
      },
 | 
						|
    ];
 | 
						|
    let target = this.iterator._getDocShell(window).chromeEventHandler;
 | 
						|
    target.addEventListener("MozAfterPaint", dict.highlightListeners[0]);
 | 
						|
    target.addEventListener("resize", dict.highlightListeners[1]);
 | 
						|
    target.addEventListener("scroll", dict.highlightListeners[2], {
 | 
						|
      capture: true,
 | 
						|
      passive: true,
 | 
						|
    });
 | 
						|
    target.addEventListener("click", dict.highlightListeners[3]);
 | 
						|
    target.addEventListener("selectstart", dict.highlightListeners[4]);
 | 
						|
    window.document.addEventListener(
 | 
						|
      "visibilitychange",
 | 
						|
      dict.highlightListeners[5]
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Remove event listeners from content.
 | 
						|
   *
 | 
						|
   * @param {nsIDOMWindow} window
 | 
						|
   */
 | 
						|
  _removeModalHighlightListeners(window) {
 | 
						|
    window = this.getTopWindow(window);
 | 
						|
    let dict = this.getForWindow(window);
 | 
						|
    if (!dict.highlightListeners) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    let target = this.iterator._getDocShell(window).chromeEventHandler;
 | 
						|
    target.removeEventListener("MozAfterPaint", dict.highlightListeners[0]);
 | 
						|
    target.removeEventListener("resize", dict.highlightListeners[1]);
 | 
						|
    target.removeEventListener("scroll", dict.highlightListeners[2], {
 | 
						|
      capture: true,
 | 
						|
      passive: true,
 | 
						|
    });
 | 
						|
    target.removeEventListener("click", dict.highlightListeners[3]);
 | 
						|
    target.removeEventListener("selectstart", dict.highlightListeners[4]);
 | 
						|
    window.document.removeEventListener(
 | 
						|
      "visibilitychange",
 | 
						|
      dict.highlightListeners[5]
 | 
						|
    );
 | 
						|
 | 
						|
    dict.highlightListeners = null;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * For a given node returns its editable parent or null if there is none.
 | 
						|
   * It's enough to check if node is a text node and its parent's parent is
 | 
						|
   * an input or textarea.
 | 
						|
   *
 | 
						|
   * @param node the node we want to check
 | 
						|
   * @returns the first node in the parent chain that is editable,
 | 
						|
   *          null if there is no such node
 | 
						|
   */
 | 
						|
  _getEditableNode(node) {
 | 
						|
    if (
 | 
						|
      node.nodeType === node.TEXT_NODE &&
 | 
						|
      node.parentNode &&
 | 
						|
      node.parentNode.parentNode &&
 | 
						|
      (ChromeUtils.getClassName(node.parentNode.parentNode) ===
 | 
						|
        "HTMLInputElement" ||
 | 
						|
        ChromeUtils.getClassName(node.parentNode.parentNode) ===
 | 
						|
          "HTMLTextAreaElement")
 | 
						|
    ) {
 | 
						|
      return node.parentNode.parentNode;
 | 
						|
    }
 | 
						|
    return null;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Add ourselves as an nsIEditActionListener and nsIDocumentStateListener for
 | 
						|
   * a given editor
 | 
						|
   *
 | 
						|
   * @param editor the editor we'd like to listen to
 | 
						|
   */
 | 
						|
  _addEditorListeners(editor) {
 | 
						|
    if (!this._editors) {
 | 
						|
      this._editors = [];
 | 
						|
      this._stateListeners = [];
 | 
						|
    }
 | 
						|
 | 
						|
    let existingIndex = this._editors.indexOf(editor);
 | 
						|
    if (existingIndex == -1) {
 | 
						|
      let x = this._editors.length;
 | 
						|
      this._editors[x] = editor;
 | 
						|
      this._stateListeners[x] = this._createStateListener();
 | 
						|
      this._editors[x].addEditActionListener(this);
 | 
						|
      this._editors[x].addDocumentStateListener(this._stateListeners[x]);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Helper method to unhook listeners, remove cached editors
 | 
						|
   * and keep the relevant arrays in sync
 | 
						|
   *
 | 
						|
   * @param idx the index into the array of editors/state listeners
 | 
						|
   *        we wish to remove
 | 
						|
   */
 | 
						|
  _unhookListenersAtIndex(idx) {
 | 
						|
    this._editors[idx].removeEditActionListener(this);
 | 
						|
    this._editors[idx].removeDocumentStateListener(this._stateListeners[idx]);
 | 
						|
    this._editors.splice(idx, 1);
 | 
						|
    this._stateListeners.splice(idx, 1);
 | 
						|
    if (!this._editors.length) {
 | 
						|
      delete this._editors;
 | 
						|
      delete this._stateListeners;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Remove ourselves as an nsIEditActionListener and
 | 
						|
   * nsIDocumentStateListener from a given cached editor
 | 
						|
   *
 | 
						|
   * @param editor the editor we no longer wish to listen to
 | 
						|
   */
 | 
						|
  _removeEditorListeners(editor) {
 | 
						|
    // editor is an editor that we listen to, so therefore must be
 | 
						|
    // cached. Find the index of this editor
 | 
						|
    let idx = this._editors.indexOf(editor);
 | 
						|
    if (idx == -1) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    // Now unhook ourselves, and remove our cached copy
 | 
						|
    this._unhookListenersAtIndex(idx);
 | 
						|
  },
 | 
						|
 | 
						|
  /*
 | 
						|
   * nsIEditActionListener logic follows
 | 
						|
   *
 | 
						|
   * We implement this interface to allow us to catch the case where
 | 
						|
   * the findbar found a match in a HTML <input> or <textarea>. If the
 | 
						|
   * user adjusts the text in some way, it will no longer match, so we
 | 
						|
   * want to remove the highlight, rather than have it expand/contract
 | 
						|
   * when letters are added or removed.
 | 
						|
   */
 | 
						|
 | 
						|
  /**
 | 
						|
   * Helper method used to check whether a selection intersects with
 | 
						|
   * some highlighting
 | 
						|
   *
 | 
						|
   * @param selectionRange the range from the selection to check
 | 
						|
   * @param findRange the highlighted range to check against
 | 
						|
   * @returns true if they intersect, false otherwise
 | 
						|
   */
 | 
						|
  _checkOverlap(selectionRange, findRange) {
 | 
						|
    if (!selectionRange || !findRange) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    // The ranges overlap if one of the following is true:
 | 
						|
    // 1) At least one of the endpoints of the deleted selection
 | 
						|
    //    is in the find selection
 | 
						|
    // 2) At least one of the endpoints of the find selection
 | 
						|
    //    is in the deleted selection
 | 
						|
    if (
 | 
						|
      findRange.isPointInRange(
 | 
						|
        selectionRange.startContainer,
 | 
						|
        selectionRange.startOffset
 | 
						|
      )
 | 
						|
    ) {
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
    if (
 | 
						|
      findRange.isPointInRange(
 | 
						|
        selectionRange.endContainer,
 | 
						|
        selectionRange.endOffset
 | 
						|
      )
 | 
						|
    ) {
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
    if (
 | 
						|
      selectionRange.isPointInRange(
 | 
						|
        findRange.startContainer,
 | 
						|
        findRange.startOffset
 | 
						|
      )
 | 
						|
    ) {
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
    if (
 | 
						|
      selectionRange.isPointInRange(findRange.endContainer, findRange.endOffset)
 | 
						|
    ) {
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
 | 
						|
    return false;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Helper method to determine if an edit occurred within a highlight
 | 
						|
   *
 | 
						|
   * @param selection the selection we wish to check
 | 
						|
   * @param node the node we want to check is contained in selection
 | 
						|
   * @param offset the offset into node that we want to check
 | 
						|
   * @returns the range containing (node, offset) or null if no ranges
 | 
						|
   *          in the selection contain it
 | 
						|
   */
 | 
						|
  _findRange(selection, node, offset) {
 | 
						|
    let rangeCount = selection.rangeCount;
 | 
						|
    let rangeidx = 0;
 | 
						|
    let foundContainingRange = false;
 | 
						|
    let range = null;
 | 
						|
 | 
						|
    // Check to see if this node is inside one of the selection's ranges
 | 
						|
    while (!foundContainingRange && rangeidx < rangeCount) {
 | 
						|
      range = selection.getRangeAt(rangeidx);
 | 
						|
      if (range.isPointInRange(node, offset)) {
 | 
						|
        foundContainingRange = true;
 | 
						|
        break;
 | 
						|
      }
 | 
						|
      rangeidx++;
 | 
						|
    }
 | 
						|
 | 
						|
    if (foundContainingRange) {
 | 
						|
      return range;
 | 
						|
    }
 | 
						|
 | 
						|
    return null;
 | 
						|
  },
 | 
						|
 | 
						|
  // Start of nsIEditActionListener implementations
 | 
						|
 | 
						|
  WillDeleteText(textNode, offset, length) {
 | 
						|
    let editor = this._getEditableNode(textNode).editor;
 | 
						|
    let controller = editor.selectionController;
 | 
						|
    let fSelection = controller.getSelection(
 | 
						|
      Ci.nsISelectionController.SELECTION_FIND
 | 
						|
    );
 | 
						|
    let range = this._findRange(fSelection, textNode, offset);
 | 
						|
 | 
						|
    if (range) {
 | 
						|
      // Don't remove the highlighting if the deleted text is at the
 | 
						|
      // end of the range
 | 
						|
      if (textNode != range.endContainer || offset != range.endOffset) {
 | 
						|
        // Text within the highlight is being removed - the text can
 | 
						|
        // no longer be a match, so remove the highlighting
 | 
						|
        fSelection.removeRange(range);
 | 
						|
        if (fSelection.rangeCount == 0) {
 | 
						|
          this._removeEditorListeners(editor);
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  DidInsertText(textNode, offset, aString) {
 | 
						|
    let editor = this._getEditableNode(textNode).editor;
 | 
						|
    let controller = editor.selectionController;
 | 
						|
    let fSelection = controller.getSelection(
 | 
						|
      Ci.nsISelectionController.SELECTION_FIND
 | 
						|
    );
 | 
						|
    let range = this._findRange(fSelection, textNode, offset);
 | 
						|
 | 
						|
    if (range) {
 | 
						|
      // If the text was inserted before the highlight
 | 
						|
      // adjust the highlight's bounds accordingly
 | 
						|
      if (textNode == range.startContainer && offset == range.startOffset) {
 | 
						|
        range.setStart(
 | 
						|
          range.startContainer,
 | 
						|
          range.startOffset + aString.length
 | 
						|
        );
 | 
						|
      } else if (textNode != range.endContainer || offset != range.endOffset) {
 | 
						|
        // The edit occurred within the highlight - any addition of text
 | 
						|
        // will result in the text no longer being a match,
 | 
						|
        // so remove the highlighting
 | 
						|
        fSelection.removeRange(range);
 | 
						|
        if (fSelection.rangeCount == 0) {
 | 
						|
          this._removeEditorListeners(editor);
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  WillDeleteRanges(rangesToDelete) {
 | 
						|
    let { editor } = this._getEditableNode(rangesToDelete[0].startContainer);
 | 
						|
    let controller = editor.selectionController;
 | 
						|
    let fSelection = controller.getSelection(
 | 
						|
      Ci.nsISelectionController.SELECTION_FIND
 | 
						|
    );
 | 
						|
 | 
						|
    let shouldDelete = {};
 | 
						|
    let numberOfDeletedSelections = 0;
 | 
						|
    let numberOfMatches = fSelection.rangeCount;
 | 
						|
 | 
						|
    // We need to test if any ranges to be deleted
 | 
						|
    // are in any of the ranges of the find selection
 | 
						|
    // Usually both selections will only contain one range, however
 | 
						|
    // either may contain more than one.
 | 
						|
 | 
						|
    for (let fIndex = 0; fIndex < numberOfMatches; fIndex++) {
 | 
						|
      shouldDelete[fIndex] = false;
 | 
						|
      let fRange = fSelection.getRangeAt(fIndex);
 | 
						|
 | 
						|
      for (let selRange of rangesToDelete) {
 | 
						|
        if (shouldDelete[fIndex]) {
 | 
						|
          continue;
 | 
						|
        }
 | 
						|
 | 
						|
        let doesOverlap = this._checkOverlap(selRange, fRange);
 | 
						|
        if (doesOverlap) {
 | 
						|
          shouldDelete[fIndex] = true;
 | 
						|
          numberOfDeletedSelections++;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // OK, so now we know what matches (if any) are in the selection
 | 
						|
    // that is being deleted. Time to remove them.
 | 
						|
    if (!numberOfDeletedSelections) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    for (let i = numberOfMatches - 1; i >= 0; i--) {
 | 
						|
      if (shouldDelete[i]) {
 | 
						|
        fSelection.removeRange(fSelection.getRangeAt(i));
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // Remove listeners if no more highlights left
 | 
						|
    if (!fSelection.rangeCount) {
 | 
						|
      this._removeEditorListeners(editor);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /*
 | 
						|
   * nsIDocumentStateListener logic follows
 | 
						|
   *
 | 
						|
   * When attaching nsIEditActionListeners, there are no guarantees
 | 
						|
   * as to whether the findbar or the documents in the browser will get
 | 
						|
   * destructed first. This leads to the potential to either leak, or to
 | 
						|
   * hold on to a reference an editable element's editor for too long,
 | 
						|
   * preventing it from being destructed.
 | 
						|
   *
 | 
						|
   * However, when an editor's owning node is being destroyed, the editor
 | 
						|
   * sends out a DocumentWillBeDestroyed notification. We can use this to
 | 
						|
   * clean up our references to the object, to allow it to be destroyed in a
 | 
						|
   * timely fashion.
 | 
						|
   */
 | 
						|
 | 
						|
  /**
 | 
						|
   * Unhook ourselves when one of our state listeners has been called.
 | 
						|
   * This can happen in 4 cases:
 | 
						|
   *  1) The document the editor belongs to is navigated away from, and
 | 
						|
   *     the document is not being cached
 | 
						|
   *
 | 
						|
   *  2) The document the editor belongs to is expired from the cache
 | 
						|
   *
 | 
						|
   *  3) The tab containing the owning document is closed
 | 
						|
   *
 | 
						|
   *  4) The <input> or <textarea> that owns the editor is explicitly
 | 
						|
   *     removed from the DOM
 | 
						|
   *
 | 
						|
   * @param the listener that was invoked
 | 
						|
   */
 | 
						|
  _onEditorDestruction(aListener) {
 | 
						|
    // First find the index of the editor the given listener listens to.
 | 
						|
    // The listeners and editors arrays must always be in sync.
 | 
						|
    // The listener will be in our array of cached listeners, as this
 | 
						|
    // method could not have been called otherwise.
 | 
						|
    let idx = 0;
 | 
						|
    while (this._stateListeners[idx] != aListener) {
 | 
						|
      idx++;
 | 
						|
    }
 | 
						|
 | 
						|
    // Unhook both listeners
 | 
						|
    this._unhookListenersAtIndex(idx);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Creates a unique document state listener for an editor.
 | 
						|
   *
 | 
						|
   * It is not possible to simply have the findbar implement the
 | 
						|
   * listener interface itself, as it wouldn't have sufficient information
 | 
						|
   * to work out which editor was being destroyed. Therefore, we create new
 | 
						|
   * listeners on the fly, and cache them in sync with the editors they
 | 
						|
   * listen to.
 | 
						|
   */
 | 
						|
  _createStateListener() {
 | 
						|
    return {
 | 
						|
      findbar: this,
 | 
						|
 | 
						|
      QueryInterface: ChromeUtils.generateQI(["nsIDocumentStateListener"]),
 | 
						|
 | 
						|
      NotifyDocumentWillBeDestroyed() {
 | 
						|
        this.findbar._onEditorDestruction(this);
 | 
						|
      },
 | 
						|
 | 
						|
      // Unimplemented
 | 
						|
      notifyDocumentStateChanged(aDirty) {},
 | 
						|
    };
 | 
						|
  },
 | 
						|
};
 |