Bug 1279695 - update the position of ranges that are in fixed or sticky positioned container nodes at each repaint or scroll. r?jaws draft
authorMike de Boer <mdeboer@mozilla.com>
Mon, 22 Aug 2016 16:50:29 +0200
changeset 403985 a8de7d04cab9a1a6e40898a16ebfe1e9b6c8d03b
parent 403984 5262784d164fccecd73b8e56612343a2d1f611e9
child 529051 3d24f30c31c33f885d2aed9ad24dd9d65428e315
push id27065
push usermdeboer@mozilla.com
push dateMon, 22 Aug 2016 14:51:44 +0000
reviewersjaws
bugs1279695
milestone51.0a1
Bug 1279695 - update the position of ranges that are in fixed or sticky positioned container nodes at each repaint or scroll. r?jaws MozReview-Commit-ID: LA7FirpRgFp
toolkit/modules/FinderHighlighter.jsm
--- a/toolkit/modules/FinderHighlighter.jsm
+++ b/toolkit/modules/FinderHighlighter.jsm
@@ -147,19 +147,21 @@ function mockAnonymousContentNode(domNod
 /**
  * FinderHighlighter class that is used by Finder.jsm to take care of the
  * 'Highlight All' feature, which can highlight all find occurrences in a page.
  *
  * @param {Finder} finder Finder.jsm instance
  */
 function FinderHighlighter(finder) {
   this._currentFoundRange = null;
-  this._modal = Services.prefs.getBoolPref(kModalHighlightPref);
   this._highlightAll = Services.prefs.getBoolPref(kHighlightAllPref);
   this._lastIteratorParams = null;
+  this._modal = Services.prefs.getBoolPref(kModalHighlightPref);
+  this._modalHighlightRectsMap = new Map();
+  this._fixedRangesSet = new Set(); 
   this.finder = finder;
   this.visible = false;
 }
 
 FinderHighlighter.prototype = {
   get iterator() {
     if (this._iterator)
       return this._iterator;
@@ -407,43 +409,27 @@ FinderHighlighter.prototype = {
       this._currentFoundRange = foundRange;
 
       let textContent = this._getRangeContentArray(foundRange);
       if (!textContent.length) {
         this.hide(window);
         return;
       }
 
-      let rect = foundRange.getClientRects()[0];
       let fontStyle = this._getRangeFontStyle(foundRange);
       if (typeof this._brightText == "undefined") {
         this._brightText = this._isColorBright(fontStyle.color);
       }
 
-      // Text color in the outline is determined by our stylesheet.
-      delete fontStyle.color;
-
       if (!this.visible)
         this.show(window);
       else
         this._maybeCreateModalHighlightNodes(window);
 
-      outlineNode = this._modalHighlightOutline;
-      outlineNode.setTextContentForElement(kModalOutlineId + "-text", textContent.join(" "));
-      // Correct the line-height to align the text in the middle of the box.
-      fontStyle.lineHeight = rect.height + "px";
-      outlineNode.setAttributeForElement(kModalOutlineId + "-text", "style",
-        this._getHTMLFontStyle(fontStyle));
-
-      if (typeof outlineNode.getAttributeForElement(kModalOutlineId, "hidden") == "string")
-        outlineNode.removeAttributeForElement(kModalOutlineId, "hidden");
-      let { scrollX, scrollY } = this._getScrollPosition(window);
-      outlineNode.setAttributeForElement(kModalOutlineId, "style",
-        `top: ${scrollY + rect.top}px; left: ${scrollX + rect.left}px;
-        height: ${rect.height}px; width: ${rect.width}px;`);
+      this._updateRangeOutline(textContent, fontStyle);
     }
 
     outlineNode = this._modalHighlightOutline;
     try {
       outlineNode.removeAttributeForElement(kModalOutlineId, "grow");
     } catch (ex) {}
     window.requestAnimationFrame(() => {
       outlineNode.setAttributeForElement(kModalOutlineId, "grow", true);
@@ -453,18 +439,18 @@ FinderHighlighter.prototype = {
   /**
    * Invalidates the list by clearing the map of highglighted ranges that we
    * keep to build the mask for.
    */
   clear() {
     this._currentFoundRange = null;
 
     // Reset the Map, because no range references a node anymore.
-    if (this._modalHighlightRectsMap)
-      this._modalHighlightRectsMap.clear();
+    this._modalHighlightRectsMap.clear();
+    this._fixedRangesSet.clear();
   },
 
   /**
    * 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.
    */
@@ -654,44 +640,132 @@ FinderHighlighter.prototype = {
     cssColor = cssColor.match(kRGBRE);
     if (!cssColor || !cssColor.length)
       return false;
     cssColor.shift();
     return new Color(...cssColor).isBright;
   },
 
   /**
-   * Add a range to the list of ranges to highlight on, or cut out of, the dimmed
-   * background.
+   * 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.
    *
-   * @param {nsIDOMRange}  range  Range object that should be inspected
-   * @param {nsIDOMWindow} window Window object, whose DOM tree is being traversed
+   * @param  {nsIDOMRange} range Range that be enclosed in a fixed container
+   * @return {Boolean}
    */
-  _modalHighlight(range, controller, window) {
-    if (!this._getRangeContentArray(range).length)
-      return;
+  _isInFixedContainer(range) {
+    const kFixed = new Set(["fixed", "sticky"]);
+    let node = range.startContainer;
+    while (node.nodeType != 1)
+      node = node.parentNode;
+    let document = node.ownerDocument;
+    let window = document.defaultView;
+    let docEl = document.documentElement;
+    do {
+      if (kFixed.has(window.getComputedStyle(node, null).position))
+        return true;
+      node = node.parentNode;
+    } while (node && node != docEl)
+    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 {nsIDOMRange} range     Range to fetch the rectangles from
+   * @param {Object}      scrollPos Document scroll position coordinates, which
+   *                                contains the following properties:
+   *   {Number} scrollX Horizontal scroll position
+   *   {Number} scrollY Vertical scroll position
+   */
+  _updateRangeRects(range, { scrollX, scrollY }) {
     let rects = new Set();
-    // Absolute positions should include the viewport scroll offset.
-    let { scrollX, scrollY } = this._getScrollPosition(window);
+    
     // 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.
     for (let dims of range.getClientRects()) {
       rects.add({
         height: dims.bottom - dims.top,
         width: dims.right - dims.left,
         y: dims.top + scrollY,
         x: dims.left + scrollX
       });
     }
+    this._modalHighlightRectsMap.set(range, rects);
+    if (this._isInFixedContainer(range))
+      this._fixedRangesSet.add(range);
+  },
 
-    if (!this._modalHighlightRectsMap)
-      this._modalHighlightRectsMap = new Map();
-    this._modalHighlightRectsMap.set(range, rects);
+  /**
+   * Re-read the rectangles of the ranges that we keep track of separately,
+   * because they're enclosed by a position: fixed container DOM node.
+   *
+   * @param {nsIDOMWindow} window Window object, whose DOM tree is being traversed
+   */
+  _updateFixedRangesRects(window) {
+    let scrollPos = this._getScrollPosition(window);
+    for (let range of this._fixedRangesSet)
+      this._updateRangeRects(range, scrollPos);
+  },
+
+  /**
+   * Update the content, position and style of the yellow current found range
+   * outline the floats atop the mask with the dimmed background.
+   *
+   * @param {Array}  [textContent] Array of text that's inside the range. Optional,
+   *                               defaults to an empty array
+   * @param {Object} [fontStyle]   Dictionary of CSS styles in camelCase as
+   *                               returned by `_getRangeFontStyle()`. Optional
+   */
+  _updateRangeOutline(textContent = [], fontStyle = null) {
+    let outlineNode = this._modalHighlightOutline;
+    let range = this._currentFoundRange;
+    if (!outlineNode || !range)
+      return;
+    let rect = range.getClientRects()[0];
+    if (!rect)
+      return;
+
+    if (!fontStyle)
+      fontStyle = this._getRangeFontStyle(range);
+    // Text color in the outline is determined by our stylesheet.
+    delete fontStyle.color;
+
+    if (textContent.length)
+      outlineNode.setTextContentForElement(kModalOutlineId + "-text", textContent.join(" "));
+    // Correct the line-height to align the text in the middle of the box.
+    fontStyle.lineHeight = rect.height + "px";
+    outlineNode.setAttributeForElement(kModalOutlineId + "-text", "style",
+      this._getHTMLFontStyle(fontStyle));
+
+    if (typeof outlineNode.getAttributeForElement(kModalOutlineId, "hidden") == "string")
+      outlineNode.removeAttributeForElement(kModalOutlineId, "hidden");
+
+    let window = range.startContainer.ownerDocument.defaultView;
+    let { scrollX, scrollY } = this._getScrollPosition(window);
+    outlineNode.setAttributeForElement(kModalOutlineId, "style",
+      `top: ${scrollY + rect.top}px; left: ${scrollX + rect.left}px;
+      height: ${rect.height}px; width: ${rect.width}px;`);
+  },
+
+  /**
+   * Add a range to the list of ranges to highlight on, or cut out of, the dimmed
+   * background.
+   *
+   * @param {nsIDOMRange}  range  Range object that should be inspected
+   * @param {nsIDOMWindow} window Window object, whose DOM tree is being traversed
+   */
+  _modalHighlight(range, controller, window) {
+    if (!this._getRangeContentArray(range).length)
+      return;
+
+    this._updateRangeRects(range, this._getScrollPosition(window));
 
     this.show(window);
     // We don't repaint the mask right away, but pass it off to a render loop of
     // sorts.
     this._scheduleRepaintOfMask(window);
   },
 
   /**
@@ -765,25 +839,25 @@ FinderHighlighter.prototype = {
     this._lastWindowDimensions = { width, height };
     maskNode.setAttribute("id", kMaskId);
     maskNode.setAttribute("class", kMaskId + (kDebug ? ` ${kModalIdPrefix}-findbar-debug` : ""));
     maskNode.setAttribute("style", `width: ${width}px; height: ${height}px;`);
     if (this._brightText)
       maskNode.setAttribute("brighttext", "true");
 
     if (paintContent || this._modalHighlightAllMask) {
+      this._updateRangeOutline();
+      this._updateFixedRangesRects(window);
       // Create a DOM node for each rectangle representing the ranges we found.
       let maskContent = [];
       const kRectClassName = kModalIdPrefix + "-findbar-modalHighlight-rect";
-      if (this._modalHighlightRectsMap) {
-        for (let [range, rects] of this._modalHighlightRectsMap) {
-          for (let rect of rects) {
-            maskContent.push(`<div class="${kRectClassName}" style="top: ${rect.y}px;
-              left: ${rect.x}px; height: ${rect.height}px; width: ${rect.width}px;"></div>`);
-          }
+      for (let [range, rects] of this._modalHighlightRectsMap) {
+        for (let rect of rects) {
+          maskContent.push(`<div class="${kRectClassName}" style="top: ${rect.y}px;
+            left: ${rect.x}px; height: ${rect.height}px; width: ${rect.width}px;"></div>`);
         }
       }
       maskNode.innerHTML = maskContent.join("");
     }
 
     // Always remove the current mask and insert it a-fresh, because we're not
     // free to alter DOM nodes inside the CanvasFrame.
     this._removeHighlightAllMask(window);
@@ -815,40 +889,47 @@ FinderHighlighter.prototype = {
   },
 
   /**
    * 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
    * `kModalHighlightRepaintFreqMs` milliseconds.
    *
    * @param {nsIDOMWindow} window
-   * @param {Boolean}      contentChanged Whether the documents' content changed
-   *                                      in the meantime. This happens when the
-   *                                      DOM is updated whilst the page is loaded.
+   * @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 fixed positioned elements
+   *                            need to be repainted.
    */
-  _scheduleRepaintOfMask(window, contentChanged = false) {
-    if (this._modalRepaintScheduler) {
-      window.clearTimeout(this._modalRepaintScheduler);
-      this._modalRepaintScheduler = null;
-    }
+  _scheduleRepaintOfMask(window, { contentChanged, scrollOnly } = { contentChanged: false, scrollOnly: false }) {
+    let repaintFixedNodes = (scrollOnly && !!this._fixedRangesSet.size);
 
     // When we request to repaint unconditionally, we mean to call
     // `_repaintHighlightAllMask()` right after the timeout.
     if (!this._unconditionalRepaintRequested)
-      this._unconditionalRepaintRequested = !contentChanged;
+      this._unconditionalRepaintRequested = !contentChanged || repaintFixedNodes;
+
+    if (this._modalRepaintScheduler)
+      return;
 
     this._modalRepaintScheduler = window.setTimeout(() => {
+      this._modalRepaintScheduler = null;
+
       if (this._unconditionalRepaintRequested) {
         this._unconditionalRepaintRequested = false;
         this._repaintHighlightAllMask(window);
         return;
       }
 
       let { width, height } = this._getWindowDimensions(window);
-      if (!this._modalHighlightRectsMap ||
+      if (!this._modalHighlightRectsMap.size ||
           (Math.abs(this._lastWindowDimensions.width - width) < kContentChangeThresholdPx &&
            Math.abs(this._lastWindowDimensions.height - height) < kContentChangeThresholdPx)) {
         return;
       }
 
       this.iterator.restart(this.finder);
       this._lastWindowDimensions = { width, height };
       this._repaintHighlightAllMask(window);
@@ -886,36 +967,41 @@ FinderHighlighter.prototype = {
    *
    * @param {nsIDOMWindow} window
    */
   _addModalHighlightListeners(window) {
     if (this._highlightListeners)
       return;
 
     this._highlightListeners = [
-      this._scheduleRepaintOfMask.bind(this, window, true),
+      this._scheduleRepaintOfMask.bind(this, window, { contentChanged: true }),
+      this._scheduleRepaintOfMask.bind(this, window, { scrollOnly: true }),
       this.hide.bind(this, window, null)
     ];
     let target = this.iterator._getDocShell(window).chromeEventHandler;
     target.addEventListener("MozAfterPaint", this._highlightListeners[0]);
-    window.addEventListener("click", this._highlightListeners[1]);
+    window.addEventListener("DOMMouseScroll", this._highlightListeners[1]);
+    window.addEventListener("mousewheel", this._highlightListeners[1]);
+    window.addEventListener("click", this._highlightListeners[2]);
   },
 
   /**
    * Remove event listeners from content.
    *
    * @param {nsIDOMWindow} window
    */
   _removeModalHighlightListeners(window) {
     if (!this._highlightListeners)
       return;
 
     let target = this.iterator._getDocShell(window).chromeEventHandler;
     target.removeEventListener("MozAfterPaint", this._highlightListeners[0]);
-    window.removeEventListener("click", this._highlightListeners[1]);
+    window.removeEventListener("DOMMouseScroll", this._highlightListeners[1]);
+    window.removeEventListener("mousewheel", this._highlightListeners[1]);
+    window.removeEventListener("click", this._highlightListeners[2]);
 
     this._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
    * instance of nsIDOMNSEditableElement.