@eclipse-scout/core
Version:
Eclipse Scout runtime
233 lines (200 loc) • 8.16 kB
text/typescript
/*
* 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;
}