UNPKG

@dcloudio/uni-debugger

Version:

uni-app debugger

1,300 lines (1,151 loc) 35.6 kB
/* * Copyright (C) 2010 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * @unrestricted */ UI.TabbedPane = class extends UI.VBox { constructor() { super(true); this.registerRequiredCSS('ui/tabbedPane.css'); this.element.classList.add('tabbed-pane'); this.contentElement.classList.add('tabbed-pane-shadow'); this.contentElement.tabIndex = -1; this._headerElement = this.contentElement.createChild('div', 'tabbed-pane-header'); this._headerContentsElement = this._headerElement.createChild('div', 'tabbed-pane-header-contents'); this._tabSlider = createElementWithClass('div', 'tabbed-pane-tab-slider'); this._tabsElement = this._headerContentsElement.createChild('div', 'tabbed-pane-header-tabs'); this._tabsElement.setAttribute('role', 'tablist'); this._tabsElement.addEventListener('keydown', this._keyDown.bind(this), false); this._contentElement = this.contentElement.createChild('div', 'tabbed-pane-content'); this._contentElement.setAttribute('role', 'tabpanel'); this._contentElement.createChild('slot'); /** @type {!Array.<!UI.TabbedPaneTab>} */ this._tabs = []; /** @type {!Array.<!UI.TabbedPaneTab>} */ this._tabsHistory = []; /** @type {!Map<string, !UI.TabbedPaneTab>} */ this._tabsById = new Map(); this._currentTabLocked = false; this._autoSelectFirstItemOnShow = true; this._triggerDropDownTimeout = null; this._dropDownButton = this._createDropDownButton(); UI.zoomManager.addEventListener(UI.ZoomManager.Events.ZoomChanged, this._zoomChanged, this); this.makeTabSlider(); } /** * @param {string} name */ setAccessibleName(name) { UI.ARIAUtils.setAccessibleName(this._tabsElement, name); } /** * @param {boolean} locked */ setCurrentTabLocked(locked) { this._currentTabLocked = locked; this._headerElement.classList.toggle('locked', this._currentTabLocked); } /** * @param {boolean} autoSelect */ setAutoSelectFirstItemOnShow(autoSelect) { this._autoSelectFirstItemOnShow = autoSelect; } /** * @return {?UI.Widget} */ get visibleView() { return this._currentTab ? this._currentTab.view : null; } /** * @return {!Array.<string>} */ tabIds() { return this._tabs.map(tab => tab._id); } /** * @param {string} tabId * @return {number} */ tabIndex(tabId) { return this._tabs.findIndex(tab => tab.id === tabId); } /** * @return {!Array.<!UI.Widget>} */ tabViews() { return this._tabs.map(tab => tab.view); } /** * @param {string} tabId * @return {?UI.Widget} */ tabView(tabId) { return this._tabsById.has(tabId) ? this._tabsById.get(tabId).view : null; } /** * @return {?string} */ get selectedTabId() { return this._currentTab ? this._currentTab.id : null; } /** * @param {boolean} shrinkableTabs */ setShrinkableTabs(shrinkableTabs) { this._shrinkableTabs = shrinkableTabs; } makeVerticalTabLayout() { this._verticalTabLayout = true; this._setTabSlider(false); this.contentElement.classList.add('vertical-tab-layout'); this.invalidateConstraints(); } /** * @param {boolean} closeableTabs */ setCloseableTabs(closeableTabs) { this._closeableTabs = closeableTabs; } /** * @override */ focus() { if (this.visibleView) this.visibleView.focus(); else this.contentElement.focus(); } /** * @return {!Element} */ headerElement() { return this._headerElement; } /** * @param {string} id * @return {boolean} */ isTabCloseable(id) { const tab = this._tabsById.get(id); return tab ? tab.isCloseable() : false; } /** * @param {!UI.TabbedPaneTabDelegate} delegate */ setTabDelegate(delegate) { const tabs = this._tabs.slice(); for (let i = 0; i < tabs.length; ++i) tabs[i].setDelegate(delegate); this._delegate = delegate; } /** * @param {string} id * @param {string} tabTitle * @param {!UI.Widget} view * @param {string=} tabTooltip * @param {boolean=} userGesture * @param {boolean=} isCloseable * @param {number=} index */ appendTab(id, tabTitle, view, tabTooltip, userGesture, isCloseable, index) { isCloseable = typeof isCloseable === 'boolean' ? isCloseable : this._closeableTabs; const tab = new UI.TabbedPaneTab(this, id, tabTitle, isCloseable, view, tabTooltip); tab.setDelegate(this._delegate); console.assert(!this._tabsById.has(id), `Tabbed pane already contains a tab with id '${id}'`); this._tabsById.set(id, tab); if (index !== undefined) this._tabs.splice(index, 0, tab); else this._tabs.push(tab); this._tabsHistory.push(tab); if (this._tabsHistory[0] === tab && this.isShowing()) this.selectTab(tab.id, userGesture); this._updateTabElements(); } /** * @param {string} id * @param {boolean=} userGesture */ closeTab(id, userGesture) { this.closeTabs([id], userGesture); } /** * @param {!Array.<string>} ids * @param {boolean=} userGesture */ closeTabs(ids, userGesture) { const focused = this.hasFocus(); for (let i = 0; i < ids.length; ++i) this._innerCloseTab(ids[i], userGesture); this._updateTabElements(); if (this._tabsHistory.length) this.selectTab(this._tabsHistory[0].id, false); if (focused) this.focus(); } /** * @param {string} id * @param {boolean=} userGesture */ _innerCloseTab(id, userGesture) { if (!this._tabsById.has(id)) return; if (userGesture && !this._tabsById.get(id)._closeable) return; if (this._currentTab && this._currentTab.id === id) this._hideCurrentTab(); const tab = this._tabsById.get(id); this._tabsById.delete(id); this._tabsHistory.splice(this._tabsHistory.indexOf(tab), 1); this._tabs.splice(this._tabs.indexOf(tab), 1); if (tab._shown) this._hideTabElement(tab); const eventData = {tabId: id, view: tab.view, isUserGesture: userGesture}; this.dispatchEventToListeners(UI.TabbedPane.Events.TabClosed, eventData); return true; } /** * @param {string} tabId * @return {boolean} */ hasTab(tabId) { return this._tabsById.has(tabId); } /** * @return {!Array.<string>} */ allTabs() { return this._tabs.map(function(tab) { return tab.id; }); } /** * @param {string} id * @return {!Array.<string>} */ otherTabs(id) { const result = []; for (let i = 0; i < this._tabs.length; ++i) { if (this._tabs[i].id !== id) result.push(this._tabs[i].id); } return result; } /** * @param {string} id * @return {!Array.<string>} */ _tabsToTheRight(id) { let index = -1; for (let i = 0; i < this._tabs.length; ++i) { if (this._tabs[i].id === id) { index = i; break; } } if (index === -1) return []; return this._tabs.slice(index + 1).map(function(tab) { return tab.id; }); } _viewHasFocus() { if (this.visibleView) return this.visibleView.hasFocus(); return this.contentElement === this.contentElement.getComponentRoot().activeElement; } /** * @param {string} id * @param {boolean=} userGesture * @return {boolean} */ selectTab(id, userGesture) { if (this._currentTabLocked) return false; const focused = this._viewHasFocus(); const tab = this._tabsById.get(id); if (!tab) return false; if (this._currentTab && this._currentTab.id === id) return true; this.suspendInvalidations(); this._hideCurrentTab(); this._showTab(tab); this.resumeInvalidations(); this._currentTab = tab; this._tabsHistory.splice(this._tabsHistory.indexOf(tab), 1); this._tabsHistory.splice(0, 0, tab); this._updateTabElements(); if (focused) this.focus(); const eventData = {tabId: id, view: tab.view, isUserGesture: userGesture}; this.dispatchEventToListeners(UI.TabbedPane.Events.TabSelected, eventData); return true; } selectNextTab() { const index = this._tabs.indexOf(this._currentTab); const nextIndex = mod(index + 1, this._tabs.length); this.selectTab(this._tabs[nextIndex].id, true); } selectPrevTab() { const index = this._tabs.indexOf(this._currentTab); const nextIndex = mod(index - 1, this._tabs.length); this.selectTab(this._tabs[nextIndex].id, true); } /** * @param {number} tabsCount * @return {!Array.<string>} */ lastOpenedTabIds(tabsCount) { function tabToTabId(tab) { return tab.id; } return this._tabsHistory.slice(0, tabsCount).map(tabToTabId); } /** * @param {string} id * @param {?UI.Icon} icon */ setTabIcon(id, icon) { const tab = this._tabsById.get(id); tab._setIcon(icon); this._updateTabElements(); } /** * @param {string} id * @param {boolean} enabled */ setTabEnabled(id, enabled) { const tab = this._tabsById.get(id); tab.tabElement.classList.toggle('disabled', !enabled); } /** * @param {string} id * @param {string} className * @param {boolean=} force */ toggleTabClass(id, className, force) { const tab = this._tabsById.get(id); if (tab._toggleClass(className, force)) this._updateTabElements(); } /** * @param {!Common.Event} event */ _zoomChanged(event) { for (let i = 0; i < this._tabs.length; ++i) delete this._tabs[i]._measuredWidth; if (this.isShowing()) this._updateTabElements(); } /** * @param {string} id * @param {string} tabTitle * @param {string=} tabTooltip */ changeTabTitle(id, tabTitle, tabTooltip) { const tab = this._tabsById.get(id); if (tabTooltip !== undefined) tab.tooltip = tabTooltip; if (tab.title !== tabTitle) { tab.title = tabTitle; this._updateTabElements(); } } /** * @param {string} id * @param {!UI.Widget} view */ changeTabView(id, view) { const tab = this._tabsById.get(id); if (tab.view === view) return; this.suspendInvalidations(); const isSelected = this._currentTab && this._currentTab.id === id; const shouldFocus = tab.view.hasFocus(); if (isSelected) this._hideTab(tab); tab.view = view; if (isSelected) this._showTab(tab); if (shouldFocus) tab.view.focus(); this.resumeInvalidations(); } /** * @override */ onResize() { this._updateTabElements(); } headerResized() { this._updateTabElements(); } /** * @override */ wasShown() { const effectiveTab = this._currentTab || this._tabsHistory[0]; if (effectiveTab && this._autoSelectFirstItemOnShow) this.selectTab(effectiveTab.id); } makeTabSlider() { if (this._verticalTabLayout) return; this._setTabSlider(true); } /** * @param {boolean} enable */ _setTabSlider(enable) { this._sliderEnabled = enable; this._tabSlider.classList.toggle('enabled', enable); } /** * @override * @return {!UI.Constraints} */ calculateConstraints() { let constraints = super.calculateConstraints(); const minContentConstraints = new UI.Constraints(new UI.Size(0, 0), new UI.Size(50, 50)); constraints = constraints.widthToMax(minContentConstraints).heightToMax(minContentConstraints); if (this._verticalTabLayout) constraints = constraints.addWidth(new UI.Constraints(new UI.Size(120, 0))); else constraints = constraints.addHeight(new UI.Constraints(new UI.Size(0, 30))); return constraints; } _updateTabElements() { UI.invokeOnceAfterBatchUpdate(this, this._innerUpdateTabElements); } /** * @param {!Element} element */ setPlaceholderElement(element) { this._placeholderElement = element; if (this._placeholderContainerElement) { this._placeholderContainerElement.removeChildren(); this._placeholderContainerElement.appendChild(element); } } _innerUpdateTabElements() { if (!this.isShowing()) return; if (!this._tabs.length) { this._contentElement.classList.add('has-no-tabs'); if (this._placeholderElement && !this._placeholderContainerElement) { this._placeholderContainerElement = this._contentElement.createChild('div', 'tabbed-pane-placeholder fill'); this._placeholderContainerElement.appendChild(this._placeholderElement); } } else { this._contentElement.classList.remove('has-no-tabs'); if (this._placeholderContainerElement) { this._placeholderContainerElement.remove(); delete this._placeholderContainerElement; } } this._measureDropDownButton(); this._updateWidths(); this._updateTabsDropDown(); this._updateTabSlider(); } /** * @param {number} index * @param {!UI.TabbedPaneTab} tab */ _showTabElement(index, tab) { if (index >= this._tabsElement.children.length) this._tabsElement.appendChild(tab.tabElement); else this._tabsElement.insertBefore(tab.tabElement, this._tabsElement.children[index]); tab._shown = true; } /** * @param {!UI.TabbedPaneTab} tab */ _hideTabElement(tab) { this._tabsElement.removeChild(tab.tabElement); tab._shown = false; } _createDropDownButton() { const dropDownContainer = createElementWithClass('div', 'tabbed-pane-header-tabs-drop-down-container'); const chevronIcon = UI.Icon.create('largeicon-chevron', 'chevron-icon'); dropDownContainer.appendChild(chevronIcon); dropDownContainer.addEventListener('click', this._dropDownClicked.bind(this)); dropDownContainer.addEventListener('mousedown', event => { if (event.which !== 1 || this._triggerDropDownTimeout) return; this._triggerDropDownTimeout = setTimeout(this._dropDownClicked.bind(this, event), 200); }); return dropDownContainer; } /** * @param {!Event} event */ _dropDownClicked(event) { if (event.which !== 1) return; if (this._triggerDropDownTimeout) { clearTimeout(this._triggerDropDownTimeout); this._triggerDropDownTimeout = null; } const rect = this._dropDownButton.getBoundingClientRect(); const menu = new UI.ContextMenu(event, false, rect.left, rect.bottom); for (let i = 0; i < this._tabs.length; ++i) { const tab = this._tabs[i]; if (tab._shown) continue; menu.defaultSection().appendCheckboxItem( tab.title, this._dropDownMenuItemSelected.bind(this, tab), this._tabsHistory[0] === tab); } menu.show(); } /** * @param {!UI.TabbedPaneTab} tab */ _dropDownMenuItemSelected(tab) { this._lastSelectedOverflowTab = tab; this.selectTab(tab.id, true); } _totalWidth() { return this._headerContentsElement.getBoundingClientRect().width; } /** * @return {number} */ _numberOfTabsShown() { let numTabsShown = 0; for (const tab of this._tabs) { if (tab._shown) numTabsShown++; } return numTabsShown; } disableOverflowMenu() { this._overflowDisabled = true; } _updateTabsDropDown() { const tabsToShowIndexes = this._tabsToShowIndexes( this._tabs, this._tabsHistory, this._totalWidth(), this._measuredDropDownButtonWidth || 0); if (this._lastSelectedOverflowTab && this._numberOfTabsShown() !== tabsToShowIndexes.length) { delete this._lastSelectedOverflowTab; this._updateTabsDropDown(); return; } for (let i = 0; i < this._tabs.length; ++i) { if (this._tabs[i]._shown && tabsToShowIndexes.indexOf(i) === -1) this._hideTabElement(this._tabs[i]); } for (let i = 0; i < tabsToShowIndexes.length; ++i) { const tab = this._tabs[tabsToShowIndexes[i]]; if (!tab._shown) this._showTabElement(i, tab); } if (!this._overflowDisabled) this._maybeShowDropDown(tabsToShowIndexes.length !== this._tabs.length); } /** * @param {boolean} hasMoreTabs */ _maybeShowDropDown(hasMoreTabs) { if (hasMoreTabs && !this._dropDownButton.parentElement) this._headerContentsElement.appendChild(this._dropDownButton); else if (!hasMoreTabs && this._dropDownButton.parentElement) this._headerContentsElement.removeChild(this._dropDownButton); } _measureDropDownButton() { if (this._overflowDisabled || this._measuredDropDownButtonWidth) return; this._dropDownButton.classList.add('measuring'); this._headerContentsElement.appendChild(this._dropDownButton); this._measuredDropDownButtonWidth = this._dropDownButton.getBoundingClientRect().width; this._headerContentsElement.removeChild(this._dropDownButton); this._dropDownButton.classList.remove('measuring'); } _updateWidths() { const measuredWidths = this._measureWidths(); const maxWidth = this._shrinkableTabs ? this._calculateMaxWidth(measuredWidths.slice(), this._totalWidth()) : Number.MAX_VALUE; let i = 0; for (const tab of this._tabs) tab.setWidth(this._verticalTabLayout ? -1 : Math.min(maxWidth, measuredWidths[i++])); } _measureWidths() { // Add all elements to measure into this._tabsElement this._tabsElement.style.setProperty('width', '2000px'); const measuringTabElements = []; for (const tab of this._tabs) { if (typeof tab._measuredWidth === 'number') continue; const measuringTabElement = tab._createTabElement(true); measuringTabElement.__tab = tab; measuringTabElements.push(measuringTabElement); this._tabsElement.appendChild(measuringTabElement); } // Perform measurement for (let i = 0; i < measuringTabElements.length; ++i) { const width = measuringTabElements[i].getBoundingClientRect().width; measuringTabElements[i].__tab._measuredWidth = Math.ceil(width); } // Nuke elements from the UI for (let i = 0; i < measuringTabElements.length; ++i) measuringTabElements[i].remove(); // Combine the results. const measuredWidths = []; for (const tab of this._tabs) measuredWidths.push(tab._measuredWidth); this._tabsElement.style.removeProperty('width'); return measuredWidths; } /** * @param {!Array.<number>} measuredWidths * @param {number} totalWidth */ _calculateMaxWidth(measuredWidths, totalWidth) { if (!measuredWidths.length) return 0; measuredWidths.sort(function(x, y) { return x - y; }); let totalMeasuredWidth = 0; for (let i = 0; i < measuredWidths.length; ++i) totalMeasuredWidth += measuredWidths[i]; if (totalWidth >= totalMeasuredWidth) return measuredWidths[measuredWidths.length - 1]; let totalExtraWidth = 0; for (let i = measuredWidths.length - 1; i > 0; --i) { const extraWidth = measuredWidths[i] - measuredWidths[i - 1]; totalExtraWidth += (measuredWidths.length - i) * extraWidth; if (totalWidth + totalExtraWidth >= totalMeasuredWidth) { return measuredWidths[i - 1] + (totalWidth + totalExtraWidth - totalMeasuredWidth) / (measuredWidths.length - i); } } return totalWidth / measuredWidths.length; } /** * @param {!Array.<!UI.TabbedPaneTab>} tabsOrdered * @param {!Array.<!UI.TabbedPaneTab>} tabsHistory * @param {number} totalWidth * @param {number} measuredDropDownButtonWidth * @return {!Array.<number>} */ _tabsToShowIndexes(tabsOrdered, tabsHistory, totalWidth, measuredDropDownButtonWidth) { const tabsToShowIndexes = []; let totalTabsWidth = 0; const tabCount = tabsOrdered.length; const tabsToLookAt = tabsOrdered.slice(0); if (this._currentTab !== undefined) tabsToLookAt.unshift(tabsToLookAt.splice(tabsToLookAt.indexOf(this._currentTab), 1)[0]); if (this._lastSelectedOverflowTab !== undefined) tabsToLookAt.unshift(tabsToLookAt.splice(tabsToLookAt.indexOf(this._lastSelectedOverflowTab), 1)[0]); for (let i = 0; i < tabCount; ++i) { const tab = this._automaticReorder ? tabsHistory[i] : tabsToLookAt[i]; totalTabsWidth += tab.width(); let minimalRequiredWidth = totalTabsWidth; if (i !== tabCount - 1) minimalRequiredWidth += measuredDropDownButtonWidth; if (!this._verticalTabLayout && minimalRequiredWidth > totalWidth) break; tabsToShowIndexes.push(tabsOrdered.indexOf(tab)); } tabsToShowIndexes.sort(function(x, y) { return x - y; }); return tabsToShowIndexes; } _hideCurrentTab() { if (!this._currentTab) return; this._hideTab(this._currentTab); delete this._currentTab; } /** * @param {!UI.TabbedPaneTab} tab */ _showTab(tab) { tab.tabElement.tabIndex = 0; tab.tabElement.classList.add('selected'); UI.ARIAUtils.setSelected(tab.tabElement, true); tab.view.show(this.element); this._updateTabSlider(); } _updateTabSlider() { if (!this._sliderEnabled) return; if (!this._currentTab) { this._tabSlider.style.width = 0; return; } let left = 0; for (let i = 0; i < this._tabs.length && this._currentTab !== this._tabs[i]; i++) { if (this._tabs[i]._shown) left += this._tabs[i]._measuredWidth; } const sliderWidth = this._currentTab._shown ? this._currentTab._measuredWidth : this._dropDownButton.offsetWidth; const scaleFactor = window.devicePixelRatio >= 1.5 ? ' scaleY(0.75)' : ''; this._tabSlider.style.transform = 'translateX(' + left + 'px)' + scaleFactor; this._tabSlider.style.width = sliderWidth + 'px'; if (this._tabSlider.parentElement !== this._headerContentsElement) this._headerContentsElement.appendChild(this._tabSlider); } /** * @param {!UI.TabbedPaneTab} tab */ _hideTab(tab) { tab.tabElement.removeAttribute('tabIndex'); tab.tabElement.classList.remove('selected'); tab.tabElement.setAttribute('aria-selected', 'false'); tab.view.detach(); } /** * @override * @return {!Array.<!Element>} */ elementsToRestoreScrollPositionsFor() { return [this._contentElement]; } /** * @param {!UI.TabbedPaneTab} tab * @param {number} index */ _insertBefore(tab, index) { this._tabsElement.insertBefore(tab.tabElement, this._tabsElement.childNodes[index]); const oldIndex = this._tabs.indexOf(tab); this._tabs.splice(oldIndex, 1); if (oldIndex < index) --index; this._tabs.splice(index, 0, tab); this.dispatchEventToListeners(UI.TabbedPane.Events.TabOrderChanged, this._tabs); } /** * @return {!UI.Toolbar} */ leftToolbar() { if (!this._leftToolbar) { this._leftToolbar = new UI.Toolbar('tabbed-pane-left-toolbar'); this._headerElement.insertBefore(this._leftToolbar.element, this._headerElement.firstChild); } return this._leftToolbar; } /** * @return {!UI.Toolbar} */ rightToolbar() { if (!this._rightToolbar) { this._rightToolbar = new UI.Toolbar('tabbed-pane-right-toolbar'); this._headerElement.appendChild(this._rightToolbar.element); } return this._rightToolbar; } /** * @param {boolean} allow * @param {boolean=} automatic */ setAllowTabReorder(allow, automatic) { this._allowTabReorder = allow; this._automaticReorder = automatic; } /** * @param {!Event} event */ _keyDown(event) { if (!this._currentTab) return; let nextTabElement = null; switch (event.key) { case 'ArrowUp': case 'ArrowLeft': nextTabElement = this._currentTab.tabElement.previousElementSibling; if (!nextTabElement && !this._dropDownButton.parentElement) nextTabElement = this._currentTab.tabElement.parentElement.lastElementChild; break; case 'ArrowDown': case 'ArrowRight': nextTabElement = this._currentTab.tabElement.nextElementSibling; if (!nextTabElement && !this._dropDownButton.parentElement) nextTabElement = this._currentTab.tabElement.parentElement.firstElementChild; break; case 'Enter': case 'Space': this._currentTab.view.focus(); return; default: return; } if (!nextTabElement) { this._dropDownButton.click(); return; } const tab = this._tabs.find(tab => tab.tabElement === nextTabElement); this.selectTab(tab.id, true); nextTabElement.focus(); } }; /** @enum {symbol} */ UI.TabbedPane.Events = { TabSelected: Symbol('TabSelected'), TabClosed: Symbol('TabClosed'), TabOrderChanged: Symbol('TabOrderChanged') }; /** * @unrestricted */ UI.TabbedPaneTab = class { /** * @param {!UI.TabbedPane} tabbedPane * @param {string} id * @param {string} title * @param {boolean} closeable * @param {!UI.Widget} view * @param {string=} tooltip */ constructor(tabbedPane, id, title, closeable, view, tooltip) { this._closeable = closeable; this._tabbedPane = tabbedPane; this._id = id; this._title = title; this._tooltip = tooltip; this._view = view; this._shown = false; /** @type {number} */ this._measuredWidth; /** @type {!Element|undefined} */ this._tabElement; /** @type {?Element} */ this._iconContainer = null; } /** * @return {string} */ get id() { return this._id; } /** * @return {string} */ get title() { return this._title; } /** * @param {string} title */ set title(title) { if (title === this._title) return; this._title = title; if (this._titleElement) this._titleElement.textContent = title; delete this._measuredWidth; } /** * @return {boolean} */ isCloseable() { return this._closeable; } /** * @param {?UI.Icon} icon */ _setIcon(icon) { this._icon = icon; if (this._tabElement) this._createIconElement(this._tabElement, this._titleElement, false); delete this._measuredWidth; } /** * @param {string} className * @param {boolean=} force * @return {boolean} */ _toggleClass(className, force) { const element = this.tabElement; const hasClass = element.classList.contains(className); if (hasClass === force) return false; element.classList.toggle(className, force); delete this._measuredWidth; return true; } /** * @return {!UI.Widget} */ get view() { return this._view; } /** * @param {!UI.Widget} view */ set view(view) { this._view = view; } /** * @return {string|undefined} */ get tooltip() { return this._tooltip; } /** * @param {string|undefined} tooltip */ set tooltip(tooltip) { this._tooltip = tooltip; if (this._titleElement) this._titleElement.title = tooltip || ''; } /** * @return {!Element} */ get tabElement() { if (!this._tabElement) this._tabElement = this._createTabElement(false); return this._tabElement; } /** * @return {number} */ width() { return this._width; } /** * @param {number} width */ setWidth(width) { this.tabElement.style.width = width === -1 ? '' : (width + 'px'); this._width = width; } /** * @param {!UI.TabbedPaneTabDelegate} delegate */ setDelegate(delegate) { this._delegate = delegate; } /** * @param {!Element} tabElement * @param {!Element} titleElement * @param {boolean} measuring */ _createIconElement(tabElement, titleElement, measuring) { if (tabElement.__iconElement) { tabElement.__iconElement.remove(); tabElement.__iconElement = null; } if (!this._icon) return; const iconContainer = createElementWithClass('span', 'tabbed-pane-header-tab-icon'); const iconNode = measuring ? this._icon.cloneNode(true) : this._icon; iconContainer.appendChild(iconNode); tabElement.insertBefore(iconContainer, titleElement); tabElement.__iconElement = iconContainer; } /** * @param {boolean} measuring * @return {!Element} */ _createTabElement(measuring) { const tabElement = createElementWithClass('div', 'tabbed-pane-header-tab'); tabElement.id = 'tab-' + this._id; UI.ARIAUtils.markAsTab(tabElement); UI.ARIAUtils.setSelected(tabElement, false); const titleElement = tabElement.createChild('span', 'tabbed-pane-header-tab-title'); titleElement.textContent = this.title; titleElement.title = this.tooltip || ''; this._createIconElement(tabElement, titleElement, measuring); if (!measuring) this._titleElement = titleElement; if (this._closeable) { tabElement.createChild('div', 'tabbed-pane-close-button', 'dt-close-button').gray = true; tabElement.classList.add('closeable'); } if (measuring) { tabElement.classList.add('measuring'); } else { tabElement.addEventListener('click', this._tabClicked.bind(this), false); tabElement.addEventListener('auxclick', this._tabClicked.bind(this), false); tabElement.addEventListener('mousedown', this._tabMouseDown.bind(this), false); tabElement.addEventListener('mouseup', this._tabMouseUp.bind(this), false); tabElement.addEventListener('contextmenu', this._tabContextMenu.bind(this), false); if (this._tabbedPane._allowTabReorder) { UI.installDragHandle( tabElement, this._startTabDragging.bind(this), this._tabDragging.bind(this), this._endTabDragging.bind(this), '-webkit-grabbing', 'pointer', 200); } } return tabElement; } /** * @param {!Event} event */ _tabClicked(event) { const middleButton = event.button === 1; const shouldClose = this._closeable && (middleButton || event.target.classList.contains('tabbed-pane-close-button')); if (!shouldClose) { this._tabbedPane.focus(); return; } this._closeTabs([this.id]); event.consume(true); } /** * @param {!Event} event */ _tabMouseDown(event) { if (event.target.classList.contains('tabbed-pane-close-button') || event.button === 1) return; this._tabbedPane.selectTab(this.id, true); } /** * @param {!Event} event */ _tabMouseUp(event) { // This is needed to prevent middle-click pasting on linux when tabs are clicked. if (event.button === 1) event.consume(true); } /** * @param {!Array.<string>} ids */ _closeTabs(ids) { if (this._delegate) { this._delegate.closeTabs(this._tabbedPane, ids); return; } this._tabbedPane.closeTabs(ids, true); } _tabContextMenu(event) { /** * @this {UI.TabbedPaneTab} */ function close() { this._closeTabs([this.id]); } /** * @this {UI.TabbedPaneTab} */ function closeOthers() { this._closeTabs(this._tabbedPane.otherTabs(this.id)); } /** * @this {UI.TabbedPaneTab} */ function closeAll() { this._closeTabs(this._tabbedPane.allTabs()); } /** * @this {UI.TabbedPaneTab} */ function closeToTheRight() { this._closeTabs(this._tabbedPane._tabsToTheRight(this.id)); } const contextMenu = new UI.ContextMenu(event); if (this._closeable) { contextMenu.defaultSection().appendItem(Common.UIString('Close'), close.bind(this)); contextMenu.defaultSection().appendItem(Common.UIString('Close others'), closeOthers.bind(this)); contextMenu.defaultSection().appendItem(Common.UIString('Close tabs to the right'), closeToTheRight.bind(this)); contextMenu.defaultSection().appendItem(Common.UIString('Close all'), closeAll.bind(this)); } if (this._delegate) this._delegate.onContextMenu(this.id, contextMenu); contextMenu.show(); } /** * @param {!Event} event * @return {boolean} */ _startTabDragging(event) { if (event.target.classList.contains('tabbed-pane-close-button')) return false; this._dragStartX = event.pageX; this._tabElement.classList.add('dragging'); this._tabbedPane._tabSlider.remove(); return true; } /** * @param {!Event} event */ _tabDragging(event) { const tabElements = this._tabbedPane._tabsElement.childNodes; for (let i = 0; i < tabElements.length; ++i) { let tabElement = tabElements[i]; if (tabElement === this._tabElement) continue; const intersects = tabElement.offsetLeft + tabElement.clientWidth > this._tabElement.offsetLeft && this._tabElement.offsetLeft + this._tabElement.clientWidth > tabElement.offsetLeft; if (!intersects) continue; if (Math.abs(event.pageX - this._dragStartX) < tabElement.clientWidth / 2 + 5) break; if (event.pageX - this._dragStartX > 0) { tabElement = tabElement.nextSibling; ++i; } const oldOffsetLeft = this._tabElement.offsetLeft; this._tabbedPane._insertBefore(this, i); this._dragStartX += this._tabElement.offsetLeft - oldOffsetLeft; break; } if (!this._tabElement.previousSibling && event.pageX - this._dragStartX < 0) { this._tabElement.style.setProperty('left', '0px'); return; } if (!this._tabElement.nextSibling && event.pageX - this._dragStartX > 0) { this._tabElement.style.setProperty('left', '0px'); return; } this._tabElement.style.setProperty('left', (event.pageX - this._dragStartX) + 'px'); } /** * @param {!Event} event */ _endTabDragging(event) { this._tabElement.classList.remove('dragging'); this._tabElement.style.removeProperty('left'); delete this._dragStartX; this._tabbedPane._updateTabSlider(); } }; /** * @interface */ UI.TabbedPaneTabDelegate = function() {}; UI.TabbedPaneTabDelegate.prototype = { /** * @param {!UI.TabbedPane} tabbedPane * @param {!Array.<string>} ids */ closeTabs(tabbedPane, ids) {}, /** * @param {string} tabId * @param {!UI.ContextMenu} contextMenu */ onContextMenu(tabId, contextMenu) {} };