tor-browser

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

SourcesTree.js (12927B)


      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 // Dependencies
      6 import React, {
      7  Component,
      8  Fragment,
      9 } from "devtools/client/shared/vendor/react";
     10 import {
     11  div,
     12  button,
     13  span,
     14  footer,
     15 } from "devtools/client/shared/vendor/react-dom-factories";
     16 import PropTypes from "devtools/client/shared/vendor/react-prop-types";
     17 import { connect } from "devtools/client/shared/vendor/react-redux";
     18 const MenuButton = require("resource://devtools/client/shared/components/menu/MenuButton.js");
     19 const MenuItem = require("resource://devtools/client/shared/components/menu/MenuItem.js");
     20 const MenuList = require("resource://devtools/client/shared/components/menu/MenuList.js");
     21 import { prefs } from "../../utils/prefs";
     22 
     23 // Selectors
     24 import {
     25  getMainThreadHost,
     26  getExpandedState,
     27  getProjectDirectoryRoot,
     28  getProjectDirectoryRootName,
     29  getProjectDirectoryRootFullName,
     30  getSourcesTreeSources,
     31  getFocusedSourceItem,
     32  getHideIgnoredSources,
     33 } from "../../selectors/index";
     34 
     35 // Actions
     36 import actions from "../../actions/index";
     37 
     38 // Components
     39 import SourcesTreeItem from "./SourcesTreeItem";
     40 import DebuggerImage from "../shared/DebuggerImage";
     41 
     42 const classnames = require("resource://devtools/client/shared/classnames.js");
     43 const Tree = require("resource://devtools/client/shared/components/Tree.js");
     44 
     45 function shouldAutoExpand(item, mainThreadHost) {
     46  // There is only one case where we want to force auto expand,
     47  // when we are on the group of the page's domain.
     48  return item.type == "group" && item.groupName === mainThreadHost;
     49 }
     50 
     51 class SourcesTree extends Component {
     52  constructor(props) {
     53    super(props);
     54 
     55    this.state = {
     56      hasOverflow: undefined,
     57    };
     58 
     59    // Monitor resize to check if the source tree shows a scrollbar.
     60    this.onResize = this.onResize.bind(this);
     61    this.resizeObserver = new ResizeObserver(this.onResize);
     62  }
     63 
     64  static get propTypes() {
     65    return {
     66      mainThreadHost: PropTypes.string,
     67      expanded: PropTypes.object.isRequired,
     68      focusItem: PropTypes.func.isRequired,
     69      focused: PropTypes.object,
     70      projectRoot: PropTypes.string.isRequired,
     71      selectSource: PropTypes.func.isRequired,
     72      setExpandedState: PropTypes.func.isRequired,
     73      rootItems: PropTypes.array.isRequired,
     74      clearProjectDirectoryRoot: PropTypes.func.isRequired,
     75      projectRootName: PropTypes.string.isRequired,
     76      setHideOrShowIgnoredSources: PropTypes.func.isRequired,
     77      hideIgnoredSources: PropTypes.bool.isRequired,
     78    };
     79  }
     80 
     81  onResize() {
     82    const tree = this.refs.tree;
     83    if (!tree) {
     84      return;
     85    }
     86 
     87    // "treeRef" is created via createRef() in the Tree component.
     88    const treeEl = tree.treeRef.current;
     89    const hasOverflow = treeEl.scrollHeight > treeEl.clientHeight;
     90    if (hasOverflow !== this.state.hasOverflow) {
     91      this.setState({ hasOverflow });
     92    }
     93  }
     94 
     95  componentDidUpdate() {
     96    this.onResize();
     97  }
     98 
     99  componentDidMount() {
    100    this.resizeObserver.observe(this.refs.pane);
    101    this.onResize();
    102  }
    103 
    104  componentWillUnmount() {
    105    this.resizeObserver.disconnect();
    106  }
    107 
    108  selectSourceItem = item => {
    109    // Note that when the source is pretty printed, `item.source` still refers to the minified source.
    110    // `mayBeSelectMappedSource` function within selectSource/selectLocation action will handle this edgecase
    111    // and ensure selecting the pretty printed source, if relevant.
    112    this.props.selectSource(item.source, item.sourceActor);
    113  };
    114 
    115  onFocus = item => {
    116    this.props.focusItem(item);
    117  };
    118 
    119  onActivate = item => {
    120    if (item.type == "source") {
    121      this.selectSourceItem(item);
    122    }
    123  };
    124 
    125  onExpand = (item, shouldIncludeChildren) => {
    126    this.setExpanded(item, true, shouldIncludeChildren);
    127  };
    128 
    129  onCollapse = (item, shouldIncludeChildren) => {
    130    this.setExpanded(item, false, shouldIncludeChildren);
    131  };
    132 
    133  setExpanded = (item, isExpanded, shouldIncludeChildren) => {
    134    // Note that setExpandedState relies on us to clone this Set
    135    // which is going to be store as-is in the reducer.
    136    const expanded = new Set(this.props.expanded);
    137 
    138    let changed = false;
    139    const expandItem = i => {
    140      const key = this.getKey(i);
    141      if (isExpanded) {
    142        changed |= !expanded.has(key);
    143        expanded.add(key);
    144      } else {
    145        changed |= expanded.has(key);
    146        expanded.delete(key);
    147      }
    148    };
    149    expandItem(item);
    150 
    151    if (shouldIncludeChildren) {
    152      let parents = [item];
    153      while (parents.length) {
    154        const children = [];
    155        for (const parent of parents) {
    156          for (const child of this.getChildren(parent)) {
    157            expandItem(child);
    158            children.push(child);
    159          }
    160        }
    161        parents = children;
    162      }
    163    }
    164    if (changed) {
    165      this.props.setExpandedState(expanded);
    166    }
    167  };
    168 
    169  isEmpty() {
    170    return !this.getRoots().length;
    171  }
    172 
    173  renderEmptyElement(message) {
    174    return div(
    175      {
    176        key: "empty",
    177        className: "no-sources-message",
    178      },
    179      message
    180    );
    181  }
    182 
    183  getRoots = () => {
    184    return this.props.rootItems;
    185  };
    186 
    187  getKey = item => {
    188    // As this is used as React key in Tree component,
    189    // we need to update the key when switching to a new project root
    190    // otherwise these items won't be updated and will have a buggy padding start.
    191    const { projectRoot } = this.props;
    192    if (projectRoot) {
    193      return projectRoot + item.uniquePath;
    194    }
    195    return item.uniquePath;
    196  };
    197 
    198  getChildren = item => {
    199    // This is the precial magic that coalesce "empty" folders,
    200    // i.e folders which have only one sub-folder as children.
    201    function skipEmptyDirectories(directory) {
    202      if (directory.type != "directory") {
    203        return directory;
    204      }
    205      if (
    206        directory.children.length == 1 &&
    207        directory.children[0].type == "directory"
    208      ) {
    209        return skipEmptyDirectories(directory.children[0]);
    210      }
    211      return directory;
    212    }
    213    if (item.type == "thread") {
    214      return item.children;
    215    } else if (item.type == "group" || item.type == "directory") {
    216      return item.children.map(skipEmptyDirectories);
    217    }
    218    return [];
    219  };
    220 
    221  getParent = item => {
    222    if (item.type == "thread") {
    223      return null;
    224    }
    225    const { rootItems } = this.props;
    226    // This is the second magic which skip empty folders
    227    // (See getChildren comment)
    228    function skipEmptyDirectories(directory) {
    229      if (
    230        directory.type == "group" ||
    231        directory.type == "thread" ||
    232        rootItems.includes(directory)
    233      ) {
    234        return directory;
    235      }
    236      if (
    237        directory.children.length == 1 &&
    238        directory.children[0].type == "directory"
    239      ) {
    240        return skipEmptyDirectories(directory.parent);
    241      }
    242      return directory;
    243    }
    244    return skipEmptyDirectories(item.parent);
    245  };
    246 
    247  renderProjectRootHeader() {
    248    const { projectRootName, projectRootFullName } = this.props;
    249 
    250    if (!projectRootName) {
    251      return null;
    252    }
    253    return div(
    254      {
    255        key: "root",
    256        className: "sources-clear-root-container",
    257      },
    258      button(
    259        {
    260          className: "sources-clear-root",
    261          onClick: () => this.props.clearProjectDirectoryRoot(),
    262          title: L10N.getFormatStr("removeDirectoryRoot.label"),
    263        },
    264        React.createElement(DebuggerImage, {
    265          name: "back",
    266        })
    267      ),
    268      div({ className: "devtools-separator" }),
    269      span(
    270        {
    271          className: "sources-clear-root-label",
    272          title: L10N.getFormatStr(
    273            "directoryRoot.tooltip.label",
    274            projectRootFullName || projectRootName
    275          ),
    276        },
    277        projectRootName
    278      )
    279    );
    280  }
    281 
    282  renderItem = (item, depth, focused, arrow, expanded) => {
    283    const { mainThreadHost } = this.props;
    284    return React.createElement(SourcesTreeItem, {
    285      arrow,
    286      item,
    287      depth,
    288      focused,
    289      autoExpand: shouldAutoExpand(item, mainThreadHost),
    290      expanded,
    291      focusItem: this.onFocus,
    292      selectSourceItem: this.selectSourceItem,
    293      setExpanded: this.setExpanded,
    294      getParent: this.getParent,
    295    });
    296  };
    297 
    298  renderTree() {
    299    const { expanded, focused } = this.props;
    300 
    301    const treeProps = {
    302      autoExpandAll: false,
    303      autoExpandDepth: 1,
    304      expanded,
    305      focused,
    306      getChildren: this.getChildren,
    307      getParent: this.getParent,
    308      getKey: this.getKey,
    309      getRoots: this.getRoots,
    310      onCollapse: this.onCollapse,
    311      onExpand: this.onExpand,
    312      onFocus: this.onFocus,
    313      isExpanded: item => {
    314        return this.props.expanded.has(this.getKey(item));
    315      },
    316      onActivate: this.onActivate,
    317      ref: "tree",
    318      renderItem: this.renderItem,
    319      preventBlur: true,
    320    };
    321    return React.createElement(Tree, treeProps);
    322  }
    323 
    324  renderPane(child) {
    325    const { projectRoot } = this.props;
    326    return div(
    327      {
    328        key: "pane",
    329        className: classnames("sources-pane", {
    330          "sources-list-custom-root": !!projectRoot,
    331        }),
    332      },
    333      child
    334    );
    335  }
    336 
    337  renderFooter() {
    338    if (this.props.hideIgnoredSources) {
    339      return footer(
    340        {
    341          className: "source-list-footer",
    342        },
    343        L10N.getStr("ignoredSourcesHidden"),
    344        button(
    345          {
    346            className: "devtools-togglebutton",
    347            onClick: () => this.props.setHideOrShowIgnoredSources(false),
    348            title: L10N.getStr("showIgnoredSources.tooltip.label"),
    349          },
    350          L10N.getStr("showIgnoredSources")
    351        )
    352      );
    353    }
    354    return null;
    355  }
    356 
    357  renderSettingsButton() {
    358    const { toolboxDoc } = this.context;
    359    return React.createElement(
    360      MenuButton,
    361      {
    362        menuId: "sources-tree-settings-menu-button",
    363        toolboxDoc,
    364        className:
    365          "devtools-button command-bar-button debugger-settings-menu-button",
    366        title: L10N.getStr("sources-settings.button.label"),
    367        "aria-label": L10N.getStr("sources-settings.button.label"),
    368      },
    369      () => this.renderSettingsMenuItems()
    370    );
    371  }
    372 
    373  renderSettingsMenuItems() {
    374    return React.createElement(
    375      MenuList,
    376      {
    377        id: "sources-tree-settings-menu-list",
    378      },
    379      React.createElement(MenuItem, {
    380        key: "debugger-settings-menu-item-hide-ignored-sources",
    381        className: "menu-item debugger-settings-menu-item-hide-ignored-sources",
    382        checked: prefs.hideIgnoredSources,
    383        label: L10N.getStr("settings.hideIgnoredSources.label"),
    384        tooltip: L10N.getStr("settings.hideIgnoredSources.tooltip"),
    385        onClick: () =>
    386          this.props.setHideOrShowIgnoredSources(!prefs.hideIgnoredSources),
    387      }),
    388      React.createElement(MenuItem, {
    389        key: "debugger-settings-menu-item-show-content-scripts",
    390        className: "menu-item debugger-settings-menu-item-show-content-scripts",
    391        checked: prefs.showContentScripts,
    392        label: L10N.getStr("sources-settings.showContentScripts.label"),
    393        tooltip: L10N.getStr("sources-settings.showContentScripts.tooltip"),
    394        onClick: () =>
    395          this.props.setShowContentScripts(!prefs.showContentScripts),
    396      })
    397    );
    398  }
    399 
    400  render() {
    401    const { projectRoot } = this.props;
    402    return div(
    403      {
    404        key: "pane",
    405        ref: "pane",
    406        className: classnames("sources-list", {
    407          "sources-list-custom-root": !!projectRoot,
    408          "sources-list-has-overflow": this.state.hasOverflow,
    409        }),
    410      },
    411      this.renderSettingsButton(),
    412      this.renderProjectRootHeader(),
    413      this.isEmpty()
    414        ? this.renderEmptyElement(
    415            L10N.getStr(
    416              projectRoot ? "noSourcesInDirectoryRootText" : "noSourcesText"
    417            )
    418          )
    419        : React.createElement(
    420            Fragment,
    421            null,
    422            this.renderTree(),
    423            this.renderFooter()
    424          )
    425    );
    426  }
    427 }
    428 
    429 SourcesTree.contextTypes = {
    430  toolboxDoc: PropTypes.object,
    431 };
    432 
    433 const mapStateToProps = state => {
    434  return {
    435    mainThreadHost: getMainThreadHost(state),
    436    expanded: getExpandedState(state),
    437    focused: getFocusedSourceItem(state),
    438    projectRoot: getProjectDirectoryRoot(state),
    439    rootItems: getSourcesTreeSources(state),
    440    projectRootName: getProjectDirectoryRootName(state),
    441    projectRootFullName: getProjectDirectoryRootFullName(state),
    442    hideIgnoredSources: getHideIgnoredSources(state),
    443  };
    444 };
    445 
    446 export default connect(mapStateToProps, {
    447  selectSource: actions.selectSource,
    448  setExpandedState: actions.setExpandedState,
    449  focusItem: actions.focusItem,
    450  clearProjectDirectoryRoot: actions.clearProjectDirectoryRoot,
    451  setHideOrShowIgnoredSources: actions.setHideOrShowIgnoredSources,
    452  setShowContentScripts: actions.setShowContentScripts,
    453 })(SourcesTree);