toolsidebar.js (8299B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const EventEmitter = require("resource://devtools/shared/event-emitter.js"); 8 9 class ToolSidebar extends EventEmitter { 10 constructor(tabbox, panel, options = {}) { 11 super(); 12 13 this.#tabbox = tabbox; 14 this.#panelDoc = this.#tabbox.ownerDocument; 15 this.#toolPanel = panel; 16 this.#options = options; 17 18 if (!options.disableTelemetry) { 19 this.#telemetry = this.#toolPanel.telemetry; 20 } 21 22 if (this.#options.hideTabstripe) { 23 this.#tabbox.setAttribute("hidetabs", "true"); 24 } 25 26 this.render(); 27 28 this.#toolPanel.emit("sidebar-created", this); 29 } 30 31 TABPANEL_ID_PREFIX = "sidebar-panel-"; 32 #currentTool; 33 #destroyed; 34 #options; 35 #panelDoc; 36 #tabbar; 37 #tabbox; 38 #telemetry; 39 #toolNames; 40 #toolPanel; 41 42 // React 43 44 get React() { 45 return this.#toolPanel.React; 46 } 47 48 get ReactDOM() { 49 return this.#toolPanel.ReactDOM; 50 } 51 52 get browserRequire() { 53 return this.#toolPanel.browserRequire; 54 } 55 56 get InspectorTabPanel() { 57 return this.#toolPanel.InspectorTabPanel; 58 } 59 60 get TabBar() { 61 return this.#toolPanel.TabBar; 62 } 63 64 // Rendering 65 66 render() { 67 const sidebar = this.TabBar({ 68 menuDocument: this.#toolPanel.toolbox.doc, 69 showAllTabsMenu: true, 70 allTabsMenuButtonTooltip: this.#options.allTabsMenuButtonTooltip, 71 sidebarToggleButton: this.#options.sidebarToggleButton, 72 onSelect: this.handleSelectionChange.bind(this), 73 }); 74 75 this.#tabbar = this.ReactDOM.render(sidebar, this.#tabbox); 76 } 77 78 /** 79 * Adds all the queued tabs. 80 */ 81 addAllQueuedTabs() { 82 this.#tabbar.addAllQueuedTabs(); 83 } 84 85 /** 86 * Register a side-panel tab. 87 * 88 * @param {string} tab uniq id 89 * @param {string} title tab title 90 * @param {React.Component} panel component. See `InspectorPanelTab` as an example. 91 * @param {boolean} selected true if the panel should be selected 92 * @param {number} index the position where the tab should be inserted 93 */ 94 addTab(id, title, panel, selected, index) { 95 this.#tabbar.addTab(id, title, selected, panel, null, index); 96 this.emit("new-tab-registered", id); 97 } 98 99 /** 100 * Helper API for adding side-panels that use existing DOM nodes 101 * (defined within inspector.xhtml) as the content. 102 * 103 * @param {string} tab uniq id 104 * @param {string} title tab title 105 * @param {boolean} selected true if the panel should be selected 106 * @param {number} index the position where the tab should be inserted 107 */ 108 addExistingTab(id, title, selected, index) { 109 const panel = this.InspectorTabPanel({ 110 id, 111 idPrefix: this.TABPANEL_ID_PREFIX, 112 key: id, 113 title, 114 }); 115 116 this.addTab(id, title, panel, selected, index); 117 } 118 119 /** 120 * Queues a side-panel tab to be added.. 121 * 122 * @param {string} tab uniq id 123 * @param {string} title tab title 124 * @param {React.Component} panel component. See `InspectorPanelTab` as an example. 125 * @param {boolean} selected true if the panel should be selected 126 * @param {number} index the position where the tab should be inserted 127 */ 128 queueTab(id, title, panel, selected, index) { 129 this.#tabbar.queueTab(id, title, selected, panel, null, index); 130 this.emit("new-tab-registered", id); 131 } 132 133 /** 134 * Helper API for queuing side-panels that use existing DOM nodes 135 * (defined within inspector.xhtml) as the content. 136 * 137 * @param {string} tab uniq id 138 * @param {string} title tab title 139 * @param {boolean} selected true if the panel should be selected 140 * @param {number} index the position where the tab should be inserted 141 */ 142 queueExistingTab(id, title, selected, index) { 143 const panel = this.InspectorTabPanel({ 144 id, 145 idPrefix: this.TABPANEL_ID_PREFIX, 146 key: id, 147 title, 148 }); 149 150 this.queueTab(id, title, panel, selected, index); 151 } 152 153 /** 154 * Remove an existing tab. 155 * 156 * @param {string} tabId The ID of the tab that was used to register it, or 157 * the tab id attribute value if the tab existed before the sidebar 158 * got created. 159 */ 160 removeTab(tabId) { 161 this.#tabbar.removeTab(tabId); 162 163 this.emit("tab-unregistered", tabId); 164 } 165 166 /** 167 * Show or hide a specific tab. 168 * 169 * @param {boolean} isVisible True to show the tab/tabpanel, False to hide it. 170 * @param {string} id The ID of the tab to be hidden. 171 */ 172 toggleTab(isVisible, id) { 173 this.#tabbar.toggleTab(id, isVisible); 174 } 175 176 /** 177 * Select a specific tab. 178 */ 179 select(id) { 180 this.#tabbar.select(id); 181 } 182 183 /** 184 * Return the id of the selected tab. 185 */ 186 getCurrentTabID() { 187 return this.#currentTool; 188 } 189 190 /** 191 * Returns the requested tab panel based on the id. 192 * 193 * @param {string} id 194 * @return {DOMNode} 195 */ 196 getTabPanel(id) { 197 // Search with and without the ID prefix as there might have been existing 198 // tabpanels by the time the sidebar got created 199 return this.#panelDoc.querySelector( 200 "#" + this.TABPANEL_ID_PREFIX + id + ", #" + id 201 ); 202 } 203 204 /** 205 * Event handler. 206 */ 207 handleSelectionChange(id) { 208 if (this.#destroyed) { 209 return; 210 } 211 212 const previousTool = this.#currentTool; 213 if (previousTool) { 214 this.emit(previousTool + "-unselected"); 215 } 216 217 this.#currentTool = id; 218 219 this.updateTelemetryOnChange(id, previousTool); 220 this.emit(this.#currentTool + "-selected"); 221 this.emit("select", this.#currentTool); 222 } 223 224 /** 225 * Log toolClosed and toolOpened events on telemetry. 226 * 227 * @param {string} currentToolId 228 * id of the tool being selected. 229 * @param {string} previousToolId 230 * id of the previously selected tool. 231 */ 232 updateTelemetryOnChange(currentToolId, previousToolId) { 233 if (currentToolId === previousToolId || !this.#telemetry) { 234 // Skip telemetry if the tool id did not change or telemetry is unavailable. 235 return; 236 } 237 238 currentToolId = this.getTelemetryPanelNameOrOther(currentToolId); 239 240 if (previousToolId) { 241 previousToolId = this.getTelemetryPanelNameOrOther(previousToolId); 242 this.#telemetry.toolClosed(previousToolId, this); 243 244 this.#telemetry.recordEvent("sidepanel_changed", "inspector", null, { 245 oldpanel: previousToolId, 246 newpanel: currentToolId, 247 os: this.#telemetry.osNameAndVersion, 248 }); 249 } 250 this.#telemetry.toolOpened(currentToolId, this); 251 } 252 253 /** 254 * Returns a panel id in the case of built in panels or "other" in the case of 255 * third party panels. This is necessary due to limitations in addon id strings, 256 * the permitted length of event telemetry property values and what we actually 257 * want to see in our telemetry. 258 * 259 * @param {string} id 260 * The panel id we would like to process. 261 */ 262 getTelemetryPanelNameOrOther(id) { 263 if (!this.#toolNames) { 264 // Get all built in tool ids. We identify third party tool ids by checking 265 // for a "-", which shows it originates from an addon. 266 const ids = this.#tabbar.state.tabs.map(({ id: toolId }) => { 267 return toolId.includes("-") ? "other" : toolId; 268 }); 269 270 this.#toolNames = new Set(ids); 271 } 272 273 if (!this.#toolNames.has(id)) { 274 return "other"; 275 } 276 277 return id; 278 } 279 280 /** 281 * Show the sidebar. 282 * 283 * @param {string} id 284 * The sidebar tab id to select. 285 */ 286 show(id) { 287 this.#tabbox.hidden = false; 288 289 // If an id is given, select the corresponding sidebar tab. 290 if (id) { 291 this.select(id); 292 } 293 294 this.emit("show"); 295 } 296 297 /** 298 * Show the sidebar. 299 */ 300 hide() { 301 this.#tabbox.hidden = true; 302 303 this.emit("hide"); 304 } 305 306 /** 307 * Clean-up. 308 */ 309 destroy() { 310 if (this.#destroyed) { 311 return; 312 } 313 this.#destroyed = true; 314 315 this.emit("destroy"); 316 317 if (this.#currentTool && this.#telemetry) { 318 this.#telemetry.toolClosed(this.#currentTool, this); 319 } 320 321 this.#toolPanel.emit("sidebar-destroyed", this); 322 323 this.ReactDOM.unmountComponentAtNode(this.#tabbox); 324 325 this.#tabbox = null; 326 this.#telemetry = null; 327 this.#panelDoc = null; 328 this.#toolPanel = null; 329 } 330 } 331 332 exports.ToolSidebar = ToolSidebar;