@eclipse-scout/core
Version:
Eclipse Scout runtime
312 lines (268 loc) • 9.11 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, Dimension, EllipsisMenu, graphics, HtmlCompPrefSizeOptions, Menu, MenuBox, menus as menuUtil} from '../../index';
export class MenuBoxLayout extends AbstractLayout {
menuBox: MenuBox;
/** References to prevent too many DOM updates */
firstMenu: Menu;
lastMenu: Menu;
protected _ellipsis: EllipsisMenu;
constructor(menuBox: MenuBox) {
super();
this.menuBox = menuBox;
this.firstMenu = null;
this.lastMenu = null;
}
override layout($container: JQuery) {
let htmlContainer = this.menuBox.htmlComp,
containerSize = htmlContainer.size(),
menus = this.visibleMenus(),
menusWidth = 0;
// Make sure open popups are at the correct position after layouting
this.menuBox.session.layoutValidator.schedulePostValidateFunction(() => {
menus.forEach(menu => {
if (menu.popup) {
menu.popup.position();
}
});
});
this.updateFirstAndLastMenuMarker(menus);
this.undoCollapse(menus);
this.undoCompact(menus);
this.undoShrink(menus);
menusWidth = this.actualPrefSize(menus).width;
if (menusWidth <= containerSize.width) {
// OK, every menu fits into container
return;
}
// Menus don't fit
// First approach: Set menuBox into compact mode
this.compact(menus);
menusWidth = this.actualPrefSize(menus).width;
if (menusWidth <= containerSize.width) {
// OK, every menu fits into container
return;
}
// Second approach: Make text invisible and only show the icon (if available)
this.shrink(menus);
menusWidth = this.actualPrefSize(menus).width;
if (menusWidth <= containerSize.width) {
// OK, every menu fits into container
return;
}
// Third approach: Create ellipsis and move overflown menus into it
this.collapse(containerSize, menusWidth);
}
override preferredLayoutSize($container: JQuery, options?: HtmlCompPrefSizeOptions): Dimension {
let menus = this.visibleMenus();
this.updateFirstAndLastMenuMarker(menus);
this.undoCollapse(menus);
this.undoCompact(menus);
this.undoShrink(menus);
return this.actualPrefSize();
}
compact(menus?: Menu[]) {
this.menuBox.htmlComp.suppressInvalidate = true;
this.menuBox.makeCompact();
this.menuBox.htmlComp.suppressInvalidate = false;
this.compactMenus(menus);
}
undoCompact(menus?: Menu[]) {
this.menuBox.htmlComp.suppressInvalidate = true;
this.menuBox.undoMakeCompact();
this.menuBox.htmlComp.suppressInvalidate = false;
this.undoCompactMenus(menus);
}
/**
* Sets all menus into compact mode.
*/
compactMenus(menus?: Menu[]) {
menus = menus || this.visibleMenus();
menus.forEach(menu => {
menu.htmlComp.suppressInvalidate = true;
menu.makeCompact();
menu.htmlComp.suppressInvalidate = false;
});
if (this._ellipsis) {
this._ellipsis.setCompact(true);
}
}
/**
* Restores to the previous state of the compact property.
*/
undoCompactMenus(menus?: Menu[]) {
menus = menus || this.visibleMenus();
menus.forEach(menu => {
menu.htmlComp.suppressInvalidate = true;
menu.undoMakeCompact();
menu.htmlComp.suppressInvalidate = false;
});
if (this._ellipsis) {
this._ellipsis.setCompact(false);
}
}
shrink(menus?: Menu[]) {
this.shrinkMenus(menus);
}
/**
* Makes the text invisible of all menus with an icon.
*/
shrinkMenus(menus?: Menu[]) {
menus = menus || this.visibleMenus();
menus.forEach(menu => {
menu.htmlComp.suppressInvalidate = true;
menu.shrink();
menu.htmlComp.suppressInvalidate = false;
});
}
undoShrink(menus?: Menu[]) {
this.undoShrinkMenus(menus);
}
undoShrinkMenus(menus?: Menu[]) {
menus = menus || this.visibleMenus();
menus.forEach(menu => {
menu.htmlComp.suppressInvalidate = true;
menu.undoShrink();
menu.htmlComp.suppressInvalidate = false;
});
}
collapse(containerSize: Dimension, menusWidth: number) {
this._createAndRenderEllipsis(this.menuBox.$container);
let collapsedMenus = this._moveOverflowMenusIntoEllipsis(containerSize, menusWidth);
this.updateFirstAndLastMenuMarker(collapsedMenus);
}
/**
* Undoes the collapsing by removing ellipsis and rendering non-rendered menus.
*/
undoCollapse(menus?: Menu[]) {
menus = menus || this.visibleMenus();
this._destroyEllipsis();
this._removeMenusFromEllipsis(menus);
}
protected _createAndRenderEllipsis($container: JQuery) {
let ellipsis = menuUtil.createEllipsisMenu({
parent: this.menuBox,
hidden: false,
compact: this.menuBox.compact
});
ellipsis.uiCssClass = this.menuBox.uiMenuCssClass;
ellipsis.render($container);
this._ellipsis = ellipsis;
}
protected _destroyEllipsis() {
if (this._ellipsis) {
this._ellipsis.destroy();
this._ellipsis = null;
}
}
/**
* Moves every menu which doesn't fit into the container into the ellipsis menu.
* Returns the list of "surviving" menus (with the ellipsis menu being the last element).
*/
protected _moveOverflowMenusIntoEllipsis(containerSize: Dimension, menusWidth: number): Menu[] {
let collapsedMenus: Menu[] = [this._ellipsis];
let ellipsisSize = graphics.size(this._ellipsis.$container, true);
menusWidth += ellipsisSize.width;
this.visibleMenus().slice().reverse().forEach(menu => {
if (menusWidth > containerSize.width) {
// Menu does not fit -> move to ellipsis menu
let menuSize = graphics.size(menu.$container, true);
menusWidth -= menuSize.width;
menuUtil.moveMenuIntoEllipsis(menu, this._ellipsis);
} else {
collapsedMenus.unshift(menu); // add as first element
}
});
return collapsedMenus;
}
protected _removeMenusFromEllipsis(menus?: Menu[]) {
menus = menus || this.visibleMenus();
menus.forEach(menu => menuUtil.removeMenuFromEllipsis(menu, this.menuBox.$container));
}
actualPrefSize(menus?: Menu[]): Dimension {
menus = menus || this.visibleMenus();
let menusWidth = this._menusWidth(menus);
let prefSize = graphics.prefSize(this.menuBox.$container);
prefSize.width = menusWidth + this.menuBox.htmlComp.insets().horizontal();
// If the value is fractional (e.g. 310.00000762939453) and the browser rounds it down when the size is set to the menu box,
// the prefSize (menusWidth in layout()) will always be bigger than the containerSize
// -> round up pref size to prevent this
return prefSize.ceil();
}
/**
* @returns the current width of all menus incl. the ellipsis
*/
protected _menusWidth(menus?: Menu[]): number {
let menusWidth = 0;
let size = menu => graphics.size(menu.htmlComp.$comp, {includeMargin: true, exact: true});
menus = menus || this.visibleMenus();
menus.forEach(menu => {
if (menu.rendered) {
menusWidth += size(menu).width;
}
});
if (this._ellipsis) {
menusWidth += size(this._ellipsis).width;
}
return menusWidth;
}
compactPrefSize(menus?: Menu[]): Dimension {
menus = menus || this.visibleMenus();
this.updateFirstAndLastMenuMarker(menus);
this.undoCollapse(menus);
this.undoShrink(menus);
this.compact(menus);
return this.actualPrefSize();
}
shrinkPrefSize(menus?: Menu[]): Dimension {
menus = menus || this.visibleMenus();
this.updateFirstAndLastMenuMarker(menus);
this.undoCollapse(menus);
this.compact(menus);
this.shrink(menus);
return this.actualPrefSize();
}
visibleMenus(): Menu[] {
return this.menuBox.menus.filter(menu => menu.visible);
}
updateFirstAndLastMenuMarker(menus?: Menu[]) {
// Find first and last rendered menu
let firstMenu = null;
let lastMenu = null;
(menus || []).forEach(menu => {
if (menu.rendered) {
if (!firstMenu) {
firstMenu = menu;
}
lastMenu = menu;
}
});
// Check if first or last menu has changed (prevents unnecessary DOM updates)
if (firstMenu !== this.firstMenu || lastMenu !== this.lastMenu) {
// Remove existing markers
if (this.firstMenu && this.firstMenu.rendered) {
this.firstMenu.$container.removeClass('first');
}
if (this.lastMenu && this.lastMenu.rendered) {
this.lastMenu.$container.removeClass('last');
}
// Remember found menus
this.firstMenu = firstMenu;
this.lastMenu = lastMenu;
// Add markers to found menus
if (this.firstMenu) {
this.firstMenu.$container.addClass('first');
}
if (this.lastMenu) {
this.lastMenu.$container.addClass('last');
}
}
}
}