@eclipse-scout/core
Version:
Eclipse Scout runtime
312 lines (264 loc) • 8.79 kB
text/typescript
/*
* Copyright (c) 2010, 2025 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 {
arrays, EllipsisMenu, EnumObject, EventHandler, HtmlComponent, InitModelOf, KeyStrokeContext, ObjectIdProvider, PropertyChangeEvent, scout, SomeRequired, strings, Tab, TabAreaEventMap, TabAreaLayout, TabAreaLeftKeyStroke, TabAreaModel,
TabAreaRightKeyStroke, TabBox, TabItem, Widget
} from '../../../index';
export type TabAreaStyle = EnumObject<typeof TabArea.DisplayStyle>;
export class TabArea extends Widget implements TabAreaModel {
declare model: TabAreaModel;
declare initModel: SomeRequired<this['model'], 'parent' | 'tabBox'>;
declare eventMap: TabAreaEventMap;
declare self: TabArea;
tabBox: TabBox;
tabs: Tab[];
displayStyle: TabAreaStyle;
hasSubLabel: boolean;
selectedTab: Tab;
ellipsis: EllipsisMenu;
$selectionMarker: JQuery;
protected _tabItemPropertyChangeHandler: EventHandler<PropertyChangeEvent>;
protected _tabPropertyChangeHandler: EventHandler<PropertyChangeEvent>;
constructor() {
super();
this.tabBox = null;
this.tabs = [];
this.displayStyle = TabArea.DisplayStyle.DEFAULT;
this.hasSubLabel = false;
this.selectedTab = null;
this._tabItemPropertyChangeHandler = this._onTabItemPropertyChange.bind(this);
this._tabPropertyChangeHandler = this._onTabPropertyChange.bind(this);
this.ellipsis = null;
this.$selectionMarker = null;
}
static DisplayStyle = {
DEFAULT: 'default',
SPREAD_EVEN: 'spreadEven'
} as const;
protected override _init(options: InitModelOf<this>) {
super._init(options);
this.tabBox = options.tabBox;
this.ellipsis = scout.create(EllipsisMenu, {
parent: this,
cssClass: 'overflow-tab-item unfocusable',
iconId: null,
inheritAccessibility: false,
text: '0' // Initialize with the normal value to prevent unnecessary layout invalidation by the TabAreaLayout if ellipsis menus is not visible
});
}
protected override _createKeyStrokeContext(): KeyStrokeContext {
return new KeyStrokeContext();
}
protected override _initKeyStrokeContext() {
super._initKeyStrokeContext();
this.keyStrokeContext.registerKeyStrokes([
new TabAreaLeftKeyStroke(this),
new TabAreaRightKeyStroke(this)
]);
}
protected override _render() {
this.$container = this.$parent.appendDiv('tab-area');
this.htmlComp = HtmlComponent.install(this.$container, this.session);
this.htmlComp.setLayout(new TabAreaLayout(this));
this.ellipsis.render(this.$container);
this.$selectionMarker = this.$container.appendDiv('selection-marker');
}
protected override _renderProperties() {
super._renderProperties();
this._renderTabs();
this._renderSelectedTab();
this._renderHasSubLabel();
this._renderDisplayStyle();
}
protected override _remove() {
super._remove();
this._removeTabs();
}
setSelectedTabItem(tabItem: TabItem) {
this.setSelectedTab(this.getTabForItem(tabItem));
}
getTabForItem(tabItem: TabItem): Tab {
return arrays.find(this.tabs, tab => tab.tabItem === tabItem);
}
setSelectedTab(tab: Tab) {
this.setProperty('selectedTab', tab);
}
protected _setSelectedTab(tab: Tab) {
if (this.selectedTab) {
this.selectedTab.setSelected(false);
}
if (tab) {
tab.setSelected(true);
}
this._setProperty('selectedTab', tab);
this._setTabbableItem(tab);
}
protected _renderSelectedTab() {
// force a re-layout in case the selected tab is overflown. The layout will ensure the selected tab is visible.
if (this.selectedTab && this.selectedTab.overflown) {
this.invalidateLayoutTree();
}
}
isTabItemFocused(tabItem: TabItem): boolean {
return this.getTabForItem(tabItem).isFocused();
}
focusTabItem(tabItem: TabItem): boolean {
return this.focusTab(this.getTabForItem(tabItem));
}
focusTab(tab: Tab): boolean {
return tab.focus();
}
setTabItems(tabItems: TabItem[]) {
this.setProperty('tabs', tabItems);
this._updateHasSubLabel();
this.invalidateLayoutTree();
}
protected _setTabs(tabItems: TabItem[]) {
let tabsToRemove = this.tabs.slice(),
tabs = tabItems.map(tabItem => {
let tab = this.getTabForItem(tabItem);
if (!tab) {
tab = scout.create(Tab, {
parent: this,
tabItem: tabItem
});
tabItem.on('propertyChange', this._tabItemPropertyChangeHandler);
tab.on('propertyChange', this._tabPropertyChangeHandler);
} else {
arrays.remove(tabsToRemove, tab);
}
return tab;
});
// un-register model listeners
tabsToRemove.forEach(tab => {
tab.tabItem.off('propertyChange', this._tabItemPropertyChangeHandler);
});
this._removeTabs(tabsToRemove);
this._setProperty('tabs', tabs);
}
protected _renderTabs() {
this.tabs.slice().reverse().forEach((tab, index, items) => {
if (!tab.rendered) {
tab.render();
}
tab.$container
.on('blur', this._onTabItemBlur.bind(this))
.on('focus', this._onTabItemFocus.bind(this));
tab.$container.prependTo(this.$container);
tab.$container
.on('blur', this._onTabItemBlur.bind(this))
.on('focus', this._onTabItemFocus.bind(this));
});
}
protected _removeTabs(tabs?: Tab[]) {
tabs = tabs || this.tabs;
tabs.forEach(tab => {
tab.remove();
});
}
setDisplayStyle(displayStyle: TabAreaStyle) {
this.setProperty('displayStyle', displayStyle);
}
protected _renderDisplayStyle() {
this.$container.toggleClass('spread-even', this.displayStyle === TabArea.DisplayStyle.SPREAD_EVEN);
this.invalidateLayoutTree();
}
protected _onTabItemFocus() {
this.setFocused(true);
}
protected _onTabItemBlur() {
this.setFocused(false);
}
protected _updateHasSubLabel() {
let items = this.visibleTabs();
this._setHasSubLabel(items.some(item => {
return strings.hasText(item.subLabel);
}));
}
visibleTabs(): Tab[] {
return this.tabs.filter(tab => tab.visible);
}
protected _setHasSubLabel(hasSubLabel: boolean) {
if (this.hasSubLabel === hasSubLabel) {
return;
}
this._setProperty('hasSubLabel', hasSubLabel);
if (this.rendered) {
this._renderHasSubLabel();
}
}
protected _renderHasSubLabel() {
this.$container.toggleClass('has-sub-label', this.hasSubLabel);
// Invalidate other tabs as well because the class has an impact on their size, too
this.visibleTabs().forEach(tab => tab.invalidateLayout());
this.invalidateLayoutTree();
}
selectNextTab(focusTab: boolean) {
this._moveSelectionHorizontal(true, focusTab);
}
selectPreviousTab(focusTab: boolean) {
this._moveSelectionHorizontal(false, focusTab);
}
protected _moveSelectionHorizontal(directionRight: boolean, focusTab: boolean) {
let tabItems = this.tabs.slice(),
$focusedElement = this.$container.activeElement(),
selectNext = false;
if (!directionRight) {
tabItems.reverse();
selectNext = $focusedElement[0] === this.ellipsis.$container[0];
}
tabItems.forEach(function(item, index) {
if (selectNext && item.visible && !item.overflown) {
this.setSelectedTab(item);
this._setTabbableItem(item);
if (focusTab) {
item.focus();
}
selectNext = false;
return;
}
if ($focusedElement[0] === item.$container[0]) {
selectNext = true;
}
}, this);
if (directionRight && selectNext && this.ellipsis.isTabTarget()) {
this._setTabbableItem(this.ellipsis);
if (focusTab) {
this.ellipsis.focus();
}
}
}
protected _setTabbableItem(tab: Tab | EllipsisMenu) {
let tabs = this.tabs;
if (tab) {
// clear old tabbable
this.ellipsis.setTabbable(false);
tabs.forEach(item => {
item.setTabbable(false);
});
tab.setTabbable(true);
}
}
protected _onTabPropertyChange(event: PropertyChangeEvent<any, Tab>) {
if (event.propertyName === 'selected') {
this.setSelectedTab(event.source);
}
}
protected _onTabItemPropertyChange(event: PropertyChangeEvent) {
if (event.propertyName === 'visible') {
this._updateHasSubLabel();
this.invalidateLayoutTree();
}
if (event.propertyName === 'subLabel') {
this._updateHasSubLabel();
}
}
}
ObjectIdProvider.uuidPathSkipWidgets.add(TabArea);