@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
1,414 lines (1,256 loc) • 166 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2026 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
/*eslint-disable max-len */
// Provides class sap.ui.model.odata.ODataTreeBindingFlat
sap.ui.define([
"sap/base/assert",
"sap/base/Log",
"sap/base/util/extend",
"sap/base/util/isEmptyObject",
"sap/base/util/uid",
"sap/ui/model/_Helper",
"sap/ui/model/ChangeReason",
"sap/ui/model/Context",
"sap/ui/model/Filter",
"sap/ui/model/TreeBinding",
"sap/ui/model/TreeBindingUtils",
"sap/ui/model/odata/v2/ODataTreeBinding"
], function(assert, Log, extend, isEmptyObject, uid, _Helper, ChangeReason, Context, Filter,
TreeBinding, TreeBindingUtils, ODataTreeBinding) {
"use strict";
var sClassName = "sap.ui.model.odata.ODataTreeBindingFlat";
/**
* Adapter for TreeBindings to add the ListBinding functionality and use the
* tree structure in list based controls.
*
* @alias sap.ui.model.odata.ODataTreeBindingFlat
* @function
*
* @public
*/
var ODataTreeBindingFlat = function() {
// ensure only TreeBindings are enhanced which have not been enhanced yet
if (!(this instanceof TreeBinding) || this._bIsAdapted) {
return;
}
// apply the methods of the adapters prototype to the TreeBinding instance
for (var fn in ODataTreeBindingFlat.prototype) {
if (ODataTreeBindingFlat.prototype.hasOwnProperty(fn)) {
this[fn] = ODataTreeBindingFlat.prototype[fn];
}
}
// make sure we have a parameter object
this.mParameters = this.mParameters || {};
// keep track of the page-size for expand request
this._iPageSize = 0;
// flat data structure to store the tree nodes depth-first ordered
this._aNodes = this._aNodes || [];
// the node cache for the last requested nodes (via getContexts)
this._aNodeCache = [];
// the tree states
this._aCollapsed = this._aCollapsed || [];
this._aExpanded = this._aExpanded || [];
this._aRemoved = [];
this._aAdded = [];
this._aNodeChanges = [];
this._aAllChangedNodes = [];
this._aTurnedToLeaf = [];
// a map of all subtree handles, which are removed from the tree (and may be re-inserted)
this._mSubtreeHandles = {};
// lowest server-index level, will be kept correct when loading new entries
this._iLowestServerLevel = null;
// selection state
this._aExpandedAfterSelectAll = this._aExpandedAfterSelectAll || [];
this._mSelected = this._mSelected || {};
this._mDeselected = this._mDeselected || {};
this._bSelectAll = false;
// the delta variable for calculating the correct binding-length (used e.g. for sizing the scrollbar)
this._iLengthDelta = 0;
//default value for collapse recursive
if (this.mParameters.collapseRecursive === undefined) {
this.bCollapseRecursive = true;
} else {
this.bCollapseRecursive = !!this.mParameters.collapseRecursive;
}
this._bIsAdapted = true;
this._bReadOnly = true;
this._aPendingRequests = [];
this._aPendingChildrenRequests = [];
this._aPendingSubtreeRequests = [];
// TODO: No longer required in legacy-free UI5
// Whether ODataTreeBindingFlat#submitChanges has been called
this._bSubmitChangesCalled = false;
};
/**
* Sets the number of expanded levels.
*
* @param {number} iLevels The number of levels which should be expanded, minimum is 0
*
* @protected
* @see sap.ui.model.odata.v2.ODataTreeBinding#setNumberOfExpandedLevels
* @ui5-restricted sap.ui.table.AnalyticalTable
*/
ODataTreeBindingFlat.prototype.setNumberOfExpandedLevels = function(iLevels) {
this.resetData();
ODataTreeBinding.prototype.setNumberOfExpandedLevels.apply(this, arguments);
};
/**
* Gets an array of contexts for the requested part of the tree.
*
* @param {number} [iStartIndex=0]
* The index of the first requested context
* @param {number} [iLength]
* The maximum number of returned contexts; if not given the model's size limit is used; see
* {@link sap.ui.model.Model#setSizeLimit}
* @param {number} [iThreshold=0]
* The maximum number of contexts to read to read additionally as buffer
* @return {sap.ui.model.Context[]}
* The requested tree contexts
*
* @protected
*/
ODataTreeBindingFlat.prototype.getContexts = function (iStartIndex, iLength, iThreshold) {
return this._getContextsOrNodes(false, iStartIndex, iLength, iThreshold);
};
/**
* Gets an array of either node objects or contexts for the requested part of the tree.
*
* @param {boolean} bReturnNodes
* Whether to return node objects or contexts
* @param {number} [iStartIndex=0]
* The index of the first requested node or context
* @param {number} [iLength]
* The maximum number of returned nodes or contexts; if not given the model's size limit is
* used; see {@link sap.ui.model.Model#setSizeLimit}
* @param {number} [iThreshold=0]
* The maximum number of nodes or contexts to read additionally as buffer
* @return {object[]|sap.ui.model.Context[]}
* The requested tree nodes or contexts
*
* @private
*/
ODataTreeBindingFlat.prototype._getContextsOrNodes = function (bReturnNodes, iStartIndex,
iLength, iThreshold) {
if (!this.isResolved() || this.isInitial()) {
return [];
}
iStartIndex = iStartIndex || 0;
iLength = iLength || this.oModel.iSizeLimit;
iThreshold = iThreshold || 0;
this._iPageSize = iLength;
this._iThreshold = iThreshold;
// shortcut for initial load
if (this._aNodes.length == 0 && !this.isLengthFinal()) {
this._loadData(iStartIndex, iLength, iThreshold);
}
// cut out the requested section from the tree
var aResultContexts = [];
var aNodes = this._retrieveNodeSection(iStartIndex, iLength);
// clear node cache
this._aNodeCache = [];
// calculate $skip and $top values
var iSkip;
var iTop = 0;
var iLastServerIndex = 0;
// potentially missing entries for each parent deeper than the # of expanded levels
var mGaps = {};
for (var i = 0; i < aNodes.length; i++) {
var oNode = aNodes[i];
// cache node for more efficient access on the currently visible nodes in the TreeTable
// only cache nodes which are real contexts
this._aNodeCache[iStartIndex + i] = oNode && oNode.context ? oNode : undefined;
aResultContexts.push(oNode.context);
if (!oNode.context) {
if (oNode.serverIndex != undefined) { // 0 is a valid server-index!
// construct the $skip to $top range for server indexed nodes
if (iSkip == undefined) {
iSkip = oNode.serverIndex;
}
iLastServerIndex = oNode.serverIndex;
} else if (oNode.positionInParent != undefined) { //0 is a valid index here too
// nodes, which don't have a context but a positionInParent property, are manually expanded missing nodes
var oParent = oNode.parent;
mGaps[oParent.key] = mGaps[oParent.key] || [];
mGaps[oParent.key].push(oNode);
}
}
}
// $top needs to be at minimum 1
iTop = 1 + Math.max(iLastServerIndex - (iSkip || 0), 0);
//if something is missing on the server indexed nodes -> request it
if (iSkip != undefined && iTop) {
this._loadData(iSkip, iTop, iThreshold);
}
//check if we are missing some manually expanded nodes
for (var sMissingKey in mGaps) {
var oRequestParameters = this._calculateRequestParameters(mGaps[sMissingKey]);
this._loadChildren(mGaps[sMissingKey][0].parent, oRequestParameters.skip, oRequestParameters.top);
}
// either return nodes or contexts
if (bReturnNodes) {
return aNodes;
} else {
return aResultContexts;
}
};
/**
* Calculates the $skip and $top for the OData request.
*
* @param {object[]} aMissing An array of missing nodes
* @returns {object} An object with <code>skip</code> and <code>top</code>
*
* @private
*/
ODataTreeBindingFlat.prototype._calculateRequestParameters = function (aMissing) {
var i,
iMissingSkip = aMissing[0].positionInParent,
oParent = aMissing[0].parent,
iMissingLength = Math.min(iMissingSkip + Math.max(this._iThreshold, aMissing.length),
oParent.children.length);
for (i = iMissingSkip; i < iMissingLength; i++) {
var oChild = oParent.children[i];
if (oChild) {
break;
}
}
return {
skip: iMissingSkip,
top: i - iMissingSkip
};
};
/**
* Cuts out a piece from the tree.
*
* @param {number} iStartIndex The first index to cut out
* @param {number} iLength The number of nodes to cut
*
* @returns {object[]} The cut nodes
*
* @private
*/
ODataTreeBindingFlat.prototype._retrieveNodeSection = function (iStartIndex, iLength) {
return this._bReadOnly
? this._indexRetrieveNodeSection(iStartIndex, iLength)
: this._mapRetrieveNodeSection(iStartIndex, iLength);
};
/**
* Checks whether there is a pending request to get the children for the given node key.
*
* @param {string} sNodeKey The node key
*
* @returns {boolean} Whether there is a pending request fetching the children of the given node
*
* @private
*/
ODataTreeBindingFlat.prototype._hasPendingRequest = function (sNodeKey) {
return this._aPendingChildrenRequests.some((oRequest) => oRequest.sParent === sNodeKey);
};
/**
* Turn the given node to a leaf
*
* @param {object} oNode The node
*
* @private
*/
ODataTreeBindingFlat.prototype._turnNodeToLeaf = function (oNode) {
oNode.nodeState.collapsed = false;
oNode.nodeState.expanded = false;
oNode.nodeState.isLeaf = true;
oNode.nodeState.wasExpanded = true;
this._aTurnedToLeaf.push(oNode);
};
/*
* @private
*/
ODataTreeBindingFlat.prototype._mapRetrieveNodeSection = function (iStartIndex, iLength) {
const iLastNodeIndex = this.getLength() - 1;
var iNodeCounter = -1;
var aNodes = [];
let oPreviousNode;
this._map((oNode, oRecursionBreaker, sIndexType, iIndex, oParent) => {
iNodeCounter++;
if (oNode && this._aRemoved.length) {
// check if previous node has to be a leaf node
if (oPreviousNode && oPreviousNode.nodeState.expanded
&& oPreviousNode.level >= oNode.level
&& !this._hasPendingRequest(oPreviousNode.key)) {
this._turnNodeToLeaf(oPreviousNode);
} else if (oNode.nodeState.expanded && iLastNodeIndex === iNodeCounter
&& !this._hasPendingRequest(oNode.key)) {
// Leaf transformation process for the last element in the tree
this._turnNodeToLeaf(oNode);
}
}
oPreviousNode = oNode;
if (iNodeCounter >= iStartIndex) {
// if we have a missing node and it is a server-indexed node -> introduce a gap object
if (!oNode) {
if (sIndexType == "serverIndex") {
oNode = {
serverIndex: iIndex
};
} else if (sIndexType == "positionInParent") {
oNode = {
positionInParent: iIndex,
parent: oParent
};
}
}
aNodes.push(oNode);
}
if (aNodes.length >= iLength) {
oRecursionBreaker.broken = true;
}
});
return aNodes;
};
/*
* @private
*/
ODataTreeBindingFlat.prototype._indexRetrieveNodeSection = function (iStartIndex, iLength) {
var i, aNodes = [], oNodeInfo, oNode;
for (i = iStartIndex; i < iStartIndex + iLength; i++) {
oNodeInfo = this.getNodeInfoByRowIndex(i);
if (oNodeInfo.index !== undefined && oNodeInfo.index < this._aNodes.length) {
oNode = this._aNodes[oNodeInfo.index];
if (!oNode) {
oNode = {
serverIndex: oNodeInfo.index
};
}
} else if (oNodeInfo.parent) {
oNode = oNodeInfo.parent.children[oNodeInfo.childIndex];
if (!oNode) {
oNode = {
parent: oNodeInfo.parent,
positionInParent: oNodeInfo.childIndex
};
}
}
if (oNode) {
aNodes.push(oNode);
oNode = null;
}
}
return aNodes;
};
/**
* Gets an array of nodes for the requested part of the tree.
*
* @param {number} [iStartIndex=0]
* The index of the first requested node
* @param {number} [iLength]
* The maximum number of returned nodes; if not given the model's size limit is used; see
* {@link sap.ui.model.Model#setSizeLimit}
* @param {number} [iThreshold=0]
* The maximum number of nodes to read additionally as buffer
* @return {Object[]}
* The requested tree nodes
*
* @protected
* @ui5-restricted sap.ui.table.TreeTable
*/
ODataTreeBindingFlat.prototype.getNodes = function (iStartIndex, iLength, iThreshold) {
return this._getContextsOrNodes(true, iStartIndex, iLength, iThreshold);
};
/**
* Applies the given callback function to all tree nodes including server-index nodes and deep
* nodes. It iterates all tree nodes unless the property <code>broken</code> of the callback
* function parameter <code>oRecursionBreaker</code> is set to <code>true</code>.
*
* @param {function} fnMap
* This callback function is called for all nodes of this tree. It has no return value and
* gets the following parameters:
* <ul>
* <li>{object} oNode: The current tree node</li>
* <li>{object} oRecursionBreaker: An object reference that allows to interrupt calling the
* callback function with further tree nodes</li>
* <li>{object} oRecursionBreaker.broken=false: Whether the recursion has to be interrupted
* when the current <code>oNode</code> has finished processing</li>
* <li>{string} sIndexType: Describes the node type ("serverIndex" for nodes on the highest
* hierarchy, "positionInParent" for nodes in subtrees and "newNode" for newly added
* nodes)</li>
* <li>{int} [iIndex]: The structured position in the tree accessible with the property
* described in <code>sIndexType</code></li>
* <li>{object} [oParent]: The parent node of the current tree node</li>
* </ul>
*
* @private
*/
ODataTreeBindingFlat.prototype._map = function (fnMap) {
var fnCheckNodeForAddedSubtrees, fnTraverseAddedSubtree, fnTraverseDeepSubtree,
fnTraverseFlatSubtree,
oRecursionBreaker = {broken: false};
/**
* Helper function to iterate all added subtrees of a node.
*
* @param {object} oNode The node to check for subtrees
*/
fnCheckNodeForAddedSubtrees = function (oNode) {
// if there are subnodes added to the current node -> traverse them first (added nodes are at the top, before any children)
if (oNode.addedSubtrees.length > 0 && !oNode.nodeState.collapsed) {
// an added subtree can be either a deep or a flat tree (depending on the addContexts) call
for (var j = 0; j < oNode.addedSubtrees.length; j++) {
var oSubtreeHandle = oNode.addedSubtrees[j];
fnTraverseAddedSubtree(oNode, oSubtreeHandle);
if (oRecursionBreaker.broken) {
return;
}
}
}
};
/**
* Traverses a re-inserted or newly added subtree.
* This can be a combination of flat and deep trees.
*
* Decides if the traversal has to branche over to a flat or a deep part of the tree.
*
* @param {object} oNode
* The parent node
* @param {object} oSubtreeHandle
* The subtree handle, inside there is either a deep or a flat tree stored
*/
fnTraverseAddedSubtree = function (oNode, oSubtreeHandle) {
var oSubtree = oSubtreeHandle._getSubtree();
if (oSubtreeHandle) {
// subtree is flat
if (Array.isArray(oSubtree)) {
if (oSubtreeHandle._oSubtreeRoot) {
// jump to a certain position in the flat structure and map the nodes
fnTraverseFlatSubtree(oSubtree, oSubtreeHandle._oSubtreeRoot.serverIndex, oSubtreeHandle._oSubtreeRoot, oSubtreeHandle._oSubtreeRoot.originalLevel || 0, oNode.level + 1);
} else {
// newly added nodes
fnTraverseFlatSubtree(oSubtree, null, null, 0, oNode.level + 1);
}
} else {
// subtree is deep
oSubtreeHandle._oSubtreeRoot.level = oNode.level + 1;
fnTraverseDeepSubtree(oSubtreeHandle._oSubtreeRoot, false, oSubtreeHandle._oNewParentNode, -1, oSubtreeHandle._oSubtreeRoot);
}
}
};
/**
* Recursive Tree Traversal
* @param {object} oNode the current node
* @param {boolean} bIgnore a flag to indicate if the node should be mapped
* @param {object} oParent the parent node of oNode
* @param {int} iPositionInParent the position of oNode in the children-array of oParent
* @param {object} [oIgnoreRemoveForNode] Newly inserted node which shouldn't be ignored
*/
fnTraverseDeepSubtree
= function (oNode, bIgnore, oParent, iPositionInParent, oIgnoreRemoveForNode) {
// ignore node if it was already mapped or is removed (except if it was reinserted, denoted by oIgnoreRemoveForNode)
if (!bIgnore) {
if (!oNode.nodeState.removed || oIgnoreRemoveForNode == oNode) {
fnMap(oNode, oRecursionBreaker, "positionInParent", iPositionInParent, oParent);
if (oRecursionBreaker.broken) {
return;
}
}
}
fnCheckNodeForAddedSubtrees(oNode);
if (oRecursionBreaker.broken) {
return;
}
// if the node also has children AND is expanded, dig deeper
if (oNode && oNode.children && oNode.nodeState.expanded) {
for (var i = 0; i < oNode.children.length; i++) {
var oChildNode = oNode.children[i];
// Make sure that the level of all child nodes are adapted to the parent level,
// this is necessary if the parent node was placed in a different leveled subtree.
// Ignore removed nodes, which are not re-inserted.
// Re-inserted deep nodes will be regarded in fnTraverseAddedSubtree.
if (oChildNode && !oChildNode.nodeState.removed && !oChildNode.nodeState.reinserted) {
oChildNode.level = oNode.level + 1;
}
// only dive deeper if we have a gap (entry which has to be loaded) or a defined node is NOT removed
if (oChildNode && !oChildNode.nodeState.removed) {
fnTraverseDeepSubtree(oChildNode, false, oNode, i, oIgnoreRemoveForNode);
} else if (!oChildNode) {
fnMap(oChildNode, oRecursionBreaker, "positionInParent", i, oNode);
}
if (oRecursionBreaker.broken) {
return;
}
}
}
};
/**
* Traverses a flat portion of the tree (or rather the given array).
*
* @param {object[]} aFlatTree
* The flat tree to traverse
* @param {number} iServerIndexOffset
* The server-index position, used to calculate $skip/$top
* @param {object} oIgnoreRemoveForNode
* Newly inserted node which shouldn't be ignored
* @param {number} iSubtreeBaseLevel
* Base level of the subtree
* @param {number} iNewParentBaseLevel
* Base level of the new parent
*/
fnTraverseFlatSubtree
= function (aFlatTree, iServerIndexOffset, oIgnoreRemoveForNode, iSubtreeBaseLevel,
iNewParentBaseLevel) {
//count the nodes until we find the correct index
for (var i = 0; i < aFlatTree.length; i++) {
var oNode = aFlatTree[i];
// If the node is removed -> ignore it
// BEWARE:
// If a removed range is reinserted again, we will deliver it instead of jumping over it.
// This is denoted by the "oIgnoreRemoveForNode", this is a node which will be served but only if it was traversed by fnTraverseAddedSubtree
if (oNode && oNode.nodeState && oNode.nodeState.removed && oNode != oIgnoreRemoveForNode) {
// only jump over the magnitude range if the node was not initially collapsed/server-expanded
if (!oNode.initiallyCollapsed) {
i += oNode.magnitude;
}
continue;
}
// calculate level shift if necessary (added subtrees are differently indented than before removal)
if (oNode && iSubtreeBaseLevel >= 0 && iNewParentBaseLevel >= 0) {
oNode.level = oNode.originalLevel || 0;
var iLevelDifNormalized = (oNode.level - iSubtreeBaseLevel) || 0;
oNode.level = iNewParentBaseLevel + iLevelDifNormalized || 0;
}
if (iServerIndexOffset === null) {
fnMap(oNode, oRecursionBreaker, "newNode");
} else {
// call map for the node itself, before traversing to any children/siblings
// the server-index position is used to calculate the $skip/$top values for loading the missing entries
fnMap(oNode, oRecursionBreaker, "serverIndex", iServerIndexOffset + i);
}
if (oRecursionBreaker.broken) {
return;
}
// if we have a node, lets see if we have to dig deeper or jump over some entries
if (oNode && oNode.nodeState) {
// jump over collapsed nodes by the enclosing magnitude
if (!oNode.initiallyCollapsed && oNode.nodeState.collapsed) {
i += oNode.magnitude;
} else if (oNode.initiallyCollapsed && oNode.nodeState.expanded) {
// look into expanded nodes deeper than the initial expand level
// the node itself will be ignored, since its fnMap was already called
fnTraverseDeepSubtree(oNode, true);
if (oRecursionBreaker.broken) {
return;
}
} else if (!oNode.initiallyCollapsed && oNode.nodeState.expanded) {
// before going to the next flat node (children|sibling), we look at the added subtrees in between
// this is only necessary for expanded server-indexed nodes
fnCheckNodeForAddedSubtrees(oNode);
}
}
// break recursion after fnMap or traversal function calls
if (oRecursionBreaker.broken) {
return;
}
}
};
//kickstart the traversal from the original flat nodes array (no server-index offset -> 0)
fnTraverseFlatSubtree(this._aNodes, 0, null);
};
/**
* Loads the server-index nodes within the range [iSkip, iSkip + iTop + iThreshold) and merges the nodes into
* the inner structure.
*
* @param {int} iSkip The start index of the loading
* @param {int} iTop The number of nodes to be loaded
* @param {int} iThreshold The size of the buffer
* @return {Promise<Object>} The promise resolves if the reload finishes successfully, otherwise it's rejected. The promise
* resolves with an object which has the calculated iSkip, iTop and the loaded content under
* property oData. It rejects with the error object which is returned from the server.
*
* @private
*/
ODataTreeBindingFlat.prototype._loadData = function (iSkip, iTop, iThreshold) {
var that = this;
if (!this.bSkipDataEvents) {
this.fireDataRequested();
}
this.bSkipDataEvents = false;
return this._requestServerIndexNodes(iSkip, iTop, iThreshold).then(function(oResponseData) {
that._addServerIndexNodes(oResponseData.oData, oResponseData.iSkip);
that._fireChange({reason: ChangeReason.Change});
that.fireDataReceived({data: oResponseData.oData});
}, function(oError) {
var bAborted = oError.statusCode === 0;
if (!bAborted) {
// reset data and trigger update
that._aNodes = [];
that._bLengthFinal = true;
that._fireChange({reason: ChangeReason.Change});
that.fireDataReceived();
}
});
};
/**
* Reloads the server-index nodes within the range [iSkip, iSkip + iTop) and merges them into the inner structure.
*
* @param {int} iSkip The start index of the loading
* @param {int} iTop The number of nodes to be loaded
* @param {boolean} bInlineCount Whether the inline count for all pages is requested
* @return {Promise<Object>} The promise resolves if the reload finishes successfully, otherwise it's rejected. The promise
* resolves with an object which has the calculated iSkip, iTop and the loaded content under
* property oData. It rejects with the error object which is returned from the server.
*
* @private
*/
ODataTreeBindingFlat.prototype._restoreServerIndexNodes = function (iSkip, iTop, bInlineCount) {
var that = this;
return this._requestServerIndexNodes(iSkip, iTop, 0, bInlineCount).then(function(oResponseData) {
that._addServerIndexNodes(oResponseData.oData, oResponseData.iSkip);
return oResponseData;
});
};
/**
* Merges the nodes in parameter oData into the inner structure
*
* @param {object} oData The content which contains the nodes from the backend
* @param {int} iSkip The start index for the merging into inner structure
*
* @private
*/
ODataTreeBindingFlat.prototype._addServerIndexNodes = function (oData, iSkip) {
var oEntry, sKey, iIndex, i,
// the function is used to test whether one of its ascendant is expanded after the selectAll
fnTest = function(oNode, index) {
return (!oNode.isDeepOne && !oNode.initiallyCollapsed
&& oNode.serverIndex < iIndex && oNode.serverIndex + oNode.magnitude >= iIndex);
};
// $inlinecount is in oData.__count, the $count is just oData
if (!this._bLengthFinal) {
var iCount = oData.__count ? parseInt(oData.__count) : 0;
this._aNodes[iCount - 1] = undefined;
this._bLengthFinal = true;
}
//merge data into flat array structure
if (oData.results && oData.results.length > 0) {
for (i = 0; i < oData.results.length; i++) {
oEntry = oData.results[i];
sKey = this.oModel.getKey(oEntry);
iIndex = iSkip + i;
const vMagnitude = oEntry[this.oTreeProperties["hierarchy-node-descendant-count-for"]];
let iMagnitude = Number(vMagnitude);
// check the magnitude attribute whether it's greater or equal than 0
if (iMagnitude < 0) {
iMagnitude = 0;
Log.error("The entry data with key '" + sKey + "' under binding path '" + this.getPath() + "' has a negative 'hierarchy-node-descendant-count-for' which isn't allowed.");
}
if (!Number.isSafeInteger(iMagnitude)) {
Log.error("The value of magnitude is not a safe integer: " + JSON.stringify(vMagnitude),
this.getResolvedPath(), sClassName);
}
var oNode = this._aNodes[iIndex] = this._aNodes[iIndex] || {
key: sKey,
context: this.oModel.getContext("/" + sKey,
this.oModel.resolveDeep(this.sPath, this.oContext) + sKey.slice(sKey.indexOf("("))),
magnitude: iMagnitude,
level: oEntry[this.oTreeProperties["hierarchy-level-for"]],
originalLevel: oEntry[this.oTreeProperties["hierarchy-level-for"]],
initiallyCollapsed: oEntry[this.oTreeProperties["hierarchy-drill-state-for"]] === "collapsed",
initiallyIsLeaf : oEntry[this.oTreeProperties["hierarchy-drill-state-for"]] === "leaf",
nodeState: {
isLeaf: oEntry[this.oTreeProperties["hierarchy-drill-state-for"]] === "leaf",
expanded: oEntry[this.oTreeProperties["hierarchy-drill-state-for"]] === "expanded",
collapsed: oEntry[this.oTreeProperties["hierarchy-drill-state-for"]] === "collapsed",
selected: this._mSelected[sKey] ? this._mSelected[sKey].nodeState.selected : false
},
children: [],
// an array containing all added subtrees, may be new context nodes or nodes which were removed previously
addedSubtrees: [],
serverIndex: iIndex,
// a server indexed node is not attributed with a parent, in contrast to the manually expanded nodes
parent: null,
originalParent : null,
isDeepOne: false
};
// track the lowest server-index level --> used to find out if a node is on the top level
if (this._iLowestServerLevel === null) {
this._iLowestServerLevel = oNode.level;
} else {
this._iLowestServerLevel = Math.min(this._iLowestServerLevel, oNode.level);
}
// selection update if we are in select-all mode
if (this._bSelectAll) {
if (!this._aExpandedAfterSelectAll.some(fnTest)) {
this.setNodeSelection(oNode, true);
}
}
}
}
};
/**
* Loads the server-index nodes based on the given range and the initial expand level.
*
* @param {int} iSkip The start index of the loading
* @param {int} iTop The number of nodes to be loaded
* @param {int} iThreshold The size of the buffer
* @param {boolean} bInlineCount Whether the inline count for all pages is requested
* @return {Promise<Object>} The promise resolves if the reload finishes successfully, otherwise it's rejected. The promise
* resolves with an object which has the calculated iSkip, iTop and the loaded content under
* property oData. It rejects with the error object which is returned from the server.
*
* @private
*/
ODataTreeBindingFlat.prototype._requestServerIndexNodes = function (iSkip, iTop, iThreshold, bInlineCount) {
return new Promise(function(resolve, reject) {
var oRequest = {
iSkip: iSkip,
iTop: iTop + (iThreshold || 0), // Top also contains threshold if applicable
iThreshold: iThreshold
// oRequestHandle: <will be set later>
};
// Order pending requests by index
this._aPendingRequests.sort(function(a, b) {
return a.iSkip - b.iSkip;
});
// Check pending requests:
// - adjust new request if pending requests already cover parts of it (delta determination)
// - ignore(/abort) new request if pending requests already cover it in full
// - cancel pending requests if the new request covers them in full plus additional data.
// handles will be aborted on filter/sort calls.
for (var i = 0; i < this._aPendingRequests.length; i++) {
if (TreeBindingUtils._determineRequestDelta(oRequest, this._aPendingRequests[i]) === false) {
return; // ignore this request
}
}
// Convenience
iSkip = oRequest.iSkip;
iTop = oRequest.iTop;
function _handleSuccess (oData) {
// Remove request from array
var idx = this._aPendingRequests.indexOf(oRequest);
this._aPendingRequests.splice(idx, 1);
resolve({
oData: oData,
iSkip: iSkip,
iTop: iTop
});
}
function _handleError (oError) {
// Remove request from array
var idx = this._aPendingRequests.indexOf(oRequest);
this._aPendingRequests.splice(idx, 1);
reject(oError);
}
var aUrlParameters = ["$skip=" + iSkip, "$top=" + iTop];
// request inlinecount only once
if (!this._bLengthFinal || bInlineCount) {
aUrlParameters.push("$inlinecount=allpages");
}
// add custom parameters (including $selects)
if (this.sCustomParams) {
aUrlParameters.push(this.sCustomParams);
}
// construct multi-filter for level filter and application filters
var oLevelFilter = new Filter(this.oTreeProperties["hierarchy-level-for"], "LE", this.getNumberOfExpandedLevels());
var aFilters = [oLevelFilter];
this._checkFilterForTreeProperties();
const oCombinedFilter = this.getCombinedFilter();
if (oCombinedFilter) {
aFilters.push(oCombinedFilter);
}
var sAbsolutePath = this.getResolvedPath();
if (sAbsolutePath) {
oRequest.oRequestHandle = this.oModel.read(sAbsolutePath, {
headers: this._getHeaders(),
urlParameters: aUrlParameters,
filters: [new Filter({
filters: aFilters,
and: true
})],
sorters: this.aSorters || [],
success: _handleSuccess.bind(this),
error: _handleError.bind(this),
groupId: this.sRefreshGroupId ? this.sRefreshGroupId : this.sGroupId
});
this._aPendingRequests.push(oRequest);
}
}.bind(this));
};
/*
* @private
*/
ODataTreeBindingFlat.prototype._propagateMagnitudeChange = function(oParent, iDelta) {
// propagate the magnitude along the parent chain, up to the top parent which is a
// server indexed node (checked by oParent.parent == null)
// first magnitude starting point is the no. of direct children/the childCount
while (oParent != null && (oParent.initiallyCollapsed || oParent.isDeepOne)) {
oParent.magnitude += iDelta;
if (!oParent.nodeState.expanded) {
return;
}
//up one level, ends at parent == null
oParent = oParent.parent;
}
};
/*
* Calculates the magnitude of a server index node after the initial loading
*
* @private
*/
ODataTreeBindingFlat.prototype._getInitialMagnitude = function(oNode) {
var iDelta = 0,
oChild;
if (oNode.isDeepOne) { // Use original value (not "new")
return 0;
}
if (oNode.children) {
for (var i = 0; i < oNode.children.length; i++) {
oChild = oNode.children[i];
iDelta += oChild.magnitude + 1;
}
}
return oNode.magnitude - iDelta;
};
/**
* Loads the direct children of the <code>oParentNode</code> within the range [iSkip, iSkip + iTop) and merge the
* new nodes into the <code>children</code> array under the parent node.
*
* @param {object} oParentNode The parent node under which the children are loaded
* @param {int} iSkip The start index of the loading
* @param {int} iTop The number of nodes which will be loaded
*
* @private
*/
ODataTreeBindingFlat.prototype._loadChildren = function(oParentNode, iSkip, iTop) {
var that = this;
if (!this.bSkipDataEvents) {
this.fireDataRequested();
}
this.bSkipDataEvents = false;
this._requestChildren(oParentNode, iSkip, iTop).then(function(oResponseData) {
that._addChildNodes(oResponseData.oData, oParentNode, oResponseData.iSkip);
that._fireChange({reason: ChangeReason.Change});
that.fireDataReceived({data: oResponseData.oData});
}, function(oError) {
var bAborted = oError.statusCode === 0;
if (!bAborted) {
// reset data and trigger update
if (oParentNode.childCount === undefined) {
oParentNode.children = [];
oParentNode.childCount = 0;
that._fireChange({reason: ChangeReason.Change});
}
that.fireDataReceived();
}
});
};
/**
* Reloads the child nodes of the <code>oParentNode</code> within the range [iSkip, iSkip + iTop) and merges them into the inner structure.
*
* After the child nodes are loaded, the parent node is expanded again.
*
* @param {object} oParentNode The parent node under which the children are reloaded
* @param {int} iSkip The start index of the loading
* @param {int} iTop The number of nodes to be loaded
* @return {Promise<Object>} The promise resolves if the reload finishes successfully, otherwise it's rejected. The promise
* resolves with an object which has the calculated iSkip, iTop and the loaded content under
* property oData. It rejects with the error object which is returned from the server.
*
* @private
*/
ODataTreeBindingFlat.prototype._restoreChildren = function(oParentNode, iSkip, iTop) {
var that = this,
// get the updated key from the context in case of insert
sParentId = oParentNode.context.getProperty(this.oTreeProperties["hierarchy-node-for"]);
// sParentKey = this.oModel.getKey(oParentNode.context);
return this._requestChildren(oParentNode, iSkip, iTop, true/*request inline count*/).then(function(oResponseData) {
var oNewParentNode;
that._map(function(oNode, oRecursionBreaker) {
if (oNode && oNode.context.getProperty(that.oTreeProperties["hierarchy-node-for"]) === sParentId) {
oNewParentNode = oNode;
oRecursionBreaker.broken = true;
}
});
if (oNewParentNode) {
that._addChildNodes(oResponseData.oData, oNewParentNode, oResponseData.iSkip);
that.expand(oNewParentNode, true);
}
return oResponseData;
});
};
/**
* Merges the nodes in <code>oData</code> into the <code>children</code> property under <code>oParentNode</code>.
*
* @param {object} oData The content which contains the nodes from the backed
* @param {object} oParentNode The parent node where the child nodes are saved
* @param {int} iSkip The start index for the merging into inner structure
*
* @private
*/
ODataTreeBindingFlat.prototype._addChildNodes = function(oData, oParentNode, iSkip) {
// $inlinecount is in oData.__count
// $count is just the 'oData' argument
if (oParentNode.childCount == undefined && oData && oData.__count) {
var iCount = oData.__count ? parseInt(oData.__count) : 0;
oParentNode.childCount = iCount;
oParentNode.children[iCount - 1] = undefined;
if (oParentNode.nodeState.expanded) {
// propagate the magnitude along the parent chain
this._propagateMagnitudeChange(oParentNode, iCount);
} else {
// If parent node is not expanded, do not propagate magnitude change up to its parents
oParentNode.magnitude = iCount;
}
// once when we reload data and know the direct-child count,
// we have to keep track of the expanded state for the newly loaded nodes, so the length delta can be calculated
this._cleanTreeStateMaps();
}
//merge data into flat array structure
if (oData.results && oData.results.length > 0) {
for (var i = 0; i < oData.results.length; i++) {
var oEntry = oData.results[i];
this._createChildNode(oEntry, oParentNode, iSkip + i);
}
}
};
/*
* @private
*/
ODataTreeBindingFlat.prototype._createChildNode = function(oEntry, oParentNode, iPositionInParent) {
var sKey = this.oModel.getKey(oEntry);
var iContainingServerIndex;
if (oParentNode.containingServerIndex !== undefined) {
iContainingServerIndex = oParentNode.containingServerIndex;
} else {
iContainingServerIndex = oParentNode.serverIndex;
}
var oNode = oParentNode.children[iPositionInParent] = oParentNode.children[iPositionInParent] || {
key: sKey,
context: this.oModel.getContext("/" + sKey,
this.oModel.resolveDeep(this.sPath, this.oContext) + sKey.slice(sKey.indexOf("("))),
//sub-child nodes have a magnitude of 0 at their first loading time
magnitude: 0,
//level is either given by the back-end or simply 1 level deeper than the parent
level: oParentNode.level + 1,
originalLevel: oParentNode.level + 1,
initiallyCollapsed: oEntry[this.oTreeProperties["hierarchy-drill-state-for"]] === "collapsed",
initiallyIsLeaf : oEntry[this.oTreeProperties["hierarchy-drill-state-for"]] === "leaf",
//node state is also given by the back-end
nodeState: {
isLeaf: oEntry[this.oTreeProperties["hierarchy-drill-state-for"]] === "leaf",
expanded: oEntry[this.oTreeProperties["hierarchy-drill-state-for"]] === "expanded",
collapsed: oEntry[this.oTreeProperties["hierarchy-drill-state-for"]] === "collapsed",
selected: this._mSelected[sKey] ? this._mSelected[sKey].nodeState.selected : false
},
positionInParent: iPositionInParent,
children: [],
// an array containing all added subtrees, may be new context nodes or nodes which were removed previously
addedSubtrees: [],
// a reference on the parent node, will only be set for manually expanded nodes, server-indexed node have a parent of null
parent: oParentNode,
// a reference on the original parent node, this property should not be changed by any algorithms, its used later to construct correct delete requests
originalParent: oParentNode,
// marks a node as a manually expanded one
isDeepOne: true,
// the deep child nodes have the same containing server index as the parent node
// the parent node is either a server-index node or another deep node which already has a containing-server-index
containingServerIndex: iContainingServerIndex
};
if (this._bSelectAll && this._aExpandedAfterSelectAll.indexOf(oParentNode) === -1) {
this.setNodeSelection(oNode, true);
}
return oNode;
};
/**
* Loads the child nodes based on the given range and <code>oParentNode</code>
*
* @param {object} oParentNode The node under which the children are loaded
* @param {int} iSkip The start index of the loading
* @param {int} iTop The number of nodes to be loaded
* @param {boolean} bInlineCount Whether the inline count should be requested from the backend
* @return {Promise<Object>} The promise resolves if the reload finishes successfully, otherwise it's rejected. The promise
* resolves with an object which has the calculated iSkip, iTop and the loaded content under
* property oData. It rejects with the error object which is returned from the server.
*
* @private
*/
ODataTreeBindingFlat.prototype._requestChildren = function (oParentNode, iSkip, iTop, bInlineCount) {
return new Promise(function(resolve, reject) {
var oRequest = {
sParent: oParentNode.key,
iSkip: iSkip,
iTop: iTop
// oRequestHandle: <will be set later>
};
// Order pending requests by index
this._aPendingChildrenRequests.sort(function(a, b) {
return a.iSkip - b.iSkip;
});
// Check pending requests:
// - adjust new request and remove parts that are already covered by pending requests
// - ignore (abort) a new request if it is already covered by pending requests
// - cancel pending requests if it is covered by the new request and additional data is added
// handles will be aborted on filter/sort calls
for (var i = 0; i < this._aPendingChildrenRequests.length; i++) {
var oPendingRequest = this._aPendingChildrenRequests[i];
if (oPendingRequest.sParent === oRequest.sParent) { // Parent key must match
if (TreeBindingUtils._determineRequestDelta(oRequest, oPendingRequest) === false) {
return; // ignore this request
}
}
}
// Convenience
iSkip = oRequest.iSkip;
iTop = oRequest.iTop;
function _handleSuccess (oData) {
// Remove request from array
var idx = this._aPendingChildrenRequests.indexOf(oRequest);
this._aPendingChildrenRequests.splice(idx, 1);
resolve({
oData: oData,
iSkip: iSkip,
iTop: iTop
});
}
function _handleError (oError) {
// Remove request from array
var idx = this._aPendingChildrenRequests.indexOf(oRequest);
this._aPendingChildrenRequests.splice(idx, 1);
reject(oError);
}
var aUrlParameters = ["$skip=" + iSkip, "$top=" + iTop];
// request inlinecount only once or inline count is needed explicitly
if (oParentNode.childCount == undefined || bInlineCount) {
aUrlParameters.push("$inlinecount=allpages");
}
// add custom parameters (including $selects)
if (this.sCustomParams) {
aUrlParameters.push(this.sCustomParams);
}
// construct multi-filter for level filter and application filters
var oLevelFilter = new Filter(this.oTreeProperties["hierarchy-parent-node-for"], "EQ", oParentNode.context.getProperty(this.oTreeProperties["hierarchy-node-for"]));
var aFilters = [oLevelFilter];
if (this.aApplicationFilters) {
aFilters = aFilters.concat(this.aApplicationFilters);
}
var sAbsolutePath = this.getResolvedPath();
if (sAbsolutePath) {
oRequest.oRequestHandle = this.oModel.read(sAbsolutePath, {
headers: this._getHeaders(),
urlParameters: aUrlParameters,
filters: [new Filter({
filters: aFilters,
and: true
})],
sorters: this.aSorters || [],
success: _handleSuccess.bind(this),
error: _handleError.bind(this),
groupId: this.sRefreshGroupId ? this.sRefreshGroupId : this.sGroupId
});
this._aPendingChildrenRequests.push(oRequest);
}
}.bind(this));
};
/**
* Loads the complete subtree of a given node up to a given level
*
* @param {object} oParentNode The root node of the requested subtree
* @param {int} iLevel The maximum expansion level of the subtree
* @return {Promise<Object>} Promise that resolves with the response data, parent key and level.
It rejects with the error object which is returned from the server.
*
* @private
*/
ODataTreeBindingFlat.prototype._loadSubTree = function (oParentNode, iLevel) {
var that = this;
var missingSectionsLoaded;
if (oParentNode.serverIndex !== undefined && !oParentNode.initiallyCollapsed) {
//returns the nodes flat starting from the parent to the last one inside the magnitude range
var aMissingSections = [];
var oSection;
var iSubTreeStart = oParentNode.serverIndex + 1;
var iSubTreeEnd = iSubTreeStart + oParentNode.magnitude;
for (var i = iSubTreeStart; i < iSubTreeEnd; i++) {
if (this._aNodes[i] === undefined) {
if (!oSection) {
oSection = {
iSkip: i,
iTop: 1
};
aMissingSections.push(oSection);
} else {
oSection.iTop++;
}
} else {
oSection = null;
}
}
if (aMissingSections.length) {
missingSectionsLoaded = Promise.all(aMissingSections.map(function (oMissingSection) {
return that._loadData(oMissingSection.iSkip, oMissingSection.iTop);
}));
}
}
if (!missingSectionsLoaded) {
missingSectionsLoaded = Promise.resolve();
}
return missingSectionsLoaded.then(function () {
if (!that.bSkipDataEvents) {
that.fireDataRequested();
}
that.bSkipDataEvents = false;
return that._requestSubTree(oParentNode, iLevel).then(function(oResponseData) {
that._addSubTree(oResponseData.oData, oParentNode);
that.fireDataReceived({data: oResponseData.oData});
}, function(oError) {
Log.warning("ODataTreeBindingFlat: Error during subtree request", oError.message);
var bAborted = oError.statusCode === 0;
if (!bAborted) {
that.fireDataReceived();
}
});
});
};
/**
* Merges the subtree in <code>oData</code> into the inner structure and expands it
*
* @param {object} oData The content which contains the nodes from the backed
* @param {object} oSubTreeRootNode The root node of the subtree
*
* @private
*/
ODataTreeBindingFlat.prototype._addSubTree = function(oData, oSubTreeRootNode) {
if (oData.results && oData.results.length > 0) {
var sNodeId, sParentNodeId, oEntry, oNode, oParentNode,
aAlreadyLoadedNodes = [],
mParentNodes = {},
i, j, k;
if (oSubTreeRootNode.serverIndex !== undefined && !oSubTreeRootNode.initiallyCollapsed) {
aAlreadyLoadedNodes = this._aNodes.slice(oSubTreeRootNode.serverIndex, oSubTreeRootNode.serverIndex + oSubTreeRootNode.magnitude + 1);
} else {
aAlreadyLoadedNodes.push(oSubTreeRootNode);
}
for (j = aAlreadyLoadedNodes.length - 1; j >= 0; j--) {
oNode = aAlreadyLoadedNodes[j];
if (oNode.nodeState.isLeaf) {
continue; // Skip leaf nodes - they can't be parents
}
if (oNode.initiallyCollapsed || oNode.isDeepOne) {
oNode.childCount = undefined; // We know all the children
// Changes to a collapsed nodes magnitude must not be propagated
if (oNode.magnitude && oNode.nodeState.expanded) {
// Propagate negative magnitude change before resetting nodes magnitude
this._propagateMagnitudeChange(oNode.parent, -oNode.magnitude);
}
oNode.magnitude = 0;
}
mParentNodes[oNode.context.getProperty(this.oTreeProperties["hierarchy-node-for"])] = oNode;
}
for (i = 0; i < oData.results.length; i++) {
oEntry = oData.results[i];
sNodeId = oEntry[this.oTreeProperties["hierarchy-node-for"]];
if (mParentNodes[sNodeId]) {
// Node already loaded as server index node
continue;
}
sParentNodeId = oEntry[this.oTreeProperties["hierarchy-parent-node-for"]];
oParentNode = mParentNodes[sParentNodeId];
if (oParentNode.childCount === undefined) {
oParentNode.childCount = 0;
}
oNode = oParentNode.children[oParentNode.childCount];
if (oNode) {
// Reuse existing nodes
aAlreadyLoadedNodes.push(oNode);
if (oNode.childCount) {
oNode.childCount = undefined;
if (oNode.initiallyCollapsed || oNode.isDeepOne) {
oNode.magnitude = 0;
}
}
} else {
// Create new node
oNode = this._createChildNode(oEntry, oParentNode, oParentNode.childCount);
if (oNode.nodeState.expanded) {
this._aExpanded.push(oNode);
this._sortNodes(this._aExpanded);
}
}
oParentNode.childCount++;
if (oParentNode.nodeState.expanded) {
// propagate the magnitude along the parent chain
this._propagateMagnitudeChange(oParentNode, 1);
} else {
// If parent node is not expanded, do not propagate magnitude change up to its parents
oParentNode.magnitude++;
}
if (!oNode.nodeState.isLeaf) {
mParentNodes[sNodeId] = oNode;
}
}
for (k = aAlreadyLoadedNodes.length - 1; k >= 0; k--) {
oNode = aAlreadyLoadedNodes[k];
if (!oNode.nodeState.expanded && !oNode.nodeState.isLeaf) {
this.expand(oNode, true);
}
}
}
};
/**
* Loads the complete subtree of a given node up to a given level
*
* @param {object} oParentNode The root node of the requested subtree
* @param {int} iLevel The maximum expansion level of the subtree
* @return {Promise<Object>} Promise that resolves with the response data, parent key and level.
It rejects with the error object which is returned from the server.
*
* @private
*/
ODataTreeBindingFlat.prototype._requestSubTree = function (oParentNode, iLevel) {
return new Promise(function(resolve, reject) {
var oRequest = {
sParent: oParentNode.key,
iLevel: iLevel
// oRequestHandle: <will be set later>
};
// Check pending requests:
for (var i = 0; i < this._aPendingSubtreeRequests.length; i++) {
var oPendingRequest = this._aPendingSubtreeRequests[i];
if (oPendingRequest.sParent === oRequest.sParent && oPendingRequest.iLevel === oRequest.iLevel) {
// Same request => ignore new request
return;
}
}
function _handleSuccess (oData) {
// Remove request from array
var idx = this._aPendingSubtreeRequests.indexOf(oRequest);
this._aPendingSubtreeRequests.splice(idx, 1);
resolve({
oData: oData,
sParent: oRequest.sParent,
iLevel: oRequest.iLevel
});
}
function _handleError (oError) {
// Remove request from array
var idx = this._aPendingSubtreeRequests.indexOf(oRequest);
this._aPendingSubtreeRequests.splice(idx, 1);
reject(oError);
}
var aUrlParameters = [];
// add custom parameters (including $selects)
if (this.sCustomParams) {
aUrlParameters.push(this.sCustomParams);
}
// construct multi-filter for level filter and application filters
const sHierarchyNodeForProperty = this.oTreeProperties["hierarchy-node-for"];
const oNodeFilter = new Filter(sHierarchyNodeForProperty, "EQ",
oParentNode.context.getProperty(sHierarchyNodeForProperty));
var oLevelFilter = new Filter(this.oTreeProperties["hierarchy-level-for"], "LE", iLevel);
var aFilters = [oNodeFilter, oLevelFilter];
this._checkFilterForTreeProperties();
const oCombinedFilter = this.getCombinedFilter();
if (oCombinedFilter) {
aFilters.push(oCombinedFilter);
}
var sAbsolutePath = this.getResolvedPath();
if (sAbsolutePath) {
oRequest.oRequestHandle = this.oModel.read(sAbsolutePath, {
headers: this._getHeaders(),
urlParameters: aUrlParameters,
fil