@openui5/sap.m
Version:
OpenUI5 UI Library sap.m
1,433 lines (1,238 loc) • 109 kB
JavaScript
/*!
* 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.ListBase.
sap.ui.define([
"sap/base/i18n/Localization",
"sap/ui/core/ControlBehavior",
"sap/ui/core/RenderManager",
"sap/ui/Device",
"sap/ui/model/ChangeReason",
"sap/ui/core/Control",
"sap/ui/core/Element",
"sap/ui/core/InvisibleText",
"sap/ui/core/LabelEnablement",
"sap/ui/core/delegate/ItemNavigation",
"./library",
"sap/ui/core/library",
"./InstanceManager",
"./GrowingEnablement",
"./GroupHeaderListItem",
"./ListItemBase",
"./ListBaseRenderer",
"sap/base/strings/capitalize",
"sap/ui/thirdparty/jquery",
"sap/base/Log",
"sap/ui/core/InvisibleMessage",
"sap/m/table/Util",
"sap/ui/core/Lib",
"sap/ui/dom/jquery/Selectors", // jQuery custom selectors ":sapTabbable"
"sap/ui/dom/jquery/Aria" // jQuery Plugin "addAriaLabelledBy", "removeAriaLabelledBy"
],
function(
Localization,
ControlBehavior,
RenderManager,
Device,
ChangeReason,
Control,
Element,
InvisibleText,
LabelEnablement,
ItemNavigation,
library,
coreLibrary,
InstanceManager,
GrowingEnablement,
GroupHeaderListItem,
ListItemBase,
ListBaseRenderer,
capitalize,
jQuery,
Log,
InvisibleMessage,
Util,
Library
) {
"use strict";
// shortcut for enums
const {
ListType: ListItemType,
ListGrowingDirection,
SwipeDirection,
ListSeparators,
ListMode,
ListHeaderDesign,
Sticky,
MultiSelectMode
} = library;
/**
* Constructor for a new ListBase.
*
* @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
* The <code>sap.m.ListBase</code> control provides a base functionality of the <code>sap.m.List</code> and <code>sap.m.Table</code> controls. Selection, deletion, unread states and inset style are also maintained in <code>sap.m.ListBase</code>.
*
* See section "{@link topic:295e44b2d0144318bcb7bdd56bfa5189 List, List Item, and Table}"
* in the documentation for an introduction to subclasses of <code>sap.m.ListBase</code> control.
* More information on how to use binding-related functionality, such as {@link topic:ec79a5d5918f4f7f9cbc2150e66778cc Sorting, Grouping, and Filtering},
* is also available in the documentation.
*
* <b>Note:</b> The ListBase including all contained items may be completely re-rendered when the data of a bound model is changed. Due to the limited hardware resources of mobile devices this can lead to longer delays for lists that contain many items. As such the usage of a list is not recommended for these use cases.
* @extends sap.ui.core.Control
*
* @author SAP SE
* @version 1.146.0
*
* @constructor
* @public
* @since 1.16
* @alias sap.m.ListBase
*/
var ListBase = Control.extend("sap.m.ListBase", /** @lends sap.m.ListBase.prototype */ {
metadata : {
library : "sap.m",
dnd : true,
properties : {
/**
* Defines the indentation of the container. Setting it to <code>true</code> indents the list.
*/
inset : {type : "boolean", group : "Appearance", defaultValue : false},
/**
* Defines the header text that appears in the control.
* <b>Note:</b>
* If the <code>headerToolbar</code> aggregation is set, then this property is ignored.
* If this is the case, use, for example, a <code>sap.m.Title</code> control in the toolbar to define a header.
*/
headerText : {type : "string", group : "Misc", defaultValue : null},
/**
* Defines the semantic header level of the header text (see {@link #getHeaderText headerText} property}).
* This information is, for example, used by assistive technologies, such as screenreaders, to create a hierarchical site
* map for faster navigation.
* Depending on this setting, either the ARIA equivalent of an HTML h1-h6 element is used or, when using the
* <code>Auto</code> level, no explicit level information is used.
*
* <b>Note:</b>
* If the <code>headerToolbar</code> aggregation is set, then this property is ignored.
* If this is the case, use, for example, a <code>sap.m.Title</code> control in the toolbar to define a header.
*
* @since 1.117.0
*/
headerLevel : {type : "sap.ui.core.TitleLevel", group : "Misc", defaultValue : coreLibrary.TitleLevel.Auto},
/**
* Defines the header style of the control. Possible values are <code>Standard</code> and <code>Plain</code>.
* @since 1.14
* @deprecated Since version 1.16. No longer has any functionality.
*/
headerDesign : {type : "sap.m.ListHeaderDesign", group : "Appearance", defaultValue : ListHeaderDesign.Standard, deprecated: true},
/**
* Defines the footer text that appears in the control.
*/
footerText : {type : "string", group : "Misc", defaultValue : null},
/**
* Defines the mode of the control (e.g. <code>None</code>, <code>SingleSelect</code>, <code>MultiSelect</code>, <code>Delete</code>).
*/
mode : {type : "sap.m.ListMode", group : "Behavior", defaultValue : ListMode.None},
/**
* Sets the width of the control.
*/
width : {type : "sap.ui.core.CSSSize", group : "Dimension", defaultValue : "100%"},
/**
* Defines whether the items are selectable by clicking on the item itself (<code>true</code>) rather than having to set the selection control first.
* <b>Note:</b> The <code>SingleSelectMaster</code> mode also provides this functionality by default.
*/
includeItemInSelection : {type : "boolean", group : "Behavior", defaultValue : false},
/**
* Activates the unread indicator for all items, if set to <code>true</code>.
*/
showUnread : {type : "boolean", group : "Misc", defaultValue : false},
/**
* This text is displayed if the control contains no items.
* <b>Note:</b> If both a <code>noDataText</code> property and a <code>noData</code> aggregation are provided, the <code>noData</code> aggregation takes priority.
* If the <code>noData</code> aggregation is undefined or set to null, the <code>noDataText</code> property is used instead.
*/
noDataText : {type : "string", group : "Misc", defaultValue : null},
/**
* Defines whether or not the text specified in the <code>noDataText</code> property is displayed.
*/
showNoData : {type : "boolean", group : "Misc", defaultValue : true},
/**
* When this property is set to <code>true</code>, the control will automatically display a busy indicator when it detects that data is being loaded. This busy indicator blocks the interaction with the items until data loading is finished.
* By default, the busy indicator will be shown after one second. This behavior can be customized by setting the <code>busyIndicatorDelay</code> property.
* @since 1.20.2
*/
enableBusyIndicator : {type : "boolean", group : "Behavior", defaultValue : true},
/**
* Defines if animations will be shown while switching between modes.
*/
modeAnimationOn : {type : "boolean", group : "Misc", defaultValue : true},
/**
* Defines which item separator style will be used.
*/
showSeparators : {type : "sap.m.ListSeparators", group : "Appearance", defaultValue : ListSeparators.All},
/**
* Defines the direction of the swipe movement (e.g LeftToRight, RightToLeft, Both) to display the control defined in the <code>swipeContent</code> aggregation.
*/
swipeDirection : {type : "sap.m.SwipeDirection", group : "Misc", defaultValue : SwipeDirection.Both},
/**
* If set to <code>true</code>, enables the growing feature of the control to load more items by requesting from the model.
* <b>Note:</b>: This feature only works when an <code>items</code> aggregation is bound. Growing must not be used together with two-way binding.
* @since 1.16.0
*/
growing : {type : "boolean", group : "Behavior", defaultValue : false},
/**
* Defines the number of items to be requested from the model for each grow.
* This property can only be used if the <code>growing</code> property is set to <code>true</code>.
* @since 1.16.0
*/
growingThreshold : {type : "int", group : "Misc", defaultValue : 20},
/**
* Defines the text displayed on the growing button. The default is a translated text ("More") coming from the message bundle.
* This property can only be used if the <code>growing</code> property is set to <code>true</code>.
* @since 1.16.0
*/
growingTriggerText : {type : "string", group : "Appearance", defaultValue : null},
/**
* If set to true, the user can scroll down/up to load more items. Otherwise a growing button is displayed at the bottom/top of the control.
* <b>Note:</b> This property can only be used if the <code>growing</code> property is set to <code>true</code> and only if there is one instance of <code>sap.m.List</code> or <code>sap.m.Table</code> inside the scrollable scroll container (e.g <code>sap.m.Page</code>).
* @since 1.16.0
*/
growingScrollToLoad : {type : "boolean", group : "Behavior", defaultValue : false},
/**
* Defines the direction of the growing feature.
* If set to <code>Downwards</code> the user has to scroll down to load more items or the growing button is displayed at the bottom.
* If set to <code>Upwards</code> the user has to scroll up to load more items or the growing button is displayed at the top.
* @since 1.40.0
*/
growingDirection : {type : "sap.m.ListGrowingDirection", group : "Behavior", defaultValue : ListGrowingDirection.Downwards},
/**
* If set to true, this control remembers and retains the selection of the items after a binding update has been performed (e.g. sorting, filtering).
* <b>Note:</b> This feature works only if two-way data binding for the <code>selected</code> property of the item is not used. It also needs to be turned off if the binding context of the item does not always point to the same entry in the model, for example, if the order of the data in the <code>JSONModel</code> is changed.
* <b>Note:</b> This feature leverages the built-in selection mechanism of the corresponding binding context if the OData V4 model is used. Therefore, all binding-relevant limitations apply in this context as well. For more details, see the {@link sap.ui.model.odata.v4.Context#setSelected setSelected}, the {@link sap.ui.model.odata.v4.ODataModel#bindList bindList}, and the {@link sap.ui.model.odata.v4.ODataMetaModel#requestValueListInfo requestValueListInfo} API documentation. Do not enable this feature if <code>$$sharedRequest</code> or <code>$$clearSelectionOnFilter</code> is active.
* <b>Note:</b> If this property is set to <code>false</code>, a possible binding context update of items (for example, filtering or sorting the list binding) would clear the selection of the items.
* @since 1.16.6
*/
rememberSelections : {type : "boolean", group : "Behavior", defaultValue : true},
/**
* Defines keyboard handling behavior of the control.
* @since 1.38.0
*/
keyboardMode : {type : "sap.m.ListKeyboardMode", group : "Behavior", defaultValue : "Navigation" },
/**
* Defines the section of the control that remains fixed at the top of the page during vertical scrolling as long as the control is in the viewport.
*
* <b>Note:</b> Enabling sticky column headers in List controls will not have any effect.
*
* There are some known restrictions. A few are given below:
* <ul>
* <li>If the control is placed in layout containers that have the <code>overflow: hidden</code> or <code>overflow: auto</code> style definition, this can
* prevent the sticky elements of the control from becoming fixed at the top of the viewport.</li>
* <li>If sticky column headers are enabled in the <code>sap.m.Table</code> control, setting focus on the column headers will let the table scroll to the top.</li>
* <li>A transparent toolbar design is not supported for sticky bars. The toolbar will automatically get an intransparent background color.</li>
* <li>This feature supports only the default height of the toolbar control and the column headers.</li>
* <li>When sticky group headers are enabled, wrapping in the column headers is not supported.</li>
* </ul>
*
* @since 1.58
*/
sticky : {type : "sap.m.Sticky[]", group : "Appearance"},
/**
* Defines the multi-selection mode for the control.
*
* If the property is set to <code>ClearAll</code>, then selecting items via the
* keyboard shortcut <i>CTRL + A</i> and via the <code>selectAll</code> method is not possible.
* See {@link #selectAll selectAll} for more details.
* A selection of multiple items is still possible using the range selection feature.
* For more information about the range selection, see {@link topic:8a0d4efa29d44ef39219c18d832012da Keyboard Handling for Item Selection}.
*
* <b>Only relevant for <code>sap.m.Table</code>:</b>
* If <code>ClearAll</code> is set, the table renders a Deselect All icon in the column header,
* otherwise a Select All checkbox is shown. The Select All checkbox allows the user to select all the
* items in the control, and the Deselect All icon deselects the items.
*
* <b>Note:</b> This property must be used with the <code>MultiSelect</code> mode.
*
* @since 1.93
*/
multiSelectMode : {type: "sap.m.MultiSelectMode", group: "Behavior", defaultValue: MultiSelectMode.Default},
/**
* Defines the maximum number of item actions.
*
* If the number of item actions exceeds the <code>itemActionCount</code> property value, an overflow button will appear, providing access to the additional actions.
*
* <b>Note:</b> Only values between <code>0-2</code> enables the use of the new <code>actions</code> aggregation. When enabled, the {@link sap.m.ListMode Delete} mode and the {@link sap.m.ListType Detail} list item type have no effect. Instead, dedicated actions of {@link sap.m.ListItemActionType type} <code>Delete</code> or <code>Edit</code> should be used.
*
* @since 1.137
*/
itemActionCount : {type : "int", group: "Misc", defaultValue: -1}
},
defaultAggregation : "items",
aggregations : {
/**
* Defines the items contained within this control.
*/
items : {type : "sap.m.ListItemBase", multiple : true, singularName : "item", bindable : "bindable", selector: "#{id} .sapMListItems", dnd : true},
/**
* User can swipe to bring in this control on the right hand side of an item.
* <b>Note:</b>
* <ul>
* <li>For non-touch screen devices, this functionality is ignored.</li>
* <li>There is no accessible alternative provided by the control for swiping.
* Applications that use this functionality must provide an accessible alternative UI to perform the same action.</li>
* <ul>
*/
swipeContent : {type : "sap.ui.core.Control", multiple : false},
/**
* The header area can be used as a toolbar to add extra controls for user interactions.
* <b>Note:</b> When set, this overwrites the <code>headerText</code> property.
* @since 1.16
*/
headerToolbar : {type : "sap.m.Toolbar", multiple : false},
/**
* A toolbar that is placed below the header to show extra information to the user.
* @since 1.16
*/
infoToolbar : {type : "sap.m.Toolbar", multiple : false},
/**
* Defines the context menu of the items.
*
* @since 1.54
*/
contextMenu : {type : "sap.ui.core.IContextMenu", multiple : false},
/**
* Defines the message strip to display binding-related messages.
* @since 1.73
*/
_messageStrip: {type : "sap.m.MessageStrip", multiple : false, visibility : "hidden"},
/**
* Defines the custom visualization if there is no data available.
* <b>Note:</b> If both a <code>noDataText</code> property and a <code>noData</code> aggregation are provided, the <code>noData</code> aggregation takes priority.
* If the <code>noData</code> aggregation is undefined or set to null, the <code>noDataText</code> property is used instead.
* @since 1.101
*/
noData: {type: "sap.ui.core.Control", multiple: false, altTypes: ["string"]}
},
associations: {
/**
* Association to controls / ids which label this control (see WAI-ARIA attribute aria-labelledby).
* @since 1.28.0
*/
ariaLabelledBy: { type: "sap.ui.core.Control", multiple: true, singularName: "ariaLabelledBy" }
},
events : {
/**
* Fires when selection is changed via user interaction. In <code>MultiSelect</code> mode, this event is also fired on deselection.
* @deprecated Since version 1.16.
* Use the <code>selectionChange</code> event instead.
*/
select : {deprecated: true,
parameters : {
/**
* The item which fired the select event.
*/
listItem : {type : "sap.m.ListItemBase"}
}
},
/**
* Fires when selection is changed via user interaction inside the control.
* @since 1.16
*/
selectionChange : {
parameters : {
/**
* The item whose selection has changed. In <code>MultiSelect</code> mode, only the up-most selected item is returned. This parameter can be used for single-selection modes.
*/
listItem : {type : "sap.m.ListItemBase"},
/**
* Array of items whose selection has changed. This parameter can be used for <code>MultiSelect</code> mode.
*/
listItems : {type : "sap.m.ListItemBase[]"},
/**
* Indicates whether the <code>listItem</code> parameter is selected or not.
*/
selected : {type : "boolean"},
/**
* Indicates whether the select all action is triggered or not.
*/
selectAll : {type : "boolean"}
}
},
/**
* Fires when delete icon is pressed by user.
*/
"delete" : {
parameters : {
/**
* The item which fired the delete event.
*/
listItem : {type : "sap.m.ListItemBase"}
}
},
/**
* Fires after user's swipe action and before the <code>swipeContent</code> is shown. On the <code>swipe</code> event handler, <code>swipeContent</code> can be changed according to the swiped item.
* Calling the <code>preventDefault</code> method of the event cancels the swipe action.
*
* <b>Note:</b> There is no accessible alternative provided by the control for swiping.
* Applications that use this functionality must provide an accessible alternative UI to perform the same action.
*/
swipe : {allowPreventDefault : true,
parameters : {
/**
* The item which fired the swipe.
*/
listItem : {type : "sap.m.ListItemBase"},
/**
* Aggregated <code>swipeContent</code> control that is shown on the right hand side of the item.
*/
swipeContent : {type : "sap.ui.core.Control"},
/**
* Holds which control caused the swipe event within the item.
*/
srcControl : {type : "sap.ui.core.Control"},
/**
* Shows in which direction the user swipes and can have the value <code>BeginToEnd</code> (left to right in LTR languages
* and right to left in RTL languages) or <code>EndToBegin</code> (right to left in LTR languages
* and left to right in RTL languages)
*/
swipeDirection : {type : "sap.m.SwipeDirection"}
}
},
/**
* Fires before the new growing chunk is requested from the model.
* @since 1.16
* @deprecated Since version 1.16.3.
* Instead, use <code>updateStarted</code> event with listening <code>changeReason</code>.
*/
growingStarted : {deprecated: true,
parameters : {
/**
* Actual number of items.
*/
actual : {type : "int"},
/**
* Total number of items.
*/
total : {type : "int"}
}
},
/**
* Fires after the new growing chunk has been fetched from the model and processed by the control.
* @since 1.16
* @deprecated Since version 1.16.3.
* Instead, use "updateFinished" event.
*/
growingFinished : {deprecated: true,
parameters : {
/**
* Actual number of items.
*/
actual : {type : "int"},
/**
* Total number of items.
*/
total : {type : "int"}
}
},
/**
* Fires before <code>items</code> binding is updated (e.g. sorting, filtering)
*
* <b>Note:</b> Event handler should not invalidate the control.
* @since 1.16.3
*/
updateStarted : {
parameters : {
/**
* The reason of the update, e.g. Binding, Filter, Sort, Growing, Change, Refresh, Context.
*/
reason : {type : "string"},
/**
* Actual number of items.
*/
actual : {type : "int"},
/**
* The total count of bound items. This can be used if the <code>growing</code> property is set to <code>true</code>.
*/
total : {type : "int"}
}
},
/**
* Fires after <code>items</code> binding is updated and processed by the control.
* @since 1.16.3
*/
updateFinished : {
parameters : {
/**
* The reason of the update, e.g. Binding, Filter, Sort, Growing, Change, Refresh, Context.
*/
reason : {type : "string"},
/**
* Actual number of items.
*/
actual : {type : "int"},
/**
* The total count of bound items. This can be used if the <code>growing</code> property is set to <code>true</code>.
*/
total : {type : "int"}
}
},
/**
* Fires when an item is pressed unless the item's <code>type</code> property is <code>Inactive</code>.
* @since 1.20
*/
itemPress : {
parameters : {
/**
* The item which fired the pressed event.
*/
listItem : {type : "sap.m.ListItemBase"},
/**
* The control which caused the press event within the container.
*/
srcControl : {type : "sap.ui.core.Control"}
}
},
/**
* Fired when the context menu is opened.
* When the context menu is opened, the binding context of the item is set to the given <code>contextMenu</code>.
* @since 1.54
*/
beforeOpenContextMenu : {
allowPreventDefault : true,
parameters : {
/**
* Item in which the context menu was opened.
*/
listItem : {type : "sap.m.ListItemBase"}
}
},
/**
* Fired when an item action is pressed.
* @since 1.137
*/
itemActionPress: {
parameters: {
/**
* The list item action that fired the event
*/
action : {type : "sap.m.ListItemAction"},
/**
* The list item in which the action was performed
*/
listItem : {type : "sap.m.ListItemBase"}
}
}
},
designtime: "sap/m/designtime/ListBase.designtime"
},
renderer: ListBaseRenderer
});
// announce accessibility details at the initial focus
ListBase.prototype.bAnnounceDetails = true;
ListBase.getInvisibleText = function() {
if (!this.oInvisibleText) {
this.oInvisibleText = new InvisibleText().toStatic();
}
return this.oInvisibleText;
};
// class name for the navigation items
ListBase.prototype.sNavItemClass = "sapMLIB";
ListBase.prototype.init = function() {
this._aNavSections = [];
this._aSelectedPaths = [];
this._iItemNeedsHighlight = 0;
this._iItemNeedsNavigated = 0;
this._bItemsBeingBound = false;
this._bSkippedInvalidationOnRebind = false;
this.data("sap-ui-fastnavgroup", "true", true); // Define group for F6 handling
};
ListBase.prototype.onBeforeRendering = function() {
this._bRendering = true;
this._bActiveItem = false;
this._aNavSections = [];
this._removeSwipeContent();
};
ListBase.prototype.onAfterRendering = function() {
this._bRendering = false;
this._sLastMode = this.getMode();
this._startItemNavigation(true);
};
ListBase.prototype.exit = function () {
this._aNavSections = [];
this._aSelectedPaths = [];
this._destroyGrowingDelegate();
this._destroyItemNavigation();
this._oLastFakeFocusedItem = null;
};
// this gets called only with oData Model when first load or filter/sort
ListBase.prototype.refreshItems = function(sReason) {
this._bRefreshItems = true;
this._clearUnboundSelections(sReason);
if (this._oGrowingDelegate) {
// inform growing delegate to handle
this._oGrowingDelegate.refreshItems(sReason);
} else {
// if data multiple time requested during the ongoing request
// UI5 cancels the previous requests then we should fire updateStarted once
if (!this._bReceivingData) {
// handle update started event
this._updateStarted(sReason);
this._bReceivingData = true;
}
// for flat list get all data
this.refreshAggregation("items");
}
};
// this gets called via JSON and OData model when binding is updated
// if there is no data this should get called anyway
// TODO: if there is a network error this will not get called
// but we need to turn back to initial state
ListBase.prototype.updateItems = function(sReason, oEventInfo) {
// Special handling for "AutoExpandSelect" of the V4 ODataModel.
if (oEventInfo && oEventInfo.detailedReason === "AddVirtualContext") {
createVirtualItem(this);
if (this._oGrowingDelegate) {
this._oGrowingDelegate.reset(true);
}
return;
} else if (oEventInfo && oEventInfo.detailedReason === "RemoveVirtualContext") {
destroyVirtualItem(this);
return;
}
if (this._bSkippedInvalidationOnRebind && this.getBinding("items").getLength() === 0) {
this.invalidate();
}
this._clearUnboundSelections(sReason);
if (this._oGrowingDelegate) {
// inform growing delegate to handle
this._oGrowingDelegate.updateItems(sReason);
} else {
if (this._bReceivingData) {
// if we are receiving the data this should be oDataModel
// updateStarted is already handled before on refreshItems
// here items binding is updated because data is came from server
// so we can convert the flag for the next request
this._bReceivingData = false;
} else {
// if data is not requested this should be JSON Model
// data is already in memory and will not be requested
// so we do not need to change the flag
// this._bReceivingData should be always false
this._updateStarted(sReason);
}
// for flat list update items aggregation
this.updateAggregation("items");
// items binding are updated
this._updateFinished();
}
this._updateInvisibleGroupText();
this._bSkippedInvalidationOnRebind = false;
};
function createVirtualItem(oList) {
var oBinding = oList.getBinding("items");
var oBindingInfo = oList.getBindingInfo("items");
var iLen = oList.getGrowing() ? oList.getGrowingThreshold() : oBindingInfo.length;
var iIdx = oList.getGrowing() || !oBindingInfo.startIndex ? 0 : oBindingInfo.startIndex;
var oVirtualContext = oBinding.getContexts(iIdx, iLen)[0];
destroyVirtualItem(oList);
oList._oVirtualItem = GrowingEnablement.createItem(oVirtualContext, oBindingInfo, "virtual");
oList.addAggregation("dependents", oList._oVirtualItem, true);
}
function destroyVirtualItem(oList) {
if (oList._oVirtualItem) {
oList._oVirtualItem.destroy();
delete oList._oVirtualItem;
}
}
ListBase.prototype.setBindingContext = function(oContext, sModelName) {
var sItemsModelName = (this.getBindingInfo("items") || {}).model;
if (sItemsModelName === sModelName) {
this._resetItemsBinding();
}
return Control.prototype.setBindingContext.apply(this, arguments);
};
ListBase.prototype.bindAggregation = function(sName) {
this._bItemsBeingBound = sName === "items";
destroyVirtualItem(this);
Control.prototype.bindAggregation.apply(this, arguments);
this._bItemsBeingBound = false;
return this;
};
ListBase.prototype._bindAggregation = function(sName, oBindingInfo) {
function addBindingListener(oBindingInfo, sEventName, fHandler) {
oBindingInfo.events = oBindingInfo.events || {};
if (!oBindingInfo.events[sEventName]) {
oBindingInfo.events[sEventName] = fHandler;
} else {
// Wrap the event handler of the other party to add our handler.
var fOriginalHandler = oBindingInfo.events[sEventName];
oBindingInfo.events[sEventName] = function() {
fHandler.apply(this, arguments);
fOriginalHandler.apply(this, arguments);
};
}
}
if (sName === "items") {
this._resetItemsBinding();
addBindingListener(oBindingInfo, "dataRequested", this._onBindingDataRequestedListener.bind(this));
addBindingListener(oBindingInfo, "dataReceived", this._onBindingDataReceivedListener.bind(this));
}
Control.prototype._bindAggregation.call(this, sName, oBindingInfo);
if (sName === "items" && this.getModel(oBindingInfo.model).isA("sap.ui.model.odata.v4.ODataModel")) {
this.getBinding("items").attachEvent("selectionChanged", onBindingSelectionChanged, this);
}
};
ListBase.prototype._onBindingDataRequestedListener = function(oEvent) {
this._showBusyIndicator();
if (this._dataReceivedHandlerId != null) {
clearTimeout(this._dataReceivedHandlerId);
delete this._dataReceivedHandlerId;
}
};
ListBase.prototype._onBindingDataReceivedListener = function(oEvent) {
if (this._dataReceivedHandlerId != null) {
clearTimeout(this._dataReceivedHandlerId);
delete this._dataReceivedHandlerId;
}
// The list will be set to busy when a request is sent, and set to not busy when a response is received.
// Under certain conditions it can happen that there are multiple requests in the request queue of the binding, which will be processed
// sequentially. In this case the busy indicator will be shown and hidden multiple times (flickering) until all requests have been
// processed. With this timer we avoid the flickering, as the list will only be set to not busy after all requests have been processed.
this._dataReceivedHandlerId = setTimeout(function() {
this._hideBusyIndicator();
delete this._dataReceivedHandlerId;
}.bind(this), 0);
if (this._oGrowingDelegate) {
// inform growing delegate to handle
this._oGrowingDelegate._onBindingDataReceivedListener(oEvent);
}
};
async function onBindingSelectionChanged(oEvent) {
const oContext = oEvent.getParameter("context");
if (!this._bSelectionMode) {
return;
}
if (oContext.getBinding().getHeaderContext() === oContext) {
if (oContext.isSelected()) {
Log.warning("Selecting the header context does not affect the list selection", this);
} else {
this.removeSelections(true, true);
}
return;
}
const sModelName = this.getBindingInfo("items").model;
const oItem = this.getItems().find((oItem) => oItem.getBindingContext(sModelName) === oContext);
if (!oItem) {
if (oContext.isSelected()) {
Log.warning("Selecting a context that is not related to an existing item does not affect the list selection", this);
}
return;
}
await Promise.resolve(); // ListItemBase first needs to update its selected property, otherwise the selectionChange event is fired too often.
const bContextIsSelected = oContext.isSelected();
this.setSelectedItem(oItem, bContextIsSelected, bContextIsSelected !== oItem.getSelected());
if (bContextIsSelected && this.getMode().includes("SingleSelect")) {
oContext.getBinding().getAllCurrentContexts().forEach((oCurrentContext) => {
if (oCurrentContext !== oContext) {
oCurrentContext.setSelected(false);
}
});
}
}
ListBase.prototype.destroyItems = function(bSuppressInvalidate) {
// check whether we have items to destroy or not
if (!this.getItems(true).length) {
return this;
}
// suppress the synchronous DOM removal of the aggregation destroy
this.destroyAggregation("items", "KeepDom");
// invalidate to update the DOM on the next tick of the RenderManager
if (!bSuppressInvalidate) {
if (this._bItemsBeingBound) {
this._bSkippedInvalidationOnRebind = true;
} else {
this.invalidate();
}
}
return this;
};
ListBase.prototype.getItems = function(bReadOnly) {
if (bReadOnly) {
return this.mAggregations["items"] || [];
}
return this.getAggregation("items", []);
};
ListBase.prototype.getId = function(sSuffix) {
var sId = this.sId;
return sSuffix ? sId + "-" + sSuffix : sId;
};
ListBase.prototype.setGrowing = function(bGrowing) {
bGrowing = !!bGrowing;
if (this.getGrowing() != bGrowing) {
this.setProperty("growing", bGrowing, !bGrowing);
if (bGrowing) {
this._oGrowingDelegate = new GrowingEnablement(this);
} else if (this._oGrowingDelegate) {
this._oGrowingDelegate.destroy();
this._oGrowingDelegate = null;
}
}
return this;
};
ListBase.prototype.setGrowingThreshold = function(iThreshold) {
return this.setProperty("growingThreshold", iThreshold, true);
};
ListBase.prototype.setEnableBusyIndicator = function(bEnable) {
this.setProperty("enableBusyIndicator", bEnable, true);
this._hideBusyIndicator();
return this;
};
ListBase.prototype.setNoData = function (vNoData) {
this.setAggregation("noData", vNoData, true);
if (typeof vNoData === "string") {
this.$("nodata-text").text(vNoData);
} else if (vNoData) {
this.invalidate();
} else if (!vNoData) {
this.$("nodata-text").text(this.getNoDataText());
}
return this;
};
ListBase.prototype.setNoDataText = function(sNoDataText) {
this.setProperty("noDataText", sNoDataText, true);
if (!this.getNoData()) {
// only set noDataText, if noData aggregation is not specified
this.$("nodata-text").text(this.getNoDataText());
}
return this;
};
ListBase.prototype.getNoDataText = function(bCheckBusy) {
// check busy state
if (bCheckBusy && this._bBusy) {
return "";
}
// return no data text from resource bundle when there is no custom
var sNoDataText = this.getProperty("noDataText");
sNoDataText = sNoDataText || Library.getResourceBundleFor("sap.m").getText("LIST_NO_DATA");
return sNoDataText;
};
/**
* Returns selected list item. When no item is selected, "null" is returned. When "multi-selection" is enabled and multiple items are selected, only the up-most selected item is returned.
*
* @type sap.m.ListItemBase
* @public
*/
ListBase.prototype.getSelectedItem = function() {
var aItems = this.getItems(true);
for (var i = 0; i < aItems.length; i++) {
if (aItems[i].getSelected()) {
return aItems[i];
}
}
return null;
};
/**
* Selects or deselects the given list item.
*
* @param {sap.m.ListItemBase} oListItem The list item whose selection is changed
* @param {boolean} [bSelect=true] Sets selected status of the list item provided
* @param {boolean} [bFireEvent=false] Determines whether the <code>selectionChange</code> event is fired by this method call (as of version 1.121)
* @returns {this} Reference to <code>this</code> in order to allow method chaining
* @public
*/
ListBase.prototype.setSelectedItem = function(oListItem, bSelect, bFireEvent) {
if (this.indexOfItem(oListItem) < 0) {
Log.warning("setSelectedItem is called without valid ListItem parameter on " + this);
return this;
}
if (this._bSelectionMode) {
oListItem.setSelected((bSelect === undefined) ? true : !!bSelect);
bFireEvent && this._fireSelectionChangeEvent([oListItem]);
}
return this;
};
/**
* Returns an array containing the selected list items. If no items are selected, an empty array is returned.
*
* @type sap.m.ListItemBase[]
* @public
*/
ListBase.prototype.getSelectedItems = function() {
return this.getItems(true).filter(function(oItem) {
return oItem.getSelected();
});
};
/**
* Sets a list item to be selected by id. In single mode the method removes the previous selection.
*
* @param {string} sId
* The id of the list item whose selection to be changed.
* @param {boolean} [bSelect=true]
* Sets selected status of the list item
* @type this
* @public
*/
ListBase.prototype.setSelectedItemById = function(sId, bSelect) {
var oListItem = Element.getElementById(sId);
return this.setSelectedItem(oListItem, bSelect);
};
/**
* Returns the binding contexts of the selected items.
* Note: This method returns an empty array if no databinding is used.
*
* @param {boolean} [bAll=false]
* Set true to include even invisible selected items(e.g. the selections from the previous filters).
* Note: In single selection modes, only the last selected item's binding context is returned in array.
* @type sap.ui.model.Context[]
* @public
* @since 1.18.6
*/
ListBase.prototype.getSelectedContexts = function(bAll) {
const oBindingInfo = this.getBindingInfo("items");
const sModelName = oBindingInfo?.model;
const oModel = this.getModel(sModelName);
// only deal with binding case
if (!oBindingInfo || !oModel) {
return [];
}
// return binding contexts from all selection paths
if (bAll && this.getRememberSelections()) {
// in ODataV4Model getAllCurrentContexts will also include previously selected contexts
if (oModel.isA("sap.ui.model.odata.v4.ODataModel")) {
const aContexts = this.getBinding("items").getAllCurrentContexts?.() || [];
return aContexts.filter((oContext) => this._aSelectedPaths.includes(oContext.getPath()));
}
// for all other models, ask model to provide context over binding path
return this._aSelectedPaths.map((sPath) => oModel.getContext(sPath));
}
// return binding context of current selected items
return this.getSelectedItems().map((oItem) => oItem.getBindingContext(sModelName));
};
/**
* Removes visible selections of the current selection mode.
*
* @param {boolean} [bAll=false] If the <code>rememberSelection</code> property is set to <code>true</code>, this control preserves selections after filtering or sorting. Set this parameter to <code>true</code> to remove all selections (as of version 1.16)
* @param {boolean} [bFireEvent=false] Determines whether the <code>selectionChange</code> event is fired by this method call (as of version 1.121)
* @returns {this} Reference to <code>this</code> in order to allow method chaining
* @public
*/
ListBase.prototype.removeSelections = function(bAll, bFireEvent, _bDetectBinding) {
var aChangedListItems = [];
this._oSelectedItem = null;
if (bAll) {
this._aSelectedPaths = [];
if (!_bDetectBinding) {
const oBinding = this.getBinding("items");
const aContexts = oBinding?.getAllCurrentContexts?.() || [];
aContexts[0]?.setSelected && aContexts.forEach((oContext) => oContext.setSelected(false));
}
}
this.getItems(true).forEach(function(oItem) {
if (!oItem.getSelected()) {
return;
}
// if the selected property is two-way bound then we do not need to update the selection
if (_bDetectBinding && oItem.isSelectedBoundTwoWay()) {
return;
}
oItem.setSelected(false, true);
aChangedListItems.push(oItem);
!bAll && this._updateSelectedPaths(oItem);
}, this);
if (bFireEvent && aChangedListItems.length) {
this._fireSelectionChangeEvent(aChangedListItems);
}
return this;
};
/**
* Selects all items in the <code>MultiSelection</code> mode.
*
* <b>Note:</b> If <code>growing</code> is enabled, only the visible items in the list are selected.
* Since version 1.93, the items are not selected if <code>getMultiSelectMode=ClearAll</code>.
*
* @param {boolean} [bFireEvent=false] Determines whether the <code>selectionChange</code> event is fired by this method call (as of version 1.121)
* @returns {this} Reference to <code>this</code> in order to allow method chaining
* @public
* @since 1.16
*/
ListBase.prototype.selectAll = function (bFireEvent) {
if (this.getMode() != "MultiSelect" || this.getMultiSelectMode() == MultiSelectMode.ClearAll) {
return this;
}
var aChangedListItems = [];
this.getItems(true).forEach(function(oItem) {
if (oItem.isSelectable() && !oItem.getSelected()) {
oItem.setSelected(true, true);
aChangedListItems.push(oItem);
this._updateSelectedPaths(oItem);
}
}, this);
if (bFireEvent && aChangedListItems.length) {
this._fireSelectionChangeEvent(aChangedListItems, bFireEvent);
}
var iSelectableItemCount = this.getItems().filter(function(oListItem) {
return oListItem.isSelectable();
}).length;
if (bFireEvent && this.getGrowing() && this.getMultiSelectMode() === "SelectAll" && this.getBinding("items")?.getLength() > iSelectableItemCount) {
var oSelectAllDomRef = this._getSelectAllCheckbox ? this._getSelectAllCheckbox() : undefined;
if (oSelectAllDomRef) {
Util.showSelectionLimitPopover(iSelectableItemCount, oSelectAllDomRef);
}
}
return this;
};
/**
* Returns the last list mode, the mode that is rendered
* This can be used to detect mode changes during rendering
*
* @protected
*/
ListBase.prototype.getLastMode = function(sMode) {
return this._sLastMode;
};
ListBase.prototype.setMode = function(sMode) {
sMode = this.validateProperty("mode", sMode);
var sOldMode = this.getMode();
if (sOldMode == sMode) {
return this;
}
// determine the selection mode
this._bSelectionMode = sMode.indexOf("Select") > -1;
// remove selections if mode is not a selection mode
if (!this._bSelectionMode) {
this.removeSelections(true);
} else {
// update selection status of items
var aSelecteds = this.getSelectedItems();
if (aSelecteds.length > 1) {
// remove selection if there are more than one item is selected
this.removeSelections(true);
} else if (sOldMode === ListMode.MultiSelect) {
// if old mode is multi select then we need to remember selected item
// in case of new item selection right after setMode call
this._oSelectedItem = aSelecteds[0];
}
}
// update property with invalidate
return this.setProperty("mode", sMode);
};
/**
* Returns growing information as object with "actual" and "total" keys.
* Note: This function returns "null" if "growing" feature is disabled.
*
* @returns {{actual: int, total: int} | null}
* @public
* @since 1.16
*/
ListBase.prototype.getGrowingInfo = function() {
return this._oGrowingDelegate ? this._oGrowingDelegate.getInfo() : null;
};
ListBase.prototype.setRememberSelections = function(bRemember) {
this.setProperty("rememberSelections", bRemember, true);
!this.getRememberSelections() && (this._aSelectedPaths = []);
return this;
};
/*
* Sets internal remembered selected context paths.
* This method can be called to reset remembered selection
* and does not change selection of the items until binding update.
*
* @param {string[]} aSelectedPaths valid binding context path array
* @since 1.26
* @protected
*/
ListBase.prototype.setSelectedContextPaths = function(aSelectedPaths) {
this._aSelectedPaths = aSelectedPaths || [];
};
/*
* Returns internal remembered selected context paths as a copy if rememberSelections is set to true,
* else returns the binding context path for the current selected items.
*
* @return {string[]} selected items binding context path
* @since 1.26
* @protected
*/
ListBase.prototype.getSelectedContextPaths = function(bAll) {
// return this selectedPaths if rememberSelections is true
if (!bAll || (bAll && this.getRememberSelections())) {
return this._aSelectedPaths.slice(0);
}
// return the binding context path of current selected items
return this.getSelectedItems().map(function(oItem) {
return oItem.getBindingContextPath();
});
};
/* Determines whether all selectable items are selected or not
* @protected
*/
ListBase.prototype.isAllSelectableSelected = function() {
if (this.getMode() != ListMode.MultiSelect) {
return false;
}
var aItems = this.getItems(true),
iSelectedItemCount = this.getSelectedItems().length,
iSelectableItemCount = aItems.filter(function(oItem) {
return oItem.isSelectable();
}).length;
return (aItems.length > 0) && (iSelectedItemCount == iSelectableItemCount);
};
/*
* Returns only visible items
* @protected
*/
ListBase.prototype.getVisibleItems = function() {
return this.getItems(true).filter(function(oItem) {
return oItem.getVisible();
});
};
// return whether list has active item or not
ListBase.prototype.getActiveItem = function() {
return this._bActiveItem;
};
// this gets called when items DOM is changed
ListBase.prototype.onItemDOMUpdate = function(oListItem) {
if (!this._bRendering && this.bOutput) {
this._startItemNavigation(true);
}
var bVisibleItems = this.getVisibleItems().length > 0;
if (!bVisibleItems && !this._bInvalidatedForNoData) {
this.invalidate();
this._bInvalidatedForNoData = true;
} else if (bVisibleItems && this._bInvalidatedForNoData) {
this.invalidate();
this._bInvalidatedForNoData = false;
}
};
// this gets called when items active state is changed
ListBase.prototype.onItemActiveChange = function(oListItem, bActive) {
this._bActiveItem = bActive;
};
// this gets called when item type column requirement is changed
ListBase.prototype.onItemHighlightChange = function(oItem, bNeedsHighlight) {
this._iItemNeedsHighlight += (bNeedsHighlight ? 1 : -1);
// update highlight visibility
if (this._iItemNeedsHighlight == 1 && bNeedsHighlight) {
this.$("listUl").addClass("sapMListHighlight");
} else if (this._iItemNeedsHighlight == 0) {
this.$("listUl").removeClass("sapMListHighlight");
}
};
ListBase.prototype.onItemNavigatedChange = function(oItem, bNeedsNavigated) {
this._iItemNeedsNavigated += (bNeedsNavigated ? 1 : -1);
// update navigated visibility
if (this._iItemNeedsNavigated == 1 && bNeedsNavigated) {
this.$("listUl").addClass("sapMListNavigated");
} else if (this._iItemNeedsNavigated == 0) {
this.$("listUl").removeClass("sapMListNavigated");
}
};
// this gets called when selected property of the ListItem is changed
ListBase.prototype.onItemSelectedChange = function(oListItem, bSelected) {
if (this.getMode() == ListMode.MultiSelect) {
this._updateSelectedPaths(oListItem, bSelected);
return;
}
if (bSelected) {
this._aSelectedPaths = [];
this._oSelectedItem && this._oSelectedItem.setSelected(false, true);
this._oSelectedItem = oListItem;
} else if (this._oSelectedItem === oListItem) {
this._oSelectedItem = null;
}
// update selection path for the list item
this._updateSelectedPaths(oListItem, bSelected);
};
// this gets called after the selected property of the ListItem is changed
ListBase.prototype.onItemAfterSelectedChange = function(oListItem, bSelected) {
this.fireEvent("itemSelectedChange", {
listItem: oListItem,
selected: bSelected
});
};
/*
* Returns items container DOM reference
* @protected
*/
ListBase.prototype.getItemsContainerDomRef = function() {
return this.getDomRef("listUl");
};
/*
* This hook method is called if a sticky header is activated and additional height needs to be added in the calculation of the scrolling position.
* @protected
*/
ListBase.prototype.getStickyFocusOffset = function() {
return 0;
};
ListBase.prototype.checkGrowingFromScratch = function() {};
/*
* This hook method gets called if growing feature is enabled and before new page loaded
* @protected
*/
ListBase.prototype.onBeforePageLoaded = function(oGrowingInfo, sChangeReason) {
this._fireUpdateStarted(sChangeReason, oGrowingInfo);
this.fireGrowingStarted(oGrowingInfo);
};
/*
* This hook method get called if growing feature is enabled and after page loaded
* @protected
*/
ListBase.prototype.onAfterPageLoaded = function(oGrowingInfo, sChangeReason) {
this._updateStickyClasses();
this._oLastGroupHeaderBeforeGrowing = null;
this._fireUpdateFinished(oGrowingInfo);
this.fireGrowingFinished(oGrowingInfo);
};
/*
* Adds navigation section that we can be navigate with alt + down/up
* @protected
*/
ListBase.prototype.addNavSection = function(sId) {
this._aNavSections.push(sId);
return sId;
};
/*
* Returns the max items count.
* If aggregation items is bound the count will be the length of the binding
* otherwise the length of the list items aggregation will be returned
* @protected
*/
ListBase.prototype.getMaxItemsCount = function() {
var oBinding = this.getBinding("items");
if (oBinding && oBinding.getLength) {
return oBinding.getLength() || 0;
}
return this.getItems(true).length;
};
/*
* This hook method is called from renderer to determine whether items should render or not
* @protected
*/
ListBase.prototype.shouldRenderItems = function() {
return true;
};
/*
* This hook method is called from GrowingEnablement to determine whether
* growing should suppress List invalidation
* @protected
*/
ListBase.prototype.shouldGrowingSuppressInvalidation = function() {
return true;
};
// when new items binding we should turn back to initial state
ListBase.prototype._resetItemsBinding = function() {
if (this.isBound("items")) {
this._bUpdating = false;
this._bReceivingData = false;
this.removeSelections(true, false, true);
this._oGrowingDelegate && this._oGrowingDelegate.reset();
this._hideBusyIndicator();
/* reset focused position */
if (this._oItemNavigation && document.activeElement.id != this.getId("nodata")) {
this._oItemNavigation.iFocusedIndex = -1;
}
}
};
// clear the s