tor-browser

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

Footer.js (14431B)


      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 import React, { PureComponent } from "devtools/client/shared/vendor/react";
      6 import {
      7  div,
      8  button,
      9  span,
     10  hr,
     11 } from "devtools/client/shared/vendor/react-dom-factories";
     12 import PropTypes from "devtools/client/shared/vendor/react-prop-types";
     13 import { connect } from "devtools/client/shared/vendor/react-redux";
     14 import actions from "../../actions/index";
     15 import {
     16  getSelectedSource,
     17  getSelectedLocation,
     18  getSelectedSourceTextContent,
     19  getPrettySource,
     20  getPaneCollapse,
     21  isSourceBlackBoxed,
     22  canPrettyPrintSource,
     23  getPrettyPrintMessage,
     24  isSourceOnSourceMapIgnoreList,
     25  isSourceMapIgnoreListEnabled,
     26  getSelectedMappedSource,
     27  getSourceMapErrorForSourceActor,
     28  areSourceMapsEnabled,
     29  getShouldSelectOriginalLocation,
     30  isSourceActorWithSourceMap,
     31  getSourceMapResolvedURL,
     32  isSelectedMappedSourceLoading,
     33 } from "../../selectors/index";
     34 
     35 import { shouldBlackbox } from "../../utils/source";
     36 
     37 import { PaneToggleButton } from "../shared/Button/index";
     38 import DebuggerImage from "../shared/DebuggerImage";
     39 
     40 const classnames = require("resource://devtools/client/shared/classnames.js");
     41 const MenuButton = require("resource://devtools/client/shared/components/menu/MenuButton.js");
     42 const MenuItem = require("resource://devtools/client/shared/components/menu/MenuItem.js");
     43 const MenuList = require("resource://devtools/client/shared/components/menu/MenuList.js");
     44 
     45 class SourceFooter extends PureComponent {
     46  static get propTypes() {
     47    return {
     48      canPrettyPrint: PropTypes.bool.isRequired,
     49      prettyPrintMessage: PropTypes.string,
     50      endPanelCollapsed: PropTypes.bool.isRequired,
     51      horizontal: PropTypes.bool.isRequired,
     52      jumpToMappedLocation: PropTypes.func.isRequired,
     53      mappedSource: PropTypes.object,
     54      selectedSource: PropTypes.object,
     55      selectedLocation: PropTypes.object,
     56      isSelectedSourceBlackBoxed: PropTypes.bool,
     57      sourceLoaded: PropTypes.bool.isRequired,
     58      toggleBlackBox: PropTypes.func.isRequired,
     59      togglePaneCollapse: PropTypes.func.isRequired,
     60      prettyPrintAndSelectSource: PropTypes.func.isRequired,
     61      isSourceOnIgnoreList: PropTypes.bool.isRequired,
     62    };
     63  }
     64 
     65  prettyPrintButton() {
     66    const {
     67      selectedSource,
     68      canPrettyPrint,
     69      prettyPrintMessage,
     70      prettyPrintAndSelectSource,
     71      removePrettyPrintedSource,
     72      sourceLoaded,
     73    } = this.props;
     74 
     75    if (!selectedSource) {
     76      return null;
     77    }
     78 
     79    if (!sourceLoaded && selectedSource.isPrettyPrinted) {
     80      return div(
     81        {
     82          className: "action",
     83          key: "pretty-loader",
     84        },
     85        React.createElement(DebuggerImage, {
     86          name: "loader",
     87          className: "spin",
     88        })
     89      );
     90    }
     91 
     92    const type = "prettyPrint";
     93    return button(
     94      {
     95        onClick: () => {
     96          if (selectedSource.isPrettyPrinted) {
     97            removePrettyPrintedSource(selectedSource);
     98            return;
     99          }
    100          if (!canPrettyPrint) {
    101            return;
    102          }
    103          prettyPrintAndSelectSource(selectedSource);
    104        },
    105        className: classnames("action", type, {
    106          pretty: selectedSource.isPrettyPrinted,
    107        }),
    108        key: type,
    109        title: prettyPrintMessage,
    110        "aria-label": prettyPrintMessage,
    111        disabled: !canPrettyPrint && !selectedSource.isPrettyPrinted,
    112      },
    113      React.createElement(DebuggerImage, {
    114        name: type,
    115      })
    116    );
    117  }
    118 
    119  blackBoxButton() {
    120    const {
    121      selectedSource,
    122      isSelectedSourceBlackBoxed,
    123      toggleBlackBox,
    124      sourceLoaded,
    125      isSourceOnIgnoreList,
    126    } = this.props;
    127 
    128    if (!selectedSource || !shouldBlackbox(selectedSource)) {
    129      return null;
    130    }
    131 
    132    let tooltip = isSelectedSourceBlackBoxed
    133      ? L10N.getStr("sourceFooter.unignore")
    134      : L10N.getStr("sourceFooter.ignore");
    135 
    136    if (isSourceOnIgnoreList) {
    137      tooltip = L10N.getStr("sourceFooter.ignoreList");
    138    }
    139 
    140    const type = "black-box";
    141    return button(
    142      {
    143        onClick: () => toggleBlackBox(selectedSource),
    144        className: classnames("action", type, {
    145          active: sourceLoaded,
    146          blackboxed: isSelectedSourceBlackBoxed || isSourceOnIgnoreList,
    147        }),
    148        key: type,
    149        title: tooltip,
    150        "aria-label": tooltip,
    151        disabled: isSourceOnIgnoreList,
    152      },
    153      React.createElement(DebuggerImage, {
    154        name: "blackBox",
    155      })
    156    );
    157  }
    158 
    159  renderToggleButton() {
    160    if (this.props.horizontal) {
    161      return null;
    162    }
    163    return React.createElement(PaneToggleButton, {
    164      key: "toggle",
    165      collapsed: this.props.endPanelCollapsed,
    166      horizontal: this.props.horizontal,
    167      handleClick: this.props.togglePaneCollapse,
    168      position: "end",
    169    });
    170  }
    171 
    172  renderCommands() {
    173    const commands = [
    174      this.blackBoxButton(),
    175      this.prettyPrintButton(),
    176      this.renderSourceMapButton(),
    177    ].filter(Boolean);
    178 
    179    return commands.length
    180      ? div(
    181          {
    182            className: "commands",
    183          },
    184          commands
    185        )
    186      : null;
    187  }
    188 
    189  renderMappedSource() {
    190    const { mappedSource, jumpToMappedLocation, selectedLocation } = this.props;
    191 
    192    if (!mappedSource) {
    193      return null;
    194    }
    195 
    196    const tooltip = L10N.getFormatStr(
    197      mappedSource.isOriginal
    198        ? "sourceFooter.mappedGeneratedSource.tooltip"
    199        : "sourceFooter.mappedOriginalSource.tooltip",
    200      mappedSource.url
    201    );
    202    const label = L10N.getFormatStr(
    203      mappedSource.isOriginal
    204        ? "sourceFooter.mappedOriginalSource.title"
    205        : "sourceFooter.mappedGeneratedSource.title",
    206      mappedSource.shortName
    207    );
    208    return button(
    209      {
    210        className: "mapped-source",
    211        onClick: () => jumpToMappedLocation(selectedLocation),
    212        title: tooltip,
    213      },
    214      span(null, label)
    215    );
    216  }
    217 
    218  renderCursorPosition() {
    219    // When we open a new source, there is no particular location selected and the line will be set to zero or falsy
    220    if (!this.props.selectedLocation || !this.props.selectedLocation.line) {
    221      return null;
    222    }
    223 
    224    // Note that line is 1-based while column is 0-based.
    225    const { line, column } = this.props.selectedLocation;
    226 
    227    const text = L10N.getFormatStr(
    228      "sourceFooter.currentCursorPosition",
    229      line,
    230      column + 1
    231    );
    232    const title = L10N.getFormatStr(
    233      "sourceFooter.currentCursorPosition.tooltip",
    234      line,
    235      column + 1
    236    );
    237    return div(
    238      {
    239        className: "cursor-position",
    240        title,
    241      },
    242      text
    243    );
    244  }
    245 
    246  getSourceMapLabel() {
    247    if (!this.props.selectedLocation) {
    248      return undefined;
    249    }
    250    if (!this.props.areSourceMapsEnabled) {
    251      return L10N.getStr("sourceFooter.sourceMapButton.disabled");
    252    }
    253    if (this.props.sourceMapError) {
    254      return undefined;
    255    }
    256    if (!this.props.isSourceActorWithSourceMap) {
    257      return L10N.getStr("sourceFooter.sourceMapButton.sourceNotMapped");
    258    }
    259    if (
    260      this.props.selectedLocation.source.isOriginal &&
    261      !this.props.selectedLocation.source.isPrettyPrinted
    262    ) {
    263      return L10N.getStr("sourceFooter.sourceMapButton.isOriginalSource");
    264    }
    265    return L10N.getStr("sourceFooter.sourceMapButton.isBundleSource");
    266  }
    267 
    268  getSourceMapTitle() {
    269    if (this.props.sourceMapError) {
    270      return L10N.getFormatStr(
    271        "sourceFooter.sourceMapButton.errorTitle",
    272        this.props.sourceMapError
    273      );
    274    }
    275    if (this.props.isSourceMapLoading) {
    276      return L10N.getStr("sourceFooter.sourceMapButton.loadingTitle");
    277    }
    278    return L10N.getStr("sourceFooter.sourceMapButton.title");
    279  }
    280 
    281  renderSourceMapButton() {
    282    const { toolboxDoc } = this.context;
    283 
    284    const selectedSource = this.props.selectedLocation?.source;
    285    return React.createElement(
    286      MenuButton,
    287      {
    288        menuId: "debugger-source-map-button",
    289        key: "debugger-source-map-button",
    290        toolboxDoc,
    291        className: classnames("devtools-button", "debugger-source-map-button", {
    292          error: !!this.props.sourceMapError,
    293          loading: this.props.isSourceMapLoading,
    294          disabled: !this.props.areSourceMapsEnabled,
    295          "not-mapped":
    296            (!selectedSource?.isOriginal || selectedSource?.isPrettyPrinted) &&
    297            !this.props.isSourceActorWithSourceMap,
    298          original:
    299            selectedSource?.isOriginal && !selectedSource.isPrettyPrinted,
    300        }),
    301        title: this.getSourceMapTitle(),
    302        label: this.getSourceMapLabel(),
    303        icon: true,
    304      },
    305      () => this.renderSourceMapMenuItems()
    306    );
    307  }
    308 
    309  renderSourceMapMenuItems() {
    310    const items = [
    311      React.createElement(MenuItem, {
    312        className: "menu-item debugger-source-map-enabled",
    313        checked: this.props.areSourceMapsEnabled,
    314        label: L10N.getStr("sourceFooter.sourceMapButton.enable"),
    315        onClick: this.toggleSourceMaps,
    316      }),
    317      hr(),
    318      React.createElement(MenuItem, {
    319        className: "menu-item debugger-source-map-open-original",
    320        checked: this.props.shouldSelectOriginalLocation,
    321        label: L10N.getStr(
    322          "sourceFooter.sourceMapButton.showOriginalSourceByDefault"
    323        ),
    324        onClick: this.toggleSelectOriginalByDefault,
    325      }),
    326    ];
    327 
    328    if (this.props.mappedSource) {
    329      items.push(
    330        React.createElement(MenuItem, {
    331          className: "menu-item debugger-jump-mapped-source",
    332          label: this.props.mappedSource.isOriginal
    333            ? L10N.getStr("sourceFooter.sourceMapButton.jumpToOriginalSource")
    334            : L10N.getStr("sourceFooter.sourceMapButton.jumpToGeneratedSource"),
    335          tooltip: this.props.mappedSource.url,
    336          onClick: () =>
    337            this.props.jumpToMappedLocation(this.props.selectedLocation),
    338        })
    339      );
    340    }
    341 
    342    if (this.props.resolvedSourceMapURL) {
    343      items.push(
    344        React.createElement(MenuItem, {
    345          className: "menu-item debugger-source-map-link",
    346          label: L10N.getStr(
    347            "sourceFooter.sourceMapButton.openSourceMapInNewTab"
    348          ),
    349          onClick: this.openSourceMap,
    350        })
    351      );
    352    }
    353    return React.createElement(
    354      MenuList,
    355      {
    356        id: "debugger-source-map-list",
    357      },
    358      items
    359    );
    360  }
    361 
    362  openSourceMap = () => {
    363    let line, column;
    364    if (
    365      this.props.sourceMapError &&
    366      this.props.sourceMapError.includes("JSON.parse")
    367    ) {
    368      const match = this.props.sourceMapError.match(
    369        /at line (\d+) column (\d+)/
    370      );
    371      if (match) {
    372        line = match[1];
    373        column = match[2];
    374      }
    375    }
    376    this.props.openSourceMap(
    377      this.props.resolvedSourceMapURL || this.props.selectedLocation.source.url,
    378      line,
    379      column
    380    );
    381  };
    382 
    383  toggleSourceMaps = () => {
    384    this.props.toggleSourceMapsEnabled(!this.props.areSourceMapsEnabled);
    385  };
    386 
    387  toggleSelectOriginalByDefault = () => {
    388    this.props.setDefaultSelectedLocation(
    389      !this.props.shouldSelectOriginalLocation
    390    );
    391    this.props.jumpToMappedSelectedLocation();
    392  };
    393 
    394  render() {
    395    return div(
    396      {
    397        className: "source-footer",
    398      },
    399      div(
    400        {
    401          className: "source-footer-start",
    402        },
    403        this.renderCommands()
    404      ),
    405      div(
    406        {
    407          className: "source-footer-end",
    408        },
    409        this.renderMappedSource(),
    410        this.renderCursorPosition(),
    411        this.renderToggleButton()
    412      )
    413    );
    414  }
    415 }
    416 SourceFooter.contextTypes = {
    417  toolboxDoc: PropTypes.object,
    418 };
    419 
    420 const mapStateToProps = state => {
    421  const selectedSource = getSelectedSource(state);
    422  const selectedLocation = getSelectedLocation(state);
    423  const sourceTextContent = getSelectedSourceTextContent(state);
    424 
    425  const areSourceMapsEnabledProp = areSourceMapsEnabled(state);
    426  const isSourceActorWithSourceMapProp = selectedLocation?.sourceActor
    427    ? isSourceActorWithSourceMap(state, selectedLocation?.sourceActor.id)
    428    : false;
    429  const sourceMapError = selectedLocation?.sourceActor
    430    ? getSourceMapErrorForSourceActor(state, selectedLocation.sourceActor.id)
    431    : null;
    432  const mappedSource = getSelectedMappedSource(state);
    433 
    434  const isSourceMapLoading =
    435    areSourceMapsEnabledProp &&
    436    isSourceActorWithSourceMapProp &&
    437    // `mappedSource` will be null while loading, we need another way to know when it is done computing
    438    !mappedSource &&
    439    isSelectedMappedSourceLoading(state) &&
    440    !sourceMapError &&
    441    !selectedSource?.isPrettyPrinted;
    442 
    443  return {
    444    selectedSource,
    445    selectedLocation,
    446    isSelectedSourceBlackBoxed: selectedSource
    447      ? isSourceBlackBoxed(state, selectedSource)
    448      : null,
    449    isSourceOnIgnoreList:
    450      isSourceMapIgnoreListEnabled(state) &&
    451      isSourceOnSourceMapIgnoreList(state, selectedSource),
    452    sourceLoaded: !!sourceTextContent,
    453    mappedSource,
    454    isSourceMapLoading,
    455    prettySource: getPrettySource(
    456      state,
    457      selectedSource ? selectedSource.id : null
    458    ),
    459    endPanelCollapsed: getPaneCollapse(state, "end"),
    460    canPrettyPrint: selectedLocation
    461      ? canPrettyPrintSource(
    462          state,
    463          selectedSource,
    464          selectedLocation.sourceActor
    465        )
    466      : false,
    467    prettyPrintMessage: selectedLocation
    468      ? getPrettyPrintMessage(state, selectedLocation)
    469      : null,
    470 
    471    sourceMapError,
    472    resolvedSourceMapURL: selectedLocation?.sourceActor
    473      ? getSourceMapResolvedURL(state, selectedLocation.sourceActor.id)
    474      : null,
    475    isSourceActorWithSourceMap: isSourceActorWithSourceMapProp,
    476 
    477    areSourceMapsEnabled: areSourceMapsEnabledProp,
    478    shouldSelectOriginalLocation: getShouldSelectOriginalLocation(state),
    479  };
    480 };
    481 
    482 export default connect(mapStateToProps, {
    483  removePrettyPrintedSource: actions.removePrettyPrintedSource,
    484  prettyPrintAndSelectSource: actions.prettyPrintAndSelectSource,
    485  toggleBlackBox: actions.toggleBlackBox,
    486  jumpToMappedLocation: actions.jumpToMappedLocation,
    487  togglePaneCollapse: actions.togglePaneCollapse,
    488  toggleSourceMapsEnabled: actions.toggleSourceMapsEnabled,
    489  setDefaultSelectedLocation: actions.setDefaultSelectedLocation,
    490  jumpToMappedSelectedLocation: actions.jumpToMappedSelectedLocation,
    491  openSourceMap: actions.openSourceMap,
    492 })(SourceFooter);