tor-browser

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

CompatibilityView.js (8795B)


      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  createFactory,
      9  createElement,
     10 } = require("resource://devtools/client/shared/vendor/react.mjs");
     11 const {
     12  Provider,
     13 } = require("resource://devtools/client/shared/vendor/react-redux.js");
     14 
     15 const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
     16 const LocalizationProvider = createFactory(FluentReact.LocalizationProvider);
     17 
     18 const compatibilityReducer = require("resource://devtools/client/inspector/compatibility/reducers/compatibility.js");
     19 const {
     20  appendNode,
     21  clearDestroyedNodes,
     22  initUserSettings,
     23  removeNode,
     24  updateNodes,
     25  updateSelectedNode,
     26  updateTopLevelTarget,
     27  updateNode,
     28 } = require("resource://devtools/client/inspector/compatibility/actions/compatibility.js");
     29 
     30 const CompatibilityApp = createFactory(
     31  require("resource://devtools/client/inspector/compatibility/components/CompatibilityApp.js")
     32 );
     33 
     34 class CompatibilityView {
     35  constructor(inspector) {
     36    this.inspector = inspector;
     37 
     38    this.inspector.store.injectReducer("compatibility", compatibilityReducer);
     39 
     40    this.#init();
     41  }
     42 
     43  #isChangeAddedWhileHidden;
     44  #previousChangedSelector;
     45  #updateNodesTimeoutId;
     46 
     47  destroy() {
     48    try {
     49      this.resourceCommand.unwatchResources(
     50        [this.resourceCommand.TYPES.CSS_CHANGE],
     51        {
     52          onAvailable: this.#onResourceAvailable,
     53        }
     54      );
     55    } catch (e) {
     56      // If unwatchResources is called before finishing process of watchResources,
     57      // unwatchResources throws an error during stopping listener.
     58    }
     59 
     60    this.inspector.off("new-root", this.#onTopLevelTargetChanged);
     61    this.inspector.off("markupmutation", this.#onMarkupMutation);
     62    this.inspector.selection.off("new-node-front", this.#onSelectedNodeChanged);
     63    this.inspector.sidebar.off(
     64      "compatibilityview-selected",
     65      this.#onPanelSelected
     66    );
     67    this.inspector = null;
     68  }
     69 
     70  get resourceCommand() {
     71    return this.inspector.commands.resourceCommand;
     72  }
     73 
     74  async #init() {
     75    const { setSelectedNode } = this.inspector.getCommonComponentProps();
     76    const compatibilityApp = new CompatibilityApp({
     77      setSelectedNode,
     78    });
     79 
     80    this.provider = createElement(
     81      Provider,
     82      {
     83        id: "compatibilityview",
     84        store: this.inspector.store,
     85      },
     86      LocalizationProvider(
     87        {
     88          bundles: this.inspector.fluentL10n.getBundles(),
     89          parseMarkup: this.#parseMarkup,
     90        },
     91        compatibilityApp
     92      )
     93    );
     94 
     95    await this.inspector.store.dispatch(initUserSettings());
     96    // awaiting for `initUserSettings` makes us miss the initial "compatibilityview-selected"
     97    // event, so we need to manually call #onPanelSelected to fetch compatibility issues
     98    // for the selected node (and the whole page).
     99    this.#onPanelSelected();
    100 
    101    this.inspector.on("new-root", this.#onTopLevelTargetChanged);
    102    this.inspector.on("markupmutation", this.#onMarkupMutation);
    103    this.inspector.selection.on("new-node-front", this.#onSelectedNodeChanged);
    104    this.inspector.sidebar.on(
    105      "compatibilityview-selected",
    106      this.#onPanelSelected
    107    );
    108 
    109    await this.resourceCommand.watchResources(
    110      [this.resourceCommand.TYPES.CSS_CHANGE],
    111      {
    112        onAvailable: this.#onResourceAvailable,
    113        // CSS changes made before opening Compatibility View are already applied to
    114        // corresponding DOM at this point, so existing resources can be ignored here.
    115        ignoreExistingResources: true,
    116      }
    117    );
    118 
    119    this.inspector.emitForTests("compatibilityview-initialized");
    120  }
    121 
    122  #isAvailable() {
    123    return (
    124      this.inspector &&
    125      this.inspector.sidebar &&
    126      this.inspector.sidebar.getCurrentTabID() === "compatibilityview" &&
    127      this.inspector.selection &&
    128      this.inspector.selection.isConnected()
    129    );
    130  }
    131 
    132  #parseMarkup = () => {
    133    // Using a BrowserLoader for the inspector is currently blocked on performance regressions,
    134    // see Bug 1471853.
    135    throw new Error(
    136      "The inspector cannot use tags in ftl strings because it does not run in a BrowserLoader"
    137    );
    138  };
    139 
    140  #onChangeAdded = ({ selector }) => {
    141    if (!this.#isAvailable()) {
    142      // In order to update this panel if a change is added while hiding this panel.
    143      this.#isChangeAddedWhileHidden = true;
    144      return;
    145    }
    146 
    147    this.#isChangeAddedWhileHidden = false;
    148 
    149    // We need to debounce updating nodes since we might get CSS_CHANGE resources for
    150    // every typed character until fixing bug 1503036.
    151    if (this.#previousChangedSelector === selector) {
    152      clearTimeout(this.#updateNodesTimeoutId);
    153    }
    154    this.#previousChangedSelector = selector;
    155 
    156    this.#updateNodesTimeoutId = setTimeout(() => {
    157      // TODO: In case of keyframes changes, the selector given from changes actor is
    158      // keyframe-selector such as "from" and "100%", not selector for node. Thus,
    159      // we need to address this case.
    160      this.inspector.store.dispatch(updateNodes(selector));
    161    }, 500);
    162  };
    163 
    164  #onMarkupMutation = mutations => {
    165    // Since the mutations are throttled (in WalkerActor#getMutations), we might get the
    166    // same nodeFront multiple times.
    167    // Put them in a Set so we don't call updateNode more than once for a given front.
    168    const targetsWithAttributeMutation = new Set();
    169    const childListMutation = [];
    170 
    171    for (const mutation of mutations) {
    172      if (
    173        mutation.type === "attributes" &&
    174        (mutation.attributeName === "style" ||
    175          mutation.attributeName === "class")
    176      ) {
    177        targetsWithAttributeMutation.add(mutation.target);
    178      }
    179      if (mutation.type === "childList") {
    180        childListMutation.push(mutation);
    181      }
    182    }
    183 
    184    if (
    185      targetsWithAttributeMutation.size === 0 &&
    186      childListMutation.length === 0
    187    ) {
    188      return;
    189    }
    190 
    191    if (!this.#isAvailable()) {
    192      // In order to update this panel if a change is added while hiding this panel.
    193      this.#isChangeAddedWhileHidden = true;
    194      return;
    195    }
    196 
    197    this.#isChangeAddedWhileHidden = false;
    198 
    199    // Resource Watcher doesn't respond to programmatic inline CSS
    200    // change. This check can be removed once the following bug is resolved
    201    // https://bugzilla.mozilla.org/show_bug.cgi?id=1506160
    202    for (const target of targetsWithAttributeMutation) {
    203      this.inspector.store.dispatch(updateNode(target));
    204    }
    205 
    206    // Destroyed nodes can be cleaned up
    207    // once at the end if necessary
    208    let cleanupDestroyedNodes = false;
    209    for (const { removed, target } of childListMutation) {
    210      if (!removed.length) {
    211        this.inspector.store.dispatch(appendNode(target));
    212        continue;
    213      }
    214 
    215      const retainedNodes = removed.filter(node => node && !node.isDestroyed());
    216      cleanupDestroyedNodes =
    217        cleanupDestroyedNodes || retainedNodes.length !== removed.length;
    218 
    219      for (const retainedNode of retainedNodes) {
    220        this.inspector.store.dispatch(removeNode(retainedNode));
    221      }
    222    }
    223 
    224    if (cleanupDestroyedNodes) {
    225      this.inspector.store.dispatch(clearDestroyedNodes());
    226    }
    227  };
    228 
    229  #onPanelSelected = () => {
    230    const { selectedNode, topLevelTarget } =
    231      this.inspector.store.getState().compatibility;
    232 
    233    // Update if the selected node is changed or new change is added while the panel was hidden.
    234    if (
    235      this.inspector.selection.nodeFront !== selectedNode ||
    236      this.#isChangeAddedWhileHidden
    237    ) {
    238      this.#onSelectedNodeChanged();
    239    }
    240 
    241    // Update if the top target has changed or new change is added while the panel was hidden.
    242    if (
    243      this.inspector.toolbox.target !== topLevelTarget ||
    244      this.#isChangeAddedWhileHidden
    245    ) {
    246      this.#onTopLevelTargetChanged();
    247    }
    248 
    249    this.#isChangeAddedWhileHidden = false;
    250  };
    251 
    252  #onSelectedNodeChanged = () => {
    253    if (!this.#isAvailable()) {
    254      return;
    255    }
    256 
    257    this.inspector.store.dispatch(
    258      updateSelectedNode(this.inspector.selection.nodeFront)
    259    );
    260  };
    261 
    262  #onResourceAvailable = resources => {
    263    for (const resource of resources) {
    264      // Style changes applied inline directly to
    265      // the element and its changes are monitored by
    266      // #onMarkupMutation via markupmutation events.
    267      // Hence those changes can be ignored here
    268      if (resource.source?.type !== "element") {
    269        this.#onChangeAdded(resource);
    270      }
    271    }
    272  };
    273 
    274  #onTopLevelTargetChanged = () => {
    275    if (!this.#isAvailable()) {
    276      return;
    277    }
    278 
    279    this.inspector.store.dispatch(
    280      updateTopLevelTarget(this.inspector.toolbox.target)
    281    );
    282  };
    283 }
    284 
    285 module.exports = CompatibilityView;