/* 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/. */ /* eslint-env browser */ "use strict"; const { Component, createFactory, createRef, } = require("resource://devtools/client/shared/vendor/react.js"); const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); const Sidebar = createFactory( require("resource://devtools/client/shared/components/Sidebar.js") ); loader.lazyRequireGetter( this, "Menu", "resource://devtools/client/framework/menu.js" ); loader.lazyRequireGetter( this, "MenuItem", "resource://devtools/client/framework/menu-item.js" ); // Shortcuts const { div } = dom; /** * Renders Tabbar component. */ class Tabbar extends Component { static get propTypes() { return { children: PropTypes.array, menuDocument: PropTypes.object, onSelect: PropTypes.func, showAllTabsMenu: PropTypes.bool, allTabsMenuButtonTooltip: PropTypes.string, activeTabId: PropTypes.string, renderOnlySelected: PropTypes.bool, sidebarToggleButton: PropTypes.shape({ // Set to true if collapsed. collapsed: PropTypes.bool.isRequired, // Tooltip text used when the button indicates expanded state. collapsePaneTitle: PropTypes.string.isRequired, // Tooltip text used when the button indicates collapsed state. expandPaneTitle: PropTypes.string.isRequired, // Click callback onClick: PropTypes.func.isRequired, // align toggle button to right alignRight: PropTypes.bool, // if set to true toggle-button rotate 90 canVerticalSplit: PropTypes.bool, }), }; } static get defaultProps() { return { menuDocument: window.parent.document, showAllTabsMenu: false, }; } constructor(props, context) { super(props, context); const { activeTabId, children = [] } = props; const tabs = this.createTabs(children); const activeTab = tabs.findIndex((tab, index) => tab.id === activeTabId); this.state = { activeTab: activeTab === -1 ? 0 : activeTab, tabs, }; // Array of queued tabs to add to the Tabbar. this.queuedTabs = []; this.createTabs = this.createTabs.bind(this); this.addTab = this.addTab.bind(this); this.addAllQueuedTabs = this.addAllQueuedTabs.bind(this); this.queueTab = this.queueTab.bind(this); this.toggleTab = this.toggleTab.bind(this); this.removeTab = this.removeTab.bind(this); this.select = this.select.bind(this); this.getTabIndex = this.getTabIndex.bind(this); this.getTabId = this.getTabId.bind(this); this.getCurrentTabId = this.getCurrentTabId.bind(this); this.onTabChanged = this.onTabChanged.bind(this); this.onAllTabsMenuClick = this.onAllTabsMenuClick.bind(this); this.renderTab = this.renderTab.bind(this); this.tabbarRef = createRef(); } // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 UNSAFE_componentWillReceiveProps(nextProps) { const { activeTabId, children = [] } = nextProps; const tabs = this.createTabs(children); const activeTab = tabs.findIndex((tab, index) => tab.id === activeTabId); if ( activeTab !== this.state.activeTab || children !== this.props.children ) { this.setState({ activeTab: activeTab === -1 ? 0 : activeTab, tabs, }); } } createTabs(children) { return children .filter(panel => panel) .map((panel, index) => Object.assign({}, children[index], { id: panel.props.id || index, panel, title: panel.props.title, }) ); } // Public API addTab(id, title, selected = false, panel, url, index = -1) { const tabs = this.state.tabs.slice(); if (index >= 0) { tabs.splice(index, 0, { id, title, panel, url }); } else { tabs.push({ id, title, panel, url }); } const newState = Object.assign({}, this.state, { tabs, }); if (selected) { newState.activeTab = index >= 0 ? index : tabs.length - 1; } this.setState(newState, () => { if (this.props.onSelect && selected) { this.props.onSelect(id); } }); } addAllQueuedTabs() { if (!this.queuedTabs.length) { return; } const tabs = this.state.tabs.slice(); // Preselect the first sidebar tab if none was explicitly selected. let activeTab = 0; let activeId = this.queuedTabs[0].id; for (const { id, index, panel, selected, title, url } of this.queuedTabs) { if (index >= 0) { tabs.splice(index, 0, { id, title, panel, url }); } else { tabs.push({ id, title, panel, url }); } if (selected) { activeId = id; activeTab = index >= 0 ? index : tabs.length - 1; } } const newState = Object.assign({}, this.state, { activeTab, tabs, }); this.setState(newState, () => { if (this.props.onSelect) { this.props.onSelect(activeId); } }); this.queuedTabs = []; } /** * Queues a tab to be added. This is more performant than calling addTab for every * single tab to be added since we will limit the number of renders happening when * a new state is set. Once all the tabs to be added have been queued, call * addAllQueuedTabs() to populate the TabBar with all the queued tabs. */ queueTab(id, title, selected = false, panel, url, index = -1) { this.queuedTabs.push({ id, index, panel, selected, title, url, }); } toggleTab(tabId, isVisible) { const index = this.getTabIndex(tabId); if (index < 0) { return; } const tabs = this.state.tabs.slice(); tabs[index] = Object.assign({}, tabs[index], { isVisible, }); this.setState( Object.assign({}, this.state, { tabs, }) ); } removeTab(tabId) { const index = this.getTabIndex(tabId); if (index < 0) { return; } const tabs = this.state.tabs.slice(); tabs.splice(index, 1); let activeTab = this.state.activeTab - 1; activeTab = activeTab === -1 ? 0 : activeTab; this.setState( Object.assign({}, this.state, { activeTab, tabs, }), () => { // Select the next active tab and force the select event handler to initialize // the panel if needed. if (tabs.length && this.props.onSelect) { this.props.onSelect(this.getTabId(activeTab)); } } ); } select(tabId) { const docRef = this.tabbarRef.current.ownerDocument; const index = this.getTabIndex(tabId); if (index < 0) { return; } const newState = Object.assign({}, this.state, { activeTab: index, }); const tabDomElement = docRef.querySelector(`[data-tab-index="${index}"]`); if (tabDomElement) { tabDomElement.scrollIntoView(); } this.setState(newState, () => { if (this.props.onSelect) { this.props.onSelect(tabId); } }); } // Helpers getTabIndex(tabId) { let tabIndex = -1; this.state.tabs.forEach((tab, index) => { if (tab.id === tabId) { tabIndex = index; } }); return tabIndex; } getTabId(index) { return this.state.tabs[index].id; } getCurrentTabId() { return this.state.tabs[this.state.activeTab].id; } // Event Handlers onTabChanged(index) { this.setState( { activeTab: index, }, () => { if (this.props.onSelect) { this.props.onSelect(this.state.tabs[index].id); } } ); } onAllTabsMenuClick(event) { const menu = new Menu(); const target = event.target; // Generate list of menu items from the list of tabs. this.state.tabs.forEach(tab => { menu.append( new MenuItem({ label: tab.title, type: "checkbox", checked: this.getCurrentTabId() === tab.id, click: () => this.select(tab.id), }) ); }); // Show a drop down menu with frames. menu.popupAtTarget(target); return menu; } // Rendering renderTab(tab) { if (typeof tab.panel === "function") { return tab.panel({ key: tab.id, title: tab.title, id: tab.id, url: tab.url, }); } return tab.panel; } render() { const tabs = this.state.tabs.map(tab => this.renderTab(tab)); return div( { className: "devtools-sidebar-tabs", ref: this.tabbarRef, }, Sidebar( { onAllTabsMenuClick: this.onAllTabsMenuClick, renderOnlySelected: this.props.renderOnlySelected, showAllTabsMenu: this.props.showAllTabsMenu, allTabsMenuButtonTooltip: this.props.allTabsMenuButtonTooltip, sidebarToggleButton: this.props.sidebarToggleButton, activeTab: this.state.activeTab, onAfterChange: this.onTabChanged, }, tabs ) ); } } module.exports = Tabbar;