/* 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 => 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 => 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;