@eclipse-scout/core
Version:
Eclipse Scout runtime
236 lines (206 loc) • 8.38 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 {AbstractLayout, arrays, Dimension, EllipsisMenu, graphics, HtmlComponent, HtmlCompPrefSizeOptions, Menu, MenuBar, scout} from '../../index';
export class MenuBarLayout extends AbstractLayout {
collapsed: boolean;
protected _menuBar: MenuBar;
protected _overflowMenuItems: Menu[];
constructor(menuBar: MenuBar) {
super();
this._menuBar = menuBar;
this._overflowMenuItems = [];
this.collapsed = false;
}
override layout($container: JQuery) {
let menuItems = this._menuBar.orderedMenuItems.left.concat(this._menuBar.orderedMenuItems.right);
let visibleMenuItems = this.visibleMenuItems();
let htmlContainer = HtmlComponent.get($container);
let ellipsis = arrays.find(menuItems, menuItem => menuItem.ellipsis) as EllipsisMenu;
this._setFirstLastMenuMarker(visibleMenuItems); // is required to determine available size correctly
this.preferredLayoutSize($container, {
widthHint: htmlContainer.availableSize().width,
undo: false
});
// first set visible to ensure the correct menu gets the tabindex. Therefore, the ellipsis visibility is split.
if (ellipsis && this._overflowMenuItems.length > 0) {
ellipsis.setHidden(false);
}
visibleMenuItems.forEach(menuItem => menuItem._setOverflown(false));
this._overflowMenuItems.forEach(menuItem => menuItem._setOverflown(true));
if (ellipsis && this._overflowMenuItems.length === 0) {
ellipsis.setHidden(true);
}
// remove all separators
this._overflowMenuItems = this._overflowMenuItems.filter(menuItem => !menuItem.separator);
if (ellipsis) {
ellipsis._closePopup();
ellipsis.setChildActions(this._overflowMenuItems);
}
// trigger menu items layout
visibleMenuItems.forEach(menuItem => menuItem.validateLayout());
visibleMenuItems.forEach(menuItem => {
// Make sure open popups are at the correct position after layouting
if (menuItem.popup) {
menuItem.popup.position();
}
});
}
override preferredLayoutSize($container: JQuery, options?: HtmlCompPrefSizeOptions & { undo?: boolean }): Dimension {
this._overflowMenuItems = [];
if (!this._menuBar.visible) {
return new Dimension(0, 0);
}
let visibleMenuItems = this.visibleMenuItems();
let overflowMenuItems = visibleMenuItems.filter(menuItem => {
let overflown = menuItem.overflown;
menuItem._setOverflown(false);
return overflown;
});
let shrunkMenuItems = visibleMenuItems.filter(menuItem => {
let shrunk = !menuItem.textVisible;
this.undoShrink([menuItem]);
return shrunk;
});
let notShrunkMenuItems = [...visibleMenuItems];
let htmlComp = HtmlComponent.get($container);
let prefSize = new Dimension(0, 0);
let prefWidth = Number.MAX_VALUE;
// 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();
}
if (prefWidth <= 0) {
// shortcut for minimum size.
prefSize = this._minSize(visibleMenuItems);
} else {
prefSize = this._prefSize(visibleMenuItems);
if (prefSize.width > prefWidth) {
this.shrink(visibleMenuItems);
}
prefSize = this._prefSizeWithOverflow(visibleMenuItems, prefWidth);
}
if (scout.nvl(options.undo, true)) {
// Reset state
this.undoOverflow(overflowMenuItems);
this.undoShrink(notShrunkMenuItems);
this.shrink(shrunkMenuItems);
}
return prefSize.add(htmlComp.insets());
}
/**
* Moves menu items into _overflowMenuItems until {@link prefSize.width} is smaller than prefWidth.
* The moved menu items will be removed from the given visibleMenuItems parameter.
* @returns the calculated preferred size
*/
protected _prefSizeWithOverflow(visibleMenuItems: Menu[], prefWidth: number): Dimension {
let overflowableIndexes = [];
visibleMenuItems.forEach((menuItem, index) => {
if (menuItem.stackable) {
overflowableIndexes.push(index);
}
});
let overflowIndex = -1;
this._setFirstLastMenuMarker(visibleMenuItems);
let prefSize = this._prefSize(visibleMenuItems);
while (prefSize.width > prefWidth && overflowableIndexes.length > 0) {
if (this._menuBar.ellipsisPosition === MenuBar.EllipsisPosition.RIGHT) {
overflowIndex = overflowableIndexes.splice(-1)[0];
} else {
overflowIndex = overflowableIndexes.splice(0, 1)[0] - this._overflowMenuItems.length;
}
this._overflowMenuItems.splice(0, 0, visibleMenuItems[overflowIndex]);
visibleMenuItems.splice(overflowIndex, 1);
this._setFirstLastMenuMarker(visibleMenuItems);
prefSize = this._prefSize(visibleMenuItems);
}
return prefSize;
}
protected _minSize(visibleMenuItems: Menu[]): Dimension {
let minVisibleMenuItems = visibleMenuItems.filter(menuItem => !menuItem.stackable);
this.shrink(visibleMenuItems);
this._setFirstLastMenuMarker(minVisibleMenuItems, true);
return this._prefSize(minVisibleMenuItems, true);
}
protected _prefSize(menuItems: Menu[], considerEllipsis?: boolean): Dimension {
let prefSize = new Dimension(0, 0);
considerEllipsis = scout.nvl(considerEllipsis, this._overflowMenuItems.length > 0);
this._setFirstLastMenuMarker(menuItems, considerEllipsis);
menuItems.forEach(menuItem => {
let itemSize = new Dimension(0, 0);
if (menuItem.ellipsis) {
if (considerEllipsis) {
itemSize = this._menuItemSize(menuItem);
}
} else {
itemSize = this._menuItemSize(menuItem);
}
prefSize.height = Math.max(prefSize.height, itemSize.height);
prefSize.width += itemSize.width;
});
return prefSize;
}
protected _menuItemSize(menuItem: Menu): Dimension {
let classList = menuItem.$container.attr('class');
menuItem.$container.removeClass('overflown');
menuItem.$container.removeClass('hidden');
menuItem.htmlComp.invalidateLayout();
let prefSize = menuItem.htmlComp.prefSize({
useCssSize: true,
exact: true
}).add(graphics.margins(menuItem.$container));
menuItem.$container.attrOrRemove('class', classList);
return prefSize;
}
protected _setFirstLastMenuMarker(visibleMenuItems: Menu[], considerEllipsis?: boolean) {
let menuItems = visibleMenuItems;
considerEllipsis = scout.nvl(considerEllipsis, this._overflowMenuItems.length > 0);
// reset
this._menuBar.orderedMenuItems.all.forEach(menuItem => menuItem.$container.removeClass('first last'));
// set first and last
if (!considerEllipsis) {
// remove ellipsis
menuItems = menuItems.filter(menuItem => !menuItem.ellipsis);
}
if (menuItems.length > 0) {
menuItems[0].$container.addClass('first');
menuItems[menuItems.length - 1].$container.addClass('last');
}
}
undoOverflow(overflowMenuItems: Menu[]) {
overflowMenuItems.forEach(menuItem => menuItem._setOverflown(true));
}
/**
* Makes the text invisible of all shrinkable menus with an icon
*/
shrink(menus: Menu[]) {
menus.forEach(menu => {
menu.htmlComp.suppressInvalidate = true;
menu.shrink();
menu.htmlComp.suppressInvalidate = false;
});
}
undoShrink(menus: Menu[]) {
menus.forEach(menu => {
menu.htmlComp.suppressInvalidate = true;
menu.undoShrink();
menu.htmlComp.suppressInvalidate = false;
});
}
visibleMenuItems(): Menu[] {
return this._menuBar.orderedMenuItems.all.filter(menuItem => menuItem.visible);
}
/* --- STATIC HELPERS ------------------------------------------------------------- */
static size(htmlMenuBar: HtmlComponent, containerSize: Dimension): Dimension {
let menuBarSize = htmlMenuBar.prefSize({widthHint: containerSize.width});
menuBarSize.width = containerSize.width;
menuBarSize = menuBarSize.subtract(htmlMenuBar.margins());
return menuBarSize;
}
}