tor-browser

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

highlighters-overlay.js (64942B)


      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 {
      8  safeAsyncMethod,
      9 } = require("resource://devtools/shared/async-utils.js");
     10 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
     11 const WalkerEventListener = require("resource://devtools/client/inspector/shared/walker-event-listener.js");
     12 const {
     13  VIEW_NODE_VALUE_TYPE,
     14  VIEW_NODE_SHAPE_POINT_TYPE,
     15 } = require("resource://devtools/client/inspector/shared/node-types.js");
     16 
     17 const { TYPES } = ChromeUtils.importESModule(
     18  "resource://devtools/shared/highlighters.mjs"
     19 );
     20 
     21 loader.lazyRequireGetter(
     22  this,
     23  "parseURL",
     24  "resource://devtools/client/shared/source-utils.js",
     25  true
     26 );
     27 loader.lazyRequireGetter(
     28  this,
     29  "asyncStorage",
     30  "resource://devtools/shared/async-storage.js"
     31 );
     32 loader.lazyRequireGetter(
     33  this,
     34  "gridsReducer",
     35  "resource://devtools/client/inspector/grids/reducers/grids.js"
     36 );
     37 loader.lazyRequireGetter(
     38  this,
     39  "highlighterSettingsReducer",
     40  "resource://devtools/client/inspector/grids/reducers/highlighter-settings.js"
     41 );
     42 loader.lazyRequireGetter(
     43  this,
     44  "flexboxReducer",
     45  "resource://devtools/client/inspector/flexbox/reducers/flexbox.js"
     46 );
     47 loader.lazyRequireGetter(
     48  this,
     49  "deepEqual",
     50  "resource://devtools/shared/DevToolsUtils.js",
     51  true
     52 );
     53 loader.lazyGetter(this, "HighlightersBundle", () => {
     54  return new Localization(["devtools/shared/highlighters.ftl"], true);
     55 });
     56 
     57 const DEFAULT_HIGHLIGHTER_COLOR = "#9400FF";
     58 const SUBGRID_PARENT_ALPHA = 0.5;
     59 
     60 /**
     61 * While refactoring to an abstracted way to show and hide highlighters,
     62 * we did not update all tests and code paths which listen for exact events.
     63 *
     64 * When we show or hide highlighters we reference this mapping to
     65 * emit events that consumers may be listening to.
     66 *
     67 * This list should go away as we incrementally rewrite tests to use
     68 * abstract event names with data payloads indicating the highlighter.
     69 *
     70 * DO NOT OPTIMIZE THIS MAPPING AS CONCATENATED SUBSTRINGS!
     71 * It makes it difficult to do project-wide searches for exact matches.
     72 */
     73 const HIGHLIGHTER_EVENTS = {
     74  [TYPES.GRID]: {
     75    shown: "grid-highlighter-shown",
     76    hidden: "grid-highlighter-hidden",
     77  },
     78  [TYPES.GEOMETRY]: {
     79    shown: "geometry-editor-highlighter-shown",
     80    hidden: "geometry-editor-highlighter-hidden",
     81  },
     82  [TYPES.SHAPES]: {
     83    shown: "shapes-highlighter-shown",
     84    hidden: "shapes-highlighter-hidden",
     85  },
     86  [TYPES.TRANSFORM]: {
     87    shown: "css-transform-highlighter-shown",
     88    hidden: "css-transform-highlighter-hidden",
     89  },
     90 };
     91 
     92 // Tool IDs mapped by highlighter type. Used to log telemetry for opening & closing tools.
     93 const GLEAN_TOOL_IDS = {
     94  [TYPES.FLEXBOX]: "flexbox_highlighter",
     95  [TYPES.GRID]: "grid_highlighter",
     96 };
     97 
     98 // Glean counter names mapped by highlighter type. Used to log telemetry about highlighter triggers.
     99 const GLEAN_COUNTER_NAMES = {
    100  [TYPES.FLEXBOX]: {
    101    layout: "devtoolsLayoutFlexboxhighlighter",
    102    markup: "devtoolsMarkupFlexboxhighlighter",
    103    rule: "devtoolsRulesFlexboxhighlighter",
    104  },
    105 
    106  [TYPES.GRID]: {
    107    grid: "devtoolsGridGridinspector",
    108    markup: "devtoolsMarkupGridinspector",
    109    rule: "devtoolsRulesGridinspector",
    110  },
    111 };
    112 
    113 /**
    114 * HighlightersOverlay manages the visibility of highlighters in the Inspector.
    115 */
    116 class HighlightersOverlay {
    117  /**
    118   * @param  {Inspector} inspector
    119   *         Inspector toolbox panel.
    120   */
    121  constructor(inspector) {
    122    this.inspector = inspector;
    123    this.store = this.inspector.store;
    124 
    125    this.telemetry = this.inspector.telemetry;
    126    this.maxGridHighlighters = Services.prefs.getIntPref(
    127      "devtools.gridinspector.maxHighlighters"
    128    );
    129 
    130    // Collection of instantiated highlighter actors like FlexboxHighlighter,
    131    // ShapesHighlighter and GeometryEditorHighlighter.
    132    this.highlighters = {};
    133    // Map of grid container node to an object with the grid highlighter instance
    134    // and, if the node is a subgrid, the parent grid node and parent grid highlighter.
    135    // Ex: {NodeFront} => {
    136    //  highlighter: {CustomHighlighterFront},
    137    //  parentGridNode: {NodeFront|null},
    138    //  parentGridHighlighter: {CustomHighlighterFront|null}
    139    // }
    140    this.gridHighlighters = new Map();
    141    // Collection of instantiated in-context editors, like ShapesInContextEditor, which
    142    // behave like highlighters but with added editing capabilities that need to map value
    143    // changes to properties in the Rule view.
    144    this.editors = {};
    145 
    146    // Highlighter state.
    147    this.state = {
    148      // Map of grid container NodeFront to the their stored grid options
    149      // Used to restore grid highlighters on reload (should be migrated to
    150      // #restorableHighlighters in Bug 1572652).
    151      grids: new Map(),
    152      // Shape Path Editor highlighter options.
    153      // Used as a cache for the latest configuration when showing the highlighter.
    154      // It is reused and augmented when hovering coordinates in the Rules view which
    155      // mark the corresponding points in the highlighter overlay.
    156      shapes: {},
    157    };
    158 
    159    // NodeFront of element that is highlighted by the geometry editor.
    160    this.geometryEditorHighlighterShown = null;
    161    // Name of the highlighter shown on mouse hover.
    162    this.hoveredHighlighterShown = null;
    163    // NodeFront of the shape that is highlighted
    164    this.shapesHighlighterShown = null;
    165 
    166    this.onClick = this.onClick.bind(this);
    167    this.onDisplayChange = this.onDisplayChange.bind(this);
    168    this.onMarkupMutation = this.onMarkupMutation.bind(this);
    169 
    170    this.onMouseMove = this.onMouseMove.bind(this);
    171    this.onMouseOut = this.onMouseOut.bind(this);
    172    this.hideAllHighlighters = this.hideAllHighlighters.bind(this);
    173    this.hideFlexboxHighlighter = this.hideFlexboxHighlighter.bind(this);
    174    this.hideGridHighlighter = this.hideGridHighlighter.bind(this);
    175    this.hideShapesHighlighter = this.hideShapesHighlighter.bind(this);
    176    this.showFlexboxHighlighter = this.showFlexboxHighlighter.bind(this);
    177    this.showGridHighlighter = this.showGridHighlighter.bind(this);
    178    this.showShapesHighlighter = this.showShapesHighlighter.bind(this);
    179    this.onShapesHighlighterShown = this.onShapesHighlighterShown.bind(this);
    180    this.onShapesHighlighterHidden = this.onShapesHighlighterHidden.bind(this);
    181 
    182    // Catch unexpected errors from async functions if the manager has been destroyed.
    183    this.hideHighlighterType = safeAsyncMethod(
    184      this.hideHighlighterType.bind(this),
    185      () => this.destroyed
    186    );
    187    this.showHighlighterTypeForNode = safeAsyncMethod(
    188      this.showHighlighterTypeForNode.bind(this),
    189      () => this.destroyed
    190    );
    191    this.showGridHighlighter = safeAsyncMethod(
    192      this.showGridHighlighter.bind(this),
    193      () => this.destroyed
    194    );
    195    this.restoreState = safeAsyncMethod(
    196      this.restoreState.bind(this),
    197      () => this.destroyed
    198    );
    199 
    200    // Add inspector events, not specific to a given view.
    201    this.inspector.on("markupmutation", this.onMarkupMutation);
    202 
    203    this.resourceCommand = this.inspector.commands.resourceCommand;
    204    this.resourceCommand.watchResources(
    205      [this.resourceCommand.TYPES.ROOT_NODE],
    206      { onAvailable: this.#onResourceAvailable }
    207    );
    208 
    209    this.walkerEventListener = new WalkerEventListener(this.inspector, {
    210      "display-change": this.onDisplayChange,
    211    });
    212 
    213    if (this.toolbox.win.matchMedia("(prefers-reduced-motion)").matches) {
    214      this.#showSimpleHighlightersMessage();
    215    }
    216 
    217    EventEmitter.decorate(this);
    218  }
    219 
    220  // Map of active highlighter types to objects with the highlighted nodeFront and the
    221  // highlighter instance. Ex: "BoxModelHighlighter" => { nodeFront, highlighter }
    222  // It will fully replace this.highlighters when all highlighter consumers are updated
    223  // to use it as the single source of truth for which highlighters are visible.
    224  #activeHighlighters = new Map();
    225  // Map of highlighter types to symbols. Showing highlighters is an async operation,
    226  // until it doesn't complete, this map will be populated with the requested type and
    227  // a unique symbol identifying that request. Once completed, the entry is removed.
    228  #pendingHighlighters = new Map();
    229  // Map of highlighter types to objects with metadata used to restore active
    230  // highlighters after a page reload.
    231  #restorableHighlighters = new Map();
    232 
    233  #lastHovered = null;
    234 
    235  get inspectorFront() {
    236    return this.inspector.inspectorFront;
    237  }
    238 
    239  get target() {
    240    return this.inspector.currentTarget;
    241  }
    242 
    243  get toolbox() {
    244    return this.inspector.toolbox;
    245  }
    246 
    247  /**
    248   * Optionally run some operations right after showing a highlighter of a given type,
    249   * but before notifying consumers by emitting the "highlighter-shown" event.
    250   *
    251   * This is a chance to run some non-essential operations like: logging telemetry data,
    252   * storing metadata about the highlighter to enable restoring it after refresh, etc.
    253   *
    254   * @param  {string} type
    255   *          Highlighter type shown.
    256   * @param  {NodeFront} nodeFront
    257   *          Node front of the element that was highlighted.
    258   * @param  {Options} options
    259   *          Optional object with options passed to the highlighter.
    260   */
    261  #afterShowHighlighterTypeForNode(type, nodeFront, options) {
    262    switch (type) {
    263      // Log telemetry for showing the flexbox and grid highlighters.
    264      case TYPES.FLEXBOX:
    265      case TYPES.GRID: {
    266        const toolID = GLEAN_TOOL_IDS[type];
    267        if (toolID) {
    268          this.telemetry.toolOpened(toolID, this);
    269        }
    270 
    271        const counterName = GLEAN_COUNTER_NAMES[type]?.[options?.trigger];
    272        if (counterName) {
    273          Glean[counterName].opened.add(1);
    274        }
    275 
    276        break;
    277      }
    278    }
    279 
    280    // Set metadata necessary to restore the active highlighter upon page refresh.
    281    if (type === TYPES.FLEXBOX) {
    282      const { url } = this.target;
    283      const selectors = [...this.inspector.selectionCssSelectors];
    284 
    285      this.#restorableHighlighters.set(type, {
    286        options,
    287        selectors,
    288        type,
    289        url,
    290      });
    291    }
    292  }
    293 
    294  /**
    295   * Optionally run some operations before showing a highlighter of a given type.
    296   *
    297   * Depending its type, before showing a new instance of a highlighter, we may do extra
    298   * operations, like hiding another visible highlighter, or preventing the show
    299   * operation, for example due to a duplicate call with the same arguments.
    300   *
    301   * Returns a promise that resovles with a boolean indicating whether to skip showing
    302   * the highlighter with these arguments.
    303   *
    304   * @param  {string} type
    305   *          Highlighter type to show.
    306   * @param  {NodeFront} nodeFront
    307   *          Node front of the element to be highlighted.
    308   * @param  {Options} options
    309   *          Optional object with options to pass to the highlighter.
    310   * @return {Promise}
    311   */
    312  async #beforeShowHighlighterTypeForNode(type, nodeFront, options) {
    313    // Get the data associated with the visible highlighter of this type, if any.
    314    const {
    315      highlighter: activeHighlighter,
    316      nodeFront: activeNodeFront,
    317      options: activeOptions,
    318      timer: activeTimer,
    319    } = this.getDataForActiveHighlighter(type);
    320 
    321    // There isn't an active highlighter of this type. Early return, proceed with showing.
    322    if (!activeHighlighter) {
    323      return false;
    324    }
    325 
    326    // Whether conditions are met to skip showing the highlighter (ex: duplicate calls).
    327    let skipShow = false;
    328 
    329    // Clear any autohide timer associated with this highlighter type.
    330    // This clears any existing timer for duplicate calls to show() if:
    331    // - called with different options.duration
    332    // - called once with options.duration, then without (see deepEqual() above)
    333    clearTimeout(activeTimer);
    334 
    335    switch (type) {
    336      // Hide the visible selector highlighter if called for the same node,
    337      // but with a different selector.
    338      case TYPES.SELECTOR:
    339        if (
    340          nodeFront === activeNodeFront &&
    341          options?.selector !== activeOptions?.selector
    342        ) {
    343          await this.hideHighlighterType(TYPES.SELECTOR);
    344        }
    345        break;
    346 
    347      // For others, hide the existing highlighter before showing it for a different node.
    348      // Else, if the node is the same and options are the same, skip a duplicate call.
    349      // Duplicate calls to show the highlighter for the same node are allowed
    350      // if the options are different (for example, when scheduling autohide).
    351      default:
    352        if (nodeFront !== activeNodeFront) {
    353          await this.hideHighlighterType(type);
    354        } else if (deepEqual(options, activeOptions)) {
    355          skipShow = true;
    356        }
    357    }
    358 
    359    return skipShow;
    360  }
    361 
    362  /**
    363   * Optionally run some operations before hiding a highlighter of a given type.
    364   * Runs only if a highlighter of that type exists.
    365   *
    366   * @param {string} type
    367   *         highlighter type
    368   * @return {Promise}
    369   */
    370  #beforeHideHighlighterType(type) {
    371    switch (type) {
    372      // Log telemetry for hiding the flexbox and grid highlighters.
    373      case TYPES.FLEXBOX:
    374      case TYPES.GRID: {
    375        const toolID = GLEAN_TOOL_IDS[type];
    376        const conditions = {
    377          [TYPES.FLEXBOX]: () => {
    378            // always stop the timer when the flexbox highlighter is about to be hidden.
    379            return true;
    380          },
    381          [TYPES.GRID]: () => {
    382            // stop the timer only once the last grid highlighter is about to be hidden.
    383            return this.gridHighlighters.size === 1;
    384          },
    385        };
    386 
    387        if (toolID && conditions[type].call(this)) {
    388          this.telemetry.toolClosed(toolID, this);
    389        }
    390 
    391        break;
    392      }
    393    }
    394  }
    395 
    396  /**
    397   * Get the maximum number of possible active highlighter instances of a given type.
    398   *
    399   * @param  {string} type
    400   *         Highlighter type
    401   * @return {number}
    402   *         Default 1
    403   */
    404  #getMaxActiveHighlighters(type) {
    405    let max;
    406 
    407    switch (type) {
    408      // Grid highligthters are special (there is a parent-child relationship between
    409      // subgrid and parent grid) so we suppport multiple visible instances.
    410      // Grid highlighters are performance-intensive and this limit is somewhat arbitrary
    411      // to guard against performance degradation.
    412      case TYPES.GRID:
    413        max = this.maxGridHighlighters;
    414        break;
    415      // By default, for all other highlighter types, only one instance may visible.
    416      // Before showing a new highlighter, any other instance will be hidden.
    417      default:
    418        max = 1;
    419    }
    420 
    421    return max;
    422  }
    423 
    424  /**
    425   * Get a highlighter instance of the given type for the given node front.
    426   *
    427   * @param  {string} type
    428   *         Highlighter type.
    429   * @param  {NodeFront} nodeFront
    430   *         Node front of the element to be highlighted with the requested highlighter.
    431   * @return {Promise}
    432   *         Promise which resolves with a highlighter instance
    433   */
    434  async #getHighlighterTypeForNode(type, nodeFront) {
    435    const { inspectorFront } = nodeFront;
    436    const max = this.#getMaxActiveHighlighters(type);
    437    let highlighter;
    438 
    439    // If only one highlighter instance may be visible, get a highlighter front
    440    // and cache it to return it on future requests.
    441    // Otherwise, return a new highlighter front every time and clean-up manually.
    442    if (max === 1) {
    443      highlighter = await inspectorFront.getOrCreateHighlighterByType(type);
    444    } else {
    445      highlighter = await inspectorFront.getHighlighterByType(type);
    446    }
    447 
    448    return highlighter;
    449  }
    450 
    451  /**
    452   * Get the currently active highlighter of a given type.
    453   *
    454   * @param  {string} type
    455   *         Highlighter type.
    456   * @return {Highlighter|null}
    457   *         Highlighter instance
    458   *         or null if no highlighter of that type is active.
    459   */
    460  getActiveHighlighter(type) {
    461    if (!this.#activeHighlighters.has(type)) {
    462      return null;
    463    }
    464 
    465    const { highlighter } = this.#activeHighlighters.get(type);
    466    return highlighter;
    467  }
    468 
    469  /**
    470   * Get an object with data associated with the active highlighter of a given type.
    471   * This data object contains:
    472   *   - nodeFront: NodeFront of the highlighted node
    473   *   - highlighter: Highlighter instance
    474   *   - options: Configuration options passed to the highlighter
    475   *   - timer: (Optional) index of timer set with setTimout() to autohide the highlighter
    476   * Returns an empty object if a highlighter of the given type is not active.
    477   *
    478   * @param  {string} type
    479   *         Highlighter type.
    480   * @return {object}
    481   */
    482  getDataForActiveHighlighter(type) {
    483    if (!this.#activeHighlighters.has(type)) {
    484      return {};
    485    }
    486 
    487    return this.#activeHighlighters.get(type);
    488  }
    489 
    490  /**
    491   * Get the configuration options of the active highlighter of a given type.
    492   *
    493   * @param  {string} type
    494   *         Highlighter type.
    495   * @return {object}
    496   */
    497  getOptionsForActiveHighlighter(type) {
    498    const { options } = this.getDataForActiveHighlighter(type);
    499    return options;
    500  }
    501 
    502  /**
    503   * Get the node front highlighted by a given highlighter type.
    504   *
    505   * @param  {string} type
    506   *         Highlighter type.
    507   * @return {NodeFront|null}
    508   *         Node front of the element currently being highlighted
    509   *         or null if no highlighter of that type is active.
    510   */
    511  getNodeForActiveHighlighter(type) {
    512    if (!this.#activeHighlighters.has(type)) {
    513      return null;
    514    }
    515 
    516    const { nodeFront } = this.#activeHighlighters.get(type);
    517    return nodeFront;
    518  }
    519 
    520  /**
    521   * Highlight a given node front with a given type of highlighter.
    522   *
    523   * Highlighters are shown for one node at a time. Before showing the same highlighter
    524   * type on another node, it will first be hidden from the previously highlighted node.
    525   * In pages with frames running in different processes, this ensures highlighters from
    526   * other frames do not stay visible.
    527   *
    528   * @param  {string} type
    529   *          Highlighter type to show.
    530   * @param  {NodeFront} nodeFront
    531   *          Node front of the element to be highlighted.
    532   * @param  {Options} options
    533   *         Optional object with options to pass to the highlighter.
    534   * @return {Promise}
    535   */
    536  async showHighlighterTypeForNode(type, nodeFront, options) {
    537    const promise = this.#beforeShowHighlighterTypeForNode(
    538      type,
    539      nodeFront,
    540      options
    541    );
    542 
    543    // Set a pending highlighter in order to detect if, while we were awaiting, there was
    544    // a more recent request to highlight a node with the same type, or a request to hide
    545    // the highlighter. Then we will abort this one in favor of the newer one.
    546    // This needs to be done before the 'await' in order to be synchronous, but after
    547    // calling #beforeShowHighlighterTypeForNode, since it can call hideHighlighterType.
    548    const id = Symbol();
    549    this.#pendingHighlighters.set(type, id);
    550    const skipShow = await promise;
    551 
    552    if (this.#pendingHighlighters.get(type) !== id) {
    553      return;
    554    } else if (skipShow || nodeFront.isDestroyed()) {
    555      this.#pendingHighlighters.delete(type);
    556      return;
    557    }
    558 
    559    const highlighter = await this.#getHighlighterTypeForNode(type, nodeFront);
    560 
    561    if (this.#pendingHighlighters.get(type) !== id) {
    562      return;
    563    }
    564    this.#pendingHighlighters.delete(type);
    565 
    566    // Set a timer to automatically hide the highlighter if a duration is provided.
    567    const timer = this.scheduleAutoHideHighlighterType(type, options?.duration);
    568    // TODO: support case for multiple highlighter instances (ex: multiple grids)
    569    this.#activeHighlighters.set(type, {
    570      nodeFront,
    571      highlighter,
    572      options,
    573      timer,
    574    });
    575    await highlighter.show(nodeFront, options);
    576    this.#afterShowHighlighterTypeForNode(type, nodeFront, options);
    577 
    578    // Emit any type-specific highlighter shown event for tests
    579    // which have not yet been updated to listen for the generic event
    580    if (HIGHLIGHTER_EVENTS[type]?.shown) {
    581      this.emit(HIGHLIGHTER_EVENTS[type].shown, nodeFront, options);
    582    }
    583    this.emit("highlighter-shown", { type, highlighter, nodeFront, options });
    584  }
    585 
    586  /**
    587   * Set a timer to automatically hide all highlighters of a given type after a delay.
    588   *
    589   * @param  {string} type
    590   *         Highlighter type to hide.
    591   * @param  {number | undefined} duration
    592   *         Delay in milliseconds after which to hide the highlighter.
    593   *         If a duration is not provided, return early without scheduling a task.
    594   * @return {number | undefined}
    595   *         Index of the scheduled task returned by setTimeout().
    596   */
    597  scheduleAutoHideHighlighterType(type, duration) {
    598    if (!duration) {
    599      return undefined;
    600    }
    601 
    602    const timer = setTimeout(async () => {
    603      await this.hideHighlighterType(type);
    604      clearTimeout(timer);
    605    }, duration);
    606 
    607    return timer;
    608  }
    609 
    610  /**
    611   * Hide all instances of a given highlighter type.
    612   *
    613   * @param  {string} type
    614   *         Highlighter type to hide.
    615   * @return {Promise}
    616   */
    617  async hideHighlighterType(type) {
    618    if (this.#pendingHighlighters.has(type)) {
    619      // Abort pending highlighters for the given type.
    620      this.#pendingHighlighters.delete(type);
    621    }
    622    if (!this.#activeHighlighters.has(type)) {
    623      return;
    624    }
    625 
    626    const data = this.getDataForActiveHighlighter(type);
    627    const { highlighter, nodeFront, timer } = data;
    628    // Clear any autohide timer associated with this highlighter type.
    629    clearTimeout(timer);
    630    // Remove any metadata used to restore this highlighter type on page refresh.
    631    this.#restorableHighlighters.delete(type);
    632    this.#activeHighlighters.delete(type);
    633    this.#beforeHideHighlighterType(type);
    634    await highlighter.hide();
    635 
    636    // Emit any type-specific highlighter hidden event for tests
    637    // which have not yet been updated to listen for the generic event
    638    if (HIGHLIGHTER_EVENTS[type]?.hidden) {
    639      this.emit(HIGHLIGHTER_EVENTS[type].hidden, nodeFront);
    640    }
    641    this.emit("highlighter-hidden", { type, ...data });
    642  }
    643 
    644  /**
    645   * Returns true if the grid highlighter can be toggled on/off for the given node, and
    646   * false otherwise. A grid container can be toggled on if the max grid highlighters
    647   * is only 1 or less than the maximum grid highlighters that can be displayed or if
    648   * the grid highlighter already highlights the given node.
    649   *
    650   * @param  {NodeFront} node
    651   *         Grid container NodeFront.
    652   * @return {boolean}
    653   */
    654  canGridHighlighterToggle(node) {
    655    return (
    656      this.maxGridHighlighters === 1 ||
    657      this.gridHighlighters.size < this.maxGridHighlighters ||
    658      this.gridHighlighters.has(node)
    659    );
    660  }
    661 
    662  /**
    663   * Returns true when the maximum number of grid highlighter instances is reached.
    664   * FIXME: Bug 1572652 should address this constraint.
    665   *
    666   * @return {boolean}
    667   */
    668  isGridHighlighterLimitReached() {
    669    return this.gridHighlighters.size === this.maxGridHighlighters;
    670  }
    671 
    672  /**
    673   * Returns whether `node` is somewhere inside the DOM of the rule view.
    674   *
    675   * @param {DOMNode} node
    676   * @return {boolean}
    677   */
    678  isRuleView(node) {
    679    return !!node.closest("#ruleview-panel");
    680  }
    681 
    682  /**
    683   * Add the highlighters overlay to the view. This will start tracking mouse events
    684   * and display highlighters when needed.
    685   *
    686   * @param  {CssRuleView|CssComputedView|LayoutView} view
    687   *         Either the rule-view or computed-view panel to add the highlighters overlay.
    688   */
    689  addToView(view) {
    690    const el = view.element;
    691    el.addEventListener("click", this.onClick, true);
    692    el.addEventListener("mousemove", this.onMouseMove);
    693    el.addEventListener("mouseout", this.onMouseOut);
    694    el.ownerDocument.defaultView.addEventListener("mouseout", this.onMouseOut);
    695  }
    696 
    697  /**
    698   * Remove the overlay from the given view. This will stop tracking mouse movement and
    699   * showing highlighters.
    700   *
    701   * @param  {CssRuleView|CssComputedView|LayoutView} view
    702   *         Either the rule-view or computed-view panel to remove the highlighters
    703   *         overlay.
    704   */
    705  removeFromView(view) {
    706    const el = view.element;
    707    el.removeEventListener("click", this.onClick, true);
    708    el.removeEventListener("mousemove", this.onMouseMove);
    709    el.removeEventListener("mouseout", this.onMouseOut);
    710  }
    711 
    712  /**
    713   * Toggle the shapes highlighter for the given node.
    714   *
    715   * @param  {NodeFront} node
    716   *         The NodeFront of the element with a shape to highlight.
    717   * @param  {object} options
    718   *         Object used for passing options to the shapes highlighter.
    719   * @param {TextProperty} textProperty
    720   *        TextProperty where to write changes.
    721   */
    722  async toggleShapesHighlighter(node, options, textProperty) {
    723    const shapesEditor = await this.getInContextEditor(node, "shapesEditor");
    724    if (!shapesEditor) {
    725      return;
    726    }
    727    shapesEditor.toggle(node, options, textProperty);
    728  }
    729 
    730  /**
    731   * Show the shapes highlighter for the given node.
    732   * This method delegates to the in-context shapes editor.
    733   *
    734   * @param  {NodeFront} node
    735   *         The NodeFront of the element with a shape to highlight.
    736   * @param  {object} options
    737   *         Object used for passing options to the shapes highlighter.
    738   */
    739  async showShapesHighlighter(node, options) {
    740    const shapesEditor = await this.getInContextEditor(node, "shapesEditor");
    741    if (!shapesEditor) {
    742      return;
    743    }
    744    shapesEditor.show(node, options);
    745  }
    746 
    747  /**
    748   * Called after the shape highlighter was shown.
    749   *
    750   * @param  {object} data
    751   *         Data associated with the event.
    752   *         Contains:
    753   *         - {NodeFront} node: The NodeFront of the element that is highlighted.
    754   *         - {Object} options: Options that were passed to ShapesHighlighter.show()
    755   */
    756  onShapesHighlighterShown(data) {
    757    const { node, options } = data;
    758    this.shapesHighlighterShown = node;
    759    this.state.shapes.options = options;
    760    this.emit("shapes-highlighter-shown", node, options);
    761  }
    762 
    763  /**
    764   * Hide the shapes highlighter if visible.
    765   * This method delegates the to the in-context shapes editor which wraps
    766   * the shapes highlighter with additional functionality.
    767   *
    768   * @param  {NodeFront} node.
    769   */
    770  async hideShapesHighlighter(node) {
    771    const shapesEditor = await this.getInContextEditor(node, "shapesEditor");
    772    if (!shapesEditor) {
    773      return;
    774    }
    775    shapesEditor.hide();
    776  }
    777 
    778  /**
    779   * Called after the shapes highlighter was hidden.
    780   */
    781  onShapesHighlighterHidden() {
    782    this.emit(
    783      "shapes-highlighter-hidden",
    784      this.shapesHighlighterShown,
    785      this.state.shapes.options
    786    );
    787    this.shapesHighlighterShown = null;
    788    this.state.shapes = {};
    789  }
    790 
    791  /**
    792   * Show the shapes highlighter for the given element, with the given point highlighted.
    793   *
    794   * @param {NodeFront} node
    795   *        The NodeFront of the element to highlight.
    796   * @param {string} point
    797   *        The point to highlight in the shapes highlighter.
    798   */
    799  async hoverPointShapesHighlighter(node, point) {
    800    if (node == this.shapesHighlighterShown) {
    801      const options = Object.assign({}, this.state.shapes.options);
    802      options.hoverPoint = point;
    803      await this.showShapesHighlighter(node, options);
    804    }
    805  }
    806 
    807  /**
    808   * Returns the flexbox highlighter color for the given node.
    809   */
    810  async getFlexboxHighlighterColor() {
    811    // Load the Redux slice for flexbox if not yet available.
    812    const state = this.store.getState();
    813    if (!state.flexbox) {
    814      this.store.injectReducer("flexbox", flexboxReducer);
    815    }
    816 
    817    // Attempt to get the flexbox highlighter color from the Redux store.
    818    const { flexbox } = this.store.getState();
    819    const color = flexbox.color;
    820 
    821    if (color) {
    822      return color;
    823    }
    824 
    825    // If the flexbox inspector has not been initialized, attempt to get the flexbox
    826    // highlighter from the async storage.
    827    const customHostColors =
    828      (await asyncStorage.getItem("flexboxInspectorHostColors")) || {};
    829 
    830    // Get the hostname, if there is no hostname, fall back on protocol
    831    // ex: `data:` uri, and `about:` pages
    832    let hostname;
    833    try {
    834      hostname =
    835        parseURL(this.target.url).hostname ||
    836        parseURL(this.target.url).protocol;
    837    } catch (e) {
    838      this.#handleRejection(e);
    839    }
    840 
    841    return hostname && customHostColors[hostname]
    842      ? customHostColors[hostname]
    843      : DEFAULT_HIGHLIGHTER_COLOR;
    844  }
    845 
    846  /**
    847   * Toggle the flexbox highlighter for the given flexbox container element.
    848   *
    849   * @param  {NodeFront} node
    850   *         The NodeFront of the flexbox container element to highlight.
    851   * @param {string} trigger
    852   *         String name matching "layout", "markup" or "rule" to indicate where the
    853   *         flexbox highlighter was toggled on from. "layout" represents the layout view.
    854   *         "markup" represents the markup view. "rule" represents the rule view.
    855   */
    856  async toggleFlexboxHighlighter(node, trigger) {
    857    const highlightedNode = this.getNodeForActiveHighlighter(TYPES.FLEXBOX);
    858    if (node == highlightedNode) {
    859      await this.hideFlexboxHighlighter(node);
    860      return;
    861    }
    862 
    863    await this.showFlexboxHighlighter(node, {}, trigger);
    864  }
    865 
    866  /**
    867   * Show the flexbox highlighter for the given flexbox container element.
    868   *
    869   * @param  {NodeFront} node
    870   *         The NodeFront of the flexbox container element to highlight.
    871   * @param  {object} options
    872   *         Object used for passing options to the flexbox highlighter.
    873   * @param {string} trigger
    874   *         String name matching "layout", "markup" or "rule" to indicate where the
    875   *         flexbox highlighter was toggled on from. "layout" represents the layout view.
    876   *         "markup" represents the markup view. "rule" represents the rule view.
    877   *         Will be passed as an option even though the highlighter doesn't use it
    878   *         in order to log telemetry in #afterShowHighlighterTypeForNode()
    879   */
    880  async showFlexboxHighlighter(node, options, trigger) {
    881    const color = await this.getFlexboxHighlighterColor(node);
    882    await this.showHighlighterTypeForNode(TYPES.FLEXBOX, node, {
    883      ...options,
    884      trigger,
    885      color,
    886    });
    887  }
    888 
    889  /**
    890   * Hide the flexbox highlighter if any instance is visible.
    891   */
    892  async hideFlexboxHighlighter() {
    893    await this.hideHighlighterType(TYPES.FLEXBOX);
    894  }
    895 
    896  /**
    897   * Create a grid highlighter settings object for the provided nodeFront.
    898   *
    899   * @param  {NodeFront} nodeFront
    900   *         The NodeFront for which we need highlighter settings.
    901   */
    902  getGridHighlighterSettings(nodeFront) {
    903    // Load the Redux slices for grids and grid highlighter settings if not yet available.
    904    const state = this.store.getState();
    905    if (!state.grids) {
    906      this.store.injectReducer("grids", gridsReducer);
    907    }
    908 
    909    if (!state.highlighterSettings) {
    910      this.store.injectReducer(
    911        "highlighterSettings",
    912        highlighterSettingsReducer
    913      );
    914    }
    915 
    916    // Get grids and grid highlighter settings from the latest Redux state
    917    // in case they were just added above.
    918    const { grids, highlighterSettings } = this.store.getState();
    919    const grid = grids.find(g => g.nodeFront === nodeFront);
    920    const color = grid ? grid.color : DEFAULT_HIGHLIGHTER_COLOR;
    921    const zIndex = grid ? grid.zIndex : 0;
    922    return Object.assign({}, highlighterSettings, { color, zIndex });
    923  }
    924 
    925  /**
    926   * Return a list of all node fronts that are highlighted with a Grid highlighter.
    927   *
    928   * @return {Array}
    929   */
    930  getHighlightedGridNodes() {
    931    return [...Array.from(this.gridHighlighters.keys())];
    932  }
    933 
    934  /**
    935   * Toggle the grid highlighter for the given grid container element.
    936   *
    937   * @param  {NodeFront} node
    938   *         The NodeFront of the grid container element to highlight.
    939   * @param {string} trigger
    940   *         String name matching "grid", "markup" or "rule" to indicate where the
    941   *         grid highlighter was toggled on from. "grid" represents the grid view.
    942   *         "markup" represents the markup view. "rule" represents the rule view.
    943   */
    944  async toggleGridHighlighter(node, trigger) {
    945    if (this.gridHighlighters.has(node)) {
    946      await this.hideGridHighlighter(node);
    947      return;
    948    }
    949 
    950    await this.showGridHighlighter(node, {}, trigger);
    951  }
    952 
    953  /**
    954   * Show the grid highlighter for the given grid container element.
    955   * Allow as many active highlighter instances as permitted by the
    956   * maxGridHighlighters limit (default 3).
    957   *
    958   * Logic of showing grid highlighters:
    959   * - GRID:
    960   *  - Show a highlighter for a grid container when explicitly requested
    961   *    (ex. click badge in Markup view) and count it against the limit.
    962   *  - When the limit of active highlighters is reached, do no show any more
    963   *    until other instances are hidden. If configured to show only one instance,
    964   *    hide the existing highlighter before showing a new one.
    965   *
    966   * - SUBGRID:
    967   *  - When a highlighter for a subgrid is shown, also show a highlighter for its parent
    968   *    grid, but with faded-out colors (serves as a visual reference for the subgrid)
    969   *  - The "active" state of the highlighter for the parent grid is not reflected
    970   *    in the UI (checkboxes in the Layout panel, badges in the Markup view, etc.)
    971   *  - The highlighter for the parent grid DOES NOT count against the highlighter limit
    972   *  - If the highlighter for the parent grid is explicitly requested to be shown
    973   *    (ex: click badge in Markup view), show it in full color and reflect its "active"
    974   *    state in the UI (checkboxes in the Layout panel, badges in the Markup view)
    975   *  - When a highlighter for a subgrid is hidden, also hide the highlighter for its
    976   *    parent grid; if the parent grid was explicitly requested separately, keep the
    977   *    highlighter for the parent grid visible, but show it in full color.
    978   *
    979   * @param  {NodeFront} node
    980   *         The NodeFront of the grid container element to highlight.
    981   * @param  {object} options
    982   *         Object used for passing options to the grid highlighter.
    983   * @param  {string} trigger
    984   *         String name matching "grid", "markup" or "rule" to indicate where the
    985   *         grid highlighter was toggled on from. "grid" represents the grid view.
    986   *         "markup" represents the markup view. "rule" represents the rule view.
    987   */
    988  async showGridHighlighter(node, options, trigger) {
    989    if (!this.gridHighlighters.has(node)) {
    990      // If only one grid highlighter can be shown at a time, hide the other instance.
    991      // Otherwise, if the max highlighter limit is reached, do not show another one.
    992      if (this.maxGridHighlighters === 1) {
    993        await this.hideGridHighlighter(
    994          this.gridHighlighters.keys().next().value
    995        );
    996      } else if (this.gridHighlighters.size === this.maxGridHighlighters) {
    997        return;
    998      }
    999    }
   1000 
   1001    // If the given node is already highlighted as the parent grid for a subgrid,
   1002    // hide the parent grid highlighter because it will be explicitly shown below.
   1003    const isHighlightedAsParentGrid = Array.from(this.gridHighlighters.values())
   1004      .map(value => value.parentGridNode)
   1005      .includes(node);
   1006    if (isHighlightedAsParentGrid) {
   1007      await this.hideParentGridHighlighter(node);
   1008    }
   1009 
   1010    // Show a translucent highlight of the parent grid container if the given node is
   1011    // a subgrid and the parent grid container is not already explicitly highlighted.
   1012    let parentGridNode = null;
   1013    let parentGridHighlighter = null;
   1014    if (node.displayType === "subgrid") {
   1015      parentGridNode = await node.walkerFront.getParentGridNode(node);
   1016      parentGridHighlighter =
   1017        await this.showParentGridHighlighter(parentGridNode);
   1018    }
   1019 
   1020    // When changing highlighter colors, we call highlighter.show() again with new options
   1021    // Reuse the active highlighter instance if present; avoid creating new highlighters
   1022    let highlighter;
   1023    if (this.gridHighlighters.has(node)) {
   1024      highlighter = this.gridHighlighters.get(node).highlighter;
   1025    }
   1026 
   1027    if (!highlighter) {
   1028      highlighter = await this.#getHighlighterTypeForNode(TYPES.GRID, node);
   1029    }
   1030 
   1031    this.gridHighlighters.set(node, {
   1032      highlighter,
   1033      parentGridNode,
   1034      parentGridHighlighter,
   1035    });
   1036 
   1037    options = { ...options, ...this.getGridHighlighterSettings(node) };
   1038    await highlighter.show(node, options);
   1039 
   1040    this.#afterShowHighlighterTypeForNode(TYPES.GRID, node, {
   1041      ...options,
   1042      trigger,
   1043    });
   1044 
   1045    try {
   1046      // Save grid highlighter state.
   1047      const { url } = this.target;
   1048 
   1049      const selectors =
   1050        await this.inspector.commands.inspectorCommand.getNodeFrontSelectorsFromTopDocument(
   1051          node
   1052        );
   1053 
   1054      this.state.grids.set(node, { selectors, options, url });
   1055 
   1056      // Emit the NodeFront of the grid container element that the grid highlighter was
   1057      // shown for, and its options for testing the highlighter setting options.
   1058      this.emit("grid-highlighter-shown", node, options);
   1059 
   1060      // XXX: Shim to use generic highlighter events until addressing Bug 1572652
   1061      // Ensures badges in the Markup view reflect the state of the grid highlighter.
   1062      this.emit("highlighter-shown", {
   1063        type: TYPES.GRID,
   1064        nodeFront: node,
   1065        highlighter,
   1066        options,
   1067      });
   1068    } catch (e) {
   1069      this.#handleRejection(e);
   1070    }
   1071  }
   1072 
   1073  /**
   1074   * Show the grid highlighter for the given subgrid's parent grid container element.
   1075   * The parent grid highlighter is shown with faded-out colors, as opposed
   1076   * to the full-color grid highlighter shown when calling showGridHighlighter().
   1077   * If the grid container is already explicitly highlighted (i.e. standalone grid),
   1078   * skip showing the another grid highlighter for it.
   1079   *
   1080   * @param   {NodeFront} node
   1081   *          The NodeFront of the parent grid container element to highlight.
   1082   * @returns {Promise}
   1083   *          Resolves with either the highlighter instance or null if it was skipped.
   1084   */
   1085  async showParentGridHighlighter(node) {
   1086    const isHighlighted = Array.from(this.gridHighlighters.keys()).includes(
   1087      node
   1088    );
   1089 
   1090    if (!node || isHighlighted) {
   1091      return null;
   1092    }
   1093 
   1094    // Get the parent grid highlighter for the parent grid container if one already exists
   1095    let highlighter = this.getParentGridHighlighter(node);
   1096    if (!highlighter) {
   1097      highlighter = await this.#getHighlighterTypeForNode(TYPES.GRID, node);
   1098    }
   1099    const options = {
   1100      ...this.getGridHighlighterSettings(node),
   1101      // Configure the highlighter with faded-out colors.
   1102      globalAlpha: SUBGRID_PARENT_ALPHA,
   1103      isParent: true,
   1104    };
   1105    await highlighter.show(node, options);
   1106 
   1107    this.emitForTests("highlighter-shown", {
   1108      type: TYPES.GRID,
   1109      nodeFront: node,
   1110      highlighter,
   1111      options,
   1112    });
   1113 
   1114    return highlighter;
   1115  }
   1116 
   1117  /**
   1118   * Get the parent grid highlighter associated with the given node
   1119   * if the node is a parent grid container for a highlighted subgrid.
   1120   *
   1121   * @param  {NodeFront} node
   1122   *         NodeFront of the parent grid container for a subgrid.
   1123   * @return {CustomHighlighterFront|null}
   1124   */
   1125  getParentGridHighlighter(node) {
   1126    // Find the highlighter map value for the subgrid whose parent grid is the given node.
   1127    const value = Array.from(this.gridHighlighters.values()).find(
   1128      ({ parentGridNode }) => {
   1129        return parentGridNode === node;
   1130      }
   1131    );
   1132 
   1133    if (!value) {
   1134      return null;
   1135    }
   1136 
   1137    const { parentGridHighlighter } = value;
   1138    return parentGridHighlighter;
   1139  }
   1140 
   1141  /**
   1142   * Restore the parent grid highlighter for a subgrid.
   1143   *
   1144   * A grid node can be highlighted both explicitly (ex: by clicking a badge in the
   1145   * Markup view) and implicitly, as a parent grid for a subgrid.
   1146   *
   1147   * An explicit grid highlighter overwrites a subgrid's parent grid highlighter.
   1148   * After an explicit grid highlighter for a node is hidden, but that node is also the
   1149   * parent grid container for a subgrid which is still highlighted, restore the implicit
   1150   * parent grid highlighter.
   1151   *
   1152   * @param  {NodeFront} node
   1153   *         NodeFront for a grid node which may also be a subgrid's parent grid
   1154   *         container.
   1155   * @return {Promise}
   1156   */
   1157  async restoreParentGridHighlighter(node) {
   1158    // Find the highlighter map entry for the subgrid whose parent grid is the given node.
   1159    const entry = Array.from(this.gridHighlighters.entries()).find(
   1160      ([, value]) => {
   1161        return value?.parentGridNode === node;
   1162      }
   1163    );
   1164 
   1165    if (!Array.isArray(entry)) {
   1166      return;
   1167    }
   1168 
   1169    const [highlightedSubgridNode, data] = entry;
   1170    if (!data.parentGridHighlighter) {
   1171      const parentGridHighlighter = await this.showParentGridHighlighter(node);
   1172      this.gridHighlighters.set(highlightedSubgridNode, {
   1173        ...data,
   1174        parentGridHighlighter,
   1175      });
   1176    }
   1177  }
   1178 
   1179  /**
   1180   * Hide the grid highlighter for the given grid container element.
   1181   *
   1182   * @param  {NodeFront} node
   1183   *         The NodeFront of the grid container element to unhighlight.
   1184   */
   1185  async hideGridHighlighter(node) {
   1186    const { highlighter, parentGridNode } =
   1187      this.gridHighlighters.get(node) || {};
   1188 
   1189    if (!highlighter) {
   1190      return;
   1191    }
   1192 
   1193    // Hide the subgrid's parent grid highlighter, if any.
   1194    if (parentGridNode) {
   1195      await this.hideParentGridHighlighter(parentGridNode);
   1196    }
   1197 
   1198    this.#beforeHideHighlighterType(TYPES.GRID);
   1199    // Don't just hide the highlighter, destroy the front instance to release memory.
   1200    // If another highlighter is shown later, a new front will be created.
   1201    highlighter.destroy();
   1202    this.gridHighlighters.delete(node);
   1203    this.state.grids.delete(node);
   1204 
   1205    // It's possible we just destroyed the grid highlighter for a node which also serves
   1206    // as a subgrid's parent grid. If so, restore the parent grid highlighter.
   1207    await this.restoreParentGridHighlighter(node);
   1208 
   1209    // Emit the NodeFront of the grid container element that the grid highlighter was
   1210    // hidden for.
   1211    this.emit("grid-highlighter-hidden", node);
   1212 
   1213    // XXX: Shim to use generic highlighter events until addressing Bug 1572652
   1214    // Ensures badges in the Markup view reflect the state of the grid highlighter.
   1215    this.emit("highlighter-hidden", {
   1216      type: TYPES.GRID,
   1217      nodeFront: node,
   1218    });
   1219  }
   1220 
   1221  /**
   1222   * Hide the parent grid highlighter for the given parent grid container element.
   1223   * If there are multiple subgrids with the same parent grid, do not hide the parent
   1224   * grid highlighter.
   1225   *
   1226   * @param  {NodeFront} node
   1227   *         The NodeFront of the parent grid container element to unhiglight.
   1228   */
   1229  async hideParentGridHighlighter(node) {
   1230    let count = 0;
   1231    let parentGridHighlighter;
   1232    let subgridNode;
   1233    for (const [key, value] of this.gridHighlighters.entries()) {
   1234      if (value.parentGridNode === node) {
   1235        parentGridHighlighter = value.parentGridHighlighter;
   1236        subgridNode = key;
   1237        count++;
   1238      }
   1239    }
   1240 
   1241    if (!parentGridHighlighter || count > 1) {
   1242      return;
   1243    }
   1244 
   1245    // Destroy the highlighter front instance to release memory.
   1246    parentGridHighlighter.destroy();
   1247 
   1248    // Update the grid highlighter entry to indicate the parent grid highlighter is gone.
   1249    this.gridHighlighters.set(subgridNode, {
   1250      ...this.gridHighlighters.get(subgridNode),
   1251      parentGridHighlighter: null,
   1252    });
   1253  }
   1254 
   1255  /**
   1256   * Toggle the geometry editor highlighter for the given element.
   1257   *
   1258   * @param {NodeFront} node
   1259   *        The NodeFront of the element to highlight.
   1260   */
   1261  async toggleGeometryHighlighter(node) {
   1262    if (node == this.geometryEditorHighlighterShown) {
   1263      await this.hideGeometryEditor();
   1264      return;
   1265    }
   1266 
   1267    await this.showGeometryEditor(node);
   1268  }
   1269 
   1270  /**
   1271   * Show the geometry editor highlightor for the given element.
   1272   *
   1273   * @param {NodeFront} node
   1274   *        THe NodeFront of the element to highlight.
   1275   */
   1276  async showGeometryEditor(node) {
   1277    const highlighter = await this.#getHighlighterTypeForNode(
   1278      TYPES.GEOMETRY,
   1279      node
   1280    );
   1281    if (!highlighter) {
   1282      return;
   1283    }
   1284 
   1285    const isShown = await highlighter.show(node);
   1286    if (!isShown) {
   1287      return;
   1288    }
   1289 
   1290    this.emit("geometry-editor-highlighter-shown");
   1291    this.geometryEditorHighlighterShown = node;
   1292  }
   1293 
   1294  /**
   1295   * Hide the geometry editor highlighter.
   1296   */
   1297  async hideGeometryEditor() {
   1298    if (!this.geometryEditorHighlighterShown) {
   1299      return;
   1300    }
   1301 
   1302    const highlighter =
   1303      this.geometryEditorHighlighterShown.inspectorFront.getKnownHighlighter(
   1304        TYPES.GEOMETRY
   1305      );
   1306 
   1307    if (!highlighter) {
   1308      return;
   1309    }
   1310 
   1311    await highlighter.hide();
   1312 
   1313    this.emit("geometry-editor-highlighter-hidden");
   1314    this.geometryEditorHighlighterShown = null;
   1315  }
   1316 
   1317  /**
   1318   * Restores the saved flexbox highlighter state.
   1319   */
   1320  async restoreFlexboxState() {
   1321    const state = this.#restorableHighlighters.get(TYPES.FLEXBOX);
   1322    if (!state) {
   1323      return;
   1324    }
   1325 
   1326    this.#restorableHighlighters.delete(TYPES.FLEXBOX);
   1327    await this.restoreState(TYPES.FLEXBOX, state, this.showFlexboxHighlighter);
   1328  }
   1329 
   1330  /**
   1331   * Restores the saved grid highlighter state.
   1332   */
   1333  async restoreGridState() {
   1334    // The NodeFronts that are used as the keys in the grid state Map are no longer in the
   1335    // tree after a reload. To clean up the grid state, we create a copy of the values of
   1336    // the grid state before restoring and clear it.
   1337    const values = [...this.state.grids.values()];
   1338    this.state.grids.clear();
   1339 
   1340    try {
   1341      for (const gridState of values) {
   1342        await this.restoreState(
   1343          TYPES.GRID,
   1344          gridState,
   1345          this.showGridHighlighter
   1346        );
   1347      }
   1348    } catch (e) {
   1349      this.#handleRejection(e);
   1350    }
   1351  }
   1352 
   1353  /**
   1354   * Helper function called by restoreFlexboxState, restoreGridState.
   1355   * Restores the saved highlighter state for the given highlighter
   1356   * and their state.
   1357   *
   1358   * @param  {string} type
   1359   *         Highlighter type to be restored.
   1360   * @param  {object} state
   1361   *         Object containing the metadata used to restore the highlighter.
   1362   *         {Array} state.selectors
   1363   *         Array of CSS selector which identifies the node to be highlighted.
   1364   *         If the node is in the top-level document, the array contains just one item.
   1365   *         Otherwise, if the node is nested within a stack of iframes, each iframe is
   1366   *         identified by its unique selector; the last item in the array identifies
   1367   *         the target node within its host iframe document.
   1368   *         {Object} state.options
   1369   *         Configuration options to use when showing the highlighter.
   1370   *         {String} state.url
   1371   *         URL of the top-level target when the metadata was stored. Used to identify
   1372   *         if there was a page refresh or a navigation away to a different page.
   1373   * @param  {Function} showFunction
   1374   *         The function that shows the highlighter
   1375   * @return {Promise} that resolves when the highlighter was restored and shown.
   1376   */
   1377  async restoreState(type, state, showFunction) {
   1378    const { selectors = [], options, url } = state;
   1379 
   1380    if (!selectors.length || url !== this.target.url) {
   1381      // Bail out if no selector was saved, or if we are on a different page.
   1382      this.emit(`highlighter-discarded`, { type });
   1383      return;
   1384    }
   1385 
   1386    const nodeFront =
   1387      await this.inspector.commands.inspectorCommand.findNodeFrontFromSelectors(
   1388        selectors
   1389      );
   1390 
   1391    if (nodeFront) {
   1392      await showFunction(nodeFront, options);
   1393      this.emit(`highlighter-restored`, { type });
   1394    } else {
   1395      this.emit(`highlighter-discarded`, { type });
   1396    }
   1397  }
   1398 
   1399  /**
   1400   * Get an instance of an in-context editor for the given type.
   1401   *
   1402   * In-context editors behave like highlighters but with added editing capabilities which
   1403   * need to write value changes back to something, like to properties in the Rule view.
   1404   * They typically exist in the context of the page, like the ShapesInContextEditor.
   1405   *
   1406   * @param  {NodeFront} node.
   1407   * @param  {string} type
   1408   *         Type of in-context editor. Currently supported: "shapesEditor"
   1409   * @return {object | null}
   1410   *         Reference to instance for given type of in-context editor or null.
   1411   */
   1412  async getInContextEditor(node, type) {
   1413    if (this.editors[type]) {
   1414      return this.editors[type];
   1415    }
   1416 
   1417    let editor;
   1418 
   1419    switch (type) {
   1420      case "shapesEditor": {
   1421        const highlighter = await this.#getHighlighterTypeForNode(
   1422          TYPES.SHAPES,
   1423          node
   1424        );
   1425        if (!highlighter) {
   1426          return null;
   1427        }
   1428        const ShapesInContextEditor = require("resource://devtools/client/shared/widgets/ShapesInContextEditor.js");
   1429 
   1430        editor = new ShapesInContextEditor(
   1431          highlighter,
   1432          this.inspector,
   1433          this.state
   1434        );
   1435        editor.on("show", this.onShapesHighlighterShown);
   1436        editor.on("hide", this.onShapesHighlighterHidden);
   1437        break;
   1438      }
   1439      default:
   1440        throw new Error(`Unsupported in-context editor '${name}'`);
   1441    }
   1442 
   1443    this.editors[type] = editor;
   1444 
   1445    return editor;
   1446  }
   1447 
   1448  /**
   1449   * Get a highlighter front given a type. It will only be initialized once.
   1450   *
   1451   * @param  {string} type
   1452   *         The highlighter type. One of this.highlighters.
   1453   * @return {Promise} that resolves to the highlighter
   1454   */
   1455  async #getHighlighter(type) {
   1456    if (this.highlighters[type]) {
   1457      return this.highlighters[type];
   1458    }
   1459 
   1460    let highlighter;
   1461 
   1462    try {
   1463      highlighter = await this.inspectorFront.getHighlighterByType(type);
   1464    } catch (e) {
   1465      this.#handleRejection(e);
   1466    }
   1467 
   1468    if (!highlighter) {
   1469      return null;
   1470    }
   1471 
   1472    this.highlighters[type] = highlighter;
   1473    return highlighter;
   1474  }
   1475 
   1476  /**
   1477   * Ignore unexpected errors from async function calls
   1478   * if HighlightersOverlay has been destroyed.
   1479   *
   1480   * @param {Error} error
   1481   */
   1482  #handleRejection = error => {
   1483    if (!this.destroyed) {
   1484      console.error(error);
   1485    }
   1486  };
   1487 
   1488  /**
   1489   * Toggle the class "active" on the given shape point in the rule view if the current
   1490   * inspector selection is highlighted by the shapes highlighter.
   1491   *
   1492   * @param {NodeFront} node
   1493   *        The NodeFront of the shape point to toggle
   1494   * @param {boolean} active
   1495   *        Whether the shape point should be active
   1496   */
   1497  _toggleShapePointActive(node, active) {
   1498    if (this.inspector.selection.nodeFront != this.shapesHighlighterShown) {
   1499      return;
   1500    }
   1501 
   1502    node.classList.toggle("active", active);
   1503  }
   1504 
   1505  /**
   1506   * Hide the currently shown hovered highlighter.
   1507   */
   1508  #hideHoveredHighlighter() {
   1509    if (
   1510      !this.hoveredHighlighterShown ||
   1511      !this.highlighters[this.hoveredHighlighterShown]
   1512    ) {
   1513      return;
   1514    }
   1515 
   1516    // For some reason, the call to highlighter.hide doesn't always return a
   1517    // promise. This causes some tests to fail when trying to install a
   1518    // rejection handler on the result of the call. To avoid this, check
   1519    // whether the result is truthy before installing the handler.
   1520    const onHidden = this.highlighters[this.hoveredHighlighterShown].hide();
   1521    if (onHidden) {
   1522      onHidden.catch(console.error);
   1523    }
   1524 
   1525    this.hoveredHighlighterShown = null;
   1526    this.emit("css-transform-highlighter-hidden");
   1527  }
   1528 
   1529  /**
   1530   * Given a node front and a function that hides the given node's highlighter, hides
   1531   * the highlighter if the node front is no longer in the DOM tree. This is called
   1532   * from the "markupmutation" event handler.
   1533   *
   1534   * @param  {NodeFront} node
   1535   *         The NodeFront of a highlighted DOM node.
   1536   * @param  {Function} hideHighlighter
   1537   *         The function that will hide the highlighter of the highlighted node.
   1538   */
   1539  async #hideHighlighterIfDeadNode(node, hideHighlighter) {
   1540    if (!node) {
   1541      return;
   1542    }
   1543 
   1544    try {
   1545      const isInTree =
   1546        node.walkerFront && (await node.walkerFront.isInDOMTree(node));
   1547      if (!isInTree) {
   1548        await hideHighlighter(node);
   1549      }
   1550    } catch (e) {
   1551      this.#handleRejection(e);
   1552    }
   1553  }
   1554 
   1555  /**
   1556   * Is the current hovered node a css transform property value in the
   1557   * computed-view.
   1558   *
   1559   * @param  {object} nodeInfo
   1560   * @return {boolean}
   1561   */
   1562  #isComputedViewTransform(nodeInfo) {
   1563    if (nodeInfo.view != "computed") {
   1564      return false;
   1565    }
   1566    return (
   1567      nodeInfo.type === VIEW_NODE_VALUE_TYPE &&
   1568      nodeInfo.value.property === "transform"
   1569    );
   1570  }
   1571 
   1572  /**
   1573   * Does the current clicked node have the shapes highlighter toggle in the
   1574   * rule-view.
   1575   *
   1576   * @param  {DOMNode} node
   1577   * @return {boolean}
   1578   */
   1579  #isRuleViewShapeSwatch(node) {
   1580    return (
   1581      this.isRuleView(node) && node.classList.contains("inspector-shapeswatch")
   1582    );
   1583  }
   1584 
   1585  /**
   1586   * Is the current hovered node a css transform property value in the rule-view.
   1587   *
   1588   * @param  {object} nodeInfo
   1589   * @return {boolean}
   1590   */
   1591  #isRuleViewTransform(nodeInfo) {
   1592    if (nodeInfo.view != "rule") {
   1593      return false;
   1594    }
   1595    const isTransform =
   1596      nodeInfo.type === VIEW_NODE_VALUE_TYPE &&
   1597      nodeInfo.value.property === "transform";
   1598    const isEnabled =
   1599      nodeInfo.value.enabled &&
   1600      !nodeInfo.value.overridden &&
   1601      !nodeInfo.value.pseudoElement;
   1602    return isTransform && isEnabled;
   1603  }
   1604 
   1605  /**
   1606   * Is the current hovered node a highlightable shape point in the rule-view.
   1607   *
   1608   * @param  {object} nodeInfo
   1609   * @return {boolean}
   1610   */
   1611  isRuleViewShapePoint(nodeInfo) {
   1612    if (nodeInfo.view != "rule") {
   1613      return false;
   1614    }
   1615    const isShape =
   1616      nodeInfo.type === VIEW_NODE_SHAPE_POINT_TYPE &&
   1617      (nodeInfo.value.property === "clip-path" ||
   1618        nodeInfo.value.property === "shape-outside");
   1619    const isEnabled =
   1620      nodeInfo.value.enabled &&
   1621      !nodeInfo.value.overridden &&
   1622      !nodeInfo.value.pseudoElement;
   1623    return (
   1624      isShape &&
   1625      isEnabled &&
   1626      nodeInfo.value.toggleActive &&
   1627      !this.state.shapes.options.transformMode
   1628    );
   1629  }
   1630 
   1631  onClick(event) {
   1632    if (this.#isRuleViewShapeSwatch(event.target)) {
   1633      event.stopPropagation();
   1634 
   1635      const view = this.inspector.getPanel("ruleview").view;
   1636      const nodeInfo = view.getNodeInfo(event.target);
   1637 
   1638      this.toggleShapesHighlighter(
   1639        this.inspector.selection.nodeFront,
   1640        {
   1641          mode: event.target.dataset.mode,
   1642          transformMode: event.metaKey || event.ctrlKey,
   1643        },
   1644        nodeInfo.value.textProperty
   1645      );
   1646    }
   1647  }
   1648 
   1649  /**
   1650   * Handler for "display-change" events from walker fronts. Hides the flexbox or
   1651   * grid highlighter if their respective node is no longer a flex container or
   1652   * grid container.
   1653   *
   1654   * @param  {Array} nodes
   1655   *         An array of nodeFronts
   1656   */
   1657  async onDisplayChange(nodes) {
   1658    const highlightedGridNodes = this.getHighlightedGridNodes();
   1659 
   1660    for (const node of nodes) {
   1661      const display = node.displayType;
   1662 
   1663      // Hide the flexbox highlighter if the node is no longer a flexbox container.
   1664      if (
   1665        display !== "flex" &&
   1666        display !== "inline-flex" &&
   1667        node == this.getNodeForActiveHighlighter(TYPES.FLEXBOX)
   1668      ) {
   1669        await this.hideFlexboxHighlighter(node);
   1670        return;
   1671      }
   1672 
   1673      // Hide the grid highlighter if the node is no longer a grid container.
   1674      if (
   1675        display !== "grid" &&
   1676        display !== "inline-grid" &&
   1677        display !== "subgrid" &&
   1678        highlightedGridNodes.includes(node)
   1679      ) {
   1680        await this.hideGridHighlighter(node);
   1681        return;
   1682      }
   1683    }
   1684  }
   1685 
   1686  onMouseMove(event) {
   1687    // Bail out if the target is the same as for the last mousemove.
   1688    if (event.target === this.#lastHovered) {
   1689      return;
   1690    }
   1691 
   1692    // Only one highlighter can be displayed at a time, hide the currently shown.
   1693    this.#hideHoveredHighlighter();
   1694 
   1695    this.#lastHovered = event.target;
   1696 
   1697    const view = this.isRuleView(this.#lastHovered)
   1698      ? this.inspector.getPanel("ruleview").view
   1699      : this.inspector.getPanel("computedview").computedView;
   1700    const nodeInfo = view.getNodeInfo(event.target);
   1701    if (!nodeInfo) {
   1702      return;
   1703    }
   1704 
   1705    if (this.isRuleViewShapePoint(nodeInfo)) {
   1706      const { point } = nodeInfo.value;
   1707      this.hoverPointShapesHighlighter(
   1708        this.inspector.selection.nodeFront,
   1709        point
   1710      );
   1711      return;
   1712    }
   1713 
   1714    // Choose the type of highlighter required for the hovered node.
   1715    let type;
   1716    if (
   1717      this.#isRuleViewTransform(nodeInfo) ||
   1718      this.#isComputedViewTransform(nodeInfo)
   1719    ) {
   1720      type = TYPES.TRANSFORM;
   1721    }
   1722 
   1723    if (type) {
   1724      this.hoveredHighlighterShown = type;
   1725      const node = this.inspector.selection.nodeFront;
   1726      this.#getHighlighter(type).then(highlighter =>
   1727        highlighter.show(node).then(shown => {
   1728          if (shown) {
   1729            this.emit("css-transform-highlighter-shown", highlighter);
   1730          }
   1731        })
   1732      );
   1733    }
   1734  }
   1735 
   1736  onMouseOut(event) {
   1737    // Only hide the highlighter if the mouse leaves the currently hovered node.
   1738    if (
   1739      !this.#lastHovered ||
   1740      (event && this.#lastHovered.contains(event.relatedTarget))
   1741    ) {
   1742      return;
   1743    }
   1744 
   1745    // Otherwise, hide the highlighter.
   1746    const view = this.isRuleView(this.#lastHovered)
   1747      ? this.inspector.getPanel("ruleview").view
   1748      : this.inspector.getPanel("computedview").computedView;
   1749    const nodeInfo = view.getNodeInfo(this.#lastHovered);
   1750    if (nodeInfo && this.isRuleViewShapePoint(nodeInfo)) {
   1751      this.hoverPointShapesHighlighter(
   1752        this.inspector.selection.nodeFront,
   1753        null
   1754      );
   1755    }
   1756    this.#lastHovered = null;
   1757    this.#hideHoveredHighlighter();
   1758  }
   1759 
   1760  /**
   1761   * Handler function called when a new root-node has been added in the
   1762   * inspector. Nodes may have been added / removed and highlighters should
   1763   * be updated.
   1764   */
   1765  #onResourceAvailable = async resources => {
   1766    for (const resource of resources) {
   1767      if (
   1768        resource.resourceType !== this.resourceCommand.TYPES.ROOT_NODE ||
   1769        // It might happen that the ROOT_NODE resource (which is a Front) is already
   1770        // destroyed, and in such case we want to ignore it.
   1771        resource.isDestroyed()
   1772      ) {
   1773        // Only handle root-node resources.
   1774        // Note that we could replace this with DOCUMENT_EVENT resources, since
   1775        // the actual root-node resource is not used here.
   1776        continue;
   1777      }
   1778 
   1779      if (resource.targetFront.isTopLevel && resource.isTopLevelDocument) {
   1780        // The topmost root node will lead to the destruction and recreation of
   1781        // the MarkupView, and highlighters will be refreshed afterwards. This is
   1782        // handled by the inspector.
   1783        continue;
   1784      }
   1785 
   1786      await this.#hideOrphanedHighlighters();
   1787    }
   1788  };
   1789 
   1790  /**
   1791   * Handler function for "markupmutation" events. Hides the flexbox/grid/shapes
   1792   * highlighter if the flexbox/grid/shapes container is no longer in the DOM tree.
   1793   */
   1794  async onMarkupMutation(mutations) {
   1795    const hasInterestingMutation = mutations.some(
   1796      mut => mut.type === "childList"
   1797    );
   1798    if (!hasInterestingMutation) {
   1799      // Bail out if the mutations did not remove nodes, or if no grid highlighter is
   1800      // displayed.
   1801      return;
   1802    }
   1803 
   1804    await this.#hideOrphanedHighlighters();
   1805  }
   1806 
   1807  /**
   1808   * Hide every active highlighter whose nodeFront is no longer present in the DOM.
   1809   * Returns a promise that resolves when all orphaned highlighters are hidden.
   1810   *
   1811   * @return {Promise}
   1812   */
   1813  async #hideOrphanedHighlighters() {
   1814    await this.#hideHighlighterIfDeadNode(
   1815      this.shapesHighlighterShown,
   1816      this.hideShapesHighlighter
   1817    );
   1818 
   1819    // Hide all active highlighters whose nodeFront is no longer attached.
   1820    const promises = [];
   1821    for (const [type, data] of this.#activeHighlighters) {
   1822      promises.push(
   1823        this.#hideHighlighterIfDeadNode(data.nodeFront, () => {
   1824          return this.hideHighlighterType(type);
   1825        })
   1826      );
   1827    }
   1828 
   1829    const highlightedGridNodes = this.getHighlightedGridNodes();
   1830    for (const node of highlightedGridNodes) {
   1831      promises.push(
   1832        this.#hideHighlighterIfDeadNode(node, this.hideGridHighlighter)
   1833      );
   1834    }
   1835 
   1836    return Promise.all(promises);
   1837  }
   1838 
   1839  /**
   1840   * Hides any visible highlighter and clear internal state. This should be called to
   1841   * have a clean slate, for example when the page navigates or when a given frame is
   1842   * selected in the iframe picker.
   1843   */
   1844  async hideAllHighlighters() {
   1845    this.destroyEditors();
   1846 
   1847    // Hide any visible highlighters and clear any timers set to autohide highlighters.
   1848    for (const { highlighter, timer } of this.#activeHighlighters.values()) {
   1849      await highlighter.hide();
   1850      clearTimeout(timer);
   1851    }
   1852 
   1853    this.#activeHighlighters.clear();
   1854    this.#pendingHighlighters.clear();
   1855    this.gridHighlighters.clear();
   1856 
   1857    this.geometryEditorHighlighterShown = null;
   1858    this.hoveredHighlighterShown = null;
   1859    this.shapesHighlighterShown = null;
   1860  }
   1861 
   1862  /**
   1863   * Display a message about the simple highlighters which can be enabled for
   1864   * users relying on prefers-reduced-motion. This message will be a toolbox
   1865   * notification, which will contain a button to open the settings panel and
   1866   * will no longer be displayed if the user decides to explicitly close the
   1867   * message.
   1868   */
   1869  #showSimpleHighlightersMessage() {
   1870    const pref = "devtools.inspector.simple-highlighters.message-dismissed";
   1871    const messageDismissed = Services.prefs.getBoolPref(pref, false);
   1872    if (messageDismissed) {
   1873      return;
   1874    }
   1875    const notificationBox = this.inspector.toolbox.getNotificationBox();
   1876    const message = HighlightersBundle.formatValueSync(
   1877      "simple-highlighters-message"
   1878    );
   1879 
   1880    notificationBox.appendNotification(
   1881      message,
   1882      "simple-highlighters-message",
   1883      null,
   1884      notificationBox.PRIORITY_INFO_MEDIUM,
   1885      [
   1886        {
   1887          label: HighlightersBundle.formatValueSync(
   1888            "simple-highlighters-settings-button"
   1889          ),
   1890          callback: async () => {
   1891            const { panelDoc } = await this.toolbox.selectTool("options");
   1892            const option = panelDoc.querySelector(
   1893              "[data-pref='devtools.inspector.simple-highlighters-reduced-motion']"
   1894            ).parentNode;
   1895            option.scrollIntoView({ block: "center" });
   1896            option.classList.add("options-panel-highlight");
   1897 
   1898            // Emit a test-only event to know when the settings panel is opened.
   1899            this.toolbox.emitForTests("test-highlighters-settings-opened");
   1900          },
   1901        },
   1902      ],
   1903      evt => {
   1904        if (evt === "removed") {
   1905          // Flip the preference when the message is dismissed.
   1906          Services.prefs.setBoolPref(pref, true);
   1907        }
   1908      }
   1909    );
   1910  }
   1911 
   1912  /**
   1913   * Destroy and clean-up all instances of in-context editors.
   1914   */
   1915  destroyEditors() {
   1916    for (const type in this.editors) {
   1917      this.editors[type].off("show");
   1918      this.editors[type].off("hide");
   1919      this.editors[type].destroy();
   1920    }
   1921 
   1922    this.editors = {};
   1923  }
   1924 
   1925  /**
   1926   * Destroy and clean-up all instances of highlighters.
   1927   */
   1928  destroyHighlighters() {
   1929    // Destroy all highlighters and clear any timers set to autohide highlighters.
   1930    const values = [
   1931      ...this.#activeHighlighters.values(),
   1932      ...this.gridHighlighters.values(),
   1933    ];
   1934    for (const { highlighter, parentGridHighlighter, timer } of values) {
   1935      if (highlighter) {
   1936        highlighter.destroy();
   1937      }
   1938 
   1939      if (parentGridHighlighter) {
   1940        parentGridHighlighter.destroy();
   1941      }
   1942 
   1943      if (timer) {
   1944        clearTimeout(timer);
   1945      }
   1946    }
   1947 
   1948    this.#activeHighlighters.clear();
   1949    this.#pendingHighlighters.clear();
   1950    this.gridHighlighters.clear();
   1951 
   1952    for (const type in this.highlighters) {
   1953      if (this.highlighters[type]) {
   1954        this.highlighters[type].finalize();
   1955        this.highlighters[type] = null;
   1956      }
   1957    }
   1958  }
   1959 
   1960  /**
   1961   * Destroy this overlay instance, removing it from the view and destroying
   1962   * all initialized highlighters.
   1963   */
   1964  destroy() {
   1965    this.inspector.off("markupmutation", this.onMarkupMutation);
   1966    this.resourceCommand.unwatchResources(
   1967      [this.resourceCommand.TYPES.ROOT_NODE],
   1968      { onAvailable: this.#onResourceAvailable }
   1969    );
   1970 
   1971    this.walkerEventListener.destroy();
   1972    this.walkerEventListener = null;
   1973 
   1974    this.destroyEditors();
   1975    this.destroyHighlighters();
   1976 
   1977    this.#lastHovered = null;
   1978 
   1979    this.inspector = null;
   1980    this.state = null;
   1981    this.store = null;
   1982    this.telemetry = null;
   1983 
   1984    this.geometryEditorHighlighterShown = null;
   1985    this.hoveredHighlighterShown = null;
   1986    this.shapesHighlighterShown = null;
   1987 
   1988    this.destroyed = true;
   1989  }
   1990 }
   1991 
   1992 HighlightersOverlay.TYPES = HighlightersOverlay.prototype.TYPES = TYPES;
   1993 
   1994 module.exports = HighlightersOverlay;