tor-browser

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

ToolboxTabs.js (9723B)


      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 file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 "use strict";
      5 
      6 const {
      7  Component,
      8  createFactory,
      9  createRef,
     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  ToolboxTabsOrderManager,
     15 } = require("resource://devtools/client/framework/toolbox-tabs-order-manager.js");
     16 
     17 const { div } = dom;
     18 
     19 const ToolboxTab = createFactory(
     20  require("resource://devtools/client/framework/components/ToolboxTab.js")
     21 );
     22 
     23 loader.lazyGetter(this, "MenuButton", function () {
     24  return createFactory(
     25    require("resource://devtools/client/shared/components/menu/MenuButton.js")
     26  );
     27 });
     28 loader.lazyGetter(this, "MenuItem", function () {
     29  return createFactory(
     30    require("resource://devtools/client/shared/components/menu/MenuItem.js")
     31  );
     32 });
     33 loader.lazyGetter(this, "MenuList", function () {
     34  return createFactory(
     35    require("resource://devtools/client/shared/components/menu/MenuList.js")
     36  );
     37 });
     38 
     39 // 26px is chevron devtools button width.(i.e. tools-chevronmenu)
     40 const CHEVRON_BUTTON_WIDTH = 26;
     41 
     42 class ToolboxTabs extends Component {
     43  // See toolbox-toolbar propTypes for details on the props used here.
     44  static get propTypes() {
     45    return {
     46      currentToolId: PropTypes.string,
     47      focusButton: PropTypes.func,
     48      focusedButton: PropTypes.string,
     49      highlightedTools: PropTypes.object,
     50      panelDefinitions: PropTypes.array,
     51      selectTool: PropTypes.func,
     52      toolbox: PropTypes.object,
     53      visibleToolboxButtonCount: PropTypes.number.isRequired,
     54      L10N: PropTypes.object,
     55      onTabsOrderUpdated: PropTypes.func.isRequired,
     56    };
     57  }
     58 
     59  constructor(props) {
     60    super(props);
     61 
     62    this.state = {
     63      // Array of overflowed tool id.
     64      overflowedTabIds: [],
     65    };
     66 
     67    this.wrapperEl = createRef();
     68 
     69    // Map with tool Id and its width size. This lifecycle is out of React's
     70    // lifecycle. If a tool is registered, ToolboxTabs will add target tool id
     71    // to this map. ToolboxTabs will never remove tool id from this cache.
     72    this._cachedToolTabsWidthMap = new Map();
     73 
     74    this._resizeTimerId = null;
     75    this.resizeHandler = this.resizeHandler.bind(this);
     76 
     77    const { toolbox, onTabsOrderUpdated, panelDefinitions } = props;
     78    this._tabsOrderManager = new ToolboxTabsOrderManager(
     79      toolbox,
     80      onTabsOrderUpdated,
     81      panelDefinitions
     82    );
     83  }
     84 
     85  componentDidMount() {
     86    window.addEventListener("resize", this.resizeHandler);
     87    this.updateCachedToolTabsWidthMap();
     88    this.updateOverflowedTabs();
     89  }
     90 
     91  // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
     92  UNSAFE_componentWillUpdate(nextProps, nextState) {
     93    if (this.shouldUpdateToolboxTabs(this.props, nextProps)) {
     94      // Force recalculate and render in this cycle if panel definition has
     95      // changed or selected tool has changed.
     96      nextState.overflowedTabIds = [];
     97    }
     98  }
     99 
    100  componentDidUpdate(prevProps) {
    101    if (this.shouldUpdateToolboxTabs(prevProps, this.props)) {
    102      this.updateCachedToolTabsWidthMap();
    103      this.updateOverflowedTabs();
    104      this._tabsOrderManager.setCurrentPanelDefinitions(
    105        this.props.panelDefinitions
    106      );
    107    }
    108  }
    109 
    110  componentWillUnmount() {
    111    window.removeEventListener("resize", this.resizeHandler);
    112    window.cancelIdleCallback(this._resizeTimerId);
    113    this._tabsOrderManager.destroy();
    114  }
    115 
    116  /**
    117   * Check if two array of ids are the same or not.
    118   */
    119  equalToolIdArray(prevPanels, nextPanels) {
    120    if (prevPanels.length !== nextPanels.length) {
    121      return false;
    122    }
    123 
    124    // Check panel definitions even if both of array size is same.
    125    // For example, the case of changing the tab's order.
    126    return prevPanels.join("-") === nextPanels.join("-");
    127  }
    128 
    129  /**
    130   * Return true if we should update the overflowed tabs.
    131   */
    132  shouldUpdateToolboxTabs(prevProps, nextProps) {
    133    if (
    134      prevProps.currentToolId !== nextProps.currentToolId ||
    135      prevProps.visibleToolboxButtonCount !==
    136        nextProps.visibleToolboxButtonCount
    137    ) {
    138      return true;
    139    }
    140 
    141    const prevPanels = prevProps.panelDefinitions.map(def => def.id);
    142    const nextPanels = nextProps.panelDefinitions.map(def => def.id);
    143    return !this.equalToolIdArray(prevPanels, nextPanels);
    144  }
    145 
    146  /**
    147   * Update the Map of tool id and tool tab width.
    148   */
    149  updateCachedToolTabsWidthMap() {
    150    const utils = window.windowUtils;
    151    // Force a reflow before calling getBoundingWithoutFlushing on each tab.
    152    this.wrapperEl.current.clientWidth;
    153 
    154    for (const tab of this.wrapperEl.current.querySelectorAll(
    155      ".devtools-tab"
    156    )) {
    157      const tabId = tab.id.replace("toolbox-tab-", "");
    158      if (!this._cachedToolTabsWidthMap.has(tabId)) {
    159        const rect = utils.getBoundsWithoutFlushing(tab);
    160        this._cachedToolTabsWidthMap.set(tabId, rect.width);
    161      }
    162    }
    163  }
    164 
    165  /**
    166   * Update the overflowed tab array from currently displayed tool tab.
    167   * If calculated result is the same as the current overflowed tab array, this
    168   * function will not update state.
    169   */
    170  updateOverflowedTabs() {
    171    const toolboxWidth = parseInt(
    172      getComputedStyle(this.wrapperEl.current).width,
    173      10
    174    );
    175    const { currentToolId } = this.props;
    176    const enabledTabs = this.props.panelDefinitions.map(def => def.id);
    177    let sumWidth = 0;
    178    const visibleTabs = [];
    179 
    180    for (const id of enabledTabs) {
    181      const width = this._cachedToolTabsWidthMap.get(id);
    182      sumWidth += width;
    183      if (sumWidth <= toolboxWidth) {
    184        visibleTabs.push(id);
    185      } else {
    186        sumWidth = sumWidth - width + CHEVRON_BUTTON_WIDTH;
    187 
    188        // If toolbox can't display the Chevron, remove the last tool tab.
    189        if (sumWidth > toolboxWidth) {
    190          const removeTabId = visibleTabs.pop();
    191          sumWidth -= this._cachedToolTabsWidthMap.get(removeTabId);
    192        }
    193        break;
    194      }
    195    }
    196 
    197    // If the selected tab is in overflowed tabs, insert it into a visible
    198    // toolbox.
    199    if (
    200      !visibleTabs.includes(currentToolId) &&
    201      enabledTabs.includes(currentToolId)
    202    ) {
    203      const selectedToolWidth = this._cachedToolTabsWidthMap.get(currentToolId);
    204      while (
    205        sumWidth + selectedToolWidth > toolboxWidth &&
    206        visibleTabs.length
    207      ) {
    208        const removingToolId = visibleTabs.pop();
    209        const removingToolWidth =
    210          this._cachedToolTabsWidthMap.get(removingToolId);
    211        sumWidth -= removingToolWidth;
    212      }
    213 
    214      // If toolbox width is narrow, toolbox display only chevron menu.
    215      // i.e. All tool tabs will overflow.
    216      if (sumWidth + selectedToolWidth <= toolboxWidth) {
    217        visibleTabs.push(currentToolId);
    218      }
    219    }
    220 
    221    const willOverflowTabs = enabledTabs.filter(
    222      id => !visibleTabs.includes(id)
    223    );
    224    if (!this.equalToolIdArray(this.state.overflowedTabIds, willOverflowTabs)) {
    225      this.setState({ overflowedTabIds: willOverflowTabs });
    226    }
    227  }
    228 
    229  resizeHandler() {
    230    window.cancelIdleCallback(this._resizeTimerId);
    231    this._resizeTimerId = window.requestIdleCallback(
    232      () => {
    233        this.updateOverflowedTabs();
    234      },
    235      { timeout: 100 }
    236    );
    237  }
    238 
    239  renderToolsChevronMenuList() {
    240    const { panelDefinitions, selectTool } = this.props;
    241 
    242    const items = [];
    243    for (const { id, label, icon } of panelDefinitions) {
    244      if (this.state.overflowedTabIds.includes(id)) {
    245        items.push(
    246          MenuItem({
    247            key: id,
    248            id: "tools-chevron-menupopup-" + id,
    249            label,
    250            type: "checkbox",
    251            onClick: () => {
    252              selectTool(id, "tab_switch");
    253            },
    254            icon,
    255          })
    256        );
    257      }
    258    }
    259 
    260    return MenuList({ id: "tools-chevron-menupopup" }, items);
    261  }
    262 
    263  /**
    264   * Render a button to access overflowed tools, displayed only when the toolbar
    265   * presents an overflow.
    266   */
    267  renderToolsChevronButton() {
    268    const { toolbox } = this.props;
    269 
    270    return MenuButton(
    271      {
    272        id: "tools-chevron-menu-button",
    273        menuId: "tools-chevron-menu-button-panel",
    274        className: "devtools-tabbar-button tools-chevron-menu",
    275        toolboxDoc: toolbox.doc,
    276      },
    277      this.renderToolsChevronMenuList()
    278    );
    279  }
    280 
    281  /**
    282   * Render all of the tabs, based on the panel definitions and builds out
    283   * a toolbox tab for each of them. Will render the chevron button if the
    284   * container has an overflow.
    285   */
    286  render() {
    287    const {
    288      currentToolId,
    289      focusButton,
    290      focusedButton,
    291      highlightedTools,
    292      panelDefinitions,
    293      selectTool,
    294    } = this.props;
    295 
    296    const tabs = panelDefinitions.map(panelDefinition => {
    297      // Don't display overflowed tab.
    298      if (!this.state.overflowedTabIds.includes(panelDefinition.id)) {
    299        return ToolboxTab({
    300          key: panelDefinition.id,
    301          currentToolId,
    302          focusButton,
    303          focusedButton,
    304          highlightedTools,
    305          panelDefinition,
    306          selectTool,
    307        });
    308      }
    309      return null;
    310    });
    311 
    312    return div(
    313      {
    314        className: "toolbox-tabs-wrapper",
    315        ref: this.wrapperEl,
    316      },
    317      div(
    318        {
    319          className: "toolbox-tabs",
    320          onMouseDown: e => this._tabsOrderManager.onMouseDown(e),
    321        },
    322        tabs,
    323        this.state.overflowedTabIds.length
    324          ? this.renderToolsChevronButton()
    325          : null
    326      )
    327    );
    328  }
    329 }
    330 
    331 module.exports = ToolboxTabs;