tor-browser

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

Tabs.js (8335B)


      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  ul,
      9  li,
     10  span,
     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 
     15 import {
     16  getOpenedSources,
     17  getSelectedSource,
     18  getIsPaused,
     19  getCurrentThread,
     20  getBlackBoxRanges,
     21 } from "../../selectors/index";
     22 import { isVisible } from "../../utils/ui";
     23 
     24 import { getHiddenTabsSources } from "../../utils/tabs";
     25 import { getFileURL } from "../../utils/source";
     26 import actions from "../../actions/index";
     27 
     28 import Tab from "./Tab";
     29 import { PaneToggleButton } from "../shared/Button/index";
     30 import Dropdown from "../shared/Dropdown";
     31 import DebuggerImage from "../shared/DebuggerImage";
     32 import CommandBar from "../SecondaryPanes/CommandBar";
     33 
     34 const { debounce } = require("resource://devtools/shared/debounce.js");
     35 
     36 class Tabs extends PureComponent {
     37  constructor(props) {
     38    super(props);
     39    this.state = {
     40      dropdownShown: false,
     41      // List of Sources objects for the tabs that overflow and are shown in the drop down menu
     42      hiddenSources: [],
     43    };
     44 
     45    this.onResize = debounce(() => {
     46      this.updateHiddenTabs();
     47    });
     48  }
     49 
     50  static get propTypes() {
     51    return {
     52      endPanelCollapsed: PropTypes.bool.isRequired,
     53      horizontal: PropTypes.bool.isRequired,
     54      isPaused: PropTypes.bool.isRequired,
     55      moveTab: PropTypes.func.isRequired,
     56      moveTabBySourceId: PropTypes.func.isRequired,
     57      selectSource: PropTypes.func.isRequired,
     58      selectedSource: PropTypes.object,
     59      blackBoxRanges: PropTypes.object.isRequired,
     60      startPanelCollapsed: PropTypes.bool.isRequired,
     61      openedSources: PropTypes.array.isRequired,
     62      togglePaneCollapse: PropTypes.func.isRequired,
     63    };
     64  }
     65 
     66  componentDidUpdate(prevProps) {
     67    if (
     68      this.props.selectedSource !== prevProps.selectedSource ||
     69      this.props.openedSources !== prevProps.openedSources
     70    ) {
     71      this.updateHiddenTabs();
     72    }
     73  }
     74 
     75  componentDidMount() {
     76    window.requestIdleCallback(this.updateHiddenTabs);
     77    window.addEventListener("resize", this.onResize);
     78    window.document
     79      .querySelector(".editor-pane")
     80      .addEventListener("resizeend", this.onResize);
     81  }
     82 
     83  componentWillUnmount() {
     84    window.removeEventListener("resize", this.onResize);
     85    window.document
     86      .querySelector(".editor-pane")
     87      .removeEventListener("resizeend", this.onResize);
     88  }
     89 
     90  /*
     91   * Updates the hiddenSourceTabs state, by
     92   * finding the source tabs which are wrapped and are not on the top row.
     93   */
     94  updateHiddenTabs = () => {
     95    if (!this.refs.sourceTabs) {
     96      // Ensure hiding the dropdown if we removed all sources.
     97      if (this.state.hiddenSources.length) {
     98        this.setState({ hiddenSources: [] });
     99      }
    100      return;
    101    }
    102    const { selectedSource, moveTab } = this.props;
    103    const sourceTabEls = this.refs.sourceTabs.children;
    104    const hiddenSources = getHiddenTabsSources(
    105      this.props.openedSources,
    106      sourceTabEls
    107    );
    108 
    109    if (
    110      selectedSource &&
    111      isVisible() &&
    112      hiddenSources.includes(selectedSource)
    113    ) {
    114      moveTab(selectedSource.url, 0);
    115      return;
    116    }
    117 
    118    this.setState({ hiddenSources });
    119  };
    120 
    121  toggleSourcesDropdown() {
    122    this.setState(prevState => ({
    123      dropdownShown: !prevState.dropdownShown,
    124    }));
    125  }
    126 
    127  getIconClass(source) {
    128    if (this.props.blackBoxRanges[source.url]) {
    129      return "blackBox";
    130    }
    131    return "file";
    132  }
    133 
    134  renderDropdownSource = source => {
    135    const { selectSource } = this.props;
    136 
    137    const onClick = () => selectSource(source);
    138    return li(
    139      {
    140        key: source.id,
    141        onClick,
    142        title: getFileURL(source, false),
    143      },
    144      React.createElement(DebuggerImage, {
    145        name: this.getIconClass(source),
    146        className: "dropdown-icon",
    147      }),
    148      span(
    149        {
    150          className: "dropdown-label",
    151        },
    152        source.shortName
    153      )
    154    );
    155  };
    156 
    157  // Note that these three listener will be called from Tab component
    158  // so that e.target will be Tab's DOM (and not Tabs one).
    159  onTabDragStart = e => {
    160    this.draggedSourceId = e.target.dataset.sourceId;
    161    this.draggedSourceIndex = e.target.dataset.index;
    162  };
    163 
    164  onTabDragEnd = () => {
    165    this.draggedSourceId = null;
    166    this.draggedSourceIndex = -1;
    167  };
    168 
    169  onTabDragOver = e => {
    170    e.preventDefault();
    171 
    172    const hoveredTabIndex = e.target.dataset.index;
    173    const { moveTabBySourceId } = this.props;
    174 
    175    if (hoveredTabIndex === this.draggedSourceIndex) {
    176      return;
    177    }
    178 
    179    const tabDOMRect = e.target.getBoundingClientRect();
    180    const { pageX: mouseCursorX } = e;
    181    if (
    182      /* Case: the mouse cursor moves into the left half of any target tab */
    183      mouseCursorX - tabDOMRect.left <
    184      tabDOMRect.width / 2
    185    ) {
    186      // The current tab goes to the left of the target tab
    187      const targetTab =
    188        hoveredTabIndex > this.draggedSourceIndex
    189          ? hoveredTabIndex - 1
    190          : hoveredTabIndex;
    191      moveTabBySourceId(this.draggedSourceId, targetTab);
    192      this.draggedSourceIndex = targetTab;
    193    } else if (
    194      /* Case: the mouse cursor moves into the right half of any target tab */
    195      mouseCursorX - tabDOMRect.left >=
    196      tabDOMRect.width / 2
    197    ) {
    198      // The current tab goes to the right of the target tab
    199      const targetTab =
    200        hoveredTabIndex < this.draggedSourceIndex
    201          ? hoveredTabIndex + 1
    202          : hoveredTabIndex;
    203      moveTabBySourceId(this.draggedSourceId, targetTab);
    204      this.draggedSourceIndex = targetTab;
    205    }
    206  };
    207 
    208  renderTabs() {
    209    const { openedSources } = this.props;
    210    if (!openedSources.length) {
    211      return null;
    212    }
    213    return div(
    214      {
    215        className: "source-tabs",
    216        ref: "sourceTabs",
    217      },
    218      openedSources.map((source, index) => {
    219        return React.createElement(Tab, {
    220          onDragStart: this.onTabDragStart,
    221          onDragOver: this.onTabDragOver,
    222          onDragEnd: this.onTabDragEnd,
    223          key: source.id,
    224          index,
    225          source,
    226        });
    227      })
    228    );
    229  }
    230 
    231  renderDropdown() {
    232    const { hiddenSources } = this.state;
    233    if (!hiddenSources || !hiddenSources.length) {
    234      return null;
    235    }
    236    const panel = ul(null, hiddenSources.map(this.renderDropdownSource));
    237    const icon = React.createElement(DebuggerImage, {
    238      name: "more-tabs",
    239    });
    240    return React.createElement(Dropdown, {
    241      panel,
    242      icon,
    243    });
    244  }
    245 
    246  renderCommandBar() {
    247    const { horizontal, endPanelCollapsed, isPaused } = this.props;
    248    if (!endPanelCollapsed || !isPaused) {
    249      return null;
    250    }
    251    return React.createElement(CommandBar, {
    252      horizontal,
    253    });
    254  }
    255 
    256  renderStartPanelToggleButton() {
    257    return React.createElement(PaneToggleButton, {
    258      position: "start",
    259      collapsed: this.props.startPanelCollapsed,
    260      handleClick: this.props.togglePaneCollapse,
    261    });
    262  }
    263 
    264  renderEndPanelToggleButton() {
    265    const { horizontal, endPanelCollapsed, togglePaneCollapse } = this.props;
    266    if (!horizontal) {
    267      return null;
    268    }
    269    return React.createElement(PaneToggleButton, {
    270      position: "end",
    271      collapsed: endPanelCollapsed,
    272      handleClick: togglePaneCollapse,
    273      horizontal,
    274    });
    275  }
    276 
    277  render() {
    278    return div(
    279      {
    280        className: "source-header",
    281      },
    282      this.renderStartPanelToggleButton(),
    283      this.renderTabs(),
    284      this.renderDropdown(),
    285      this.renderEndPanelToggleButton(),
    286      this.renderCommandBar()
    287    );
    288  }
    289 }
    290 
    291 const mapStateToProps = state => {
    292  return {
    293    selectedSource: getSelectedSource(state),
    294    openedSources: getOpenedSources(state),
    295    blackBoxRanges: getBlackBoxRanges(state),
    296    isPaused: getIsPaused(state, getCurrentThread(state)),
    297  };
    298 };
    299 
    300 export default connect(mapStateToProps, {
    301  selectSource: actions.selectSource,
    302  moveTab: actions.moveTab,
    303  moveTabBySourceId: actions.moveTabBySourceId,
    304  togglePaneCollapse: actions.togglePaneCollapse,
    305 })(Tabs);