UNPKG

@eclipse-scout/core

Version:
236 lines (206 loc) 8.38 kB
/* * 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; } }