tor-browser

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

css-registered-properties.js (8509B)


      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 /**
      8 * @typedef InspectorCSSPropertyDefinition (see InspectorUtils.webidl)
      9 * @type {object}
     10 * @property {string} name
     11 * @property {string} syntax
     12 * @property {boolean} inherits
     13 * @property {string} initialValue
     14 * @property {boolean} fromJS - true if property was registered via CSS.registerProperty
     15 */
     16 
     17 class CSSRegisteredPropertiesWatcher {
     18  #abortController;
     19  #onAvailable;
     20  #onUpdated;
     21  #onDestroyed;
     22  #registeredPropertiesCache = new Map();
     23  #styleSheetsManager;
     24  #targetActor;
     25 
     26  /**
     27   * Start watching for all registered CSS properties (@property/CSS.registerProperty)
     28   * related to a given Target Actor.
     29   *
     30   * @param TargetActor targetActor
     31   *        The target actor from which we should observe css changes.
     32   * @param Object options
     33   *        Dictionary object with following attributes:
     34   *        - onAvailable: mandatory function
     35   *        - onUpdated: mandatory function
     36   *        - onDestroyed: mandatory function
     37   *          This will be called for each resource.
     38   */
     39  async watch(targetActor, { onAvailable, onUpdated, onDestroyed }) {
     40    this.#targetActor = targetActor;
     41    this.#onAvailable = onAvailable;
     42    this.#onUpdated = onUpdated;
     43    this.#onDestroyed = onDestroyed;
     44 
     45    // Notify about existing properties
     46    const registeredProperties = this.#getRegisteredProperties();
     47    for (const registeredProperty of registeredProperties) {
     48      this.#registeredPropertiesCache.set(
     49        registeredProperty.name,
     50        registeredProperty
     51      );
     52    }
     53 
     54    this.#notifyResourcesAvailable(registeredProperties);
     55 
     56    // Listen for new properties being registered via CSS.registerProperty
     57    this.#abortController = new AbortController();
     58    const { signal } = this.#abortController;
     59    this.#targetActor.chromeEventHandler.addEventListener(
     60      "csscustompropertyregistered",
     61      this.#onCssCustomPropertyRegistered,
     62      { capture: true, signal }
     63    );
     64 
     65    // Watch for stylesheets being added/modified or destroyed, but don't handle existing
     66    // stylesheets, as we already have the existing properties from this.#getRegisteredProperties.
     67    this.#styleSheetsManager = targetActor.getStyleSheetsManager();
     68    await this.#styleSheetsManager.watch({
     69      onAvailable: this.#refreshCacheAndNotify,
     70      onUpdated: this.#refreshCacheAndNotify,
     71      onDestroyed: this.#refreshCacheAndNotify,
     72      ignoreExisting: true,
     73    });
     74  }
     75 
     76  /**
     77   * Get all the registered properties for the target actor document.
     78   *
     79   * @returns Array<InspectorCSSPropertyDefinition>
     80   */
     81  #getRegisteredProperties() {
     82    return InspectorUtils.getCSSRegisteredProperties(
     83      this.#targetActor.window.document
     84    );
     85  }
     86 
     87  /**
     88   * Compute a resourceId from a given property definition
     89   *
     90   * @param {InspectorCSSPropertyDefinition} propertyDefinition
     91   * @returns string
     92   */
     93  #getRegisteredPropertyResourceId(propertyDefinition) {
     94    return `${this.#targetActor.actorID}:css-registered-property:${
     95      propertyDefinition.name
     96    }`;
     97  }
     98 
     99  /**
    100   * Called when a stylesheet is added, removed or modified.
    101   * This will retrieve the registered properties at this very moment, and notify
    102   * about new, updated and removed registered properties.
    103   */
    104  #refreshCacheAndNotify = async () => {
    105    const registeredProperties = this.#getRegisteredProperties();
    106    const existingPropertiesNames = new Set(
    107      this.#registeredPropertiesCache.keys()
    108    );
    109 
    110    const added = [];
    111    const updated = [];
    112    const removed = [];
    113 
    114    for (const registeredProperty of registeredProperties) {
    115      // If the property isn't in the cache already, this is a new one.
    116      if (!this.#registeredPropertiesCache.has(registeredProperty.name)) {
    117        added.push(registeredProperty);
    118        this.#registeredPropertiesCache.set(
    119          registeredProperty.name,
    120          registeredProperty
    121        );
    122        continue;
    123      }
    124 
    125      // Removing existing property from the Set so we can then later get the properties
    126      // that don't exist anymore.
    127      existingPropertiesNames.delete(registeredProperty.name);
    128 
    129      // The property already existed, so we need to check if its definition was modified
    130      const cachedRegisteredProperty = this.#registeredPropertiesCache.get(
    131        registeredProperty.name
    132      );
    133 
    134      const resourceUpdates = {};
    135      let wasUpdated = false;
    136      if (registeredProperty.syntax !== cachedRegisteredProperty.syntax) {
    137        resourceUpdates.syntax = registeredProperty.syntax;
    138        wasUpdated = true;
    139      }
    140      if (registeredProperty.inherits !== cachedRegisteredProperty.inherits) {
    141        resourceUpdates.inherits = registeredProperty.inherits;
    142        wasUpdated = true;
    143      }
    144      if (
    145        registeredProperty.initialValue !==
    146        cachedRegisteredProperty.initialValue
    147      ) {
    148        resourceUpdates.initialValue = registeredProperty.initialValue;
    149        wasUpdated = true;
    150      }
    151 
    152      if (wasUpdated === true) {
    153        updated.push({
    154          registeredProperty,
    155          resourceUpdates,
    156        });
    157        this.#registeredPropertiesCache.set(
    158          registeredProperty.name,
    159          registeredProperty
    160        );
    161      }
    162    }
    163 
    164    // If there are items left in the Set, it means they weren't processed in the for loop
    165    // before, meaning they don't exist anymore.
    166    for (const registeredPropertyName of existingPropertiesNames) {
    167      removed.push(this.#registeredPropertiesCache.get(registeredPropertyName));
    168      this.#registeredPropertiesCache.delete(registeredPropertyName);
    169    }
    170 
    171    this.#notifyResourcesAvailable(added);
    172    this.#notifyResourcesUpdated(updated);
    173    this.#notifyResourcesDestroyed(removed);
    174  };
    175 
    176  /**
    177   * csscustompropertyregistered event listener callback (fired when a property
    178   * is registered via CSS.registerProperty).
    179   *
    180   * @param {CSSCustomPropertyRegisteredEvent} event
    181   */
    182  #onCssCustomPropertyRegistered = event => {
    183    // Ignore event if property was registered from a global different from the target global.
    184    if (
    185      this.#targetActor.ignoreSubFrames &&
    186      event.target.ownerGlobal !== this.#targetActor.window
    187    ) {
    188      return;
    189    }
    190 
    191    const registeredProperty = event.propertyDefinition;
    192    this.#registeredPropertiesCache.set(
    193      registeredProperty.name,
    194      registeredProperty
    195    );
    196    this.#notifyResourcesAvailable([registeredProperty]);
    197  };
    198 
    199  /**
    200   * @param {Array<InspectorCSSPropertyDefinition>} registeredProperties
    201   */
    202  #notifyResourcesAvailable = registeredProperties => {
    203    if (!registeredProperties.length) {
    204      return;
    205    }
    206 
    207    for (const registeredProperty of registeredProperties) {
    208      registeredProperty.resourceId =
    209        this.#getRegisteredPropertyResourceId(registeredProperty);
    210    }
    211    this.#onAvailable(registeredProperties);
    212  };
    213 
    214  /**
    215   * @param {Array<object>} updates: Array of update object, which have the following properties:
    216   *        - {InspectorCSSPropertyDefinition} registeredProperty: The property definition
    217   *                                            of the updated property
    218   *        - {Object} resourceUpdates: An object containing all the fields that are
    219   *                                    modified for the registered property.
    220   */
    221  #notifyResourcesUpdated = updates => {
    222    if (!updates.length) {
    223      return;
    224    }
    225 
    226    for (const update of updates) {
    227      update.resourceId = this.#getRegisteredPropertyResourceId(
    228        update.registeredProperty
    229      );
    230      // We don't need to send the property definition
    231      delete update.registeredProperty;
    232    }
    233 
    234    this.#onUpdated(updates);
    235  };
    236 
    237  /**
    238   * @param {Array<InspectorCSSPropertyDefinition>} registeredProperties
    239   */
    240  #notifyResourcesDestroyed = registeredProperties => {
    241    if (!registeredProperties.length) {
    242      return;
    243    }
    244 
    245    this.#onDestroyed(
    246      registeredProperties.map(registeredProperty =>
    247        this.#getRegisteredPropertyResourceId(registeredProperty)
    248      )
    249    );
    250  };
    251 
    252  destroy() {
    253    this.#styleSheetsManager.unwatch({
    254      onAvailable: this.#refreshCacheAndNotify,
    255      onUpdated: this.#refreshCacheAndNotify,
    256      onDestroyed: this.#refreshCacheAndNotify,
    257    });
    258 
    259    this.#abortController.abort();
    260  }
    261 }
    262 
    263 module.exports = CSSRegisteredPropertiesWatcher;