Source code
Revision control
Copy as Markdown
Other Tools
/* 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
/* 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 => 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();
}
UNSAFE_componentWillReceiveProps(nextProps) {
const { activeTabId, children = [] } = nextProps;
const tabs = this.createTabs(children);
const activeTab = tabs.findIndex(tab => 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;