From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../server/actors/highlighters/auto-refresh.js | 351 +++++++++++++++++++++ 1 file changed, 351 insertions(+) create mode 100644 devtools/server/actors/highlighters/auto-refresh.js (limited to 'devtools/server/actors/highlighters/auto-refresh.js') diff --git a/devtools/server/actors/highlighters/auto-refresh.js b/devtools/server/actors/highlighters/auto-refresh.js new file mode 100644 index 0000000000..652e8aa817 --- /dev/null +++ b/devtools/server/actors/highlighters/auto-refresh.js @@ -0,0 +1,351 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const EventEmitter = require("resource://devtools/shared/event-emitter.js"); +const { + isNodeValid, +} = require("resource://devtools/server/actors/highlighters/utils/markup.js"); +const { + getAdjustedQuads, + getWindowDimensions, +} = require("resource://devtools/shared/layout/utils.js"); + +// Note that the order of items in this array is important because it is used +// for drawing the BoxModelHighlighter's path elements correctly. +const BOX_MODEL_REGIONS = ["margin", "border", "padding", "content"]; +const QUADS_PROPS = ["p1", "p2", "p3", "p4"]; + +function arePointsDifferent(pointA, pointB) { + return ( + Math.abs(pointA.x - pointB.x) >= 0.5 || + Math.abs(pointA.y - pointB.y) >= 0.5 || + Math.abs(pointA.w - pointB.w) >= 0.5 + ); +} + +function areQuadsDifferent(oldQuads, newQuads) { + for (const region of BOX_MODEL_REGIONS) { + const { length } = oldQuads[region]; + + if (length !== newQuads[region].length) { + return true; + } + + for (let i = 0; i < length; i++) { + for (const prop of QUADS_PROPS) { + const oldPoint = oldQuads[region][i][prop]; + const newPoint = newQuads[region][i][prop]; + + if (arePointsDifferent(oldPoint, newPoint)) { + return true; + } + } + } + } + + return false; +} + +/** + * Base class for auto-refresh-on-change highlighters. Sub classes will have a + * chance to update whenever the current node's geometry changes. + * + * Sub classes must implement the following methods: + * _show: called when the highlighter should be shown, + * _hide: called when the highlighter should be hidden, + * _update: called while the highlighter is shown and the geometry of the + * current node changes. + * + * Sub classes will have access to the following properties: + * - this.currentNode: the node to be shown + * - this.currentQuads: all of the node's box model region quads + * - this.win: the current window + * + * Emits the following events: + * - shown + * - hidden + * - updated + */ +class AutoRefreshHighlighter extends EventEmitter { + constructor(highlighterEnv) { + super(); + + this.highlighterEnv = highlighterEnv; + + this._updateSimpleHighlighters = this._updateSimpleHighlighters.bind(this); + this.highlighterEnv.on( + "use-simple-highlighters-updated", + this._updateSimpleHighlighters + ); + + this.currentNode = null; + this.currentQuads = {}; + + this._winDimensions = getWindowDimensions(this.win); + this._scroll = { x: this.win.pageXOffset, y: this.win.pageYOffset }; + + this.update = this.update.bind(this); + } + + _ignoreZoom = false; + _ignoreScroll = false; + + /** + * Window corresponding to the current highlighterEnv. + */ + get win() { + if (!this.highlighterEnv) { + return null; + } + return this.highlighterEnv.window; + } + + /* Window containing the target content. */ + get contentWindow() { + return this.win; + } + + get supportsSimpleHighlighters() { + return false; + } + + /** + * Show the highlighter on a given node + * @param {DOMNode} node + * @param {Object} options + * Object used for passing options + */ + show(node, options = {}) { + const isSameNode = node === this.currentNode; + const isSameOptions = this._isSameOptions(options); + + if (!this._isNodeValid(node) || (isSameNode && isSameOptions)) { + return false; + } + + this.options = options; + + this._stopRefreshLoop(); + this.currentNode = node; + this._updateAdjustedQuads(); + this._startRefreshLoop(); + + const shown = this._show(); + if (shown) { + this.emit("shown"); + } + return shown; + } + + /** + * Hide the highlighter + */ + hide() { + if (!this.currentNode || !this.highlighterEnv.window) { + return; + } + + this._hide(); + this._stopRefreshLoop(); + this.currentNode = null; + this.currentQuads = {}; + this.options = null; + + this.emit("hidden"); + } + + /** + * Whether the current node is valid for this highlighter type. + * This is implemented by default to check if the node is an element node. Highlighter + * sub-classes should override this method if they want to highlight other node types. + * @param {DOMNode} node + * @return {Boolean} + */ + _isNodeValid(node) { + return isNodeValid(node); + } + + /** + * Are the provided options the same as the currently stored options? + * Returns false if there are no options stored currently. + */ + _isSameOptions(options) { + if (!this.options) { + return false; + } + + const keys = Object.keys(options); + + if (keys.length !== Object.keys(this.options).length) { + return false; + } + + for (const key of keys) { + if (this.options[key] !== options[key]) { + return false; + } + } + + return true; + } + + /** + * Update the stored box quads by reading the current node's box quads. + */ + _updateAdjustedQuads() { + this.currentQuads = {}; + + for (const region of BOX_MODEL_REGIONS) { + this.currentQuads[region] = getAdjustedQuads( + this.contentWindow, + this.currentNode, + region, + { ignoreScroll: this._ignoreScroll, ignoreZoom: this._ignoreZoom } + ); + } + } + + /** + * Update the knowledge we have of the current node's boxquads and return true + * if any of the points x/y or bounds have change since. + * @return {Boolean} + */ + _hasMoved() { + const oldQuads = this.currentQuads; + this._updateAdjustedQuads(); + + return areQuadsDifferent(oldQuads, this.currentQuads); + } + + /** + * Update the knowledge we have of the current window's scrolling offset, both + * horizontal and vertical, and return `true` if they have changed since. + * @return {Boolean} + */ + _hasWindowScrolled() { + if (!this.win) { + return false; + } + + const { pageXOffset, pageYOffset } = this.win; + const hasChanged = + this._scroll.x !== pageXOffset || this._scroll.y !== pageYOffset; + + this._scroll = { x: pageXOffset, y: pageYOffset }; + + return hasChanged; + } + + /** + * Update the knowledge we have of the current window's dimensions and return `true` + * if they have changed since. + * @return {Boolean} + */ + _haveWindowDimensionsChanged() { + const { width, height } = getWindowDimensions(this.win); + const haveChanged = + this._winDimensions.width !== width || + this._winDimensions.height !== height; + + this._winDimensions = { width, height }; + return haveChanged; + } + + /** + * Update the highlighter if the node has moved since the last update. + */ + update() { + if ( + !this._isNodeValid(this.currentNode) || + (!this._hasMoved() && !this._haveWindowDimensionsChanged()) + ) { + // At this point we're not calling the `_update` method. However, if the window has + // scrolled, we want to invoke `_scrollUpdate`. + if (this._hasWindowScrolled()) { + this._scrollUpdate(); + } + + return; + } + + this._update(); + this.emit("updated"); + } + + _show() { + // To be implemented by sub classes + // When called, sub classes should actually show the highlighter for + // this.currentNode, potentially using options in this.options + throw new Error("Custom highlighter class had to implement _show method"); + } + + _update() { + // To be implemented by sub classes + // When called, sub classes should update the highlighter shown for + // this.currentNode + // This is called as a result of a page zoom or repaint + throw new Error("Custom highlighter class had to implement _update method"); + } + + _scrollUpdate() { + // Can be implemented by sub classes + // When called, sub classes can upate the highlighter shown for + // this.currentNode + // This is called as a result of a page scroll + } + + _hide() { + // To be implemented by sub classes + // When called, sub classes should actually hide the highlighter + throw new Error("Custom highlighter class had to implement _hide method"); + } + + _startRefreshLoop() { + const win = this.currentNode.ownerGlobal; + this.rafID = win.requestAnimationFrame(this._startRefreshLoop.bind(this)); + this.rafWin = win; + this.update(); + } + + _stopRefreshLoop() { + if (this.rafID && !Cu.isDeadWrapper(this.rafWin)) { + this.rafWin.cancelAnimationFrame(this.rafID); + } + this.rafID = this.rafWin = null; + } + + _updateSimpleHighlighters() { + if (!this.supportsSimpleHighlighters) { + return; + } + + const root = this.getElement("root"); + if (!root) { + // Highlighters which support simple highlighters are expected to use a + // root element with the id "root". + return; + } + + // Add/remove the `user-simple-highlighters` class based on the current + // toolbox configuration. + root.classList.toggle( + "use-simple-highlighters", + this.highlighterEnv.useSimpleHighlightersForReducedMotion + ); + } + + destroy() { + this.hide(); + + this.highlighterEnv.off( + "use-simple-highlighters-updated", + this._updateSimpleHighlighters + ); + this.highlighterEnv = null; + this.currentNode = null; + } +} +exports.AutoRefreshHighlighter = AutoRefreshHighlighter; -- cgit v1.2.3