--- a/toolkit/modules/FinderHighlighter.jsm
+++ b/toolkit/modules/FinderHighlighter.jsm
@@ -15,112 +15,83 @@ 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 = 200;
+const kModalHighlightRepaintFreqMs = 100;
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 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.
+// 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 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;
-}`;
+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,
+};
function mockAnonymousContentNode(domNode) {
return {
setTextContentForElement(id, text) {
(domNode.querySelector("#" + id) || domNode).textContent = text;
},
getAttributeForElement(id, attrName) {
let node = domNode.querySelector("#" + id) || domNode;
@@ -162,54 +133,35 @@ 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.
@@ -392,18 +344,21 @@ FinderHighlighter.prototype = {
}
if (dict.modalRepaintScheduler) {
window.clearTimeout(dict.modalRepaintScheduler);
dict.modalRepaintScheduler = null;
}
dict.lastWindowDimensions = null;
- if (dict.modalHighlightOutline)
- dict.modalHighlightOutline.setAttributeForElement(kModalOutlineId, "hidden", "true");
+ if (dict.modalHighlightOutline) {
+ dict.modalHighlightOutline.setAttributeForElement(kModalOutlineId, "style",
+ dict.modalHighlightOutline.getAttributeForElement(kModalOutlineId, "style") +
+ "; opacity: 0");
+ }
this._removeHighlightAllMask(window);
this._removeModalHighlightListeners(window);
delete dict.brightText;
dict.visible = false;
},
@@ -469,22 +424,21 @@ FinderHighlighter.prototype = {
this.show(window);
else
this._maybeCreateModalHighlightNodes(window);
this._updateRangeOutline(dict, textContent, fontStyle);
}
outlineNode = dict.modalHighlightOutline;
- try {
- outlineNode.removeAttributeForElement(kModalOutlineId, "grow");
- } catch (ex) {}
- window.requestAnimationFrame(() => {
- outlineNode.setAttributeForElement(kModalOutlineId, "grow", true);
- });
+ if (dict.animation)
+ dict.animation.finish();
+ dict.animation = outlineNode.setAnimationForElement(kModalOutlineId,
+ Cu.cloneInto(kModalOutlineAnim.keyframes, window), kModalOutlineAnim.duration);
+ dict.animation.onfinish = () => dict.animation = null;
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.
@@ -495,16 +449,18 @@ 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
@@ -708,19 +664,38 @@ 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(" ");
+ 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}
*/
@@ -733,17 +708,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 fixed container
+ * @param {nsIDOMRange} range Range that be enclosed in a dynamic 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;
@@ -810,22 +785,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.
+ * 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
*/
- _updateFixedRangesRects(dict) {
+ _updateDynamicRangesRects(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);
},
/**
@@ -845,34 +820,37 @@ 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 our stylesheet.
+ // Text color in the outline is determined by kModalStyles.
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",
- `top: ${top + rect.top}px; left: ${left + rect.left}px;
- height: ${rect.height}px; width: ${rect.width}px;`);
+ 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 : []
+ ));
},
/**
* 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
@@ -915,30 +893,29 @@ 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.className = kModalOutlineId + (kDebug ? ` ${kModalIdPrefix}-findbar-debug` : "");
+ outlineBox.setAttribute("style", this._getStyleString(kModalStyles.outlineNode,
+ kDebug ? kModalStyles.outlineNodeDebug : []));
let outlineBoxText = document.createElement("span");
let attrValue = kModalOutlineId + "-text";
outlineBoxText.setAttribute("id", attrValue);
- outlineBoxText.setAttribute("class", attrValue);
+ outlineBoxText.setAttribute("style", this._getStyleString(kModalStyles.outlineText));
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.
@@ -959,32 +936,35 @@ 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("class", kMaskId + (kDebug ? ` ${kModalIdPrefix}-findbar-debug` : ""));
- maskNode.setAttribute("style", `width: ${width}px; height: ${height}px;`);
+ maskNode.setAttribute("style", this._getStyleString(kModalStyles.maskNode,
+ [ ["width", width + "px"], ["height", height + "px"] ],
+ dict.brightText ? kModalStyles.maskNodeBrightText : [],
+ kDebug ? kModalStyles.maskNodeDebug : []));
if (dict.brightText)
maskNode.setAttribute("brighttext", "true");
if (paintContent || dict.modalHighlightAllMask) {
this._updateRangeOutline(dict);
- this._updateFixedRangesRects(dict);
+ this._updateDynamicRangesRects(dict);
// Create a DOM node for each rectangle representing the ranges we found.
let maskContent = [];
- const kRectClassName = kModalIdPrefix + "-findbar-modalHighlight-rect";
+ const rectStyle = this._getStyleString(kModalStyles.maskRect,
+ dict.brightText ? kModalStyles.maskRectBrightText : []);
for (let [range, rects] of dict.modalHighlightRectsMap) {
if (dict.updateAllRanges)
rects = this._updateRangeRects(range);
for (let rect of rects) {
- maskContent.push(`<div class="${kRectClassName}" style="top: ${rect.y}px;
+ maskContent.push(`<div style="${rectStyle}; 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
@@ -1026,34 +1006,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 fixed positioned elements
- * need to be repainted.
+ * 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, scrollOnly, updateAllRanges } =
{ contentChanged: false, scrollOnly: false, updateAllRanges: false }) {
if (!this._modal)
return;
window = window.top;
let dict = this.getForWindow(window);
- let repaintFixedNodes = (scrollOnly && !!dict.dynamicRangesSet.size);
+ let repaintDynamicRanges = (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 || repaintFixedNodes;
+ 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;
dict.modalRepaintScheduler = window.setTimeout(() => {
@@ -1072,39 +1052,16 @@ 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);