tor-browser

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

TabBar.js (9308B)


      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  Component,
      9  createFactory,
     10  createRef,
     11 } = require("resource://devtools/client/shared/vendor/react.mjs");
     12 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
     13 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
     14 
     15 const Sidebar = createFactory(
     16  require("resource://devtools/client/shared/components/Sidebar.js")
     17 );
     18 
     19 loader.lazyRequireGetter(
     20  this,
     21  "Menu",
     22  "resource://devtools/client/framework/menu.js"
     23 );
     24 loader.lazyRequireGetter(
     25  this,
     26  "MenuItem",
     27  "resource://devtools/client/framework/menu-item.js"
     28 );
     29 
     30 // Shortcuts
     31 const { div } = dom;
     32 
     33 /**
     34 * Renders Tabbar component.
     35 */
     36 class Tabbar extends Component {
     37  static get propTypes() {
     38    return {
     39      children: PropTypes.array,
     40      menuDocument: PropTypes.object,
     41      onSelect: PropTypes.func,
     42      showAllTabsMenu: PropTypes.bool,
     43      allTabsMenuButtonTooltip: PropTypes.string,
     44      activeTabId: PropTypes.string,
     45      renderOnlySelected: PropTypes.bool,
     46      sidebarToggleButton: PropTypes.shape({
     47        // Set to true if collapsed.
     48        collapsed: PropTypes.bool.isRequired,
     49        // Tooltip text used when the button indicates expanded state.
     50        collapsePaneTitle: PropTypes.string.isRequired,
     51        // Tooltip text used when the button indicates collapsed state.
     52        expandPaneTitle: PropTypes.string.isRequired,
     53        // Click callback
     54        onClick: PropTypes.func.isRequired,
     55        // align toggle button to right
     56        alignRight: PropTypes.bool,
     57        // if set to true toggle-button rotate 90
     58        canVerticalSplit: PropTypes.bool,
     59      }),
     60    };
     61  }
     62 
     63  static get defaultProps() {
     64    return {
     65      menuDocument: window.parent.document,
     66      showAllTabsMenu: false,
     67    };
     68  }
     69 
     70  constructor(props, context) {
     71    super(props, context);
     72    const { activeTabId, children = [] } = props;
     73    const tabs = this.createTabs(children);
     74    const activeTab = tabs.findIndex(tab => tab.id === activeTabId);
     75 
     76    this.state = {
     77      activeTab: activeTab === -1 ? 0 : activeTab,
     78      tabs,
     79    };
     80 
     81    // Array of queued tabs to add to the Tabbar.
     82    this.queuedTabs = [];
     83 
     84    this.createTabs = this.createTabs.bind(this);
     85    this.addTab = this.addTab.bind(this);
     86    this.addAllQueuedTabs = this.addAllQueuedTabs.bind(this);
     87    this.queueTab = this.queueTab.bind(this);
     88    this.toggleTab = this.toggleTab.bind(this);
     89    this.removeTab = this.removeTab.bind(this);
     90    this.select = this.select.bind(this);
     91    this.getTabIndex = this.getTabIndex.bind(this);
     92    this.getTabId = this.getTabId.bind(this);
     93    this.getCurrentTabId = this.getCurrentTabId.bind(this);
     94    this.onTabChanged = this.onTabChanged.bind(this);
     95    this.onAllTabsMenuClick = this.onAllTabsMenuClick.bind(this);
     96    this.renderTab = this.renderTab.bind(this);
     97    this.tabbarRef = createRef();
     98  }
     99 
    100  // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
    101  UNSAFE_componentWillReceiveProps(nextProps) {
    102    const { activeTabId, children = [] } = nextProps;
    103    const tabs = this.createTabs(children);
    104    const activeTab = tabs.findIndex(tab => tab.id === activeTabId);
    105 
    106    if (
    107      activeTab !== this.state.activeTab ||
    108      children !== this.props.children
    109    ) {
    110      this.setState({
    111        activeTab: activeTab === -1 ? 0 : activeTab,
    112        tabs,
    113      });
    114    }
    115  }
    116 
    117  createTabs(children) {
    118    return children
    119      .filter(panel => panel)
    120      .map((panel, index) =>
    121        Object.assign({}, children[index], {
    122          id: panel.props.id || index,
    123          panel,
    124          title: panel.props.title,
    125        })
    126      );
    127  }
    128 
    129  // Public API
    130 
    131  addTab(id, title, selected = false, panel, url, index = -1) {
    132    const tabs = this.state.tabs.slice();
    133 
    134    if (index >= 0) {
    135      tabs.splice(index, 0, { id, title, panel, url });
    136    } else {
    137      tabs.push({ id, title, panel, url });
    138    }
    139 
    140    const newState = Object.assign({}, this.state, {
    141      tabs,
    142    });
    143 
    144    if (selected) {
    145      newState.activeTab = index >= 0 ? index : tabs.length - 1;
    146    }
    147 
    148    this.setState(newState, () => {
    149      if (this.props.onSelect && selected) {
    150        this.props.onSelect(id);
    151      }
    152    });
    153  }
    154 
    155  addAllQueuedTabs() {
    156    if (!this.queuedTabs.length) {
    157      return;
    158    }
    159 
    160    const tabs = this.state.tabs.slice();
    161 
    162    // Preselect the first sidebar tab if none was explicitly selected.
    163    let activeTab = 0;
    164    let activeId = this.queuedTabs[0].id;
    165 
    166    for (const { id, index, panel, selected, title, url } of this.queuedTabs) {
    167      if (index >= 0) {
    168        tabs.splice(index, 0, { id, title, panel, url });
    169      } else {
    170        tabs.push({ id, title, panel, url });
    171      }
    172 
    173      if (selected) {
    174        activeId = id;
    175        activeTab = index >= 0 ? index : tabs.length - 1;
    176      }
    177    }
    178 
    179    const newState = Object.assign({}, this.state, {
    180      activeTab,
    181      tabs,
    182    });
    183 
    184    this.setState(newState, () => {
    185      if (this.props.onSelect) {
    186        this.props.onSelect(activeId);
    187      }
    188    });
    189 
    190    this.queuedTabs = [];
    191  }
    192 
    193  /**
    194   * Queues a tab to be added. This is more performant than calling addTab for every
    195   * single tab to be added since we will limit the number of renders happening when
    196   * a new state is set. Once all the tabs to be added have been queued, call
    197   * addAllQueuedTabs() to populate the TabBar with all the queued tabs.
    198   */
    199  queueTab(id, title, selected = false, panel, url, index = -1) {
    200    this.queuedTabs.push({
    201      id,
    202      index,
    203      panel,
    204      selected,
    205      title,
    206      url,
    207    });
    208  }
    209 
    210  toggleTab(tabId, isVisible) {
    211    const index = this.getTabIndex(tabId);
    212    if (index < 0) {
    213      return;
    214    }
    215 
    216    const tabs = this.state.tabs.slice();
    217    tabs[index] = Object.assign({}, tabs[index], {
    218      isVisible,
    219    });
    220 
    221    this.setState(
    222      Object.assign({}, this.state, {
    223        tabs,
    224      })
    225    );
    226  }
    227 
    228  removeTab(tabId) {
    229    const index = this.getTabIndex(tabId);
    230    if (index < 0) {
    231      return;
    232    }
    233 
    234    const tabs = this.state.tabs.slice();
    235    tabs.splice(index, 1);
    236 
    237    let activeTab = this.state.activeTab - 1;
    238    activeTab = activeTab === -1 ? 0 : activeTab;
    239 
    240    this.setState(
    241      Object.assign({}, this.state, {
    242        activeTab,
    243        tabs,
    244      }),
    245      () => {
    246        // Select the next active tab and force the select event handler to initialize
    247        // the panel if needed.
    248        if (tabs.length && this.props.onSelect) {
    249          this.props.onSelect(this.getTabId(activeTab));
    250        }
    251      }
    252    );
    253  }
    254 
    255  select(tabId) {
    256    const docRef = this.tabbarRef.current.ownerDocument;
    257 
    258    const index = this.getTabIndex(tabId);
    259    if (index < 0) {
    260      return;
    261    }
    262 
    263    const newState = Object.assign({}, this.state, {
    264      activeTab: index,
    265    });
    266 
    267    const tabDomElement = docRef.querySelector(`[data-tab-index="${index}"]`);
    268 
    269    if (tabDomElement) {
    270      tabDomElement.scrollIntoView();
    271    }
    272 
    273    this.setState(newState, () => {
    274      if (this.props.onSelect) {
    275        this.props.onSelect(tabId);
    276      }
    277    });
    278  }
    279 
    280  // Helpers
    281 
    282  getTabIndex(tabId) {
    283    let tabIndex = -1;
    284    this.state.tabs.forEach((tab, index) => {
    285      if (tab.id === tabId) {
    286        tabIndex = index;
    287      }
    288    });
    289    return tabIndex;
    290  }
    291 
    292  getTabId(index) {
    293    return this.state.tabs[index].id;
    294  }
    295 
    296  getCurrentTabId() {
    297    return this.state.tabs[this.state.activeTab].id;
    298  }
    299 
    300  // Event Handlers
    301 
    302  onTabChanged(index) {
    303    this.setState(
    304      {
    305        activeTab: index,
    306      },
    307      () => {
    308        if (this.props.onSelect) {
    309          this.props.onSelect(this.state.tabs[index].id);
    310        }
    311      }
    312    );
    313  }
    314 
    315  onAllTabsMenuClick(event) {
    316    const menu = new Menu();
    317    const target = event.target;
    318 
    319    // Generate list of menu items from the list of tabs.
    320    this.state.tabs.forEach(tab => {
    321      menu.append(
    322        new MenuItem({
    323          label: tab.title,
    324          type: "checkbox",
    325          checked: this.getCurrentTabId() === tab.id,
    326          click: () => this.select(tab.id),
    327        })
    328      );
    329    });
    330 
    331    // Show a drop down menu with frames.
    332    menu.popupAtTarget(target);
    333 
    334    return menu;
    335  }
    336 
    337  // Rendering
    338 
    339  renderTab(tab) {
    340    if (typeof tab.panel === "function") {
    341      return tab.panel({
    342        key: tab.id,
    343        title: tab.title,
    344        id: tab.id,
    345        url: tab.url,
    346      });
    347    }
    348 
    349    return tab.panel;
    350  }
    351 
    352  render() {
    353    const tabs = this.state.tabs.map(tab => this.renderTab(tab));
    354 
    355    return div(
    356      {
    357        className: "devtools-sidebar-tabs",
    358        ref: this.tabbarRef,
    359      },
    360      Sidebar(
    361        {
    362          onAllTabsMenuClick: this.onAllTabsMenuClick,
    363          renderOnlySelected: this.props.renderOnlySelected,
    364          showAllTabsMenu: this.props.showAllTabsMenu,
    365          allTabsMenuButtonTooltip: this.props.allTabsMenuButtonTooltip,
    366          sidebarToggleButton: this.props.sidebarToggleButton,
    367          activeTab: this.state.activeTab,
    368          onAfterChange: this.onTabChanged,
    369        },
    370        tabs
    371      )
    372    );
    373  }
    374 }
    375 
    376 module.exports = Tabbar;