Since we're now avoiding unnecessary work, we were missing events in the openInspectorSidebarTab test helper. To fix this, we make ToolSidebar#select return a Promise that resolves once the select is done (or directly if the panel was already selected). Callsites of ToolSidebar#select are updated accordingly. We also add initialized promises to both the BoxModel and LayoutView so we can wait on those instead of waiting for specific events. We stop emitting the `grid-panel-updated` event as it's not listened for anymore. Differential Revision: https://phabricator.services.mozilla.com/D278942
390 lines
9.3 KiB
JavaScript
390 lines
9.3 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,
|
|
createRef,
|
|
} = require("resource://devtools/client/shared/vendor/react.mjs");
|
|
const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
|
|
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));
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Selects a given tab
|
|
*
|
|
* @param {string} tabId
|
|
* @returns {Promise}
|
|
*/
|
|
select(tabId) {
|
|
const docRef = this.tabbarRef.current.ownerDocument;
|
|
|
|
const index = this.getTabIndex(tabId);
|
|
if (index < 0) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
if (this.state.activeTab === index) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
const newState = Object.assign({}, this.state, {
|
|
activeTab: index,
|
|
});
|
|
|
|
const tabDomElement = docRef.querySelector(`[data-tab-index="${index}"]`);
|
|
|
|
if (tabDomElement) {
|
|
tabDomElement.scrollIntoView();
|
|
}
|
|
|
|
return new Promise(resolve => {
|
|
this.setState(newState, () => {
|
|
if (this.props.onSelect) {
|
|
this.props.onSelect(tabId);
|
|
}
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
// 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;
|