368 lines
9.7 KiB
JavaScript
368 lines
9.7 KiB
JavaScript
/* 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;
|
|
|
|
// For offset-path, the highlighter needs to be computed from the containing block
|
|
// of the node, not the node itself.
|
|
this.useContainingBlock = this.options.mode === "cssOffsetPath";
|
|
this.drawingNode = this.useContainingBlock
|
|
? InspectorUtils.containingBlockOf(this.currentNode)
|
|
: this.currentNode;
|
|
|
|
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 = {};
|
|
|
|
// If we need to use the containing block, and if it is the <html> element,
|
|
// we need to use the viewport quads.
|
|
const useViewport =
|
|
this.useContainingBlock &&
|
|
this.drawingNode === this.currentNode.ownerDocument.documentElement;
|
|
const node = useViewport
|
|
? this.drawingNode.ownerDocument
|
|
: this.drawingNode;
|
|
|
|
for (const region of BOX_MODEL_REGIONS) {
|
|
this.currentQuads[region] = getAdjustedQuads(
|
|
this.contentWindow,
|
|
node,
|
|
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;
|