tor-browser

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

ChangesApp.js (7236B)


      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  PureComponent,
     10 } = require("resource://devtools/client/shared/vendor/react.mjs");
     11 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
     12 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
     13 const {
     14  connect,
     15 } = require("resource://devtools/client/shared/vendor/react-redux.js");
     16 
     17 const CSSDeclaration = createFactory(
     18  require("resource://devtools/client/inspector/changes/components/CSSDeclaration.js")
     19 );
     20 const {
     21  getChangesTree,
     22 } = require("resource://devtools/client/inspector/changes/selectors/changes.js");
     23 const {
     24  getSourceForDisplay,
     25 } = require("resource://devtools/client/inspector/changes/utils/changes-utils.js");
     26 const {
     27  getStr,
     28 } = require("resource://devtools/client/inspector/changes/utils/l10n.js");
     29 
     30 class ChangesApp extends PureComponent {
     31  static get propTypes() {
     32    return {
     33      // Nested CSS rule tree structure of CSS changes grouped by source (stylesheet)
     34      changesTree: PropTypes.object.isRequired,
     35      // Event handler for "contextmenu" event
     36      onContextMenu: PropTypes.func.isRequired,
     37      // Event handler for click on "Copy All Changes" button
     38      onCopyAllChanges: PropTypes.func.isRequired,
     39      // Event handler for click on "Copy Rule" button
     40      onCopyRule: PropTypes.func.isRequired,
     41    };
     42  }
     43 
     44  constructor(props) {
     45    super(props);
     46  }
     47 
     48  renderCopyAllChangesButton() {
     49    const button = dom.button(
     50      {
     51        className: "changes__copy-all-changes-button",
     52        onClick: e => {
     53          e.stopPropagation();
     54          this.props.onCopyAllChanges();
     55        },
     56        title: getStr("changes.contextmenu.copyAllChangesDescription"),
     57      },
     58      getStr("changes.contextmenu.copyAllChanges")
     59    );
     60 
     61    return dom.div({ className: "changes__toolbar" }, button);
     62  }
     63 
     64  renderCopyButton(ruleId) {
     65    return dom.button(
     66      {
     67        className: "changes__copy-rule-button",
     68        onClick: e => {
     69          e.stopPropagation();
     70          this.props.onCopyRule(ruleId);
     71        },
     72        title: getStr("changes.contextmenu.copyRuleDescription"),
     73      },
     74      getStr("changes.contextmenu.copyRule")
     75    );
     76  }
     77 
     78  renderDeclarations(remove = [], add = []) {
     79    const removals = remove
     80      // Sorting changed declarations in the order they appear in the Rules view.
     81      .sort((a, b) => a.index > b.index)
     82      .map(({ property, value, index }) => {
     83        return CSSDeclaration({
     84          key: "remove-" + property + index,
     85          className: "level diff-remove",
     86          property,
     87          value,
     88        });
     89      });
     90 
     91    const additions = add
     92      // Sorting changed declarations in the order they appear in the Rules view.
     93      .sort((a, b) => a.index > b.index)
     94      .map(({ property, value, index }) => {
     95        return CSSDeclaration({
     96          key: "add-" + property + index,
     97          className: "level diff-add",
     98          property,
     99          value,
    100        });
    101      });
    102 
    103    return [removals, additions];
    104  }
    105 
    106  renderRule(ruleId, rule, level = 0) {
    107    const diffClass = rule.isNew ? "diff-add" : "";
    108    return dom.div(
    109      {
    110        key: ruleId,
    111        className: "changes__rule devtools-monospace",
    112        "data-rule-id": ruleId,
    113        style: {
    114          "--diff-level": level,
    115        },
    116      },
    117      this.renderSelectors(rule.selectors, rule.isNew),
    118      this.renderCopyButton(ruleId),
    119      // Render any nested child rules if they exist.
    120      rule.children.map(childRule => {
    121        return this.renderRule(childRule.ruleId, childRule, level + 1);
    122      }),
    123      // Render any changed CSS declarations.
    124      this.renderDeclarations(rule.remove, rule.add),
    125      // Render the closing bracket with a diff marker if necessary.
    126      dom.div({ className: `level ${diffClass}` }, "}")
    127    );
    128  }
    129 
    130  /**
    131   * Return an array of React elements for the rule's selector.
    132   *
    133   * @param  {Array} selectors
    134   *         List of strings as versions of this rule's selector over time.
    135   * @param  {boolean} isNewRule
    136   *         Whether the rule was created at runtime.
    137   * @return {Array}
    138   */
    139  renderSelectors(selectors, isNewRule) {
    140    const selectorDiffClassMap = new Map();
    141 
    142    // The selectors array has just one item if it hasn't changed. Render it as-is.
    143    // If the rule was created at runtime, mark the single selector as added.
    144    // If it has two or more items, the first item was the original selector (mark as
    145    // removed) and the last item is the current selector (mark as added).
    146    if (selectors.length === 1) {
    147      selectorDiffClassMap.set(selectors[0], isNewRule ? "diff-add" : "");
    148    } else if (selectors.length >= 2) {
    149      selectorDiffClassMap.set(selectors[0], "diff-remove");
    150      selectorDiffClassMap.set(selectors[selectors.length - 1], "diff-add");
    151    }
    152 
    153    const elements = [];
    154 
    155    for (const [selector, diffClass] of selectorDiffClassMap) {
    156      elements.push(
    157        dom.div(
    158          {
    159            key: selector,
    160            className: `level changes__selector ${diffClass}`,
    161            title: selector,
    162          },
    163          selector,
    164          dom.span({}, " {")
    165        )
    166      );
    167    }
    168 
    169    return elements;
    170  }
    171 
    172  renderDiff(changes = {}) {
    173    // Render groups of style sources: stylesheets and element style attributes.
    174    return Object.entries(changes).map(([sourceId, source]) => {
    175      const path = getSourceForDisplay(source);
    176      const { href, rules, isFramed } = source;
    177 
    178      return dom.div(
    179        {
    180          key: sourceId,
    181          "data-source-id": sourceId,
    182          className: "source",
    183        },
    184        dom.div(
    185          {
    186            className: "href",
    187            title: href,
    188          },
    189          dom.span({}, path),
    190          isFramed && this.renderFrameBadge(href)
    191        ),
    192        // Render changed rules within this source.
    193        Object.entries(rules).map(([ruleId, rule]) => {
    194          return this.renderRule(ruleId, rule);
    195        })
    196      );
    197    });
    198  }
    199 
    200  renderFrameBadge(href = "") {
    201    return dom.span(
    202      {
    203        className: "inspector-badge",
    204        title: href,
    205      },
    206      getStr("changes.iframeLabel")
    207    );
    208  }
    209 
    210  renderEmptyState() {
    211    return dom.div(
    212      { className: "devtools-sidepanel-no-result" },
    213      dom.p({}, getStr("changes.noChanges")),
    214      dom.p({}, getStr("changes.noChangesDescription"))
    215    );
    216  }
    217 
    218  render() {
    219    const hasChanges = !!Object.keys(this.props.changesTree).length;
    220    return dom.div(
    221      {
    222        className: "theme-sidebar inspector-tabpanel",
    223        id: "sidebar-panel-changes",
    224        role: "document",
    225        tabIndex: "0",
    226        onContextMenu: this.props.onContextMenu,
    227      },
    228      !hasChanges && this.renderEmptyState(),
    229      hasChanges && this.renderCopyAllChangesButton(),
    230      hasChanges && this.renderDiff(this.props.changesTree)
    231    );
    232  }
    233 }
    234 
    235 const mapStateToProps = state => {
    236  return {
    237    changesTree: getChangesTree(state.changes),
    238  };
    239 };
    240 
    241 module.exports = connect(mapStateToProps)(ChangesApp);