tor-browser

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

selection.js (9578B)


      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 
      9 loader.lazyRequireGetter(
     10  this,
     11  "nodeConstants",
     12  "resource://devtools/shared/dom-node-constants.js"
     13 );
     14 
     15 /**
     16 * Selection is a singleton belonging to the Toolbox that manages the current selected
     17 * NodeFront. In addition, it provides some helpers about the context of the selected
     18 * node.
     19 *
     20 * API
     21 *
     22 *   new Selection()
     23 *   destroy()
     24 *   nodeFront (readonly)
     25 *   setNodeFront(node, origin="unknown")
     26 *
     27 * Helpers:
     28 *
     29 *   window
     30 *   document
     31 *   isRoot()
     32 *   isNode()
     33 *   isHTMLNode()
     34 *
     35 * Check the nature of the node:
     36 *
     37 *   isElementNode()
     38 *   isAttributeNode()
     39 *   isTextNode()
     40 *   isCDATANode()
     41 *   isEntityRefNode()
     42 *   isEntityNode()
     43 *   isProcessingInstructionNode()
     44 *   isCommentNode()
     45 *   isDocumentNode()
     46 *   isDocumentTypeNode()
     47 *   isDocumentFragmentNode()
     48 *   isNotationNode()
     49 *
     50 * Events:
     51 *   "new-node-front" when the inner node changed
     52 *   "attribute-changed" when an attribute is changed
     53 *   "detached-front" when the node (or one of its parents) is removed from
     54 *   the document
     55 *   "reparented" when the node (or one of its parents) is moved under
     56 *   a different node
     57 */
     58 class Selection extends EventEmitter {
     59  constructor() {
     60    super();
     61 
     62    this.setNodeFront = this.setNodeFront.bind(this);
     63  }
     64 
     65  #nodeFront;
     66 
     67  // The WalkerFront is dynamic and is always set to the selected NodeFront's WalkerFront.
     68  #walker = null;
     69 
     70  // A single node front can be represented twice on the client when the node is a slotted
     71  // element. It will be displayed once as a direct child of the host element, and once as
     72  // a child of a slot in the "shadow DOM". The latter is called the slotted version.
     73  #isSlotted = false;
     74 
     75  #searchQuery;
     76 
     77  #onMutations = mutations => {
     78    let attributeChange = false;
     79    let pseudoChange = false;
     80    let detached = false;
     81    let detachedNodeParent = null;
     82 
     83    for (const m of mutations) {
     84      if (m.type == "attributes") {
     85        attributeChange = true;
     86      }
     87      if (m.type == "pseudoClassLock") {
     88        pseudoChange = true;
     89      }
     90      if (m.type == "childList") {
     91        if (
     92          // If the node that was selected was removed…
     93          !this.isConnected() &&
     94          // …directly in this mutation, let's pick its parent node
     95          (m.removed.some(nodeFront => nodeFront == this.nodeFront) ||
     96            // in case we don't directly get the removed node, default to the first
     97            // element being mutated in the array of mutations we received
     98            !detachedNodeParent)
     99        ) {
    100          if (this.isNode()) {
    101            detachedNodeParent = m.target;
    102          }
    103          detached = true;
    104        }
    105      }
    106    }
    107 
    108    // Fire our events depending on what changed in the mutations array
    109    if (attributeChange) {
    110      this.emit("attribute-changed");
    111    }
    112    if (pseudoChange) {
    113      this.emit("pseudoclass");
    114    }
    115    if (detached) {
    116      this.emit("detached-front", detachedNodeParent);
    117    }
    118  };
    119 
    120  destroy() {
    121    this.setWalker();
    122    this.#nodeFront = null;
    123  }
    124 
    125  /**
    126   * @param {WalkerFront|null} walker
    127   */
    128  setWalker(walker = null) {
    129    if (this.#walker) {
    130      this.#removeWalkerFrontEventListeners(this.#walker);
    131    }
    132 
    133    this.#walker = walker;
    134    if (this.#walker) {
    135      this.#setWalkerFrontEventListeners(this.#walker);
    136    }
    137  }
    138 
    139  /**
    140   * Set event listeners on the passed walker front
    141   *
    142   * @param {WalkerFront} walker
    143   */
    144  #setWalkerFrontEventListeners(walker) {
    145    walker.on("mutations", this.#onMutations);
    146  }
    147 
    148  /**
    149   * Remove event listeners we previously set on walker front
    150   *
    151   * @param {WalkerFront} walker
    152   */
    153  #removeWalkerFrontEventListeners(walker) {
    154    walker.off("mutations", this.#onMutations);
    155  }
    156 
    157  /**
    158   * Called when a target front is destroyed.
    159   *
    160   * @param {TargetFront} front
    161   * @fires detached-front
    162   */
    163  onTargetDestroyed(targetFront) {
    164    // if the current walker belongs to the target that is destroyed, emit a `detached-front`
    165    // event so consumers can act accordingly (e.g. in the inspector, another node will be
    166    // selected)
    167    if (
    168      this.#walker &&
    169      !targetFront.isTopLevel &&
    170      this.#walker.targetFront == targetFront
    171    ) {
    172      this.#removeWalkerFrontEventListeners(this.#walker);
    173      this.emit("detached-front");
    174    }
    175  }
    176 
    177  /**
    178   * Update the currently selected node-front.
    179   *
    180   * @param {NodeFront} nodeFront
    181   *        The NodeFront being selected.
    182   * @param {object} options (optional)
    183   * @param {string} options.reason: Reason that triggered the selection, will be fired with
    184   *        the "new-node-front" event.
    185   * @param {boolean} options.isSlotted: Is the selection representing the slotted version
    186   *        of the node.
    187   * @param {string} options.searchQuery: If the selection was triggered by a search, the
    188   *        query of said search
    189   */
    190  setNodeFront(
    191    nodeFront,
    192    { reason = "unknown", isSlotted = false, searchQuery = null } = {}
    193  ) {
    194    this.reason = reason;
    195 
    196    // If an inlineTextChild text node is being set, then set it's parent instead.
    197    const parentNode = nodeFront && nodeFront.parentNode();
    198    if (nodeFront && parentNode && parentNode.inlineTextChild === nodeFront) {
    199      nodeFront = parentNode;
    200    }
    201 
    202    if (this.#nodeFront == null && nodeFront == null) {
    203      // Avoid to notify multiple "unselected" events with a null/undefined nodeFront
    204      // (e.g. once when the webpage start to navigate away from the current webpage,
    205      // and then again while the new page is being loaded).
    206      return;
    207    }
    208 
    209    this.emit("node-front-will-unset");
    210 
    211    this.#isSlotted = isSlotted;
    212    this.#searchQuery = searchQuery;
    213    this.#nodeFront = nodeFront;
    214 
    215    if (nodeFront) {
    216      this.setWalker(nodeFront.walkerFront);
    217    } else {
    218      this.setWalker();
    219    }
    220 
    221    this.emit("new-node-front", nodeFront, this.reason);
    222  }
    223 
    224  get nodeFront() {
    225    return this.#nodeFront;
    226  }
    227 
    228  isRoot() {
    229    return (
    230      this.isNode() && this.isConnected() && this.#nodeFront.isDocumentElement
    231    );
    232  }
    233 
    234  isNode() {
    235    return !!this.#nodeFront;
    236  }
    237 
    238  isConnected() {
    239    let node = this.#nodeFront;
    240    if (!node || node.isDestroyed()) {
    241      return false;
    242    }
    243 
    244    while (node) {
    245      if (node === this.#walker.rootNode) {
    246        return true;
    247      }
    248      node = node.parentOrHost();
    249    }
    250    return false;
    251  }
    252 
    253  isHTMLNode() {
    254    const xhtmlNs = "http://www.w3.org/1999/xhtml";
    255    return this.isNode() && this.nodeFront.namespaceURI == xhtmlNs;
    256  }
    257 
    258  isSVGNode() {
    259    const svgNs = "http://www.w3.org/2000/svg";
    260    return this.isNode() && this.nodeFront.namespaceURI == svgNs;
    261  }
    262 
    263  isMathMLNode() {
    264    const mathmlNs = "http://www.w3.org/1998/Math/MathML";
    265    return this.isNode() && this.nodeFront.namespaceURI == mathmlNs;
    266  }
    267 
    268  // Node type
    269 
    270  isElementNode() {
    271    return (
    272      this.isNode() && this.nodeFront.nodeType == nodeConstants.ELEMENT_NODE
    273    );
    274  }
    275 
    276  isPseudoElementNode() {
    277    return this.isNode() && this.nodeFront.isPseudoElement;
    278  }
    279 
    280  isNativeAnonymousNode() {
    281    return this.isNode() && this.nodeFront.isNativeAnonymous;
    282  }
    283 
    284  isAttributeNode() {
    285    return (
    286      this.isNode() && this.nodeFront.nodeType == nodeConstants.ATTRIBUTE_NODE
    287    );
    288  }
    289 
    290  isTextNode() {
    291    return this.isNode() && this.nodeFront.nodeType == nodeConstants.TEXT_NODE;
    292  }
    293 
    294  isCDATANode() {
    295    return (
    296      this.isNode() &&
    297      this.nodeFront.nodeType == nodeConstants.CDATA_SECTION_NODE
    298    );
    299  }
    300 
    301  isEntityRefNode() {
    302    return (
    303      this.isNode() &&
    304      this.nodeFront.nodeType == nodeConstants.ENTITY_REFERENCE_NODE
    305    );
    306  }
    307 
    308  isEntityNode() {
    309    return (
    310      this.isNode() && this.nodeFront.nodeType == nodeConstants.ENTITY_NODE
    311    );
    312  }
    313 
    314  isProcessingInstructionNode() {
    315    return (
    316      this.isNode() &&
    317      this.nodeFront.nodeType == nodeConstants.PROCESSING_INSTRUCTION_NODE
    318    );
    319  }
    320 
    321  isCommentNode() {
    322    return (
    323      this.isNode() &&
    324      this.nodeFront.nodeType == nodeConstants.PROCESSING_INSTRUCTION_NODE
    325    );
    326  }
    327 
    328  isDocumentNode() {
    329    return (
    330      this.isNode() && this.nodeFront.nodeType == nodeConstants.DOCUMENT_NODE
    331    );
    332  }
    333 
    334  /**
    335   * @returns true if the selection is the <body> HTML element.
    336   */
    337  isBodyNode() {
    338    return (
    339      this.isHTMLNode() &&
    340      this.isConnected() &&
    341      this.nodeFront.nodeName === "BODY"
    342    );
    343  }
    344 
    345  /**
    346   * @returns true if the selection is the <head> HTML element.
    347   */
    348  isHeadNode() {
    349    return (
    350      this.isHTMLNode() &&
    351      this.isConnected() &&
    352      this.nodeFront.nodeName === "HEAD"
    353    );
    354  }
    355 
    356  isDocumentTypeNode() {
    357    return (
    358      this.isNode() &&
    359      this.nodeFront.nodeType == nodeConstants.DOCUMENT_TYPE_NODE
    360    );
    361  }
    362 
    363  isDocumentFragmentNode() {
    364    return (
    365      this.isNode() &&
    366      this.nodeFront.nodeType == nodeConstants.DOCUMENT_FRAGMENT_NODE
    367    );
    368  }
    369 
    370  isNotationNode() {
    371    return (
    372      this.isNode() && this.nodeFront.nodeType == nodeConstants.NOTATION_NODE
    373    );
    374  }
    375 
    376  isSlotted() {
    377    return this.#isSlotted;
    378  }
    379 
    380  isShadowRootNode() {
    381    return this.isNode() && this.nodeFront.isShadowRoot;
    382  }
    383 
    384  supportsScrollIntoView() {
    385    return this.isElementNode();
    386  }
    387 
    388  getSearchQuery() {
    389    return this.#searchQuery;
    390  }
    391 }
    392 
    393 module.exports = Selection;