forked from mirrors/gecko-dev
Fixes this warning every time the toolbox is opened: "Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of `ToolboxTabs`. See https://fb.me/react-warning-keys for more information. in MenuItem (created by ToolboxTabs) in ToolboxTabs (created by ToolboxToolbar) in div (created by ToolboxToolbar) in ToolboxToolbar (created by ToolboxController) in ToolboxController" Differential Revision: https://phabricator.services.mozilla.com/D4671 --HG-- extra : moz-landing-system : lando
286 lines
8.9 KiB
JavaScript
286 lines
8.9 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
"use strict";
|
|
|
|
const { Component, createFactory } = require("devtools/client/shared/vendor/react");
|
|
const dom = require("devtools/client/shared/vendor/react-dom-factories");
|
|
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
|
|
const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
|
|
const { div } = dom;
|
|
|
|
const MenuButton = createFactory(require("devtools/client/shared/components/menu/MenuButton"));
|
|
const MenuItem = createFactory(require("devtools/client/shared/components/menu/MenuItem"));
|
|
const MenuList = createFactory(require("devtools/client/shared/components/menu/MenuList"));
|
|
const ToolboxTab = createFactory(require("devtools/client/framework/components/ToolboxTab"));
|
|
const { ToolboxTabsOrderManager } = require("devtools/client/framework/toolbox-tabs-order-manager");
|
|
|
|
// 26px is chevron devtools button width.(i.e. tools-chevronmenu)
|
|
const CHEVRON_BUTTON_WIDTH = 26;
|
|
|
|
class ToolboxTabs extends Component {
|
|
// See toolbox-toolbar propTypes for details on the props used here.
|
|
static get propTypes() {
|
|
return {
|
|
currentToolId: PropTypes.string,
|
|
focusButton: PropTypes.func,
|
|
focusedButton: PropTypes.string,
|
|
highlightedTools: PropTypes.object,
|
|
panelDefinitions: PropTypes.array,
|
|
selectTool: PropTypes.func,
|
|
toolbox: PropTypes.object,
|
|
visibleToolboxButtonCount: PropTypes.number.isRequired,
|
|
L10N: PropTypes.object,
|
|
onTabsOrderUpdated: PropTypes.func.isRequired,
|
|
};
|
|
}
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
this.state = {
|
|
// Array of overflowed tool id.
|
|
overflowedTabIds: [],
|
|
};
|
|
|
|
// Map with tool Id and its width size. This lifecycle is out of React's
|
|
// lifecycle. If a tool is registered, ToolboxTabs will add target tool id
|
|
// to this map. ToolboxTabs will never remove tool id from this cache.
|
|
this._cachedToolTabsWidthMap = new Map();
|
|
|
|
this._resizeTimerId = null;
|
|
this.resizeHandler = this.resizeHandler.bind(this);
|
|
|
|
this._tabsOrderManager = new ToolboxTabsOrderManager(props.onTabsOrderUpdated);
|
|
}
|
|
|
|
componentDidMount() {
|
|
window.addEventListener("resize", this.resizeHandler);
|
|
this.updateCachedToolTabsWidthMap();
|
|
this.updateOverflowedTabs();
|
|
}
|
|
|
|
componentWillUpdate(nextProps, nextState) {
|
|
if (this.shouldUpdateToolboxTabs(this.props, nextProps)) {
|
|
// Force recalculate and render in this cycle if panel definition has
|
|
// changed or selected tool has changed.
|
|
nextState.overflowedTabIds = [];
|
|
}
|
|
}
|
|
|
|
componentDidUpdate(prevProps, prevState) {
|
|
if (this.shouldUpdateToolboxTabs(prevProps, this.props)) {
|
|
this.updateCachedToolTabsWidthMap();
|
|
this.updateOverflowedTabs();
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
window.removeEventListener("resize", this.resizeHandler);
|
|
window.cancelIdleCallback(this._resizeTimerId);
|
|
this._tabsOrderManager.destroy();
|
|
}
|
|
|
|
/**
|
|
* Check if two array of ids are the same or not.
|
|
*/
|
|
equalToolIdArray(prevPanels, nextPanels) {
|
|
if (prevPanels.length !== nextPanels.length) {
|
|
return false;
|
|
}
|
|
|
|
// Check panel definitions even if both of array size is same.
|
|
// For example, the case of changing the tab's order.
|
|
return prevPanels.join("-") === nextPanels.join("-");
|
|
}
|
|
|
|
/**
|
|
* Return true if we should update the overflowed tabs.
|
|
*/
|
|
shouldUpdateToolboxTabs(prevProps, nextProps) {
|
|
if (prevProps.currentToolId !== nextProps.currentToolId ||
|
|
prevProps.visibleToolboxButtonCount !== nextProps.visibleToolboxButtonCount) {
|
|
return true;
|
|
}
|
|
|
|
const prevPanels = prevProps.panelDefinitions.map(def => def.id);
|
|
const nextPanels = nextProps.panelDefinitions.map(def => def.id);
|
|
return !this.equalToolIdArray(prevPanels, nextPanels);
|
|
}
|
|
|
|
/**
|
|
* Update the Map of tool id and tool tab width.
|
|
*/
|
|
updateCachedToolTabsWidthMap() {
|
|
const thisNode = findDOMNode(this);
|
|
const utils = window.windowUtils;
|
|
// Force a reflow before calling getBoundingWithoutFlushing on each tab.
|
|
thisNode.clientWidth;
|
|
|
|
for (const tab of thisNode.querySelectorAll(".devtools-tab")) {
|
|
const tabId = tab.id.replace("toolbox-tab-", "");
|
|
if (!this._cachedToolTabsWidthMap.has(tabId)) {
|
|
const rect = utils.getBoundsWithoutFlushing(tab);
|
|
this._cachedToolTabsWidthMap.set(tabId, rect.width);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the overflowed tab array from currently displayed tool tab.
|
|
* If calculated result is the same as the current overflowed tab array, this
|
|
* function will not update state.
|
|
*/
|
|
updateOverflowedTabs() {
|
|
const node = findDOMNode(this);
|
|
const toolboxWidth = parseInt(getComputedStyle(node).width, 10);
|
|
const { currentToolId } = this.props;
|
|
const enabledTabs = this.props.panelDefinitions.map(def => def.id);
|
|
let sumWidth = 0;
|
|
const visibleTabs = [];
|
|
|
|
for (const id of enabledTabs) {
|
|
const width = this._cachedToolTabsWidthMap.get(id);
|
|
sumWidth += width;
|
|
if (sumWidth <= toolboxWidth) {
|
|
visibleTabs.push(id);
|
|
} else {
|
|
sumWidth = sumWidth - width + CHEVRON_BUTTON_WIDTH;
|
|
|
|
// If toolbox can't display the Chevron, remove the last tool tab.
|
|
if (sumWidth > toolboxWidth) {
|
|
const removeTabId = visibleTabs.pop();
|
|
sumWidth -= this._cachedToolTabsWidthMap.get(removeTabId);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If the selected tab is in overflowed tabs, insert it into a visible
|
|
// toolbox.
|
|
if (!visibleTabs.includes(currentToolId) &&
|
|
enabledTabs.includes(currentToolId)) {
|
|
const selectedToolWidth = this._cachedToolTabsWidthMap.get(currentToolId);
|
|
while ((sumWidth + selectedToolWidth) > toolboxWidth &&
|
|
visibleTabs.length > 0) {
|
|
const removingToolId = visibleTabs.pop();
|
|
const removingToolWidth = this._cachedToolTabsWidthMap.get(removingToolId);
|
|
sumWidth -= removingToolWidth;
|
|
}
|
|
|
|
// If toolbox width is narrow, toolbox display only chevron menu.
|
|
// i.e. All tool tabs will overflow.
|
|
if ((sumWidth + selectedToolWidth) <= toolboxWidth) {
|
|
visibleTabs.push(currentToolId);
|
|
}
|
|
}
|
|
|
|
const willOverflowTabs = enabledTabs.filter(id => !visibleTabs.includes(id));
|
|
if (!this.equalToolIdArray(this.state.overflowedTabIds, willOverflowTabs)) {
|
|
this.setState({ overflowedTabIds: willOverflowTabs });
|
|
}
|
|
}
|
|
|
|
resizeHandler(evt) {
|
|
window.cancelIdleCallback(this._resizeTimerId);
|
|
this._resizeTimerId = window.requestIdleCallback(() => {
|
|
this.updateOverflowedTabs();
|
|
}, { timeout: 100 });
|
|
}
|
|
|
|
renderToolsChevronMenuList() {
|
|
const {
|
|
panelDefinitions,
|
|
selectTool,
|
|
} = this.props;
|
|
|
|
const items = [];
|
|
for (const { id, label, icon } of panelDefinitions) {
|
|
if (this.state.overflowedTabIds.includes(id)) {
|
|
items.push(MenuItem({
|
|
key: id,
|
|
id: "tools-chevron-menupopup-" + id,
|
|
label,
|
|
type: "checkbox",
|
|
onClick: () => {
|
|
selectTool(id, "tab_switch");
|
|
},
|
|
icon,
|
|
}));
|
|
}
|
|
}
|
|
|
|
return MenuList({ id: "tools-chevron-menupopup" }, items);
|
|
}
|
|
|
|
/**
|
|
* Render a button to access overflowed tools, displayed only when the toolbar
|
|
* presents an overflow.
|
|
*/
|
|
renderToolsChevronButton() {
|
|
const {
|
|
toolbox,
|
|
} = this.props;
|
|
|
|
return MenuButton(
|
|
{
|
|
id: "tools-chevron-menu-button",
|
|
menuId: "tools-chevron-menu-button-panel",
|
|
className: "devtools-button tools-chevron-menu",
|
|
doc: toolbox.doc,
|
|
},
|
|
this.renderToolsChevronMenuList()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Render all of the tabs, based on the panel definitions and builds out
|
|
* a toolbox tab for each of them. Will render the chevron button if the
|
|
* container has an overflow.
|
|
*/
|
|
render() {
|
|
const {
|
|
currentToolId,
|
|
focusButton,
|
|
focusedButton,
|
|
highlightedTools,
|
|
panelDefinitions,
|
|
selectTool,
|
|
} = this.props;
|
|
|
|
this._tabsOrderManager.setCurrentPanelDefinitions(panelDefinitions);
|
|
|
|
const tabs = panelDefinitions.map(panelDefinition => {
|
|
// Don't display overflowed tab.
|
|
if (!this.state.overflowedTabIds.includes(panelDefinition.id)) {
|
|
return ToolboxTab({
|
|
key: panelDefinition.id,
|
|
currentToolId,
|
|
focusButton,
|
|
focusedButton,
|
|
highlightedTools,
|
|
panelDefinition,
|
|
selectTool,
|
|
});
|
|
}
|
|
return null;
|
|
});
|
|
|
|
return div(
|
|
{
|
|
className: "toolbox-tabs-wrapper"
|
|
},
|
|
div(
|
|
{
|
|
className: "toolbox-tabs",
|
|
onMouseDown: (e) => this._tabsOrderManager.onMouseDown(e),
|
|
},
|
|
tabs,
|
|
(this.state.overflowedTabIds.length > 0)
|
|
? this.renderToolsChevronButton() : null
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
module.exports = ToolboxTabs;
|