TabBar.js (9308B)
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 { 8 Component, 9 createFactory, 10 createRef, 11 } = require("resource://devtools/client/shared/vendor/react.mjs"); 12 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 13 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 14 15 const Sidebar = createFactory( 16 require("resource://devtools/client/shared/components/Sidebar.js") 17 ); 18 19 loader.lazyRequireGetter( 20 this, 21 "Menu", 22 "resource://devtools/client/framework/menu.js" 23 ); 24 loader.lazyRequireGetter( 25 this, 26 "MenuItem", 27 "resource://devtools/client/framework/menu-item.js" 28 ); 29 30 // Shortcuts 31 const { div } = dom; 32 33 /** 34 * Renders Tabbar component. 35 */ 36 class Tabbar extends Component { 37 static get propTypes() { 38 return { 39 children: PropTypes.array, 40 menuDocument: PropTypes.object, 41 onSelect: PropTypes.func, 42 showAllTabsMenu: PropTypes.bool, 43 allTabsMenuButtonTooltip: PropTypes.string, 44 activeTabId: PropTypes.string, 45 renderOnlySelected: PropTypes.bool, 46 sidebarToggleButton: PropTypes.shape({ 47 // Set to true if collapsed. 48 collapsed: PropTypes.bool.isRequired, 49 // Tooltip text used when the button indicates expanded state. 50 collapsePaneTitle: PropTypes.string.isRequired, 51 // Tooltip text used when the button indicates collapsed state. 52 expandPaneTitle: PropTypes.string.isRequired, 53 // Click callback 54 onClick: PropTypes.func.isRequired, 55 // align toggle button to right 56 alignRight: PropTypes.bool, 57 // if set to true toggle-button rotate 90 58 canVerticalSplit: PropTypes.bool, 59 }), 60 }; 61 } 62 63 static get defaultProps() { 64 return { 65 menuDocument: window.parent.document, 66 showAllTabsMenu: false, 67 }; 68 } 69 70 constructor(props, context) { 71 super(props, context); 72 const { activeTabId, children = [] } = props; 73 const tabs = this.createTabs(children); 74 const activeTab = tabs.findIndex(tab => tab.id === activeTabId); 75 76 this.state = { 77 activeTab: activeTab === -1 ? 0 : activeTab, 78 tabs, 79 }; 80 81 // Array of queued tabs to add to the Tabbar. 82 this.queuedTabs = []; 83 84 this.createTabs = this.createTabs.bind(this); 85 this.addTab = this.addTab.bind(this); 86 this.addAllQueuedTabs = this.addAllQueuedTabs.bind(this); 87 this.queueTab = this.queueTab.bind(this); 88 this.toggleTab = this.toggleTab.bind(this); 89 this.removeTab = this.removeTab.bind(this); 90 this.select = this.select.bind(this); 91 this.getTabIndex = this.getTabIndex.bind(this); 92 this.getTabId = this.getTabId.bind(this); 93 this.getCurrentTabId = this.getCurrentTabId.bind(this); 94 this.onTabChanged = this.onTabChanged.bind(this); 95 this.onAllTabsMenuClick = this.onAllTabsMenuClick.bind(this); 96 this.renderTab = this.renderTab.bind(this); 97 this.tabbarRef = createRef(); 98 } 99 100 // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 101 UNSAFE_componentWillReceiveProps(nextProps) { 102 const { activeTabId, children = [] } = nextProps; 103 const tabs = this.createTabs(children); 104 const activeTab = tabs.findIndex(tab => tab.id === activeTabId); 105 106 if ( 107 activeTab !== this.state.activeTab || 108 children !== this.props.children 109 ) { 110 this.setState({ 111 activeTab: activeTab === -1 ? 0 : activeTab, 112 tabs, 113 }); 114 } 115 } 116 117 createTabs(children) { 118 return children 119 .filter(panel => panel) 120 .map((panel, index) => 121 Object.assign({}, children[index], { 122 id: panel.props.id || index, 123 panel, 124 title: panel.props.title, 125 }) 126 ); 127 } 128 129 // Public API 130 131 addTab(id, title, selected = false, panel, url, index = -1) { 132 const tabs = this.state.tabs.slice(); 133 134 if (index >= 0) { 135 tabs.splice(index, 0, { id, title, panel, url }); 136 } else { 137 tabs.push({ id, title, panel, url }); 138 } 139 140 const newState = Object.assign({}, this.state, { 141 tabs, 142 }); 143 144 if (selected) { 145 newState.activeTab = index >= 0 ? index : tabs.length - 1; 146 } 147 148 this.setState(newState, () => { 149 if (this.props.onSelect && selected) { 150 this.props.onSelect(id); 151 } 152 }); 153 } 154 155 addAllQueuedTabs() { 156 if (!this.queuedTabs.length) { 157 return; 158 } 159 160 const tabs = this.state.tabs.slice(); 161 162 // Preselect the first sidebar tab if none was explicitly selected. 163 let activeTab = 0; 164 let activeId = this.queuedTabs[0].id; 165 166 for (const { id, index, panel, selected, title, url } of this.queuedTabs) { 167 if (index >= 0) { 168 tabs.splice(index, 0, { id, title, panel, url }); 169 } else { 170 tabs.push({ id, title, panel, url }); 171 } 172 173 if (selected) { 174 activeId = id; 175 activeTab = index >= 0 ? index : tabs.length - 1; 176 } 177 } 178 179 const newState = Object.assign({}, this.state, { 180 activeTab, 181 tabs, 182 }); 183 184 this.setState(newState, () => { 185 if (this.props.onSelect) { 186 this.props.onSelect(activeId); 187 } 188 }); 189 190 this.queuedTabs = []; 191 } 192 193 /** 194 * Queues a tab to be added. This is more performant than calling addTab for every 195 * single tab to be added since we will limit the number of renders happening when 196 * a new state is set. Once all the tabs to be added have been queued, call 197 * addAllQueuedTabs() to populate the TabBar with all the queued tabs. 198 */ 199 queueTab(id, title, selected = false, panel, url, index = -1) { 200 this.queuedTabs.push({ 201 id, 202 index, 203 panel, 204 selected, 205 title, 206 url, 207 }); 208 } 209 210 toggleTab(tabId, isVisible) { 211 const index = this.getTabIndex(tabId); 212 if (index < 0) { 213 return; 214 } 215 216 const tabs = this.state.tabs.slice(); 217 tabs[index] = Object.assign({}, tabs[index], { 218 isVisible, 219 }); 220 221 this.setState( 222 Object.assign({}, this.state, { 223 tabs, 224 }) 225 ); 226 } 227 228 removeTab(tabId) { 229 const index = this.getTabIndex(tabId); 230 if (index < 0) { 231 return; 232 } 233 234 const tabs = this.state.tabs.slice(); 235 tabs.splice(index, 1); 236 237 let activeTab = this.state.activeTab - 1; 238 activeTab = activeTab === -1 ? 0 : activeTab; 239 240 this.setState( 241 Object.assign({}, this.state, { 242 activeTab, 243 tabs, 244 }), 245 () => { 246 // Select the next active tab and force the select event handler to initialize 247 // the panel if needed. 248 if (tabs.length && this.props.onSelect) { 249 this.props.onSelect(this.getTabId(activeTab)); 250 } 251 } 252 ); 253 } 254 255 select(tabId) { 256 const docRef = this.tabbarRef.current.ownerDocument; 257 258 const index = this.getTabIndex(tabId); 259 if (index < 0) { 260 return; 261 } 262 263 const newState = Object.assign({}, this.state, { 264 activeTab: index, 265 }); 266 267 const tabDomElement = docRef.querySelector(`[data-tab-index="${index}"]`); 268 269 if (tabDomElement) { 270 tabDomElement.scrollIntoView(); 271 } 272 273 this.setState(newState, () => { 274 if (this.props.onSelect) { 275 this.props.onSelect(tabId); 276 } 277 }); 278 } 279 280 // Helpers 281 282 getTabIndex(tabId) { 283 let tabIndex = -1; 284 this.state.tabs.forEach((tab, index) => { 285 if (tab.id === tabId) { 286 tabIndex = index; 287 } 288 }); 289 return tabIndex; 290 } 291 292 getTabId(index) { 293 return this.state.tabs[index].id; 294 } 295 296 getCurrentTabId() { 297 return this.state.tabs[this.state.activeTab].id; 298 } 299 300 // Event Handlers 301 302 onTabChanged(index) { 303 this.setState( 304 { 305 activeTab: index, 306 }, 307 () => { 308 if (this.props.onSelect) { 309 this.props.onSelect(this.state.tabs[index].id); 310 } 311 } 312 ); 313 } 314 315 onAllTabsMenuClick(event) { 316 const menu = new Menu(); 317 const target = event.target; 318 319 // Generate list of menu items from the list of tabs. 320 this.state.tabs.forEach(tab => { 321 menu.append( 322 new MenuItem({ 323 label: tab.title, 324 type: "checkbox", 325 checked: this.getCurrentTabId() === tab.id, 326 click: () => this.select(tab.id), 327 }) 328 ); 329 }); 330 331 // Show a drop down menu with frames. 332 menu.popupAtTarget(target); 333 334 return menu; 335 } 336 337 // Rendering 338 339 renderTab(tab) { 340 if (typeof tab.panel === "function") { 341 return tab.panel({ 342 key: tab.id, 343 title: tab.title, 344 id: tab.id, 345 url: tab.url, 346 }); 347 } 348 349 return tab.panel; 350 } 351 352 render() { 353 const tabs = this.state.tabs.map(tab => this.renderTab(tab)); 354 355 return div( 356 { 357 className: "devtools-sidebar-tabs", 358 ref: this.tabbarRef, 359 }, 360 Sidebar( 361 { 362 onAllTabsMenuClick: this.onAllTabsMenuClick, 363 renderOnlySelected: this.props.renderOnlySelected, 364 showAllTabsMenu: this.props.showAllTabsMenu, 365 allTabsMenuButtonTooltip: this.props.allTabsMenuButtonTooltip, 366 sidebarToggleButton: this.props.sidebarToggleButton, 367 activeTab: this.state.activeTab, 368 onAfterChange: this.onTabChanged, 369 }, 370 tabs 371 ) 372 ); 373 } 374 } 375 376 module.exports = Tabbar;