tor-browser

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

class-list.js (8773B)


      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 // This serves as a local cache for the classes applied to each of the node we care about
     10 // here.
     11 // The map is indexed by NodeFront. Any time a new node is selected in the inspector, an
     12 // entry is added here, indexed by the corresponding NodeFront.
     13 // The value for each entry is an array of each of the class this node has. Items of this
     14 // array are objects like: { name, isApplied } where the name is the class itself, and
     15 // isApplied is a Boolean indicating if the class is applied on the node or not.
     16 const CLASSES = new WeakMap();
     17 
     18 /**
     19 * Manages the list classes per DOM elements we care about.
     20 * The actual list is stored in the CLASSES const, indexed by NodeFront objects.
     21 * The responsibility of this class is to be the source of truth for anyone who wants to
     22 * know which classes a given NodeFront has, and which of these are enabled and which are
     23 * disabled.
     24 * It also reacts to DOM mutations so the list of classes is up to date with what is in
     25 * the DOM.
     26 * It can also be used to enable/disable a given class, or add classes.
     27 *
     28 * @param {Inspector} inspector
     29 *        The current inspector instance.
     30 */
     31 class ClassList {
     32  constructor(inspector) {
     33    EventEmitter.decorate(this);
     34 
     35    this.inspector = inspector;
     36 
     37    this.onMutations = this.onMutations.bind(this);
     38    this.inspector.on("markupmutation", this.onMutations);
     39 
     40    this.classListProxyNode = this.inspector.panelDoc.createElement("div");
     41    this.previewClasses = [];
     42    this.unresolvedStateChanges = [];
     43  }
     44 
     45  destroy() {
     46    this.inspector.off("markupmutation", this.onMutations);
     47    this.inspector = null;
     48    this.classListProxyNode = null;
     49  }
     50 
     51  /**
     52   * The current node selection (which only returns if the node is an ELEMENT_NODE type
     53   * since that's the only type this model can work with.)
     54   */
     55  get currentNode() {
     56    if (
     57      this.inspector.selection.isElementNode() &&
     58      !this.inspector.selection.isPseudoElementNode()
     59    ) {
     60      return this.inspector.selection.nodeFront;
     61    }
     62    return null;
     63  }
     64 
     65  /**
     66   * The class states for the current node selection. See the documentation of the CLASSES
     67   * constant.
     68   */
     69  get currentClasses() {
     70    if (!this.currentNode) {
     71      return [];
     72    }
     73 
     74    if (!CLASSES.has(this.currentNode)) {
     75      // Use the proxy node to get a clean list of classes.
     76      this.classListProxyNode.className = this.currentNode.className;
     77      const nodeClasses = [...new Set([...this.classListProxyNode.classList])]
     78        .filter(
     79          className =>
     80            !this.previewClasses.some(
     81              previewClass =>
     82                previewClass.className === className &&
     83                !previewClass.wasAppliedOnNode
     84            )
     85        )
     86        .map(name => {
     87          return { name, isApplied: true };
     88        });
     89 
     90      CLASSES.set(this.currentNode, nodeClasses);
     91    }
     92 
     93    return CLASSES.get(this.currentNode);
     94  }
     95 
     96  /**
     97   * Same as currentClasses, but returns it in the form of a className string, where only
     98   * enabled classes are added.
     99   */
    100  get currentClassesPreview() {
    101    const currentClasses = this.currentClasses
    102      .filter(({ isApplied }) => isApplied)
    103      .map(({ name }) => name);
    104    const previewClasses = this.previewClasses
    105      .filter(previewClass => !currentClasses.includes(previewClass.className))
    106      .filter(item => item !== "")
    107      .map(({ className }) => className);
    108 
    109    return currentClasses.concat(previewClasses).join(" ").trim();
    110  }
    111 
    112  /**
    113   * Set the state for a given class on the current node.
    114   *
    115   * @param {string} name
    116   *        The class which state should be changed.
    117   * @param {boolean} isApplied
    118   *        True if the class should be enabled, false otherwise.
    119   * @return {Promise} Resolves when the change has been made in the DOM.
    120   */
    121  setClassState(name, isApplied) {
    122    // Do the change in our local model.
    123    const nodeClasses = this.currentClasses;
    124    nodeClasses.find(({ name: cName }) => cName === name).isApplied = isApplied;
    125 
    126    return this.applyClassState();
    127  }
    128 
    129  /**
    130   * Add several classes to the current node at once.
    131   *
    132   * @param {string} classNameString
    133   *        The string that contains all classes.
    134   * @return {Promise} Resolves when the change has been made in the DOM.
    135   */
    136  addClassName(classNameString) {
    137    this.classListProxyNode.className = classNameString;
    138    this.eraseClassPreview();
    139    return Promise.all(
    140      [...new Set([...this.classListProxyNode.classList])].map(name => {
    141        return this.addClass(name);
    142      })
    143    );
    144  }
    145 
    146  /**
    147   * Add a class to the current node at once.
    148   *
    149   * @param {string} name
    150   *        The class to be added.
    151   * @return {Promise} Resolves when the change has been made in the DOM.
    152   */
    153  addClass(name) {
    154    // Avoid adding the same class again.
    155    if (this.currentClasses.some(({ name: cName }) => cName === name)) {
    156      return Promise.resolve();
    157    }
    158 
    159    // Change the local model, so we retain the state of the existing classes.
    160    this.currentClasses.push({ name, isApplied: true });
    161 
    162    return this.applyClassState();
    163  }
    164 
    165  /**
    166   * Used internally by other functions like addClass or setClassState. Actually applies
    167   * the class change to the DOM.
    168   *
    169   * @return {Promise} Resolves when the change has been made in the DOM.
    170   */
    171  applyClassState() {
    172    // If there is no valid inspector selection, bail out silently. No need to report an
    173    // error here.
    174    if (!this.currentNode) {
    175      return Promise.resolve();
    176    }
    177 
    178    // Remember which node & className we applied until their mutation event is received, so we
    179    // can filter out dom mutations that are caused by us in onMutations, even in situations when
    180    // a new change is applied before that the event of the previous one has been received yet
    181    this.unresolvedStateChanges.push({
    182      node: this.currentNode,
    183      className: this.currentClassesPreview,
    184    });
    185 
    186    // Apply the change to the node.
    187    const mod = this.currentNode.startModifyingAttributes();
    188    mod.setAttribute("class", this.currentClassesPreview);
    189    return mod.apply();
    190  }
    191 
    192  onMutations(mutations) {
    193    for (const { type, target, attributeName } of mutations) {
    194      // Only care if this mutation is for the class attribute.
    195      if (type !== "attributes" || attributeName !== "class") {
    196        continue;
    197      }
    198 
    199      const isMutationForOurChange = this.unresolvedStateChanges.some(
    200        previousStateChange =>
    201          previousStateChange.node === target &&
    202          previousStateChange.className === target.className
    203      );
    204 
    205      if (!isMutationForOurChange) {
    206        CLASSES.delete(target);
    207        if (target === this.currentNode) {
    208          this.emit("current-node-class-changed");
    209        }
    210      } else {
    211        this.removeResolvedStateChanged(target, target.className);
    212      }
    213    }
    214  }
    215 
    216  /**
    217   * Get the available classNames in the document where the current selected node lives:
    218   * - the one already used on elements of the document
    219   * - the one defined in Stylesheets of the document
    220   *
    221   * @param {string} filter: A string the classNames should start with (an insensitive
    222   *                         case matching will be done).
    223   * @returns {Promise<Array<string>>} A promise that resolves with an array of strings
    224   *                                   matching the passed filter.
    225   */
    226  getClassNames(filter) {
    227    return this.currentNode.inspectorFront.pageStyle.getAttributesInOwnerDocument(
    228      filter,
    229      "class",
    230      this.currentNode
    231    );
    232  }
    233 
    234  previewClass(inputClasses) {
    235    if (
    236      this.previewClasses
    237        .map(previewClass => previewClass.className)
    238        .join(" ") !== inputClasses
    239    ) {
    240      this.previewClasses = [];
    241      inputClasses.split(" ").forEach(className => {
    242        this.previewClasses.push({
    243          className,
    244          wasAppliedOnNode: this.isClassAlreadyApplied(className),
    245        });
    246      });
    247      this.applyClassState();
    248    }
    249  }
    250 
    251  eraseClassPreview() {
    252    this.previewClass("");
    253  }
    254 
    255  removeResolvedStateChanged(currentNode, currentClassesPreview) {
    256    this.unresolvedStateChanges.splice(
    257      0,
    258      this.unresolvedStateChanges.findIndex(
    259        previousState =>
    260          previousState.node === currentNode &&
    261          previousState.className === currentClassesPreview
    262      ) + 1
    263    );
    264  }
    265 
    266  isClassAlreadyApplied(className) {
    267    return this.currentClasses.some(({ name }) => name === className);
    268  }
    269 }
    270 
    271 module.exports = ClassList;