/* 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 EventEmitter = require("resource://devtools/shared/event-emitter.js"); class ToolSidebar extends EventEmitter { constructor(tabbox, panel, options = {}) { super(); this.#tabbox = tabbox; this.#panelDoc = this.#tabbox.ownerDocument; this.#toolPanel = panel; this.#options = options; if (!options.disableTelemetry) { this.#telemetry = this.#toolPanel.telemetry; } if (this.#options.hideTabstripe) { this.#tabbox.setAttribute("hidetabs", "true"); } this.render(); this.#toolPanel.emit("sidebar-created", this); } TABPANEL_ID_PREFIX = "sidebar-panel-"; #currentTool; #destroyed; #options; #panelDoc; #tabbar; #tabbox; #telemetry; #toolNames; #toolPanel; // React get React() { return this.#toolPanel.React; } get ReactDOM() { return this.#toolPanel.ReactDOM; } get browserRequire() { return this.#toolPanel.browserRequire; } get InspectorTabPanel() { return this.#toolPanel.InspectorTabPanel; } get TabBar() { return this.#toolPanel.TabBar; } // Rendering render() { const sidebar = this.TabBar({ menuDocument: this.#toolPanel.toolbox.doc, showAllTabsMenu: true, allTabsMenuButtonTooltip: this.#options.allTabsMenuButtonTooltip, sidebarToggleButton: this.#options.sidebarToggleButton, onSelect: this.handleSelectionChange.bind(this), }); this.#tabbar = this.ReactDOM.render(sidebar, this.#tabbox); } /** * Adds all the queued tabs. */ addAllQueuedTabs() { this.#tabbar.addAllQueuedTabs(); } /** * Register a side-panel tab. * * @param {string} tab uniq id * @param {string} title tab title * @param {React.Component} panel component. See `InspectorPanelTab` as an example. * @param {boolean} selected true if the panel should be selected * @param {number} index the position where the tab should be inserted */ addTab(id, title, panel, selected, index) { this.#tabbar.addTab(id, title, selected, panel, null, index); this.emit("new-tab-registered", id); } /** * Helper API for adding side-panels that use existing DOM nodes * (defined within inspector.xhtml) as the content. * * @param {string} tab uniq id * @param {string} title tab title * @param {boolean} selected true if the panel should be selected * @param {number} index the position where the tab should be inserted */ addExistingTab(id, title, selected, index) { const panel = this.InspectorTabPanel({ id, idPrefix: this.TABPANEL_ID_PREFIX, key: id, title, }); this.addTab(id, title, panel, selected, index); } /** * Queues a side-panel tab to be added.. * * @param {string} tab uniq id * @param {string} title tab title * @param {React.Component} panel component. See `InspectorPanelTab` as an example. * @param {boolean} selected true if the panel should be selected * @param {number} index the position where the tab should be inserted */ queueTab(id, title, panel, selected, index) { this.#tabbar.queueTab(id, title, selected, panel, null, index); this.emit("new-tab-registered", id); } /** * Helper API for queuing side-panels that use existing DOM nodes * (defined within inspector.xhtml) as the content. * * @param {string} tab uniq id * @param {string} title tab title * @param {boolean} selected true if the panel should be selected * @param {number} index the position where the tab should be inserted */ queueExistingTab(id, title, selected, index) { const panel = this.InspectorTabPanel({ id, idPrefix: this.TABPANEL_ID_PREFIX, key: id, title, }); this.queueTab(id, title, panel, selected, index); } /** * Remove an existing tab. * * @param {string} tabId The ID of the tab that was used to register it, or * the tab id attribute value if the tab existed before the sidebar * got created. */ removeTab(tabId) { this.#tabbar.removeTab(tabId); this.emit("tab-unregistered", tabId); } /** * Show or hide a specific tab. * * @param {boolean} isVisible True to show the tab/tabpanel, False to hide it. * @param {string} id The ID of the tab to be hidden. */ toggleTab(isVisible, id) { this.#tabbar.toggleTab(id, isVisible); } /** * Select a specific tab. * * @param {string} id The ID of the tab * @returns {Promise} */ select(id) { return this.#tabbar.select(id); } /** * Return the id of the selected tab. */ getCurrentTabID() { return this.#currentTool; } /** * Returns the requested tab panel based on the id. * * @param {string} id * @return {DOMNode} */ getTabPanel(id) { // Search with and without the ID prefix as there might have been existing // tabpanels by the time the sidebar got created return this.#panelDoc.querySelector( "#" + this.TABPANEL_ID_PREFIX + id + ", #" + id ); } /** * Event handler. */ handleSelectionChange(id) { if (this.#destroyed) { return; } const previousTool = this.#currentTool; this.#currentTool = id; this.updateTelemetryOnChange(id, previousTool); this.emit(this.#currentTool + "-selected"); this.emit("select", this.#currentTool); } /** * Log toolClosed and toolOpened events on telemetry. * * @param {string} currentToolId * id of the tool being selected. * @param {string} previousToolId * id of the previously selected tool. */ updateTelemetryOnChange(currentToolId, previousToolId) { if (currentToolId === previousToolId || !this.#telemetry) { // Skip telemetry if the tool id did not change or telemetry is unavailable. return; } currentToolId = this.getTelemetryPanelNameOrOther(currentToolId); if (previousToolId) { previousToolId = this.getTelemetryPanelNameOrOther(previousToolId); this.#telemetry.toolClosed(previousToolId, this); this.#telemetry.recordEvent("sidepanel_changed", "inspector", null, { oldpanel: previousToolId, newpanel: currentToolId, os: this.#telemetry.osNameAndVersion, }); } this.#telemetry.toolOpened(currentToolId, this); } /** * Returns a panel id in the case of built in panels or "other" in the case of * third party panels. This is necessary due to limitations in addon id strings, * the permitted length of event telemetry property values and what we actually * want to see in our telemetry. * * @param {string} id * The panel id we would like to process. */ getTelemetryPanelNameOrOther(id) { if (!this.#toolNames) { // Get all built in tool ids. We identify third party tool ids by checking // for a "-", which shows it originates from an addon. const ids = this.#tabbar.state.tabs.map(({ id: toolId }) => { return toolId.includes("-") ? "other" : toolId; }); this.#toolNames = new Set(ids); } if (!this.#toolNames.has(id)) { return "other"; } return id; } /** * Show the sidebar. * * @param {string} id * The sidebar tab id to select. */ show(id) { this.#tabbox.hidden = false; // If an id is given, select the corresponding sidebar tab. if (id) { this.select(id); } this.emit("show"); } /** * Show the sidebar. */ hide() { this.#tabbox.hidden = true; this.emit("hide"); } /** * Clean-up. */ destroy() { if (this.#destroyed) { return; } this.#destroyed = true; this.emit("destroy"); if (this.#currentTool && this.#telemetry) { this.#telemetry.toolClosed(this.#currentTool, this); } this.#toolPanel.emit("sidebar-destroyed", this); this.ReactDOM.unmountComponentAtNode(this.#tabbox); this.#tabbox = null; this.#telemetry = null; this.#panelDoc = null; this.#toolPanel = null; } } exports.ToolSidebar = ToolSidebar;