/*!
* UI development toolkit for HTML5 (OpenUI5)
* (c) Copyright 2009-2018 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
// Provides class sap.m.GrowingEnablement
sap.ui.define([
'sap/ui/base/Object',
'sap/ui/core/format/NumberFormat',
'sap/m/library',
'sap/ui/model/ChangeReason',
'sap/ui/base/ManagedObjectMetadata',
'sap/ui/core/HTML',
"sap/base/security/encodeXML"
],
function(
BaseObject,
NumberFormat,
library,
ChangeReason,
ManagedObjectMetadata,
HTML,
encodeXML
) {
"use strict";
// shortcut for sap.m.ListType
var ListType = library.ListType;
// shortcut for sap.m.ListGrowingDirection
var ListGrowingDirection = library.ListGrowingDirection;
/**
* Creates a GrowingEnablement delegate that can be attached to ListBase Controls requiring capabilities for growing
*
* @extends sap.ui.base.Object
* @alias sap.m.GrowingEnablement
* @experimental Since 1.16. This class is experimental and provides only limited functionality. Also the API might be changed in future.
*
* @param {sap.m.ListBase} oControl the ListBase control of which this Growing is the delegate
*
* @constructor
* @protected
*/
var GrowingEnablement = BaseObject.extend("sap.m.GrowingEnablement", /** @lends sap.m.GrowingEnablement.prototype */ {
constructor : function(oControl) {
BaseObject.apply(this);
this._oControl = oControl;
this._oControl.bUseExtendedChangeDetection = true;
this._oControl.addDelegate(this);
/* init growing list */
var iRenderedItemsLength = this._oControl.getItems(true).length;
this._iRenderedDataItems = iRenderedItemsLength;
this._iLimit = iRenderedItemsLength;
this._bLoading = false;
this._sGroupingPath = "";
this._bDataRequested = false;
this._oContainerDomRef = null;
this._iLastItemsCount = 0;
this._iTriggerTimer = 0;
this._aChunk = [];
this._oRM = null;
},
/**
* Destroys this GrowingEnablement delegate.
* This function must be called by the control which uses this delegate in the exit
function.
*/
destroy : function() {
if (this._oTrigger) {
this._oTrigger.destroy();
this._oTrigger = null;
}
if (this._oScrollDelegate) {
this._oScrollDelegate.setGrowingList(null);
this._oScrollDelegate = null;
}
if (this._oRM) {
this._oRM.destroy();
this._oRM = null;
}
this._oControl.$("triggerList").remove();
this._oControl.bUseExtendedChangeDetection = false;
this._oControl.removeDelegate(this);
this._oContainerDomRef = null;
this._oControl = null;
},
// renders load more trigger
render : function(oRm) {
oRm.write("
");
oRm.renderControl(this._getTrigger());
oRm.write("
");
},
onAfterRendering : function() {
var oControl = this._oControl;
if (oControl.getGrowingScrollToLoad()) {
var oScrollDelegate = library.getScrollDelegate(oControl);
if (oScrollDelegate) {
this._oScrollDelegate = oScrollDelegate;
oScrollDelegate.setGrowingList(this.onScrollToLoad.bind(this), oControl.getGrowingDirection());
}
} else if (this._oScrollDelegate) {
this._oScrollDelegate.setGrowingList(null);
this._oScrollDelegate = null;
}
if (!this._bLoading) {
this._updateTriggerDelayed(false);
}
},
setTriggerText : function(sText) {
this._oControl.$("triggerText").text(sText);
},
// reset paging
reset : function() {
this._iLimit = 0;
},
// determines growing reset with binding change reason
// according to UX sort/filter/context should reset the growing
shouldReset : function(sChangeReason) {
var mChangeReason = ChangeReason;
return sChangeReason == mChangeReason.Sort ||
sChangeReason == mChangeReason.Filter ||
sChangeReason == mChangeReason.Context;
},
// get actual and total info
getInfo : function() {
return {
total : this._oControl.getMaxItemsCount(),
actual : this._iRenderedDataItems
};
},
onScrollToLoad: function() {
var oTriggerButton = this._oControl.getDomRef("triggerList");
if (this._bLoading || !oTriggerButton || oTriggerButton.style.display != "none") {
return;
}
if (this._oControl.getGrowingDirection() == ListGrowingDirection.Upwards) {
var oScrollDelegate = this._oScrollDelegate;
this._oScrollPosition = {
left : oScrollDelegate.getScrollLeft(),
top : oScrollDelegate.getScrollHeight()
};
}
this.requestNewPage();
},
// call to request new page
requestNewPage : function() {
if (!this._oControl || this._bLoading) {
return;
}
// if max item count not reached or if we do not know the count
var oBinding = this._oControl.getBinding("items");
if (oBinding && !oBinding.isLengthFinal() || this._iLimit < this._oControl.getMaxItemsCount()) {
// The GrowingEnablement has its own busy indicator. Do not show the busy indicator, if existing, of the parent control.
if (this._oControl.getMetadata().hasProperty("enableBusyIndicator")) {
this._bParentEnableBusyIndicator = this._oControl.getEnableBusyIndicator();
this._oControl.setEnableBusyIndicator(false);
}
this._iLimit += this._oControl.getGrowingThreshold();
this._updateTriggerDelayed(true);
this.updateItems("Growing");
}
},
// called before new page loaded
_onBeforePageLoaded : function(sChangeReason) {
this._bLoading = true;
this._oControl.onBeforePageLoaded(this.getInfo(), sChangeReason);
},
// called after new page loaded
_onAfterPageLoaded : function(sChangeReason) {
this._bLoading = false;
this._updateTriggerDelayed(false);
this._oControl.onAfterPageLoaded(this.getInfo(), sChangeReason);
// After the data has been loaded, restore the busy indicator handling of the parent control.
if (this._oControl.setEnableBusyIndicator) {
this._oControl.setEnableBusyIndicator(this._bParentEnableBusyIndicator);
}
},
// created and returns load more trigger
_getTrigger : function() {
var sTriggerID = this._oControl.getId() + "-trigger",
sTriggerText = this._oControl.getGrowingTriggerText();
sTriggerText = sTriggerText || sap.ui.getCore().getLibraryResourceBundle("sap.m").getText("LOAD_MORE_DATA");
this._oControl.addNavSection(sTriggerID);
if (this._oTrigger) {
this.setTriggerText(sTriggerText);
return this._oTrigger;
}
// The growing button is changed to span tag as h1 tag was semantically incorrect.
this._oTrigger = new sap.m.CustomListItem({
id: sTriggerID,
busyIndicatorDelay: 0,
type: ListType.Active,
content: new HTML({
content: '' +
'
' +
'' + encodeXML(sTriggerText) + '' +
'
' +
'
' +
'
'
})
}).setParent(this._oControl, null, true).attachPress(this.requestNewPage, this).addEventDelegate({
onsapenter : function(oEvent) {
this.requestNewPage();
oEvent.preventDefault();
},
onsapspace : function(oEvent) {
this.requestNewPage();
oEvent.preventDefault();
},
onAfterRendering : function(oEvent) {
this._oTrigger.$().attr({
"tabindex": 0,
"role": "button",
"aria-labelledby": sTriggerID + "Text" + " " + sTriggerID + "Info"
});
}
}, this);
// stop the eventing between item and the list
this._oTrigger.getList = function() {};
// defines the tag name
this._oTrigger.TagName = "div";
return this._oTrigger;
},
// returns the growing information to be shown at the growing button
_getListItemInfo : function() {
this._iLastItemsCount = this._oControl.getItems(true).length;
return ("[ " + this._iRenderedDataItems + " / " + NumberFormat.getFloatInstance().format(this._oControl.getMaxItemsCount()) + " ]");
},
// returns the first sorters grouping path when available
_getGroupingPath : function(oBinding) {
var aSorters = oBinding.aSorters || [];
var oSorter = aSorters[0] || {};
return (oSorter.fnGroup) ? oSorter.sPath || "" : "";
},
// if table has pop-in then we have two rows for one item
_getDomIndex : function(vIndex) {
if (typeof vIndex != "number") {
return vIndex;
}
if (this._oControl.hasPopin && this._oControl.hasPopin()) {
return (vIndex * 2);
}
return vIndex;
},
// determines if the scroll container of the list has enough scrollable area to hide the growing button
_getHasScrollbars : function() {
if (!this._oScrollDelegate) {
return false;
}
if (this._iRenderedDataItems >= 40) {
return true;
}
// after growing-button gets hidden scroll container should still be scrollable
return this._oScrollDelegate.getMaxScrollTop() > this._oControl.getDomRef("triggerList").clientHeight;
},
// destroy all items in the list and cleanup
destroyListItems : function(bSuppressInvalidate) {
this._oControl.destroyItems(bSuppressInvalidate);
this._iRenderedDataItems = 0;
this._aChunk = [];
},
// appends single list item to the list
addListItem : function(oContext, oBindingInfo, bSuppressInvalidate) {
var oControl = this._oControl,
oBinding = oBindingInfo.binding,
oItem = this.createListItem(oContext, oBindingInfo);
if (oBinding.isGrouped()) {
// creates group header if need
var aItems = oControl.getItems(true),
oLastItem = aItems[aItems.length - 1],
sModelName = oBindingInfo.model,
oGroupInfo = oBinding.getGroup(oItem.getBindingContext(sModelName));
if (oLastItem && oLastItem.isGroupHeader()) {
oControl.removeAggregation("items", oLastItem, true);
this._fnAppendGroupItem = this.appendGroupItem.bind(this, oGroupInfo, oLastItem, bSuppressInvalidate);
oLastItem = aItems[aItems.length - 1];
}
if (!oLastItem || oGroupInfo.key !== oBinding.getGroup(oLastItem.getBindingContext(sModelName)).key) {
var oGroupHeader = (oBindingInfo.groupHeaderFactory) ? oBindingInfo.groupHeaderFactory(oGroupInfo) : null;
if (oControl.getGrowingDirection() == ListGrowingDirection.Upwards) {
this.applyPendingGroupItem();
this._fnAppendGroupItem = this.appendGroupItem.bind(this, oGroupInfo, oGroupHeader, bSuppressInvalidate);
} else {
this.appendGroupItem(oGroupInfo, oGroupHeader, bSuppressInvalidate);
}
}
}
oControl.addAggregation("items", oItem, bSuppressInvalidate);
if (bSuppressInvalidate) {
this._aChunk.push(oItem);
}
},
applyPendingGroupItem: function() {
if (this._fnAppendGroupItem) {
this._fnAppendGroupItem();
this._fnAppendGroupItem = undefined;
}
},
appendGroupItem: function(oGroupInfo, oGroupHeader, bSuppressInvalidate) {
oGroupHeader = this._oControl.addItemGroup(oGroupInfo, oGroupHeader, bSuppressInvalidate);
if (bSuppressInvalidate) {
this._aChunk.push(oGroupHeader);
}
},
// creates list item from the factory
createListItem : function(oContext, oBindingInfo) {
this._iRenderedDataItems++;
var oItem = oBindingInfo.factory(ManagedObjectMetadata.uid("clone"), oContext);
return oItem.setBindingContext(oContext, oBindingInfo.model);
},
// update context on all items except group headers
updateItemsBindingContext : function(aContexts, oModel) {
if (!aContexts.length) {
return;
}
var aItems = this._oControl.getItems(true);
for (var i = 0, c = 0, oItem; i < aItems.length; i++) {
oItem = aItems[i];
// group headers are not in binding context
if (!oItem.isGroupHeader()) {
oItem.setBindingContext(aContexts[c++], oModel);
}
}
},
// render all the collected items in the chunk and flush them into the DOM
// vInsert whether to append (true) or replace (falsy) or to insert at a certain position (int)
applyChunk : function(vInsert, oDomRef) {
this.applyPendingGroupItem();
var iLength = this._aChunk.length;
if (!iLength) {
return;
}
if (this._oControl.getGrowingDirection() == ListGrowingDirection.Upwards) {
this._aChunk.reverse();
if (vInsert === true) {
vInsert = 0;
} else if (typeof vInsert == "number") {
vInsert = this._iRenderedDataItems - iLength - vInsert;
}
}
oDomRef = oDomRef || this._oContainerDomRef;
this._oRM = this._oRM || sap.ui.getCore().createRenderManager();
for (var i = 0; i < iLength; i++) {
this._oRM.renderControl(this._aChunk[i]);
}
this._oRM.flush(oDomRef, false, this._getDomIndex(vInsert));
this._aChunk = [];
},
// add multiple items to the list via BindingContext
addListItems : function(aContexts, oBindingInfo, bSuppressInvalidate) {
for (var i = 0; i < aContexts.length; i++) {
this.addListItem(aContexts[i], oBindingInfo, bSuppressInvalidate);
}
},
// destroy all the items and create from scratch
rebuildListItems : function(aContexts, oBindingInfo, bSuppressInvalidate) {
this.destroyListItems(bSuppressInvalidate);
this.addListItems(aContexts, oBindingInfo, bSuppressInvalidate);
if (bSuppressInvalidate) {
var bHasFocus = this._oContainerDomRef.contains(document.activeElement);
this.applyChunk(false);
bHasFocus && this._oControl.focus();
} else {
this.applyPendingGroupItem();
}
},
// inserts a single list item
insertListItem : function(oContext, oBindingInfo, iIndex) {
var oItem = this.createListItem(oContext, oBindingInfo);
this._oControl.insertAggregation("items", oItem, iIndex, true);
this._aChunk.push(oItem);
},
// destroy a single list item
deleteListItem : function(iIndex) {
this._oControl.getItems(true)[iIndex].destroy(true);
this._iRenderedDataItems--;
},
/**
* refresh items only for OData model.
*/
refreshItems : function(sChangeReason) {
if (!this._bDataRequested) {
this._bDataRequested = true;
this._onBeforePageLoaded(sChangeReason);
}
// set iItemCount to initial value if not set or no items at the control yet
if (!this._iLimit || this.shouldReset(sChangeReason) || !this._oControl.getItems(true).length) {
this._iLimit = this._oControl.getGrowingThreshold();
}
// send the request to get the context
this._oControl.getBinding("items").getContexts(0, this._iLimit);
},
/**
* update control aggregation if contexts are already available
* or send a request to get the contexts in case of ODATA model.
*/
updateItems : function(sChangeReason) {
var oControl = this._oControl,
oBinding = oControl.getBinding("items"),
oBindingInfo = oControl.getBindingInfo("items"),
aItems = oControl.getItems(true);
// set limit to initial value if not set yet or no items at the control yet
if (!this._iLimit || this.shouldReset(sChangeReason) || !aItems.length) {
this._iLimit = oControl.getGrowingThreshold();
}
// fire growing started event if data was requested this is a followup call of updateItems
if (this._bDataRequested) {
this._bDataRequested = false;
} else {
this._onBeforePageLoaded(sChangeReason);
}
// get the context from the binding or request will be sent
var aContexts = oBinding.getContexts(0, this._iLimit) || [];
// if getContexts did cause a request to be sent, set the internal flag so growing started event is not fired again
if (aContexts.dataRequested) {
this._bDataRequested = true;
// a partial response may already be contained, so only return here without updating the list when diff is empty
if (aContexts.diff && !aContexts.diff.length) {
return;
}
}
// cache dom ref for internal functions not to lookup again and again
this._oContainerDomRef = oControl.getItemsContainerDomRef();
// aContexts.diff ==> undefined : New data we should build from scratch
// aContexts.diff ==> [] : There is no diff, means data did not changed at all
// aContexts.diff ==> [{index: 0, type: "delete"}, {index: 1, type: "insert"},...] : Run the diff logic
var aDiff = aContexts.diff,
bFromScratch = false,
vInsertIndex;
// process the diff
if (!aContexts.length) {
// no context, destroy list items
this.destroyListItems();
} else if (!this._oContainerDomRef) {
// no dom ref for compatibility reason start from scratch
this.rebuildListItems(aContexts, oBindingInfo);
} else if (!aDiff || !aItems.length && aDiff.length) {
// new records need to be applied from scratch
this.rebuildListItems(aContexts, oBindingInfo, true);
} else if (oBinding.isGrouped() || oControl.checkGrowingFromScratch()) {
if (this._sGroupingPath != this._getGroupingPath(oBinding)) {
// grouping is changed so we need to rebuild the list for the group headers
bFromScratch = true;
} else {
// append items if possible
for (var i = 0; i < aDiff.length; i++) {
var oDiff = aDiff[i],
oContext = aContexts[oDiff.index];
if (oDiff.type == "delete") {
// group header may need to be deleted as well
bFromScratch = true;
break;
} else if (oDiff.index != this._iRenderedDataItems) {
// this item is not appended
bFromScratch = true;
break;
} else {
this.addListItem(oContext, oBindingInfo, true);
vInsertIndex = true;
}
}
}
} else {
if (this._sGroupingPath) {
// if it was already grouped then we need to remove group headers first
oControl.removeGroupHeaders(true);
}
vInsertIndex = -1;
var iLastInsertIndex = -1;
for (var i = 0; i < aDiff.length; i++) {
var oDiff = aDiff[i],
iDiffIndex = oDiff.index,
oContext = aContexts[iDiffIndex];
if (oDiff.type == "delete") {
if (vInsertIndex != -1) {
// this record is deleted while the chunk is getting build
this.applyChunk(vInsertIndex);
iLastInsertIndex = -1;
vInsertIndex = -1;
}
this.deleteListItem(iDiffIndex);
} else {
if (vInsertIndex == -1) {
// the subsequent of items needs to be inserted at this position
vInsertIndex = iDiffIndex;
} else if (iLastInsertIndex > -1 && iDiffIndex != iLastInsertIndex + 1) {
// this item is not simply appended to the last one but has been inserted
this.applyChunk(vInsertIndex);
vInsertIndex = iDiffIndex;
}
this.insertListItem(oContext, oBindingInfo, iDiffIndex);
iLastInsertIndex = iDiffIndex;
}
}
}
if (bFromScratch) {
this.rebuildListItems(aContexts, oBindingInfo, true);
} else if (this._oContainerDomRef && aDiff) {
// set the binding context of items inserting/deleting entries shifts the index of all following items
this.updateItemsBindingContext(aContexts, oBindingInfo.model);
this.applyChunk(vInsertIndex);
}
this._oContainerDomRef = null;
this._sGroupingPath = this._getGroupingPath(oBinding);
if (!this._bDataRequested) {
this._onAfterPageLoaded(sChangeReason);
}
},
_updateTriggerDelayed: function(bLoading) {
if (this._oControl.getGrowingScrollToLoad()) {
this._iTriggerTimer && window.cancelAnimationFrame(this._iTriggerTimer);
this._iTriggerTimer = window.requestAnimationFrame(this._updateTrigger.bind(this, bLoading));
} else {
this._updateTrigger(bLoading);
}
},
// updates the trigger state
_updateTrigger : function(bLoading) {
var oTrigger = this._oTrigger,
oControl = this._oControl;
// If there are no visible columns then also hide the trigger.
if (!oTrigger || !oControl || !oControl.shouldRenderItems() || !oControl.getDomRef()) {
return;
}
var oBinding = oControl.getBinding("items");
if (!oBinding) {
return;
}
// update busy state
oTrigger.setBusy(bLoading);
oTrigger.$().toggleClass("sapMGrowingListBusyIndicatorVisible", bLoading);
if (bLoading) {
oTrigger.setActive(false);
oControl.$("triggerList").css("display", "");
} else {
var aItems = oControl.getItems(true),
iItemsLength = aItems.length,
iBindingLength = oBinding.getLength() || 0,
bLengthFinal = oBinding.isLengthFinal(),
bHasScrollToLoad = oControl.getGrowingScrollToLoad(),
oTriggerDomRef = oTrigger.getDomRef();
// put the focus to the newly added item if growing button is pressed
if (oTriggerDomRef && oTriggerDomRef.contains(document.activeElement)) {
(aItems[this._iLastItemsCount] || oControl).focus();
}
// show, update or hide the growing button
if (!iItemsLength || !this._iLimit ||
(bLengthFinal && this._iLimit >= iBindingLength) ||
(bHasScrollToLoad && this._getHasScrollbars())) {
oControl.$("triggerList").css("display", "none");
} else {
if (bLengthFinal) {
oControl.$("triggerInfo").css("display", "block").text(this._getListItemInfo());
}
oTrigger.$().removeClass("sapMGrowingListBusyIndicatorVisible");
oControl.$("triggerList").css("display", "");
}
// at the beginning we should scroll to last item
if (bHasScrollToLoad && this._oScrollPosition === undefined && oControl.getGrowingDirection() == ListGrowingDirection.Upwards) {
this._oScrollPosition = {
left : 0,
top : 0
};
}
// scroll to last position
if (iItemsLength > 0 && this._oScrollPosition) {
var oScrollDelegate = this._oScrollDelegate,
oScrollPosition = this._oScrollPosition;
oScrollDelegate.scrollTo(oScrollPosition.left, oScrollDelegate.getScrollHeight() - oScrollPosition.top);
this._oScrollPosition = null;
}
}
}
});
return GrowingEnablement;
});