--- 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.