Bug 1290914 - use inline styles for the modal highlighting anonymous content nodes to dramatically improve performance when find in page is used on large documents. r=jaws

MozReview-Commit-ID: 3mw0gfn0w4p

--HG--
extra : rebase_source : a666239484d9a0e46b8fe062c913b1b60867742b
This commit is contained in:
Mike de Boer 2016-09-07 12:03:47 +02:00
Родитель ec93a96045
Коммит 0cd658c763
1 изменённых файлов: 115 добавлений и 158 удалений

Просмотреть файл

@ -20,7 +20,7 @@ XPCOMUtils.defineLazyGetter(this, "kDebug", () => {
});
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",
@ -31,91 +31,62 @@ const kFontPropsCamelCase = kFontPropsCSS.map(prop => {
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 {
@ -167,22 +138,6 @@ FinderHighlighter.prototype = {
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.
@ -191,8 +146,6 @@ FinderHighlighter.prototype = {
* 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
*
@ -204,7 +157,6 @@ FinderHighlighter.prototype = {
gWindows.set(window, {
dynamicRangesSet: new Set(),
frames: new Map(),
installedSheet: false,
modalHighlightRectsMap: new Map()
});
}
@ -397,8 +349,11 @@ FinderHighlighter.prototype = {
}
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);
@ -474,12 +429,11 @@ FinderHighlighter.prototype = {
}
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);
@ -500,6 +454,8 @@ FinderHighlighter.prototype = {
}
let dict = this.getForWindow(window.top);
if (dict.animation)
dict.animation.finish();
dict.currentFoundRange = null;
dict.dynamicRangesSet.clear();
dict.frames.clear();
@ -713,9 +669,28 @@ FinderHighlighter.prototype = {
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("; ");
},
/**
@ -738,7 +713,7 @@ FinderHighlighter.prototype = {
* 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) {
@ -815,12 +790,12 @@ FinderHighlighter.prototype = {
/**
* 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.
@ -850,7 +825,7 @@ FinderHighlighter.prototype = {
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)
@ -858,16 +833,19 @@ FinderHighlighter.prototype = {
// 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 : []
));
},
/**
@ -920,8 +898,6 @@ FinderHighlighter.prototype = {
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");
@ -929,11 +905,12 @@ FinderHighlighter.prototype = {
// 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);
@ -964,22 +941,25 @@ FinderHighlighter.prototype = {
// 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>`);
}
}
@ -1031,8 +1011,8 @@ FinderHighlighter.prototype = {
* 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.
*/
@ -1043,12 +1023,12 @@ FinderHighlighter.prototype = {
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;
@ -1076,29 +1056,6 @@ FinderHighlighter.prototype = {
}, 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.