UNPKG

@eclipse-scout/core

Version:
233 lines (200 loc) 8.16 kB
/* * Copyright (c) 2010, 2023 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 * which is available at https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 */ import {AbstractLayout, Dimension, EllipsisMenu, EventHandler, graphics, HtmlComponent, HtmlCompPrefSizeOptions, Menu, MenuModel, PropertyChangeEvent, scout, Tab, TabArea} from '../../../index'; import $ from 'jquery'; export class TabAreaLayout extends AbstractLayout { tabArea: TabArea; overflowTabs: Tab[]; visibleTabs: Tab[]; protected _tabAreaPropertyChangeHandler: EventHandler<PropertyChangeEvent>; constructor(tabArea: TabArea) { super(); this.tabArea = tabArea; this.overflowTabs = []; this.visibleTabs = []; this._tabAreaPropertyChangeHandler = this._onTabAreaPropertyChange.bind(this); this.tabArea.on('propertyChange', this._tabAreaPropertyChangeHandler); this.tabArea.one('remove', () => { this.tabArea.off('propertyChange', this._tabAreaPropertyChangeHandler); }); } override layout($container: JQuery) { let htmlContainer = HtmlComponent.get($container), containerSize = htmlContainer.availableSize().subtract(htmlContainer.insets()); // compute visible and overflown tabs this.preferredLayoutSize($container, { widthHint: containerSize.width }); this._layoutSelectionMarker(); } protected _layoutSelectionMarker() { let $selectionMarker = this.tabArea.$selectionMarker, selectedTab = this.tabArea.selectedTab, selectedItemBounds; if (selectedTab) { $selectionMarker.setVisible(true); selectedItemBounds = graphics.bounds(selectedTab.$container); $selectionMarker.cssLeft(selectedItemBounds.x); $selectionMarker.cssWidth(selectedItemBounds.width); } else { $selectionMarker.setVisible(false); } } protected _updateEllipsis() { let ellipsis = this.tabArea.ellipsis; ellipsis.setHidden(this.overflowTabs.length < 1); ellipsis.setText(this.overflowTabs.length + ''); this.visibleTabs.forEach(tab => tab.setOverflown(false)); this.overflowTabs.forEach(tab => tab.setOverflown(true)); ellipsis.childActions.forEach(menu => menu.destroy()); ellipsis.setChildActions(this.overflowTabs.map(tab => { let menu = scout.create(EllipsisTabMenu, { parent: ellipsis, tab: tab, // The tab reference is not used by Scout itself but may be used by others to find the menu belonging to a tab text: tab.label, visible: tab.visible }); menu.on('action', event => { $.log.isDebugEnabled() && $.log.debug('(TabAreaLayout#_onClickEllipsis) tab=' + tab); // first close popup to ensure the focus is handled in the correct focus context. ellipsis.popup.close(); tab.select(); tab.focus(); }); return menu; })); } override preferredLayoutSize($container: JQuery, options: HtmlCompPrefSizeOptions): Dimension { let htmlComp = HtmlComponent.get($container), prefSize = new Dimension(0, 0), prefWidth = Number.MAX_VALUE; this.visibleTabs = this.tabArea.visibleTabs(); let overflowableIndexes = this.visibleTabs.map((tab, index) => { if (tab.selected) { return -1; } return index; }).filter(index => index >= 0); this.overflowTabs = []; // consider avoid falsy 0 in tab-boxes a 0 withHint will be used to calculate the minimum width if (options.widthHint === 0 || options.widthHint) { prefWidth = options.widthHint - htmlComp.insets().horizontal(); } // shortcut for minimum size. if (prefWidth <= 0) { return this._minSize(this.visibleTabs).add(htmlComp.insets()); } this._setFirstLastMarker(this.visibleTabs); this._updateEllipsis(); prefSize = this._prefSize(this.visibleTabs); while (prefSize.width > prefWidth && overflowableIndexes.length > 0) { let overflowIndex = overflowableIndexes.splice(-1)[0]; this.overflowTabs.splice(0, 0, this.visibleTabs[overflowIndex]); this.visibleTabs.splice(overflowIndex, 1); this._setFirstLastMarker(this.visibleTabs); this._updateEllipsis(); // update ellipsis here already so that the prefSize on the next line is correct prefSize = this._prefSize(this.visibleTabs); } // Use the total available space if spreading tabs evenly. if (this.tabArea.displayStyle === TabArea.DisplayStyle.SPREAD_EVEN) { return graphics.prefSize($container, options); } return graphics.exactPrefSize(prefSize.add(htmlComp.insets()), options); } protected _minSize(tabs: Tab[]): Dimension { let visibleTabs = []; this.overflowTabs = tabs.filter(tab => { if (tab.selected) { visibleTabs.push(tab); return false; } return true; }); this.visibleTabs = visibleTabs; this._setFirstLastMarker(visibleTabs); return this._prefSize(visibleTabs); } protected _prefSize(tabs: Tab[], considerEllipsis?: boolean): Dimension { let prefSize = tabs .map(tab => this._tabSize(tab)) .reduce((prefSize, itemSize) => { prefSize.height = Math.max(prefSize.height, itemSize.height); prefSize.width += itemSize.width; return prefSize; }, new Dimension(0, 0)); considerEllipsis = scout.nvl(considerEllipsis, this.overflowTabs.length > 0); if (considerEllipsis) { let ellipsisSize = this._tabSize(this.tabArea.ellipsis); prefSize.height = Math.max(prefSize.height, ellipsisSize.height); prefSize.width += ellipsisSize.width; } return prefSize; } protected _setFirstLastMarker(tabs: Tab[], considerEllipsis?: boolean) { considerEllipsis = scout.nvl(considerEllipsis, this.overflowTabs.length > 0); // reset this.tabArea.tabs.forEach(tab => { tab.htmlComp.$comp.removeClass('first last'); }); this.tabArea.ellipsis.$container.removeClass('first last'); // set first and last if (tabs.length > 0) { tabs[0].$container.addClass('first'); if (considerEllipsis) { this.tabArea.ellipsis.$container.addClass('last'); } else { tabs[tabs.length - 1].$container.addClass('last'); } } } protected _tabSize(item: Tab | EllipsisMenu): Dimension { let htmlComp = item.htmlComp, prefSize, classList = htmlComp.$comp.attr('class'); // temporarily revert display style to default. otherwise the pref size of the tab item will be the size of the container. if (this.tabArea.displayStyle === TabArea.DisplayStyle.SPREAD_EVEN) { this.tabArea.$container.removeClass('spread-even'); } htmlComp.$comp.removeClass('overflown'); htmlComp.$comp.removeClass('hidden'); prefSize = htmlComp.prefSize({ exact: true }).add(graphics.margins(htmlComp.$comp)); if (item instanceof Tab && item.fieldStatus && item.fieldStatus.htmlComp) { let statusOverflownAndHidden = item.overflown && !item.fieldStatus.visible; if (statusOverflownAndHidden) { // overflown tabs have no fieldStatus: explicitly set to visible so that the real consumed space can be computed item.fieldStatus.setVisible(true); } let statusWidth = item.fieldStatus.htmlComp.prefSize({includeMargin: true}).width; if (statusOverflownAndHidden) { // restore item.fieldStatus.setVisible(false); } prefSize.width += statusWidth; } htmlComp.$comp.attrOrRemove('class', classList); if (this.tabArea.displayStyle === TabArea.DisplayStyle.SPREAD_EVEN) { this.tabArea.$container.addClass('spread-even'); } return prefSize; } protected _onTabAreaPropertyChange(event: PropertyChangeEvent) { if (event.propertyName === 'selectedTab') { this._layoutSelectionMarker(); } } } export class EllipsisTabMenu extends Menu implements EllipsisTabMenuModel { declare model: EllipsisTabMenuModel; tab: Tab; } export interface EllipsisTabMenuModel extends MenuModel { tab: Tab; }