UNPKG

golden-layout

Version:
282 lines (240 loc) 12 kB
import { AssertError } from '../errors/internal-error'; import { ComponentItem } from '../items/component-item'; import { LayoutManager } from '../layout-manager'; import { DomConstants } from '../utils/dom-constants'; import { DragListener } from '../utils/drag-listener'; import { numberToPixels, pixelsToNumber } from '../utils/utils'; import { Tab } from './tab'; /** @internal */ export class TabsContainer { // There is one tab per ComponentItem in stack. However they may not be ordered the same private readonly _tabs: Tab[] = []; private readonly _dropdownElement: HTMLElement; private readonly _element: HTMLElement; private _lastVisibleTabIndex = -1; private _dropdownActive = false; get tabs(): Tab[] { return this._tabs; } get tabCount(): number { return this._tabs.length; } get lastVisibleTabIndex(): number { return this._lastVisibleTabIndex; } get element(): HTMLElement { return this._element; } get dropdownElement(): HTMLElement { return this._dropdownElement; } get dropdownActive(): boolean { return this._dropdownActive; } constructor(private _layoutManager: LayoutManager, private _componentRemoveEvent: TabsContainer.ComponentItemRemoveEvent, private _componentFocusEvent: TabsContainer.ComponentItemFocusEvent, private _componentDragStartEvent: TabsContainer.ComponentItemDragStartEvent, private _dropdownActiveChangedEvent: TabsContainer.DropdownActiveChangedEvent, ) { this._element = document.createElement('section'); this._element.classList.add(DomConstants.ClassName.Tabs); this._dropdownElement = document.createElement('section'); this._dropdownElement.classList.add(DomConstants.ClassName.TabDropdownList); this._dropdownElement.style.display = 'none'; } destroy(): void { for (let i = 0; i < this._tabs.length; i++) { this._tabs[i].destroy(); } } /** * Creates a new tab and associates it with a contentItem * @param index - The position of the tab */ createTab(componentItem: ComponentItem, index: number): void { //If there's already a tab relating to the //content item, don't do anything for (let i = 0; i < this._tabs.length; i++) { if (this._tabs[i].componentItem === componentItem) { return; } } const tab = new Tab(this._layoutManager, componentItem, (item) => this.handleTabCloseEvent(item), (item) => this.handleTabFocusEvent(item), (x, y, dragListener, item) => this.handleTabDragStartEvent(x, y, dragListener, item)); if (index === undefined) { index = this._tabs.length; } this._tabs.splice(index, 0, tab); if (index < this._element.childNodes.length) { this._element.insertBefore(tab.element, this._element.childNodes[index]); } else { this._element.appendChild(tab.element); } } removeTab(componentItem: ComponentItem): void { // componentItem cannot be ActiveComponentItem for (let i = 0; i < this._tabs.length; i++) { if (this._tabs[i].componentItem === componentItem) { const tab = this._tabs[i]; tab.destroy(); this._tabs.splice(i, 1); return; } } throw new Error('contentItem is not controlled by this header'); } processActiveComponentChanged(newActiveComponentItem: ComponentItem): void { let activeIndex = -1; for (let i = 0; i < this._tabs.length; i++) { const isActive = this._tabs[i].componentItem === newActiveComponentItem; this._tabs[i].setActive(isActive); if (isActive) { activeIndex = i; } } if (activeIndex < 0) { throw new AssertError('HSACI56632'); } else { if (this._layoutManager.layoutConfig.settings.reorderOnTabMenuClick) { /** * If the tab selected was in the dropdown, move everything down one to make way for this one to be the first. * This will make sure the most used tabs stay visible. */ if (this._lastVisibleTabIndex !== -1 && activeIndex > this._lastVisibleTabIndex) { const activeTab = this._tabs[activeIndex]; for (let j = activeIndex; j > 0; j--) { this._tabs[j] = this._tabs[j - 1]; } this._tabs[0] = activeTab; // updateTabSizes will always be called after this and it will reposition tab elements } } } } /** * Pushes the tabs to the tab dropdown if the available space is not sufficient */ updateTabSizes(availableWidth: number, activeComponentItem: ComponentItem | undefined): void { let dropDownActive = false; const success = this.tryUpdateTabSizes(dropDownActive, availableWidth, activeComponentItem); if (!success) { dropDownActive = true; // this will always succeed this.tryUpdateTabSizes(dropDownActive, availableWidth, activeComponentItem) } if (dropDownActive !== this._dropdownActive) { this._dropdownActive = dropDownActive; this._dropdownActiveChangedEvent(); } } tryUpdateTabSizes(dropdownActive: boolean, availableWidth: number, activeComponentItem: ComponentItem | undefined): boolean { if (this._tabs.length > 0) { if (activeComponentItem === undefined) { throw new Error('non-empty tabs must have active component item'); } let cumulativeTabWidth = 0; let tabOverlapAllowanceExceeded = false; const tabOverlapAllowance = this._layoutManager.layoutConfig.settings.tabOverlapAllowance; const activeIndex = this._tabs.indexOf(activeComponentItem.tab); const activeTab = this._tabs[activeIndex]; this._lastVisibleTabIndex = -1; for (let i = 0; i < this._tabs.length; i++) { const tabElement = this._tabs[i].element; //Put the tab in the tabContainer so its true width can be checked if (tabElement.parentElement !== this._element) { this._element.appendChild(tabElement); } const tabMarginRightPixels = getComputedStyle(activeTab.element).marginRight; const tabMarginRight = pixelsToNumber(tabMarginRightPixels); const tabWidth = tabElement.offsetWidth + tabMarginRight; cumulativeTabWidth += tabWidth; //Include the active tab's width if it isn't already //This is to ensure there is room to show the active tab let visibleTabWidth = 0; if (activeIndex <= i) { visibleTabWidth = cumulativeTabWidth; } else { const activeTabMarginRightPixels = getComputedStyle(activeTab.element).marginRight; const activeTabMarginRight = pixelsToNumber(activeTabMarginRightPixels); visibleTabWidth = cumulativeTabWidth + activeTab.element.offsetWidth + activeTabMarginRight; } // If the tabs won't fit, check the overlap allowance. if (visibleTabWidth > availableWidth) { //Once allowance is exceeded, all remaining tabs go to menu. if (!tabOverlapAllowanceExceeded) { //No overlap for first tab or active tab //Overlap spreads among non-active, non-first tabs let overlap: number; if (activeIndex > 0 && activeIndex <= i) { overlap = (visibleTabWidth - availableWidth) / (i - 1); } else { overlap = (visibleTabWidth - availableWidth) / i; } //Check overlap against allowance. if (overlap < tabOverlapAllowance) { for (let j = 0; j <= i; j++) { const marginLeft = (j !== activeIndex && j !== 0) ? '-' + numberToPixels(overlap) : ''; this._tabs[j].element.style.zIndex = numberToPixels(i - j); this._tabs[j].element.style.marginLeft = marginLeft; } this._lastVisibleTabIndex = i; if (tabElement.parentElement !== this._element) { this._element.appendChild(tabElement); } } else { tabOverlapAllowanceExceeded = true; } } else if (i === activeIndex) { //Active tab should show even if allowance exceeded. (We left room.) tabElement.style.zIndex = 'auto'; tabElement.style.marginLeft = ''; if (tabElement.parentElement !== this._element) { this._element.appendChild(tabElement); } } if (tabOverlapAllowanceExceeded && i !== activeIndex) { if (dropdownActive) { //Tab menu already shown, so we just add to it. tabElement.style.zIndex = 'auto'; tabElement.style.marginLeft = ''; if (tabElement.parentElement !== this._dropdownElement) { this._dropdownElement.appendChild(tabElement); } } else { //We now know the tab menu must be shown, so we have to recalculate everything. return false; } } } else { this._lastVisibleTabIndex = i; tabElement.style.zIndex = 'auto'; tabElement.style.marginLeft = ''; if (tabElement.parentElement !== this._element) { this._element.appendChild(tabElement); } } } } return true; } /** * Shows drop down for additional tabs when there are too many to display. */ showAdditionalTabsDropdown(): void { this._dropdownElement.style.display = ''; } /** * Hides drop down for additional tabs when there are too many to display. */ hideAdditionalTabsDropdown(): void { this._dropdownElement.style.display = 'none'; } private handleTabCloseEvent(componentItem: ComponentItem) { this._componentRemoveEvent(componentItem); } private handleTabFocusEvent(componentItem: ComponentItem) { this._componentFocusEvent(componentItem); } private handleTabDragStartEvent(x: number, y: number, dragListener: DragListener, componentItem: ComponentItem) { this._componentDragStartEvent(x, y, dragListener, componentItem); } } /** @internal */ export namespace TabsContainer { export type ComponentItemRemoveEvent = (this: void, componentItem: ComponentItem) => void; export type ComponentItemFocusEvent = (this: void, componentItem: ComponentItem) => void; export type ComponentItemDragStartEvent = (this: void, x: number, y: number, dragListener: DragListener, componentItem: ComponentItem) => void; export type DropdownActiveChangedEvent = (this: void) => void; }