@openui5/sap.m
Version:
OpenUI5 UI Library sap.m
1,519 lines (1,273 loc) • 58.1 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2009-2023 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/dom/jquery/Focusable" // jQuery Plugin "lastFocusableDomRef"
], function(
Theming,
coreLibrary,
library,
Control,
Element,
ToggleButton,
InvisibleText,
Toolbar,
OverflowToolbarLayoutData,
OverflowToolbarAssociativePopover,
OverflowToolbarAssociativePopoverControls,
ResizeHandler,
IconPool,
Device,
OverflowToolbarRenderer,
Log,
Library
) {
"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.DateTimeInput}</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.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.117.4
*
* @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.ARIA_ROLE_DESCRIPTION = "OVERFLOW_TOOLBAR_ROLE_DESCRIPTION";
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._sAriaRoleDescription = Library
.getResourceBundleFor("sap.m")
.getText(OverflowToolbar.ARIA_ROLE_DESCRIPTION);
this._fnMediaChangeRef = this._fnMediaChange.bind(this);
Device.media.attachHandler(this._fnMediaChangeRef);
};
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
* @protected
*/
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();
}
};
OverflowToolbar.prototype.onsapfocusleave = function() {
this._resetChildControlFocusInfo();
};
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.registry.get(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 && !this._getOverflowButtonNeeded()) {
// 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.
* @returns {*|Array.<T>}
* @private
*/
OverflowToolbar.prototype._aggregateMovableControls = function () {
var oGroups = {},
aAggregatedControls = [],
iControlGroup,
oPriorityOrder,
sControlPriority,
iControlIndex,
aGroup;
this._aMovableControls.forEach(function (oControl) {
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) {
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 = [];
if (this._aMovableControls.length) {
aAggregatedMovableControls = this._aggregateMovableControls();
// 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;
};
/**
*
* @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._onInvalidationEventFired = function (oEvent) {
var oSource = oEvent.getSource();
// Listening for invalidation events is turned off during layout recalculation to avoid infinite loops
if (!this._bListenForInvalidationEvents) {
return;
}
if (oSource.isA("sap.m.IOverflowToolbarFlexibleContent")) {
this.fireEvent("_contentSizeChange", {
contentSize: null
});
}
// Trigger a recalculation
this._resetAndInvalidateToolbar(true);
};
/**
* Returns the size of the overflow button - hardcoded, because it cannot be determined before rendering it
* @returns {number}
* @private
*/
OverflowToolbar.prototype._getOverflowButtonSize = function () {
return this._iOverflowToolbarButtonSize;
};
/**
* Determines the optimal placement of the Popover depending on the position of the toolbar in the page
* For footer and header tags, the placement is hard-coded, for other tags - automatically detected
* @returns {sap.m.PlacementType}
* @private
*/
OverflowToolbar.prototype._getBestPopoverPlacement = function () {
var sHtmlTag = this.getHTMLTag();
// Always open above
if (sHtmlTag === "Footer") {
return PlacementType.Top;
// Always open below
} else if (sHtmlTag === "Header") {
return PlacementType.Bottom;
}
return PlacementType.Vertical;
};
/**
* Returns an array of the ids of all controls in the overflow toolbar
* @returns {*|Array}
* @private
*/
OverflowToolbar.prototype._getControlsIds = function () {
return this.getContent().map(function (item) {
return item.getId();
});
};
/**
* Returns the control index in the OverflowToolbar content aggregation or the index of a group, which is defined by the rightmost item in the group.
* @param vControl array of controls or single control
* @private
*/
OverflowToolbar.prototype._getControlIndex = function (vControl) {
return vControl.length ? vControl._index : this.indexOfContent(vControl);
};
/************************************************** STATIC ***************************************************/
/**
* Returns the optimal width of an element for the purpose of calculating the content width of the OverflowToolbar
* so that spacers f.e. don't expand too aggressively and take up the whole space
* @param oControl
* @returns {*}
* @private
*/
OverflowToolbar.prototype._getOptimalControlW