Backed out changeset e6f59b598c17 (bug 1290914)
authorSebastian Hengst <archaeopteryx@coole-files.de>
Wed, 07 Sep 2016 18:37:20 +0200
changeset 313067 85e02f8c2b692c2f241783d604e67ec15a3a785f
parent 313066 db9dfcdbef4aa204aba5d9b3c384f21ee1c66c61
child 313068 3e76c2fd3b99308b0c066f18342d744390bc2329
push id20479
push userkwierso@gmail.com
push dateThu, 08 Sep 2016 01:08:46 +0000
treeherderfx-team@fb7c6b034329 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1290914
milestone51.0a1
backs oute6f59b598c177e3b2080f0d33e59de86a318169e
Backed out changeset e6f59b598c17 (bug 1290914)
toolkit/modules/FinderHighlighter.jsm
--- a/toolkit/modules/FinderHighlighter.jsm
+++ b/toolkit/modules/FinderHighlighter.jsm
@@ -15,83 +15,112 @@ Cu.import("resource://gre/modules/XPCOMU
 XPCOMUtils.defineLazyModuleGetter(this, "Color", "resource://gre/modules/Color.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Rect", "resource://gre/modules/Geometry.jsm");
 XPCOMUtils.defineLazyGetter(this, "kDebug", () => {
   const kDebugPref = "findbar.modalHighlight.debug";
   return Services.prefs.getPrefType(kDebugPref) && Services.prefs.getBoolPref(kDebugPref);
 });
 
 const kContentChangeThresholdPx = 5;
-const kModalHighlightRepaintFreqMs = 100;
+const kModalHighlightRepaintFreqMs = 200;
 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.
+// This uuid is used to prefix HTML element IDs and classNames in order to make
+// them unique and hard to clash with IDs and classNames content authors come up
+// with, since the stylesheet for modal highlighting is inserted as an agent-sheet
+// in the active HTML document.
 const kModalIdPrefix = "cedee4d0-74c5-4f2d-ab43-4d37c0f9d463";
 const kModalOutlineId = kModalIdPrefix + "-findbar-modalHighlight-outline";
-const kModalStyles = {
-  outlineNode: [
-    ["position", "absolute"],
-    ["background", "#ffc535"],
-    ["border-radius", "3px"],
-    ["box-shadow", "0 2px 0 0 rgba(0,0,0,.1)"],
-    ["color", "#000"],
-    ["display", "-moz-box"],
-    ["margin", "-2px 0 0 -2px !important"],
-    ["padding", "2px !important"],
-    ["pointer-events", "none"],
-    ["transition-property", "opacity, transform, top, left"],
-    ["transition-duration", "50ms"],
-    ["transition-timing-function", "linear"],
-    ["z-index", 2]
-  ],
-  outlineNodeDebug: [ ["z-index", 2147483647] ],
-  outlineText: [
-    ["margin", "0 !important"],
-    ["padding", "0 !important"],
-    ["vertical-align", "top !important"]
-  ],
-  maskNode: [
-    ["background", "#000"],
-    ["mix-blend-mode", "multiply"],
-    ["opacity", ".35"],
-    ["pointer-events", "none"],
-    ["position", "absolute"],
-    ["z-index", 1]
-  ],
-  maskNodeDebug: [
-    ["z-index", 2147483646],
-    ["top", 0],
-    ["left", 0]
-  ],
-  maskNodeBrightText: [ ["background", "#fff"] ],
-  maskRect: [
-    ["background", "#fff"],
-    ["margin", "-1px 0 0 -1px !important"],
-    ["padding", "0 1px 2px 1px !important"],
-    ["position", "absolute"]
-  ],
-  maskRectBrightText: [ "background", "#000" ]
-};
-const kModalOutlineAnim = {
-  "keyframes": [
-    { transform: "scaleX(1) scaleY(1)" },
-    { transform: "scaleX(1.5) scaleY(1.5)", offset: .5, easing: "ease-in" },
-    { transform: "scaleX(1) scaleY(1)" }
-  ],
-  duration: 50,
-};
+const kModalStyle = `
+.findbar-modalHighlight-outline {
+  position: absolute;
+  background: #ffc535;
+  border-radius: 3px;
+  box-shadow: 0 2px 0 0 rgba(0,0,0,.1);
+  color: #000;
+  display: -moz-box;
+  margin: -2px 0 0 -2px !important;
+  padding: 2px !important;
+  pointer-events: none;
+  z-index: 2;
+}
+
+.findbar-modalHighlight-outline.findbar-debug {
+  z-index: 2147483647;
+}
+
+.findbar-modalHighlight-outline[grow] {
+  animation-name: findbar-modalHighlight-outlineAnim;
+}
+
+@keyframes findbar-modalHighlight-outlineAnim {
+  from {
+    transform: scaleX(0) scaleY(0);
+  }
+  50% {
+    transform: scaleX(1.5) scaleY(1.5);
+  }
+  to {
+    transform: scaleX(0) scaleY(0);
+  }
+}
+
+.findbar-modalHighlight-outline[hidden] {
+  opacity: 0;
+}
+
+.findbar-modalHighlight-outline:not([disable-transitions]) {
+  transition-property: opacity, transform, top, left;
+  transition-duration: 50ms;
+  transition-timing-function: linear;
+}
+
+.findbar-modalHighlight-outline-text {
+  margin: 0 !important;
+  padding: 0 !important;
+  vertical-align: top !important;
+}
+
+.findbar-modalHighlight-outlineMask {
+  background: #000;
+  mix-blend-mode: multiply;
+  opacity: .35;
+  pointer-events: none;
+  position: absolute;
+  z-index: 1;
+}
+
+.findbar-modalHighlight-outlineMask.findbar-debug {
+  z-index: 2147483646;
+  top: 0;
+  left: 0;
+}
+
+.findbar-modalHighlight-outlineMask[brighttext] {
+  background: #fff;
+}
+
+.findbar-modalHighlight-rect {
+  background: #fff;
+  margin: -1px 0 0 -1px !important;
+  padding: 0 1px 2px 1px !important;
+  position: absolute;
+}
+
+.findbar-modalHighlight-outlineMask[brighttext] > .findbar-modalHighlight-rect {
+  background: #000;
+}`;
 
 function mockAnonymousContentNode(domNode) {
   return {
     setTextContentForElement(id, text) {
       (domNode.querySelector("#" + id) || domNode).textContent = text;
     },
     getAttributeForElement(id, attrName) {
       let node = domNode.querySelector("#" + id) || domNode;
@@ -133,35 +162,54 @@ function FinderHighlighter(finder) {
 FinderHighlighter.prototype = {
   get iterator() {
     if (this._iterator)
       return this._iterator;
     this._iterator = Cu.import("resource://gre/modules/FinderIterator.jsm", null).FinderIterator;
     return this._iterator;
   },
 
+  get modalStyleSheet() {
+    if (!this._modalStyleSheet) {
+      this._modalStyleSheet = kModalStyle.replace(/findbar-/g,
+        kModalIdPrefix + "-findbar-");
+    }
+    return this._modalStyleSheet;
+  },
+
+  get modalStyleSheetURI() {
+    if (!this._modalStyleSheetURI) {
+      this._modalStyleSheetURI = "data:text/css;charset=utf-8," +
+        encodeURIComponent(this.modalStyleSheet.replace(/[\n]+/g, " "));
+    }
+    return this._modalStyleSheetURI;
+  },
+
   /**
    * 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
    *  - {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
+   *  - {Boolean} installedSheet         Whether the modal stylesheet was loaded
+   *                                     already
    *  - {Map}     modalHighlightRectsMap Collection of ranges and their corresponding
    *                                     Rects
    *
    * @param  {nsIDOMWindow} window
    * @return {Object}
    */
   getForWindow(window, propName = null) {
     if (!gWindows.has(window)) {
       gWindows.set(window, {
         dynamicRangesSet: new Set(),
         frames: new Map(),
+        installedSheet: false,
         modalHighlightRectsMap: new Map()
       });
     }
     return gWindows.get(window);
   },
 
   /**
    * Notify all registered listeners that the 'Highlight All' operation finished.
@@ -344,21 +392,18 @@ FinderHighlighter.prototype = {
     }
 
     if (dict.modalRepaintScheduler) {
       window.clearTimeout(dict.modalRepaintScheduler);
       dict.modalRepaintScheduler = null;
     }
     dict.lastWindowDimensions = null;
 
-    if (dict.modalHighlightOutline) {
-      dict.modalHighlightOutline.setAttributeForElement(kModalOutlineId, "style",
-        dict.modalHighlightOutline.getAttributeForElement(kModalOutlineId, "style") +
-        "; opacity: 0");
-    }
+    if (dict.modalHighlightOutline)
+      dict.modalHighlightOutline.setAttributeForElement(kModalOutlineId, "hidden", "true");
 
     this._removeHighlightAllMask(window);
     this._removeModalHighlightListeners(window);
     delete dict.brightText;
 
     dict.visible = false;
   },
 
@@ -424,21 +469,22 @@ FinderHighlighter.prototype = {
         this.show(window);
       else
         this._maybeCreateModalHighlightNodes(window);
 
       this._updateRangeOutline(dict, textContent, fontStyle);
     }
 
     outlineNode = dict.modalHighlightOutline;
-    if (dict.animation)
-      dict.animation.finish();
-    dict.animation = outlineNode.setAnimationForElement(kModalOutlineId,
-      Cu.cloneInto(kModalOutlineAnim.keyframes, window), kModalOutlineAnim.duration);
-    dict.animation.onfinish = () => dict.animation = null;
+    try {
+      outlineNode.removeAttributeForElement(kModalOutlineId, "grow");
+    } catch (ex) {}
+    window.requestAnimationFrame(() => {
+      outlineNode.setAttributeForElement(kModalOutlineId, "grow", true);
+    });
 
     if (this._highlightAll && data.searchString)
       this.highlight(true, data.searchString, data.linksOnly);
   },
 
   /**
    * Invalidates the list by clearing the map of highglighted ranges that we
    * keep to build the mask for.
@@ -449,18 +495,16 @@ FinderHighlighter.prototype = {
       for (let win of gWindows.keys())
         this.hide(win);
       // Reset the Map, because no range references a node anymore.
       gWindows.clear();
       return;
     }
 
     let dict = this.getForWindow(window.top);
-    if (dict.animation)
-      dict.animation.finish();
     dict.currentFoundRange = null;
     dict.dynamicRangesSet.clear();
     dict.frames.clear();
     dict.modalHighlightRectsMap.clear();
   },
 
   /**
    * When the current page is refreshed or navigated away from, the CanvasFrame
@@ -664,38 +708,19 @@ FinderHighlighter.prototype = {
    * @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]}`);
+      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("; ");
+    return style.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}
    */
@@ -708,17 +733,17 @@ FinderHighlighter.prototype = {
   },
 
   /**
    * 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 and elements inside
    * (i)frames.
    *
-   * @param  {nsIDOMRange} range Range that be enclosed in a dynamic container
+   * @param  {nsIDOMRange} range Range that be enclosed in a fixed container
    * @return {Boolean}
    */
   _isInDynamicContainer(range) {
     const kFixed = new Set(["fixed", "sticky"]);
     let node = range.startContainer;
     while (node.nodeType != 1)
       node = node.parentNode;
     let document = node.ownerDocument;
@@ -785,22 +810,22 @@ FinderHighlighter.prototype = {
     dict.modalHighlightRectsMap.set(range, rects);
     if (checkIfDynamic && this._isInDynamicContainer(range))
       dict.dynamicRangesSet.add(range);
     return 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 or (i)frame.
+   * because they're enclosed by a position: fixed container DOM node.
    *
    * @param {Object} dict Dictionary of properties belonging to the currently
    *                      active window
    */
-  _updateDynamicRangesRects(dict) {
+  _updateFixedRangesRects(dict) {
     for (let range of dict.dynamicRangesSet)
       this._updateRangeRects(range, false, dict);
     // Reset the frame bounds cache.
     for (let frame of dict.frames.keys())
       dict.frames.set(frame, null);
   },
 
   /**
@@ -820,37 +845,34 @@ FinderHighlighter.prototype = {
     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 kModalStyles.
+    // 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._getStyleString(kModalStyles.outlineText) + "; " +
       this._getHTMLFontStyle(fontStyle));
 
+    if (typeof outlineNode.getAttributeForElement(kModalOutlineId, "hidden") == "string")
+      outlineNode.removeAttributeForElement(kModalOutlineId, "hidden");
+
     let window = range.startContainer.ownerDocument.defaultView;
     let { left, top } = this._getRootBounds(window);
     outlineNode.setAttributeForElement(kModalOutlineId, "style",
-      this._getStyleString(kModalStyles.outlineNode, [
-        ["top", top + rect.top + "px"],
-        ["left", left + rect.left + "px"],
-        ["height", rect.height + "px"],
-        ["width", rect.width + "px"]],
-        kDebug ? kModalStyles.outlineNodeDebug : []
-    ));
+      `top: ${top + rect.top}px; left: ${left + 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
@@ -893,29 +915,30 @@ FinderHighlighter.prototype = {
       let onVisibilityChange = () => {
         document.removeEventListener("visibilitychange", onVisibilityChange);
         this._maybeCreateModalHighlightNodes(window);
       };
       document.addEventListener("visibilitychange", onVisibilityChange);
       return;
     }
 
+    this._maybeInstallStyleSheet(window);
+
     // The outline needs to be sitting inside a container, otherwise the anonymous
     // content API won't find it by its ID later...
     let container = document.createElement("div");
 
     // Create the main (yellow) highlight outline box.
     let outlineBox = document.createElement("div");
     outlineBox.setAttribute("id", kModalOutlineId);
-    outlineBox.setAttribute("style", this._getStyleString(kModalStyles.outlineNode,
-      kDebug ? kModalStyles.outlineNodeDebug : []));
+    outlineBox.className = kModalOutlineId + (kDebug ? ` ${kModalIdPrefix}-findbar-debug` : "");
     let outlineBoxText = document.createElement("span");
     let attrValue = kModalOutlineId + "-text";
     outlineBoxText.setAttribute("id", attrValue);
-    outlineBoxText.setAttribute("style", this._getStyleString(kModalStyles.outlineText));
+    outlineBoxText.setAttribute("class", attrValue);
     outlineBox.appendChild(outlineBoxText);
 
     container.appendChild(outlineBox);
     dict.modalHighlightOutline = kDebug ?
       mockAnonymousContentNode(document.body.appendChild(container.firstChild)) :
       document.insertAnonymousContent(container);
 
     // Make sure to at least show the dimmed background.
@@ -936,35 +959,32 @@ FinderHighlighter.prototype = {
     let document = window.document;
 
     const kMaskId = kModalIdPrefix + "-findbar-modalHighlight-outlineMask";
     let maskNode = document.createElement("div");
 
     // Make sure the dimmed mask node takes the full width and height that's available.
     let {width, height} = dict.lastWindowDimensions = this._getWindowDimensions(window);
     maskNode.setAttribute("id", kMaskId);
-    maskNode.setAttribute("style", this._getStyleString(kModalStyles.maskNode,
-      [ ["width", width + "px"], ["height", height + "px"] ],
-      dict.brightText ? kModalStyles.maskNodeBrightText : [],
-      kDebug ? kModalStyles.maskNodeDebug : []));
+    maskNode.setAttribute("class", kMaskId + (kDebug ? ` ${kModalIdPrefix}-findbar-debug` : ""));
+    maskNode.setAttribute("style", `width: ${width}px; height: ${height}px;`);
     if (dict.brightText)
       maskNode.setAttribute("brighttext", "true");
 
     if (paintContent || dict.modalHighlightAllMask) {
       this._updateRangeOutline(dict);
-      this._updateDynamicRangesRects(dict);
+      this._updateFixedRangesRects(dict);
       // Create a DOM node for each rectangle representing the ranges we found.
       let maskContent = [];
-      const rectStyle = this._getStyleString(kModalStyles.maskRect,
-        dict.brightText ? kModalStyles.maskRectBrightText : []);
+      const kRectClassName = kModalIdPrefix + "-findbar-modalHighlight-rect";
       for (let [range, rects] of dict.modalHighlightRectsMap) {
         if (dict.updateAllRanges)
           rects = this._updateRangeRects(range);
         for (let rect of rects) {
-          maskContent.push(`<div style="${rectStyle}; top: ${rect.y}px;
+          maskContent.push(`<div class="${kRectClassName}" style="top: ${rect.y}px;
             left: ${rect.x}px; height: ${rect.height}px; width: ${rect.width}px;"></div>`);
         }
       }
       dict.updateAllRanges = false;
       maskNode.innerHTML = maskContent.join("");
     }
 
     // Always remove the current mask and insert it a-fresh, because we're not
@@ -1006,34 +1026,34 @@ FinderHighlighter.prototype = {
    *
    * @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.
+   *                             which means that the fixed 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, scrollOnly, updateAllRanges } =
                                  { contentChanged: false, scrollOnly: false, updateAllRanges: false }) {
     if (!this._modal)
       return;
 
     window = window.top;
     let dict = this.getForWindow(window);
-    let repaintDynamicRanges = (scrollOnly && !!dict.dynamicRangesSet.size);
+    let repaintFixedNodes = (scrollOnly && !!dict.dynamicRangesSet.size);
 
     // When we request to repaint unconditionally, we mean to call
     // `_repaintHighlightAllMask()` right after the timeout.
     if (!dict.unconditionalRepaintRequested)
-      dict.unconditionalRepaintRequested = !contentChanged || repaintDynamicRanges;
+      dict.unconditionalRepaintRequested = !contentChanged || repaintFixedNodes;
     // 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;
 
     dict.modalRepaintScheduler = window.setTimeout(() => {
@@ -1052,16 +1072,39 @@ FinderHighlighter.prototype = {
           (dict.modalHighlightRectsMap.size && pageContentChanged)) {
         dict.unconditionalRepaintRequested = false;
         this._repaintHighlightAllMask(window);
       }
     }, kModalHighlightRepaintFreqMs);
   },
 
   /**
+   * The outline that shows/ highlights the current found range is styled and
+   * animated using CSS. This style can be found in `kModalStyle`, but to have it
+   * applied on any DOM node we insert using the AnonymousContent API we need to
+   * inject an agent sheet into the document.
+   *
+   * @param {nsIDOMWindow} window
+   */
+  _maybeInstallStyleSheet(window) {
+    window = window.top;
+    let dict = this.getForWindow(window);
+    let document = window.document;
+    if (dict.installedSheet == document)
+      return;
+
+    let dwu = this._getDWU(window);
+    let uri = this.modalStyleSheetURI;
+    try {
+      dwu.loadSheetUsingURIString(uri, dwu.AGENT_SHEET);
+    } catch (e) {}
+    dict.installedSheet = document;
+  },
+
+  /**
    * 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 = window.top;
     let dict = this.getForWindow(window);