tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

auto-refresh.js (9929B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
      8 const {
      9  isNodeValid,
     10 } = require("resource://devtools/server/actors/highlighters/utils/markup.js");
     11 const {
     12  getAdjustedQuads,
     13  getWindowDimensions,
     14 } = require("resource://devtools/shared/layout/utils.js");
     15 
     16 // Note that the order of items in this array is important because it is used
     17 // for drawing the BoxModelHighlighter's path elements correctly.
     18 const BOX_MODEL_REGIONS = ["margin", "border", "padding", "content"];
     19 const QUADS_PROPS = ["p1", "p2", "p3", "p4"];
     20 
     21 function arePointsDifferent(pointA, pointB) {
     22  return (
     23    Math.abs(pointA.x - pointB.x) >= 0.5 ||
     24    Math.abs(pointA.y - pointB.y) >= 0.5 ||
     25    Math.abs(pointA.w - pointB.w) >= 0.5
     26  );
     27 }
     28 
     29 function areQuadsDifferent(oldQuads, newQuads) {
     30  for (const region of BOX_MODEL_REGIONS) {
     31    const { length } = oldQuads[region];
     32 
     33    if (length !== newQuads[region].length) {
     34      return true;
     35    }
     36 
     37    for (let i = 0; i < length; i++) {
     38      for (const prop of QUADS_PROPS) {
     39        const oldPoint = oldQuads[region][i][prop];
     40        const newPoint = newQuads[region][i][prop];
     41 
     42        if (arePointsDifferent(oldPoint, newPoint)) {
     43          return true;
     44        }
     45      }
     46    }
     47  }
     48 
     49  return false;
     50 }
     51 
     52 /**
     53 * Base class for auto-refresh-on-change highlighters. Sub classes will have a
     54 * chance to update whenever the current node's geometry changes.
     55 *
     56 * Sub classes must implement the following methods:
     57 * _show: called when the highlighter should be shown,
     58 * _hide: called when the highlighter should be hidden,
     59 * _update: called while the highlighter is shown and the geometry of the
     60 *          current node changes.
     61 *
     62 * Sub classes will have access to the following properties:
     63 * - this.currentNode: the node to be shown
     64 * - this.currentQuads: all of the node's box model region quads
     65 * - this.win: the current window
     66 *
     67 * Emits the following events:
     68 * - shown
     69 * - hidden
     70 * - updated
     71 */
     72 class AutoRefreshHighlighter extends EventEmitter {
     73  constructor(highlighterEnv) {
     74    super();
     75 
     76    this.highlighterEnv = highlighterEnv;
     77 
     78    this._updateSimpleHighlighters = this._updateSimpleHighlighters.bind(this);
     79    this.highlighterEnv.on(
     80      "use-simple-highlighters-updated",
     81      this._updateSimpleHighlighters
     82    );
     83 
     84    this.currentNode = null;
     85    this.currentQuads = {};
     86 
     87    this._winDimensions = getWindowDimensions(this.win);
     88    this._scroll = { x: this.win.pageXOffset, y: this.win.pageYOffset };
     89 
     90    this.update = this.update.bind(this);
     91  }
     92 
     93  _ignoreZoom = false;
     94  _ignoreScroll = false;
     95 
     96  /**
     97   * Window corresponding to the current highlighterEnv.
     98   */
     99  get win() {
    100    if (!this.highlighterEnv) {
    101      return null;
    102    }
    103    return this.highlighterEnv.window;
    104  }
    105 
    106  /* Window containing the target content. */
    107  get contentWindow() {
    108    return this.win;
    109  }
    110 
    111  get supportsSimpleHighlighters() {
    112    return false;
    113  }
    114 
    115  /**
    116   * Show the highlighter on a given node
    117   *
    118   * @param {DOMNode} node
    119   * @param {object} options
    120   *        Object used for passing options
    121   */
    122  show(node, options = {}) {
    123    const isSameNode = node === this.currentNode;
    124    const isSameOptions = this._isSameOptions(options);
    125 
    126    if (!this._isNodeValid(node) || (isSameNode && isSameOptions)) {
    127      return false;
    128    }
    129 
    130    this.options = options;
    131 
    132    this._stopRefreshLoop();
    133    this.currentNode = node;
    134 
    135    // For offset-path, the highlighter needs to be computed from the containing block
    136    // of the node, not the node itself.
    137    this.useContainingBlock = this.options.mode === "cssOffsetPath";
    138    this.drawingNode = this.useContainingBlock
    139      ? InspectorUtils.containingBlockOf(this.currentNode)
    140      : this.currentNode;
    141 
    142    this._updateAdjustedQuads();
    143    this._startRefreshLoop();
    144 
    145    const shown = this._show();
    146    if (shown) {
    147      this.emit("shown");
    148    }
    149    return shown;
    150  }
    151 
    152  /**
    153   * Hide the highlighter
    154   */
    155  hide() {
    156    if (!this.currentNode || !this.highlighterEnv.window) {
    157      return;
    158    }
    159 
    160    this._hide();
    161    this._stopRefreshLoop();
    162    this.currentNode = null;
    163    this.currentQuads = {};
    164    this.options = null;
    165 
    166    this.emit("hidden");
    167  }
    168 
    169  /**
    170   * Whether the current node is valid for this highlighter type.
    171   * This is implemented by default to check if the node is an element node. Highlighter
    172   * sub-classes should override this method if they want to highlight other node types.
    173   *
    174   * @param {DOMNode} node
    175   * @return {boolean}
    176   */
    177  _isNodeValid(node) {
    178    return isNodeValid(node);
    179  }
    180 
    181  /**
    182   * Are the provided options the same as the currently stored options?
    183   * Returns false if there are no options stored currently.
    184   */
    185  _isSameOptions(options) {
    186    if (!this.options) {
    187      return false;
    188    }
    189 
    190    const keys = Object.keys(options);
    191 
    192    if (keys.length !== Object.keys(this.options).length) {
    193      return false;
    194    }
    195 
    196    for (const key of keys) {
    197      if (this.options[key] !== options[key]) {
    198        return false;
    199      }
    200    }
    201 
    202    return true;
    203  }
    204 
    205  /**
    206   * Update the stored box quads by reading the current node's box quads.
    207   */
    208  _updateAdjustedQuads() {
    209    this.currentQuads = {};
    210 
    211    // If we need to use the containing block, and if it is the <html> element,
    212    // we need to use the viewport quads.
    213    const useViewport =
    214      this.useContainingBlock &&
    215      this.drawingNode === this.currentNode.ownerDocument.documentElement;
    216    const node = useViewport
    217      ? this.drawingNode.ownerDocument
    218      : this.drawingNode;
    219 
    220    for (const region of BOX_MODEL_REGIONS) {
    221      this.currentQuads[region] = getAdjustedQuads(
    222        this.contentWindow,
    223        node,
    224        region,
    225        { ignoreScroll: this._ignoreScroll, ignoreZoom: this._ignoreZoom }
    226      );
    227    }
    228  }
    229 
    230  /**
    231   * Update the knowledge we have of the current node's boxquads and return true
    232   * if any of the points x/y or bounds have change since.
    233   *
    234   * @return {boolean}
    235   */
    236  _hasMoved() {
    237    const oldQuads = this.currentQuads;
    238    this._updateAdjustedQuads();
    239 
    240    return areQuadsDifferent(oldQuads, this.currentQuads);
    241  }
    242 
    243  /**
    244   * Update the knowledge we have of the current window's scrolling offset, both
    245   * horizontal and vertical, and return `true` if they have changed since.
    246   *
    247   * @return {boolean}
    248   */
    249  _hasWindowScrolled() {
    250    if (!this.win) {
    251      return false;
    252    }
    253 
    254    const { pageXOffset, pageYOffset } = this.win;
    255    const hasChanged =
    256      this._scroll.x !== pageXOffset || this._scroll.y !== pageYOffset;
    257 
    258    this._scroll = { x: pageXOffset, y: pageYOffset };
    259 
    260    return hasChanged;
    261  }
    262 
    263  /**
    264   * Update the knowledge we have of the current window's dimensions and return `true`
    265   * if they have changed since.
    266   *
    267   * @return {boolean}
    268   */
    269  _haveWindowDimensionsChanged() {
    270    const { width, height } = getWindowDimensions(this.win);
    271    const haveChanged =
    272      this._winDimensions.width !== width ||
    273      this._winDimensions.height !== height;
    274 
    275    this._winDimensions = { width, height };
    276    return haveChanged;
    277  }
    278 
    279  /**
    280   * Update the highlighter if the node has moved since the last update.
    281   */
    282  update() {
    283    if (
    284      !this._isNodeValid(this.currentNode) ||
    285      (!this._hasMoved() && !this._haveWindowDimensionsChanged())
    286    ) {
    287      // At this point we're not calling the `_update` method. However, if the window has
    288      // scrolled, we want to invoke `_scrollUpdate`.
    289      if (this._hasWindowScrolled()) {
    290        this._scrollUpdate();
    291      }
    292 
    293      return;
    294    }
    295 
    296    this._update();
    297    this.emit("updated");
    298  }
    299 
    300  _show() {
    301    // To be implemented by sub classes
    302    // When called, sub classes should actually show the highlighter for
    303    // this.currentNode, potentially using options in this.options
    304    throw new Error("Custom highlighter class had to implement _show method");
    305  }
    306 
    307  _update() {
    308    // To be implemented by sub classes
    309    // When called, sub classes should update the highlighter shown for
    310    // this.currentNode
    311    // This is called as a result of a page zoom or repaint
    312    throw new Error("Custom highlighter class had to implement _update method");
    313  }
    314 
    315  _scrollUpdate() {
    316    // Can be implemented by sub classes
    317    // When called, sub classes can upate the highlighter shown for
    318    // this.currentNode
    319    // This is called as a result of a page scroll
    320  }
    321 
    322  _hide() {
    323    // To be implemented by sub classes
    324    // When called, sub classes should actually hide the highlighter
    325    throw new Error("Custom highlighter class had to implement _hide method");
    326  }
    327 
    328  _startRefreshLoop() {
    329    const win = this.currentNode.ownerGlobal;
    330    this.rafID = win.requestAnimationFrame(this._startRefreshLoop.bind(this));
    331    this.rafWin = win;
    332    this.update();
    333  }
    334 
    335  _stopRefreshLoop() {
    336    if (this.rafID && !Cu.isDeadWrapper(this.rafWin)) {
    337      this.rafWin.cancelAnimationFrame(this.rafID);
    338    }
    339    this.rafID = this.rafWin = null;
    340  }
    341 
    342  _updateSimpleHighlighters() {
    343    if (!this.supportsSimpleHighlighters) {
    344      return;
    345    }
    346 
    347    if (!this.rootEl) {
    348      // Highlighters which support simple highlighters are expected to use a root element.
    349      return;
    350    }
    351 
    352    // Add/remove the `user-simple-highlighters` class based on the current
    353    // toolbox configuration.
    354    this.rootEl.classList.toggle(
    355      "use-simple-highlighters",
    356      this.highlighterEnv.useSimpleHighlightersForReducedMotion
    357    );
    358  }
    359 
    360  destroy() {
    361    this.hide();
    362 
    363    this.highlighterEnv.off(
    364      "use-simple-highlighters-updated",
    365      this._updateSimpleHighlighters
    366    );
    367    this.highlighterEnv = null;
    368    this.currentNode = null;
    369  }
    370 }
    371 exports.AutoRefreshHighlighter = AutoRefreshHighlighter;