diff options
Diffstat (limited to 'devtools/client/shared/components/tabs/TabBar.js')
-rw-r--r-- | devtools/client/shared/components/tabs/TabBar.js | 378 |
1 files changed, 378 insertions, 0 deletions
diff --git a/devtools/client/shared/components/tabs/TabBar.js b/devtools/client/shared/components/tabs/TabBar.js new file mode 100644 index 0000000000..730e8c7802 --- /dev/null +++ b/devtools/client/shared/components/tabs/TabBar.js @@ -0,0 +1,378 @@ +/* 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; |