tor-browser

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

node-picker.js (8981B)


      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 loader.lazyRequireGetter(
      8  this,
      9  "EventEmitter",
     10  "resource://devtools/shared/event-emitter.js"
     11 );
     12 
     13 /**
     14 * Client-side NodePicker module.
     15 * To be used by inspector front when it needs to select DOM elements.
     16 *
     17 * NodePicker is a proxy for the node picker functionality from WalkerFront instances
     18 * of all available InspectorFronts. It is a single point of entry for the client to:
     19 * - invoke actions to start and stop picking nodes on all walkers
     20 * - listen to node picker events from all walkers and relay them to subscribers
     21 *
     22 * @param {Commands} commands
     23 *        The commands object with all interfaces defined from devtools/shared/commands/
     24 */
     25 class NodePicker extends EventEmitter {
     26  constructor(commands) {
     27    super();
     28    this.commands = commands;
     29    this.targetCommand = commands.targetCommand;
     30 
     31    // Whether or not the node picker is active.
     32    this.isPicking = false;
     33    // Whether to focus the top-level frame before picking nodes.
     34    this.doFocus = false;
     35  }
     36 
     37  // The set of inspector fronts corresponding to the targets where picking happens.
     38  #currentInspectorFronts = new Set();
     39 
     40  /**
     41   * Start/stop the element picker on the debuggee target.
     42   *
     43   * @param {boolean} doFocus
     44   *        Optionally focus the content area once the picker is activated.
     45   * @return Promise that resolves when done
     46   */
     47  togglePicker = doFocus => {
     48    if (this.isPicking) {
     49      return this.stop({ canceled: true });
     50    }
     51    return this.start(doFocus);
     52  };
     53 
     54  /**
     55   * Tell the walker front corresponding to the given inspector front to enter node
     56   * picking mode (listen for mouse movements over its nodes) and set event listeners
     57   * associated with node picking: hover node, pick node, preview, cancel. See WalkerSpec.
     58   *
     59   * @param {InspectorFront} inspectorFront
     60   * @return {Promise}
     61   */
     62  #onInspectorFrontAvailable = async inspectorFront => {
     63    this.#currentInspectorFronts.add(inspectorFront);
     64    // watchFront may notify us about inspector fronts that aren't initialized yet,
     65    // so ensure waiting for initialization in order to have a defined `walker` attribute.
     66    await inspectorFront.initialize();
     67    const { walker } = inspectorFront;
     68    walker.on("picker-node-hovered", this.#onHovered);
     69    walker.on("picker-node-picked", this.#onPicked);
     70    walker.on("picker-node-previewed", this.#onPreviewed);
     71    walker.on("picker-node-canceled", this.#onCanceled);
     72    await walker.pick(this.doFocus);
     73 
     74    this.emitForTests("inspector-front-ready-for-picker", walker);
     75  };
     76 
     77  /**
     78   * Tell the walker front corresponding to the given inspector front to exit the node
     79   * picking mode and remove all event listeners associated with node picking.
     80   *
     81   * @param {InspectorFront} inspectorFront
     82   * @param {boolean} isDestroyCodePath
     83   *        Optional. If true, we assume that's when the toolbox closes
     84   *        and we should avoid doing any RDP request.
     85   * @return {Promise}
     86   */
     87  #onInspectorFrontDestroyed = async (
     88    inspectorFront,
     89    { isDestroyCodepath } = {}
     90  ) => {
     91    this.#currentInspectorFronts.delete(inspectorFront);
     92 
     93    const { walker } = inspectorFront;
     94    if (!walker) {
     95      return;
     96    }
     97 
     98    walker.off("picker-node-hovered", this.#onHovered);
     99    walker.off("picker-node-picked", this.#onPicked);
    100    walker.off("picker-node-previewed", this.#onPreviewed);
    101    walker.off("picker-node-canceled", this.#onCanceled);
    102    // Only do a RDP request if we stop the node picker from a user action.
    103    // Avoid doing one when we close the toolbox, in this scenario
    104    // the walker actor on the server side will automatically cancel the node picking.
    105    if (!isDestroyCodepath) {
    106      await walker.cancelPick();
    107    }
    108  };
    109 
    110  /**
    111   * While node picking, we want each target's walker fronts to listen for mouse
    112   * movements over their nodes and emit events. Walker fronts are obtained from
    113   * inspector fronts so we watch for the creation and destruction of inspector fronts
    114   * in order to add or remove the necessary event listeners.
    115   *
    116   * @param {TargetFront} targetFront
    117   * @return {Promise}
    118   */
    119  #onTargetAvailable = async ({ targetFront }) => {
    120    targetFront.watchFronts(
    121      "inspector",
    122      this.#onInspectorFrontAvailable,
    123      this.#onInspectorFrontDestroyed
    124    );
    125  };
    126 
    127  /**
    128   * Start the element picker.
    129   * This will instruct walker fronts of all available targets (and those of targets
    130   * created while node picking is active) to listen for mouse movements over their nodes
    131   * and trigger events when a node is hovered or picked.
    132   *
    133   * @param {boolean} doFocus
    134   *        Optionally focus the content area once the picker is activated.
    135   */
    136  start = async doFocus => {
    137    if (this.isPicking) {
    138      return;
    139    }
    140    this.isPicking = true;
    141    this.doFocus = doFocus;
    142 
    143    this.emit("picker-starting");
    144 
    145    this.targetCommand.watchTargets({
    146      types: this.targetCommand.ALL_TYPES,
    147      onAvailable: this.#onTargetAvailable,
    148    });
    149 
    150    this.emit("picker-started");
    151  };
    152 
    153  /**
    154   * Stop the element picker. Note that the picker is automatically stopped when
    155   * an element is picked.
    156   *
    157   * @param {boolean} isDestroyCodePath
    158   *        Optional. If true, we assume that's when the toolbox closes
    159   *        and we should avoid doing any RDP request.
    160   * @param {boolean} canceled
    161   *        Optional. If true, emit an additional event to notify that the
    162   *        picker was canceled, ie stopped without selecting a node.
    163   */
    164  stop = async ({ isDestroyCodepath, canceled } = {}) => {
    165    if (!this.isPicking) {
    166      return;
    167    }
    168    this.isPicking = false;
    169    this.doFocus = false;
    170 
    171    this.targetCommand.unwatchTargets({
    172      types: this.targetCommand.ALL_TYPES,
    173      onAvailable: this.#onTargetAvailable,
    174    });
    175 
    176    const promises = [];
    177    for (const inspectorFront of this.#currentInspectorFronts) {
    178      promises.push(
    179        this.#onInspectorFrontDestroyed(inspectorFront, {
    180          isDestroyCodepath,
    181        })
    182      );
    183    }
    184    await Promise.all(promises);
    185 
    186    this.#currentInspectorFronts.clear();
    187 
    188    this.emit("picker-stopped");
    189 
    190    if (canceled) {
    191      this.emit("picker-node-canceled");
    192    }
    193  };
    194 
    195  destroy() {
    196    // Do not await for stop as the isDestroy argument will make this method synchronous
    197    // and we want to avoid having an async destroy
    198    this.stop({ isDestroyCodepath: true });
    199    this.targetCommand = null;
    200    this.commands = null;
    201  }
    202 
    203  /**
    204   * When a node is hovered by the mouse when the highlighter is in picker mode
    205   *
    206   * @param {object} data
    207   *        Information about the node being hovered
    208   */
    209  #onHovered = async data => {
    210    // When debugging WebExtensions, Background page and popups are independent documents.
    211    // None is the parent of each others.
    212    // This means that if the toolbox is having the background page selected and you mouse over a popup,
    213    // the popup DOM Element won't be in the markup view as that's not in a children document of the background page.
    214    // Because of that, we have to select the hovered node's document and target in order to have it visible in the markup view.
    215    //
    216    // These top documents (background pages and popups) can actually have nested iframes,
    217    // for these, we will also select these nested iframes, even if they could theoritically be shown from the top document.
    218    if (
    219      this.targetCommand.descriptorFront.isWebExtensionDescriptor &&
    220      data.node.targetFront != this.targetCommand.selectedTargetFront
    221    ) {
    222      await this.targetCommand.selectTarget(data.node.targetFront);
    223    }
    224 
    225    this.emit("picker-node-hovered", data.node);
    226 
    227    // We're going to cleanup references for all the other walkers, so that if we hover
    228    // back the same node, we will receive a new `picker-node-hovered` event.
    229    for (const inspectorFront of this.#currentInspectorFronts) {
    230      if (inspectorFront.walker !== data.node.walkerFront) {
    231        inspectorFront.walker.clearPicker();
    232      }
    233    }
    234  };
    235 
    236  /**
    237   * When a node has been picked while the highlighter is in picker mode
    238   *
    239   * @param {object} data
    240   *        Information about the picked node
    241   */
    242  #onPicked = data => {
    243    this.emit("picker-node-picked", data.node);
    244    return this.stop();
    245  };
    246 
    247  /**
    248   * When a node has been shift-clicked (previewed) while the highlighter is in
    249   * picker mode
    250   *
    251   * @param {object} data
    252   *        Information about the picked node
    253   */
    254  #onPreviewed = data => {
    255    this.emit("picker-node-previewed", data.node);
    256  };
    257 
    258  /**
    259   * When the picker is canceled, stop the picker, and make sure the toolbox
    260   * gets the focus.
    261   */
    262  #onCanceled = () => {
    263    return this.stop({ canceled: true });
    264  };
    265 }
    266 
    267 module.exports = NodePicker;