tor-browser

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

ChangesView.js (8497B)


      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 loader.lazyRequireGetter(
     16  this,
     17  "ChangesContextMenu",
     18  "resource://devtools/client/inspector/changes/ChangesContextMenu.js"
     19 );
     20 loader.lazyRequireGetter(
     21  this,
     22  "clipboardHelper",
     23  "resource://devtools/shared/platform/clipboard.js"
     24 );
     25 
     26 const changesReducer = require("resource://devtools/client/inspector/changes/reducers/changes.js");
     27 const {
     28  getChangesStylesheet,
     29 } = require("resource://devtools/client/inspector/changes/selectors/changes.js");
     30 const {
     31  resetChanges,
     32  trackChange,
     33 } = require("resource://devtools/client/inspector/changes/actions/changes.js");
     34 
     35 const ChangesApp = createFactory(
     36  require("resource://devtools/client/inspector/changes/components/ChangesApp.js")
     37 );
     38 
     39 class ChangesView {
     40  constructor(inspector, window) {
     41    this.document = window.document;
     42    this.inspector = inspector;
     43    this.store = this.inspector.store;
     44    this.telemetry = this.inspector.telemetry;
     45    this.window = window;
     46 
     47    this.store.injectReducer("changes", changesReducer);
     48 
     49    this.onAddChange = this.onAddChange.bind(this);
     50    this.onContextMenu = this.onContextMenu.bind(this);
     51    this.onCopy = this.onCopy.bind(this);
     52    this.onCopyAllChanges = this.copyAllChanges.bind(this);
     53    this.onCopyDeclaration = this.copyDeclaration.bind(this);
     54    this.onCopyRule = this.copyRule.bind(this);
     55    this.onClearChanges = this.onClearChanges.bind(this);
     56    this.onSelectAll = this.onSelectAll.bind(this);
     57    this.onResourceAvailable = this.onResourceAvailable.bind(this);
     58 
     59    this.destroy = this.destroy.bind(this);
     60 
     61    this.init();
     62  }
     63 
     64  get contextMenu() {
     65    if (!this._contextMenu) {
     66      this._contextMenu = new ChangesContextMenu({
     67        onCopy: this.onCopy,
     68        onCopyAllChanges: this.onCopyAllChanges,
     69        onCopyDeclaration: this.onCopyDeclaration,
     70        onCopyRule: this.onCopyRule,
     71        onSelectAll: this.onSelectAll,
     72        toolboxDocument: this.inspector.toolbox.doc,
     73        window: this.window,
     74      });
     75    }
     76 
     77    return this._contextMenu;
     78  }
     79 
     80  get resourceCommand() {
     81    return this.inspector.commands.resourceCommand;
     82  }
     83 
     84  init() {
     85    const changesApp = ChangesApp({
     86      onContextMenu: this.onContextMenu,
     87      onCopyAllChanges: this.onCopyAllChanges,
     88      onCopyRule: this.onCopyRule,
     89    });
     90 
     91    // Expose the provider to let inspector.js use it in setupSidebar.
     92    this.provider = createElement(
     93      Provider,
     94      {
     95        id: "changesview",
     96        key: "changesview",
     97        store: this.store,
     98      },
     99      changesApp
    100    );
    101 
    102    this.watchResources();
    103  }
    104 
    105  async watchResources() {
    106    await this.resourceCommand.watchResources(
    107      [this.resourceCommand.TYPES.DOCUMENT_EVENT],
    108      {
    109        onAvailable: this.onResourceAvailable,
    110        // Ignore any DOCUMENT_EVENT resources that have occured in the past
    111        // and are cached by the resource command, otherwise the Changes panel will
    112        // react to them erroneously and interpret that the document is reloading *now*
    113        // which leads to clearing all stored changes.
    114        ignoreExistingResources: true,
    115      }
    116    );
    117 
    118    await this.resourceCommand.watchResources(
    119      [this.resourceCommand.TYPES.CSS_CHANGE],
    120      { onAvailable: this.onResourceAvailable }
    121    );
    122  }
    123 
    124  onResourceAvailable(resources) {
    125    for (const resource of resources) {
    126      if (resource.resourceType === this.resourceCommand.TYPES.CSS_CHANGE) {
    127        this.onAddChange(resource);
    128        continue;
    129      }
    130 
    131      if (resource.name === "dom-loading" && resource.targetFront.isTopLevel) {
    132        // will-navigate doesn't work when we navigate to a new process,
    133        // and for now, onTargetAvailable/onTargetDestroyed doesn't fire on navigation and
    134        // only when navigating to another process.
    135        // So we fallback on DOCUMENT_EVENTS to be notified when we navigate. When we
    136        // navigate within the same process as well as when we navigate to a new process.
    137        // (We would probably revisit that in bug 1632141)
    138        this.onClearChanges();
    139      }
    140    }
    141  }
    142 
    143  /**
    144   * Handler for the "Copy All Changes" button. Simple wrapper that just calls
    145   * |this.copyChanges()| with no filters in order to trigger default operation.
    146   */
    147  copyAllChanges() {
    148    this.copyChanges();
    149  }
    150 
    151  /**
    152   * Handler for the "Copy Changes" option from the context menu.
    153   * Builds a CSS text with the aggregated changes and copies it to the clipboard.
    154   *
    155   * Optional rule and source ids can be used to filter the scope of the operation:
    156   * - if both a rule id and source id are provided, copy only the changes to the
    157   * matching rule within the matching source.
    158   * - if only a source id is provided, copy the changes to all rules within the
    159   * matching source.
    160   * - if neither rule id nor source id are provided, copy the changes too all rules
    161   * within all sources.
    162   *
    163   * @param {string | null} ruleId
    164   *        Optional rule id.
    165   * @param {string | null} sourceId
    166   *        Optional source id.
    167   */
    168  copyChanges(ruleId, sourceId) {
    169    const state = this.store.getState().changes || {};
    170    const filter = {};
    171    if (ruleId) {
    172      filter.ruleIds = [ruleId];
    173    }
    174    if (sourceId) {
    175      filter.sourceIds = [sourceId];
    176    }
    177 
    178    const text = getChangesStylesheet(state, filter);
    179    clipboardHelper.copyString(text);
    180  }
    181 
    182  /**
    183   * Handler for the "Copy Declaration" option from the context menu.
    184   * Builds a CSS declaration string with the property name and value, and copies it
    185   * to the clipboard. The declaration is commented out if it is marked as removed.
    186   *
    187   * @param {DOMElement} element
    188   *        Host element of a CSS declaration rendered the Changes panel.
    189   */
    190  copyDeclaration(element) {
    191    const name = element.querySelector(
    192      ".changes__declaration-name"
    193    ).textContent;
    194    const value = element.querySelector(
    195      ".changes__declaration-value"
    196    ).textContent;
    197    const isRemoved = element.classList.contains("diff-remove");
    198    const text = isRemoved ? `/* ${name}: ${value}; */` : `${name}: ${value};`;
    199    clipboardHelper.copyString(text);
    200  }
    201 
    202  /**
    203   * Handler for the "Copy Rule" option from the context menu and "Copy Rule" button.
    204   * Gets the full content of the target CSS rule (including any changes applied)
    205   * and copies it to the clipboard.
    206   *
    207   * @param {string} ruleId
    208   *        Rule id of the target CSS rule.
    209   */
    210  async copyRule(ruleId) {
    211    const inspectorFronts = await this.inspector.getAllInspectorFronts();
    212 
    213    for (const inspectorFront of inspectorFronts) {
    214      const rule = await inspectorFront.pageStyle.getRule(ruleId);
    215 
    216      if (rule) {
    217        const text = await rule.getRuleText();
    218        clipboardHelper.copyString(text);
    219        break;
    220      }
    221    }
    222  }
    223 
    224  /**
    225   * Handler for the "Copy" option from the context menu.
    226   * Copies the current text selection to the clipboard.
    227   */
    228  onCopy() {
    229    clipboardHelper.copyString(this.window.getSelection().toString());
    230  }
    231 
    232  onAddChange(change) {
    233    // Turn data into a suitable change to send to the store.
    234    this.store.dispatch(trackChange(change));
    235  }
    236 
    237  onClearChanges() {
    238    this.store.dispatch(resetChanges());
    239  }
    240 
    241  /**
    242   * Select all text.
    243   */
    244  onSelectAll() {
    245    const selection = this.window.getSelection();
    246    selection.selectAllChildren(
    247      this.document.getElementById("sidebar-panel-changes")
    248    );
    249  }
    250 
    251  /**
    252   * Event handler for the "contextmenu" event fired when the context menu is requested.
    253   *
    254   * @param {Event} e
    255   */
    256  onContextMenu(e) {
    257    this.contextMenu.show(e);
    258  }
    259 
    260  /**
    261   * Destruction function called when the inspector is destroyed.
    262   */
    263  destroy() {
    264    this.resourceCommand.unwatchResources(
    265      [
    266        this.resourceCommand.TYPES.CSS_CHANGE,
    267        this.resourceCommand.TYPES.DOCUMENT_EVENT,
    268      ],
    269      { onAvailable: this.onResourceAvailable }
    270    );
    271 
    272    this.document = null;
    273    this.inspector = null;
    274    this.store = null;
    275 
    276    if (this._contextMenu) {
    277      this._contextMenu.destroy();
    278      this._contextMenu = null;
    279    }
    280  }
    281 }
    282 
    283 module.exports = ChangesView;