UNPKG

@openui5/sap.m

Version:

OpenUI5 UI Library sap.m

1,504 lines (1,265 loc) 60.5 kB
/*! * OpenUI5 * (c) Copyright 2026 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ // Provides control sap.m.OverflowToolbar. sap.ui.define([ "sap/ui/core/Theming", "sap/ui/core/library", "./library", "sap/ui/core/Control", "sap/ui/core/Element", "sap/m/ToggleButton", "sap/ui/core/InvisibleText", "sap/m/Toolbar", "sap/m/OverflowToolbarLayoutData", "sap/m/OverflowToolbarAssociativePopover", "sap/m/OverflowToolbarAssociativePopoverControls", 'sap/ui/core/ResizeHandler', "sap/ui/core/IconPool", "sap/ui/Device", "./OverflowToolbarRenderer", "sap/base/Log", "sap/ui/core/Lib", "sap/ui/thirdparty/jquery", "sap/ui/dom/jquery/Focusable" // jQuery Plugin "lastFocusableDomRef" ], function( Theming, coreLibrary, library, Control, Element, ToggleButton, InvisibleText, Toolbar, OverflowToolbarLayoutData, OverflowToolbarAssociativePopover, OverflowToolbarAssociativePopoverControls, ResizeHandler, IconPool, Device, OverflowToolbarRenderer, Log, Library, jQuery ) { "use strict"; // shortcut for sap.m.PlacementType var PlacementType = library.PlacementType; // shortcut for sap.m.ButtonType var ButtonType = library.ButtonType; // shortcut for sap.ui.core.aria.HasPopup var AriaHasPopup = coreLibrary.aria.HasPopup; // shortcut for sap.m.OverflowToolbarPriority var OverflowToolbarPriority = library.OverflowToolbarPriority; /** * Constructor for a new <code>OverflowToolbar</code>. * * @param {string} [sId] ID for the new control, generated automatically if no ID is given * @param {object} [mSettings] Initial settings for the new control * * @class * A container control based on {@link sap.m.Toolbar}, that provides overflow when * its content does not fit in the visible area. * * <h3>Overview</h3> * * The content of the <code>OverflowToolbar</code> moves into the overflow area from * right to left when the available space is not enough in the visible area of * the container. It can be accessed by the user through the overflow button that * opens it in a popover. * * <b>Note:</b> It is recommended that you use <code>OverflowToolbar</code> over * {@link sap.m.Toolbar}, unless you want to avoid overflow in favor of shrinking. * * <h3>Usage</h3> * * Different behavior and priorities can be set for each control inside the * <code>OverflowToolbar</code>, such as certain controls to appear only in the * overflow area or to never move there. For more information, see * {@link sap.m.OverflowToolbarLayoutData} and {@link sap.m.OverflowToolbarPriority}. * * <h3>Overflow Behavior</h3> * By default, only the following controls can move to the overflow area: * * <ul><li>{@link sap.m.Breadcrumbs}</li> * <li>{@link sap.m.Button}</li> * <li>{@link sap.m.CheckBox}</li> * <li>{@link sap.m.ComboBox}</li> * <li>{@link sap.m.DatePicker}</li> * <li>{@link sap.m.DateRangeSelection}</li> * <li>{@link sap.m.DateTimePicker}</li> * <li>{@link sap.m.GenericTag}</li> * <li>{@link sap.m.Input}</li> * <li>{@link sap.m.Label}</li> * <li>{@link sap.m.MenuButton}</li> * <li>{@link sap.m.OverflowToolbarButton}</li> * <li>{@link sap.m.OverflowToolbarToggleButton}</li> * <li>{@link sap.m.SearchField}</li> * <li>{@link sap.m.SegmentedButton}</li> * <li>{@link sap.m.Select}</li> * <li>{@link sap.m.TimePicker}</li> * <li>{@link sap.m.OverflowToolbarTokenizer}</li> * <li>{@link sap.m.ToggleButton}</li> * <li>{@link sap.m.ToolbarSeparator}</li> * <li>{@link sap.ui.comp.smartfield.SmartField}</li> * <li>{@link sap.ui.comp.smartfield.SmartLabel}</li></ul> * * Additionally, any control that implements the {@link sap.m.IOverflowToolbarContent} interface may define * its behavior (most importantly overflow behavior) when placed inside <code>OverflowToolbar</code>. * * <b>Note:</b> The <code>OverflowToolbar</code> is an adaptive container that checks the available * width and hides the part of its content that doesn't fit. It is intended that simple controls, * such as {@link sap.m.Button} and {@link sap.m.Label} are used as content. Embedding other * adaptive container controls (with the exception of {@link sap.m.Breadcrumbs}), results in competition for the available * space - both controls calculate the available space based on the other one's size and both change their * width at the same time, leading to incorrectly distributed space. * * <h3>Responsive behavior</h3> * * The height of the toolbar changes on desktop, tablet, and smartphones. * * @see {@link fiori:https://experience.sap.com/fiori-design-web/toolbar-overview/#overflow-generic Overflow Toolbar} * * @extends sap.m.Toolbar * @implements sap.ui.core.Toolbar,sap.m.IBar * * @author SAP SE * @version 1.146.0 * * @constructor * @public * @since 1.28 * @alias sap.m.OverflowToolbar * */ var OverflowToolbar = Toolbar.extend("sap.m.OverflowToolbar", { metadata: { properties : { /** * Defines whether the <code>OverflowToolbar</code> works in async mode. * * <b>Note:</b> When this property is set to <code>true</code>, the <code>OverflowToolbar</code> * makes its layout recalculations asynchronously. This way it is not blocking the thread * immediately after re-rendering or resizing. However, it may lead to flickering, when there is * a change in the width of the content of the <code>OverflowToolbar</code>. * * @since 1.67 */ asyncMode : {type : "boolean", group : "Behavior", defaultValue : false} }, aggregations: { _overflowButton: {type: "sap.m.ToggleButton", multiple: false, visibility: "hidden"}, _popover: {type: "sap.m.Popover", multiple: false, visibility: "hidden"} }, designtime: "sap/m/designtime/OverflowToolbar.designtime" }, renderer: OverflowToolbarRenderer }); /** * STATIC MEMBERS */ OverflowToolbar.TOGGLE_BUTTON_TOOLTIP = "OVERFLOW_TOOLBAR_TOGGLE_BUTTON_TOOLTIP"; OverflowToolbar.CONTENT_SIZE_TOLERANCE = 1; /** * A shorthand for calling Toolbar.prototype methods * @param {string} sFuncName - the name of the method * @param {any[]} aArguments - the arguments to pass in the form of array * @returns {*} * @private */ OverflowToolbar.prototype._callToolbarMethod = function (sFuncName, aArguments) { return Toolbar.prototype[sFuncName].apply(this, aArguments); }; /** * Initializes the control * @private * @override */ OverflowToolbar.prototype.init = function () { this._callToolbarMethod("init", arguments); // Used to store the previous width of the control to determine if a resize occurred this._iPreviousToolbarWidth = null; // When set to true, the overflow button will be rendered this._bOverflowButtonNeeded = false; // When set to true, changes to the properties of the controls in the toolbar will trigger a recalculation this._bListenForControlPropertyChanges = false; // When set to true, invalidation events will trigger a recalculation this._bListenForInvalidationEvents = false; // When set to true, controls widths, etc... will not be recalculated, because they are already cached this._bControlsInfoCached = false; // When set to true, the recalculation algorithm will bypass an optimization to determine if anything moved from/to the Popover this._bSkipOptimization = false; this._aControlSizes = {}; // A map of control id -> control *optimal* size in pixels; the optimal size is outerWidth for most controls and min-width for spacers this._iFrameRequest = null; // Overflow Button size this._iOverflowToolbarButtonSize = 0; // Overflow Button clone, it helps to calculate correct size of the button this._oOverflowToolbarButtonClone = null; // Width of the content with "NeverOverflow" priority this._iToolbarOnlyContentSize = 0; this._aMovableControls = []; // Controls that can be in the toolbar or Popover this._aToolbarOnlyControls = []; // Controls that can't go to the Popover (inputs, labels, buttons with special layout, etc...) this._aPopoverOnlyControls = []; // Controls that are forced to stay in the Popover (buttons with layout) this._aAllCollections = [this._aMovableControls, this._aToolbarOnlyControls, this._aPopoverOnlyControls]; this.addStyleClass("sapMOTB"); this._fnMediaChangeRef = this._fnMediaChange.bind(this); Device.media.attachHandler(this._fnMediaChangeRef); this._handleKeyNavigationBound = this._handleKeyNavigation.bind(this); }; OverflowToolbar.prototype.exit = function () { var oPopover = this.getAggregation("_popover"); if (oPopover) { oPopover.destroy(); } if (this._oOverflowToolbarButtonClone) { this._oOverflowToolbarButtonClone.destroy(); } if (this._iFrameRequest) { window.cancelAnimationFrame(this._iFrameRequest); this._iFrameRequest = null; } Device.media.detachHandler(this._fnMediaChangeRef); }; /** * This method is a hook for the RenderManager that gets called * during the rendering of child Controls. It allows to add, * remove and update existing accessibility attributes (ARIA) of * those controls. * * @param {sap.ui.core.Control} oElement - The Control that gets rendered by the RenderManager * @param {object} mAriaProps - The mapping of "aria-" prefixed attributes */ OverflowToolbar.prototype.enhanceAccessibilityState = function (oElement, mAriaProps) { Toolbar.prototype.enhanceAccessibilityState.apply(this, arguments); if (oElement === this.getAggregation("_overflowButton")) { this._enhanceOverflowButtonAccessibility(mAriaProps); } }; OverflowToolbar.prototype._enhanceOverflowButtonAccessibility = function (mAriaProps) { var oPopover = this.getAggregation("_popover"), oOverflowButton = this.getAggregation("_overflowButton"); if (!oOverflowButton) { return; } // the overflow toolbar button is better represented as a menu button // because it opens a menu with the overflowed content // because of that the aria-pressed is to be removed and the aria-expanded is to be added mAriaProps.expanded = oOverflowButton.getPressed(); delete mAriaProps.pressed; if (oPopover && oPopover.getDomRef()) { mAriaProps.controls = oPopover.getId(); } }; /** * Sets the <code>asyncMode</code> property. * * @since 1.67 * * @public * @param {boolean} bValue * @return {this} <code>this</code> pointer for chaining */ OverflowToolbar.prototype.setAsyncMode = function(bValue) { // No invalidation is needed return this.setProperty("asyncMode", bValue, true); }; /** * Called before the control is rendered */ OverflowToolbar.prototype.onBeforeRendering = function () { if (!this.getDomRef()) { this._bControlsInfoCached = false; } }; /** * Called after the control is rendered */ OverflowToolbar.prototype.onAfterRendering = function () { this._bInvalidatedAndNotRendered = false; if (this._bContentVisibilityChanged) { this._bControlsInfoCached = false; this._bContentVisibilityChanged = false; } // Unlike toolbar, we don't set flexbox classes here, we rather set them on a later stage only if needed if (this.getAsyncMode()) { this._doLayoutAsync().then(this._applyFocus.bind(this)); } else { this._doLayout(); this._applyFocus(); } //Attach event listened needed for the arrow key navigation if (this.getDomRef()) { this.getDomRef().removeEventListener("keydown", this._handleKeyNavigationBound); this.getDomRef().addEventListener("keydown", this._handleKeyNavigationBound); } }; OverflowToolbar.prototype.onsapfocusleave = function() { this._resetChildControlFocusInfo(); }; OverflowToolbar.prototype.onfocusfail = function(oEvent) { const oFocusLostCtrl = oEvent.srcControl; const oOverflowButton = this._getOverflowButton(); const oDomRef = this.getDomRef(); if (!oDomRef) { return; } if (!oDomRef.contains(oFocusLostCtrl.getDomRef())) { oOverflowButton?.focus(); this._bControlWasFocused = false; this._bOverflowButtonWasFocused = !!oOverflowButton; this.sFocusedChildControlId = ""; } else { const oChildren = this.getContent(); const iPos = oChildren.indexOf(oFocusLostCtrl); if (iPos !== -1) { let oFocusTarget; for (let i = iPos + 1; i < oChildren.length; i++) { const oFocusDomRef = oChildren[i].getFocusDomRef?.(); if (oDomRef.contains(oFocusDomRef) && jQuery.expr.pseudos.sapTabbable(oFocusDomRef)) { oFocusTarget = oChildren[i]; break; } } oFocusTarget ??= oOverflowButton; oFocusTarget?.focus(); this._bControlWasFocused = !oOverflowButton; this._bOverflowButtonWasFocused = !!oOverflowButton; this.sFocusedChildControlId = oFocusTarget === oOverflowButton ? "" : oFocusTarget.getId(); } } }; OverflowToolbar.prototype.setWidth = function(sWidth) { this.setProperty("width", sWidth); this._bResized = true; return this; }; /*********************************************LAYOUT*******************************************************/ /** * For the OverflowToolbar, we need to register resize listeners always, regardless of Flexbox support * @override * @private */ OverflowToolbar.prototype._doLayout = function () { var iWidth; this._recalculateOverflowButtonSize(); iWidth = this.$().is(":visible") ? this.$().width() : 0; // Stop listening for control property changes while calculating the layout to avoid an infinite loop scenario this._bListenForControlPropertyChanges = false; // Stop listening for invalidation events while calculating the layout to avoid an infinite loop scenario this._bListenForInvalidationEvents = false; // Deregister the resize handler to avoid multiple instances of the same code running at the same time this._deregisterToolbarResize(); if (this._iPreviousToolbarWidth !== iWidth) { this._bResized = true; } if (iWidth > 0) { // Cache controls widths and other info, if not done already if (!this._isControlsInfoCached() || (this._bNeedUpdateOnControlsCachedSizes && this._bResized)) { this._cacheControlsInfo(); } // A resize occurred (or was simulated by setting previous width to null to trigger a recalculation) if (this._iPreviousToolbarWidth !== iWidth) { this._iPreviousToolbarWidth = iWidth; this._setControlsOverflowAndShrinking(iWidth); this.fireEvent("_controlWidthChanged"); } } else { this._iPreviousToolbarWidth = iWidth; } // Register the resize handler again after all calculations are done and it's safe to do so // Note: unlike toolbar, we don't call registerResize, but rather registerToolbarResize here, because we handle content change separately this._registerToolbarResize(); // Start listening for property changes on the controls once again this._bListenForControlPropertyChanges = true; // Start listening for invalidation events once again this._bListenForInvalidationEvents = true; this._bResized = false; }; /** * Asynchronous layouting * @private */ OverflowToolbar.prototype._doLayoutAsync = function () { return new Promise(function(resolve, reject) { this._iFrameRequest = window.requestAnimationFrame(function () { this._doLayout(); resolve(); }.bind(this)); }.bind(this)); }; OverflowToolbar.prototype._applyFocus = function () { var oFocusedChildControl, $LastFocusableChildControl = this.$().lastFocusableDomRef(); if (this.sFocusedChildControlId) { oFocusedChildControl = Element.getElementById(this.sFocusedChildControlId); } if (oFocusedChildControl && oFocusedChildControl.getDomRef()){ oFocusedChildControl.focus(); } else if (this._bControlWasFocused) { // If a control of the toolbar was focused, and we're here, then the focused control overflowed, so set the focus to the overflow button this._getOverflowButton().focus(); this._bControlWasFocused = false; this._bOverflowButtonWasFocused = true; } else if (this._bOverflowButtonWasFocused) { if (this._getOverflowButtonNeeded()) { this._getOverflowButton().focus(); } else { // If before invalidation the overflow button was focused, and it's not visible any more, focus the last focusable control $LastFocusableChildControl && $LastFocusableChildControl.focus(); this._bOverflowButtonWasFocused = false; } } }; /** * Preserves info to retain focus on child controls upon invalidation. * @private */ OverflowToolbar.prototype._preserveChildControlFocusInfo = function () { // Preserve focus info var oElement = Element.closestTo(document.activeElement); var sActiveElementId = oElement ? oElement.getId() : null; if (this._getControlsIds().indexOf(sActiveElementId) !== -1) { this._bControlWasFocused = true; this.sFocusedChildControlId = sActiveElementId; } else if (sActiveElementId === this._getOverflowButton().getId()) { this._bOverflowButtonWasFocused = true; this.sFocusedChildControlId = ""; } }; /** * Resets focus info. * @private */ OverflowToolbar.prototype._resetChildControlFocusInfo = function () { this._bControlWasFocused = false; this._bOverflowButtonWasFocused = false; this.sFocusedChildControlId = ""; }; // register OverflowToolbar resize handler OverflowToolbar.prototype._registerToolbarResize = function() { // register resize handler only if toolbar has relative width if (Toolbar.isRelativeWidth(this.getWidth())) { var fnResizeProxy = this._handleResize.bind(this); this._sResizeListenerId = ResizeHandler.register(this, fnResizeProxy); } }; // deregister OverflowToolbar resize handlers OverflowToolbar.prototype._deregisterToolbarResize = function() { if (this._sResizeListenerId) { ResizeHandler.deregister(this._sResizeListenerId); this._sResizeListenerId = ""; } }; // Resize Handler OverflowToolbar.prototype._handleResize = function() { this._bResized = true; // fully executing _doLayout at this point poses risk of // measuring the wrong DOM, since the control is invalidated // but not yet rerendered if (this._bInvalidatedAndNotRendered) { return; } this._callDoLayout(); }; // Media Change Handler OverflowToolbar.prototype._fnMediaChange = function() { // There are some predefined device-dependent CSS classes, which depend on media queries and show/hide controls with CSS. // In some cases, upon device orientation change, some controls (previously hidden) become visible, but their width is cached as 0. // That is why here we reset _bControlsInfoCached property and _iPreviousToolbarWidth, if there is a device change (based on media queries). this._bControlsInfoCached = false; this._iPreviousToolbarWidth = null; this._callDoLayout(); }; // Calls doLayout, depending on the "asyncMode" OverflowToolbar.prototype._callDoLayout = function () { if (this.getAsyncMode()) { this._doLayoutAsync(); } else { this._doLayout(); } }; /** * Stores the sizes and other info of controls so they don't need to be recalculated again until they change * @private */ OverflowToolbar.prototype._cacheControlsInfo = function () { var aVisiblePopoverOnlyControls, bHasVisiblePopoverOnlyControls, iLeftPadding = parseInt(this.$().css("padding-right")) || 0, iRightPadding = parseInt(this.$().css("padding-left")) || 0, iOverflowButtonSize = this._getOverflowButtonSize(), iToolbarOnlyContentSize = this._iToolbarOnlyContentSize; this._iOldContentSize = this._iContentSize; this._iContentSize = 0; // The total *optimal* size of all controls in the toolbar this._iToolbarOnlyContentSize = 0; this._bNeedUpdateOnControlsCachedSizes = false; this.getContent().forEach(this._updateControlsCachedSizes, this); if (iToolbarOnlyContentSize !== this._iToolbarOnlyContentSize) { this.fireEvent("_minWidthChange", { minWidth: this._iToolbarOnlyContentSize > 0 ? this._iToolbarOnlyContentSize + iOverflowButtonSize : 0 }); } // If the system is a phone sometimes due to specificity in the flex the content can be rendered 1px larger that it should be. // This causes an overflow of the last element/button if (Device.system.phone) { this._iContentSize -= 1; } if (this._aPopoverOnlyControls.length) { aVisiblePopoverOnlyControls = this._aPopoverOnlyControls.filter(function(oControl) { return oControl.getVisible(); }); bHasVisiblePopoverOnlyControls = (aVisiblePopoverOnlyControls.length > 0); if (bHasVisiblePopoverOnlyControls) { // At least one control will be in the Popover, so the overflow button is required within content this._iContentSize += iOverflowButtonSize; } } this._bControlsInfoCached = true; // If the total width of all overflow-enabled children changed, fire a private event to notify interested parties if (this._iOldContentSize !== this._iContentSize) { this.fireEvent("_contentSizeChange", { contentSize: this._iContentSize + iLeftPadding + iRightPadding + 1 // Additional 1px to fix Browser rounding issues }); } }; /** * Updates the cached sizes of the controls * @param oControl * @private */ OverflowToolbar.prototype._updateControlsCachedSizes = function (oControl) { var sPriority, iControlSize, sWidth; sPriority = this._getControlPriority(oControl); iControlSize = this._calculateControlSize(oControl); this._aControlSizes[oControl.getId()] = iControlSize; sWidth = Toolbar.getOrigWidth(oControl.getId()); if (sWidth && Toolbar.isRelativeWidth(sWidth)) { this._bNeedUpdateOnControlsCachedSizes = true; } // Only add up the size of controls that can be shown in the toolbar, hence this addition is here if (sPriority !== OverflowToolbarPriority.AlwaysOverflow) { this._iContentSize += iControlSize; } if (sPriority === OverflowToolbarPriority.NeverOverflow) { this._iToolbarOnlyContentSize += iControlSize; } }; /** * Calculates control's size * @param oControl * @returns {number} * @private */ OverflowToolbar.prototype._calculateControlSize = function (oControl) { return this._getOptimalControlWidth(oControl, this._aControlSizes[oControl.getId()]); }; /** * Getter for the _bControlsInfoCached - its purpose it to be able to override it for edge cases to disable control caching * @returns {boolean|*} * @private */ OverflowToolbar.prototype._isControlsInfoCached = function () { return this._bControlsInfoCached; }; /** * Moves buttons to Popover * @private */ OverflowToolbar.prototype._flushButtonsToPopover = function () { this._aButtonsToMoveToPopover.forEach(this._moveButtonToPopover, this); }; /** * Invalidates OverflowToolbar if the signature of the Popover is changed * @private */ OverflowToolbar.prototype._invalidateIfHashChanged = function (sHash) { // helper: invalidate the toolbar if the signature of the Popover changed (i.e. buttons moved) if (typeof sHash === "undefined" || this._getPopover()._getContentIdsHash() !== sHash) { // Preserve focus info this._preserveChildControlFocusInfo(); this.invalidate(); } }; /** * Adds Overflow button and updates iContentSize, if it hasn't been added so far * @private */ OverflowToolbar.prototype._addOverflowButton = function () { if (!this._getOverflowButtonNeeded()) { this._iCurrentContentSize += this._getOverflowButtonSize(); this._setOverflowButtonNeeded(true); } }; /** * Aggregate the controls from this array of elements [el1, el2, el3] to an array of arrays and elements [el1, [el2, el3]]. * This is needed because groups of elements and single elements share same overflow logic. * In order to sort elements and group arrays there are _index and _priority property to group array. * @param fnFilter only elements that pass this filter will be included * @returns {*|Array.<T>} * @private */ OverflowToolbar.prototype._aggregateMovableControls = function (fnFilter) { var oGroups = {}, aAggregatedControls = [], iControlGroup, oPriorityOrder, sControlPriority, iControlIndex, aGroup; this._aMovableControls.forEach(function (oControl) { if (fnFilter && !fnFilter(oControl)) { return; } iControlGroup = OverflowToolbar._getControlGroup(oControl); oPriorityOrder = OverflowToolbar._oPriorityOrder; if (iControlGroup) { sControlPriority = this._getControlPriority(oControl); iControlIndex = this._getControlIndex(oControl); oGroups[iControlGroup] = oGroups[iControlGroup] || []; aGroup = oGroups[iControlGroup]; aGroup.unshift(oControl); // The overall group priority is the max priority of its elements if (!aGroup._priority || oPriorityOrder[aGroup._priority] < oPriorityOrder[sControlPriority]) { aGroup._priority = sControlPriority; } // The overall group index is the max index of its elements if (!aGroup._index || aGroup._index < iControlIndex) { aGroup._index = iControlIndex; } } else { aAggregatedControls.push(oControl); } }, this); // combine not grouped elements with group arrays Object.keys(oGroups).forEach(function (key) { aAggregatedControls.push(oGroups[key]); }); return aAggregatedControls; }; /** * Extracts controls to move to Overflow * @param aAggregatedMovableControls array of movable controls * @param iToolbarSize * @private */ OverflowToolbar.prototype._extractControlsToMoveToOverflow = function (aAggregatedMovableControls, iToolbarSize) { var i, vMovableControl; for (i = 0; i < aAggregatedMovableControls.length; i++) { vMovableControl = aAggregatedMovableControls[i]; // when vMovableControl is a group array if (vMovableControl.length) { vMovableControl.forEach(this._addToPopoverArrAndUpdateContentSize, this); } else { // when vMovableControl is a single element this._addToPopoverArrAndUpdateContentSize(vMovableControl); } // Add the overflow button only if there is at least one control, which will be shown in the Popover. if (this._getControlPriority(vMovableControl) !== OverflowToolbarPriority.Disappear && !vMovableControl.isA?.("sap.m.ToolbarSeparator")) { this._addOverflowButton(); } if (this._iCurrentContentSize <= iToolbarSize) { break; } } }; /** * Adds controls to Popover Array and updates the current content size * @param oControl * @private */ OverflowToolbar.prototype._addToPopoverArrAndUpdateContentSize = function (oControl) { this._aButtonsToMoveToPopover.unshift(oControl); this._iCurrentContentSize -= this._aControlSizes[oControl.getId()]; }; /** * Sorts controls by priority and index. * vControlA or vControlB can be control or group array(array of controls) they share same sorting logic. * @param vControlA * @param vControlB * @private */ OverflowToolbar.prototype._sortByPriorityAndIndex = function (vControlA, vControlB) { var oPriorityOrder = OverflowToolbar._oPriorityOrder, sControlAPriority = this._getControlPriority(vControlA), sControlBPriority = this._getControlPriority(vControlB), iPriorityCompare = oPriorityOrder[sControlAPriority] - oPriorityOrder[sControlBPriority]; if (iPriorityCompare !== 0) { return iPriorityCompare; } else { return this._getControlIndex(vControlB) - this._getControlIndex(vControlA); } }; /** * Moves controls from/to the Popover * Sets/removes flexbox css classes to/from controls * @param iToolbarSize * @private */ OverflowToolbar.prototype._setControlsOverflowAndShrinking = function (iToolbarSize) { var sIdsHash; this._iCurrentContentSize = this._iContentSize; this._aButtonsToMoveToPopover = []; // buttons that must go to the Popover // If _bSkipOptimization is set to true, this means that no controls moved from/to the overflow, but they rather changed internally // In this case we can't rely on the Popover hash to determine whether to skip one invalidation if (this._bSkipOptimization) { this._bSkipOptimization = false; } else { sIdsHash = this._getPopover()._getContentIdsHash(); // Hash of the buttons in the Popover, f.e. "__button1.__button2.__button3" } // Clean up the Popover, hide the overflow button, remove flexbox css from controls this._resetToolbar(); // If there are any Popover only controls and they are visible, add them to the PopoverOnly collection this._collectPopoverOnlyControls(); this._markControlsWithShrinkableLayoutData(); // If all content fits - put the PopoverOnly controls (if any) in the Popover and stop here // Due to rounding issues, add 1px size tolerance if (this._iCurrentContentSize <= (iToolbarSize + OverflowToolbar.CONTENT_SIZE_TOLERANCE)) { this._flushButtonsToPopover(); this._invalidateIfHashChanged(sIdsHash); return; } // Not all content fits // If there are buttons that can be moved, start moving them to the Popover until there is no more overflow left this._moveControlsToPopover(iToolbarSize); // At this point all that could be moved to the Popover, was moved (Popover only buttons, some/all movable buttons) this._flushButtonsToPopover(); // If content still doesn't fit despite moving all movable items to the Popover, set the flexbox classes if (this._iCurrentContentSize > iToolbarSize) { this._checkContents(); // This function sets the css classes to make flexbox work, despite its name } this._invalidateIfHashChanged(sIdsHash); }; /* * Iterrates through controls and marks them with shrinkable class if needed * * @private */ OverflowToolbar.prototype._markControlsWithShrinkableLayoutData = function() { this.getContent().forEach(this._markControlWithShrinkableLayoutData, this); }; /* * Moves PopoverOnly controls in Accossiative Popover * * @private */ OverflowToolbar.prototype._collectPopoverOnlyControls = function() { var oPopoverOnlyControlsLength = this._aPopoverOnlyControls.length, i, oControl; if (oPopoverOnlyControlsLength) { for (i = oPopoverOnlyControlsLength - 1; i >= 0; i--) { oControl = this._aPopoverOnlyControls[i]; if (oControl.getVisible()) { this._aButtonsToMoveToPopover.unshift(oControl); } } if (this._aButtonsToMoveToPopover.length > 0) { // At least one control will be in the Popover, so the overflow button is needed this._setOverflowButtonNeeded(true); } } }; /* * Moves controls to Popover * @param iToolbarSize * @private */ OverflowToolbar.prototype._moveControlsToPopover = function(iToolbarSize) { var aAggregatedMovableControls = [], fnControlOccupiesSpace = function(oControl) { var iCachedWidth = this._aControlSizes[oControl.getId()]; return iCachedWidth > 0 && iCachedWidth > OverflowToolbar._getControlMargins(oControl); }.bind(this); if (this._aMovableControls.length) { aAggregatedMovableControls = this._aggregateMovableControls(fnControlOccupiesSpace); // Define the overflow order, depending on items` priority and index. aAggregatedMovableControls.sort(this._sortByPriorityAndIndex.bind(this)); // Hide controls or groups while iContentSize <= iToolbarSize/ this._extractControlsToMoveToOverflow(aAggregatedMovableControls, iToolbarSize); } }; /* * Checks if the given control has shrinkable <code>LayoutData</code> or not and marks it with shrinkable class. * * @private */ OverflowToolbar.prototype._markControlWithShrinkableLayoutData = function(oControl) { var sWidth, oLayout, bShrinkable, bBreadcrumbs; // remove old class oControl.removeStyleClass(Toolbar.shrinkClass); // ignore the controls that have fixed width sWidth = Toolbar.getOrigWidth(oControl.getId()); if (!Toolbar.isRelativeWidth(sWidth)) { return; } // check shrinkable via layout data oLayout = oControl.getLayoutData(); bShrinkable = oLayout && oLayout.isA("sap.m.ToolbarLayoutData") && oLayout.getShrinkable(); // check explicitly for sap.m.Breadcrumbs, as text // controls implement sap.ui.core.IShrinkable too, // and they are not meant to be included bBreadcrumbs = oControl.isA("sap.m.Breadcrumbs"); if (bShrinkable || bBreadcrumbs) { oControl.addStyleClass(Toolbar.shrinkClass); } }; /** * Resets the toolbar by removing all special behavior from controls, returning it to its default natural state: * - all buttons removed from the Popover and put back to the toolbar * - the overflow button is removed * - all flexbox classes are removed from items * @private */ OverflowToolbar.prototype._resetToolbar = function () { // 1. Close the Popover and remove everything from it (reset overflow behavior) // Note: when the Popover is closed because of toolbar invalidation, we don't want the animation in order to avoid flickering this._getPopover().close(); this._getPopover()._getAllContent().forEach(this._restoreButtonInToolbar, this); // 2. Hide the overflow button this._setOverflowButtonNeeded(false); // 3 Remove flex classes (reset shrinking behavior) this.getContent().forEach(this._removeShrinkingClass); }; /** * Removes CSS class for shrinking * @param oControl * @private */ OverflowToolbar.prototype._removeShrinkingClass = function (oControl) { oControl.removeStyleClass(Toolbar.shrinkClass); }; /** * Called for any button that overflows * @param oButton * @private */ OverflowToolbar.prototype._moveButtonToPopover = function (oButton) { this._getPopover().addAssociatedContent(oButton); }; /** * Called when a button can fit in the toolbar and needs to be restored there * @param vButton * @private */ OverflowToolbar.prototype._restoreButtonInToolbar = function (vButton) { if (typeof vButton === "object") { vButton = vButton.getId(); } this._getPopover().removeAssociatedContent(vButton); }; /** * Closes the Popover, resets the toolbar, and re-initializes variables to force a full layout recalc * @param {boolean} bHardReset - skip the optimization, described in _setControlsOverflowAndShrinking * @private */ OverflowToolbar.prototype._resetAndInvalidateToolbar = function (bHardReset) { if (this._bIsBeingDestroyed) { return; } this._resetToolbar(); this._bControlsInfoCached = false; this._iPreviousToolbarWidth = null; if (bHardReset) { this._bSkipOptimization = true; } if (this.$().length) { this._preserveChildControlFocusInfo(); this.invalidate(); } }; OverflowToolbar.prototype.invalidate = function() { this._bInvalidatedAndNotRendered = true; Control.prototype.invalidate.apply(this, arguments); }; /****************************************SUB-COMPONENTS*****************************************************/ /** * Returns all controls from the toolbar that are not in the Popover * @returns {*|Array.<sap.ui.core.Control>} */ OverflowToolbar.prototype._getVisibleContent = function () { var aToolbarContent = this.getContent(), aPopoverContent = this._getPopover()._getAllContent(); return aToolbarContent.filter(function (oControl) { return aPopoverContent.indexOf(oControl) === -1; }); }; /** * Returns all the controls from the <code>sap.m.OverflowToolbar</code>, * that are not in the overflow area and their <code>visible</code> property is <code>true</code>. * @private * @ui5-restricted * @returns {*|Array.<sap.ui.core.Control>} */ OverflowToolbar.prototype._getVisibleAndNonOverflowContent = function () { return this._getVisibleContent().filter(function (oControl) { return oControl.getVisible(); }); }; OverflowToolbar.prototype._getToggleButton = function (sIdPrefix) { return new ToggleButton({ ariaHasPopup: AriaHasPopup.Menu, id: this.getId() + sIdPrefix, icon: IconPool.getIconURI("overflow"), press: this._overflowButtonPressed.bind(this), tooltip: Library.getResourceBundleFor("sap.m").getText(OverflowToolbar.TOGGLE_BUTTON_TOOLTIP), type: ButtonType.Transparent }); }; /** * Lazy loader for the overflow button * @returns {sap.m.Button} * @private */ OverflowToolbar.prototype._getOverflowButton = function () { var oOverflowButton; if (!this.getAggregation("_overflowButton")) { // Create the overflow button // A tooltip will be used automatically by the button // using to the icon-name provided oOverflowButton = this._getToggleButton("-overflowButton"); this.setAggregation("_overflowButton", oOverflowButton, true); } return this.getAggregation("_overflowButton"); }; OverflowToolbar.prototype._getOverflowButtonClone = function () { if (!this._oOverflowToolbarButtonClone) { this._oOverflowToolbarButtonClone = this._getToggleButton("-overflowButtonClone") .addStyleClass("sapMTBHiddenElement"); } //Removing accessibility attributes this._oOverflowToolbarButtonClone._getTooltip = function() { return ""; }; this._oOverflowToolbarButtonClone.removeAllAssociation("ariaLabelledBy"); return this._oOverflowToolbarButtonClone; }; /** * Shows the Popover * @param oEvent * @private */ OverflowToolbar.prototype._overflowButtonPressed = function (oEvent) { var oPopover = this._getPopover(), sBestPlacement = this._getBestPopoverPlacement(); if (oPopover.getPlacement() !== sBestPlacement) { oPopover.setPlacement(sBestPlacement); } if (oPopover.isOpen()) { oPopover.close(); } else { oPopover.openBy(oEvent.getSource()); } }; /** * Lazy loader for the popover * @returns {sap.m.Popover} * @private */ OverflowToolbar.prototype._getPopover = function () { var oPopover; if (!this.getAggregation("_popover")) { // Create the Popover oPopover = new OverflowToolbarAssociativePopover(this.getId() + "-popover", { showHeader: false, showArrow: false, modal: false, horizontalScrolling: Device.system.phone ? false : true, contentWidth: Device.system.phone ? "100%" : "auto", offsetY: this._detireminePopoverVerticalOffset(), ariaLabelledBy: InvisibleText.getStaticId("sap.m", "INPUT_AVALIABLE_VALUES") }); // Override popover positioning mechanism oPopover._adaptPositionParams = function () { OverflowToolbarAssociativePopover.prototype._adaptPositionParams.call(this); this._myPositions = ["end top", "begin center", "end bottom", "end center"]; this._atPositions = ["end bottom", "end center", "end top", "begin center"]; }; if (Device.system.phone) { oPopover.attachBeforeOpen(this._shiftPopupShadow, this); } // This will set the toggle button to "off" oPopover.attachAfterClose(this._popOverClosedHandler, this); this.setAggregation("_popover", oPopover, true); } return this.getAggregation("_popover"); }; /** * On mobile, remove the shadow from the top/bottom, depending on how the popover was opened * If the popup is placed on the bottom, remove the top shadow * If the popup is placed on the top, remove the bottom shadow * @private */ OverflowToolbar.prototype._shiftPopupShadow = function () { var oPopover = this._getPopover(), sPos = oPopover.getCurrentPosition(); if (sPos === PlacementType.Bottom) { oPopover.addStyleClass("sapMOTAPopoverNoShadowTop"); oPopover.removeStyleClass("sapMOTAPopoverNoShadowBottom"); } else if (sPos === PlacementType.Top) { oPopover.addStyleClass("sapMOTAPopoverNoShadowBottom"); oPopover.removeStyleClass("sapMOTAPopoverNoShadowTop"); } }; /** * Ensures that the overflowButton is no longer pressed when its popOver closes * @private */ OverflowToolbar.prototype._popOverClosedHandler = function () { this._getOverflowButton().setPressed(false); // Turn off the toggle button if (Element.closestTo(document.activeElement)) { return; } this._getOverflowButton().focus(); }; /** * @returns {boolean|*} * @private */ OverflowToolbar.prototype._getOverflowButtonNeeded = function () { return this._bOverflowButtonNeeded; }; /** * * @param {boolean} bValue * @returns {OverflowToolbar} * @private */ OverflowToolbar.prototype._setOverflowButtonNeeded = function (bValue) { if (this._bOverflowButtonNeeded !== bValue) { this._bOverflowButtonNeeded = bValue; } return this; }; OverflowToolbar.prototype._getToolbarInteractiveControls = function () { var aVisibleControls = this._getVisibleContent(), aInteractiveControls = aVisibleControls.filter(function(oControl) { return this._getControlPriority(oControl) !== OverflowToolbarPriority.AlwaysOverflow && oControl.isA("sap.m.IToolbarInteractiveControl") && typeof (oControl._getToolbarInteractive) === "function" && oControl._getToolbarInteractive(); }, this); this._getOverflowButtonNeeded() && aInteractiveControls.push(this._getOverflowButton()); return aInteractiveControls; }; /** * * @returns {number} Toolbar interactive Controls count * @private */ OverflowToolbar.prototype._getToolbarInteractiveControlsCount = function () { var aInteractiveControlsRendered = this._getVisibleAndNonOverflowContent().filter(function(oControl) { return this._getControlPriority(oControl) !== OverflowToolbarPriority.AlwaysOverflow && oControl.isA("sap.m.IToolbarInteractiveControl") && typeof (oControl._getToolbarInteractive) === "function" && oControl._getToolbarInteractive(); }, this); return this._getOverflowButtonNeeded() ? aInteractiveControlsRendered.length + 1 : aInteractiveControlsRendered.length; }; /***************************************AGGREGATIONS AND LISTENERS******************************************/ /** * Upon Control's update, move it in the suitable collections and remove it from where it is not needed any more * @private */ OverflowToolbar.prototype._updateContentInfoInControlsCollections = function () { this.getContent().forEach(function (oControl) { if (oControl) { this._removeContentFromControlsCollections(oControl); this._moveControlInSuitableCollection(oControl, this._getControlPriority(oControl)); } }, this); }; /** * Moves each control in the suitable collection - Popover only, movable controls and toolbar only * @param oControl * @param sPriority * @public */ OverflowToolbar.prototype._moveControlInSuitableCollection = function (oControl, sPriority) { var bCanMoveToOverflow = sPriority !== OverflowToolbarPriority.NeverOverflow, bAlwaysStaysInOverflow = sPriority === OverflowToolbarPriority.AlwaysOverflow; if (OverflowToolbarAssociativePopoverControls.supportsControl(oControl) && bAlwaysStaysInOverflow) { this._aPopoverOnlyControls.push(oControl); } else { if (OverflowToolbarAssociativePopoverControls.supportsControl(oControl) && bCanMoveToOverflow && oControl.getVisible()) { this._aMovableControls.push(oControl); } else { this._aToolbarOnlyControls.push(oControl); } } }; /** * Removes Control from collections * @param oControl * @public */ OverflowToolbar.prototype._removeContentFromControlsCollections = function (oControl) { var i, aCurrentCollection, iIndex; for (i = 0; i < this._aAllCollections.length; i++) { aCurrentCollection = this._aAllCollections[i]; iIndex = aCurrentCollection.indexOf(oControl); if (iIndex !== -1) { aCurrentCollection.splice(iIndex, 1); } } }; OverflowToolbar.prototype._clearAllControlsCollections = function () { this._aMovableControls = []; this._aToolbarOnlyControls = []; this._aPopoverOnlyControls = []; this._aAllCollections = [this._aMovableControls, this._aToolbarOnlyControls, this._aPopoverOnlyControls]; }; OverflowToolbar.prototype.onLayoutDataChange = function (oEvent) { this._resetAndInvalidateToolbar(true); oEvent && this._updateContentInfoInControlsCollections(); }; OverflowToolbar.prototype.addContent = function (oControl) { this._registerControlListener(oControl); this._resetAndInvalidateToolbar(false); if (oControl) { this._moveControlInSuitableCollection(oControl, this._getControlPriority(oControl)); } this._informNewFlexibleContentAdded(oControl); return this._callToolbarMethod("addContent", arguments); }; OverflowToolbar.prototype.insertContent = function (oControl, iIndex) { this._registerControlListener(oControl); this._resetAndInvalidateToolbar(false); if (oControl) { this._moveControlInSuitableCollection(oControl, this._getControlPriority(oControl)); } this._informNewFlexibleContentAdded(oControl); return this._callToolbarMethod("insertContent", arguments); }; OverflowToolbar.prototype.removeContent = function () { var vContent = this._callToolbarMethod("removeContent", arguments); if (vContent) { this._getPopover().removeAssociatedContent(vContent.getId()); } this._resetAndInvalidateToolbar(false); this._deregisterControlListener(vContent); this._removeContentFromControlsCollections(vContent); return vContent; }; OverflowToolbar.prototype.removeAllContent = function () { var aContents = this._callToolbarMethod("removeAllContent", arguments); aContents.forEach(this._deregisterControlListener, this); aContents.forEach(this._removeContentFromControlsCollections, this); this._resetAndInvalidateToolbar(false); this._clearAllControlsCollections(); return aContents; }; OverflowToolbar.prototype.destroyContent = function () { this._resetAndInvalidateToolbar(false); setTimeout(function () { this._resetAndInvalidateToolbar(false); }.bind(this), 0); this._clearAllControlsCollections(); return this._callToolbarMethod("destroyContent", arguments); }; /** * Every time a flexible control (like sap.m.GenericTag) is added to the content aggregation, * a "_contentSizeChange" event is fired to reset the DynamicPageTitle's area flex-basis. * @param oControl * @private */ OverflowToolbar.prototype._informNewFlexibleContentAdded = function (oControl) { if (oControl && oControl.isA("sap.m.IOverflowToolbarFlexibleContent")) { this.fireEvent("_contentSizeChange", { contentSize: null }); } }; /** * Every time a control is inserted in the toolbar, it must be monitored for size/visibility changes * @param oControl * @private */ OverflowToolbar.prototype._registerControlListener = function (oControl) { var aInvalidationEvents; if (oControl) { oControl.attachEvent("_change", this._onContentPropertyChangedOverflowToolbar, this); // Check if the control implements sap.m.IOverflowToolbarContent interface if (oControl.isA("sap.m.IOverflowToolbarContent")) { aInvalidationEvents = oControl.getOverflowToolbarConfig().invalidationEvents; if (aInvalidationEvents && Array.isArray(aInvalidationEvents)) { // We start to listen for events listed in invalidationEvents array of the OverflowToolbarConfig aInvalidationEvents.forEach(function (sEvent) { oControl.attachEvent(sEvent, this._onInvalidationEventFired, this); }, this); } } } }; /** * Each time a control is removed from the toolbar, detach listeners * @param oControl * @private */ OverflowToolbar.prototype._deregisterControlListener = function (oControl) { var aInvalidationEvents; if (oControl) { oControl.detachEvent("_change", this._onContentPropertyChangedOverflowToolbar, this); // Check if the control implements sap.m.IOverflowToolbarContent interface if (oControl.isA("sap.m.IOverflowToolbarContent")) { aInvalidationEvents = oControl.getOverflowToolbarConfig().invalidationEvents; if (aInvalidationEvents && Array.isArray(aInvalidationEvents)) { // We stop to listen for events listed in invalidationEvents array of the OverflowToolbarConfig aInvalidationEvents.forEach(function (sEvent) { oControl.detachEvent(sEvent, this._onInvalidationEventFired, this); }, this); } } } }; /** * Changing a property that affects toolbar content width should trigger a recalculation * This function is triggered on any property change, but will ignore some properties that are known to not affect width/visibility * @param oEvent * @private */ OverflowToolbar.prototype._onContentPropertyChangedOverflowToolbar = function (oEvent) { var oSourceControl = oEvent.getSource(), oControlConfig, sParameterName; // Move control in suitable collections if one of its properties has changed between the init and doLayout functions execution this._updateContentInfoInControlsCollections(); // Listening for property changes is turned off during layout recalculation to avoid infinite loops if (!this._bListenForControlPropertyChanges) { return; } oControlConfig = OverflowToolbarAssociativePopoverControls.getControlConfig(oSourceControl); sParameterName = oEvent.getParameter("name"); // Do nothing if the changed property belongs to invisible control if (sParameterName !== 'visible' && !oSourceControl.getVisible()) { return; } // Do nothing if the changed property is in the blocklist above if (typeof oControlConfig !== "undefined" && oControlConfig.noInvalidationProps.indexOf(sParameterName) !== -1) { return; } // If the visibility of the conent has changed, in onAfterRendering method we assure that // the cached controls' sizes will be updated, as they might not be accurate if (sParameterName === "visible") { this._bContentVisibilityChanged = true; } // If a flexible control has property modification, which might influence width, // we should notify DynamicPageTitle to reset the flex-basis of its content area if (oSourceControl.isA("sap.m.IOverflowToolbarFlexibleContent") && oSourceControl.getVisible()) { this.fireEvent("_contentSizeChange", { contentSize: null }); } // Trigger a recalculation this._resetAndInvalidateToolbar(true); }; /** * Triggered when invalidation event is fired. Resets and invalidates the OverflowToolbar. * @private */ OverflowToolbar.prototype._on