UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

1,415 lines (1,236 loc) 150 kB
/*! * OpenUI5 * (c) Copyright 2009-2021 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ // Provides class sap.ui.model.odata.ODataTreeBindingFlat sap.ui.define([ 'sap/ui/model/Filter', 'sap/ui/model/TreeBinding', 'sap/ui/model/odata/v2/ODataTreeBinding', 'sap/ui/model/ChangeReason', 'sap/ui/model/TreeBindingUtils', "sap/base/util/uid", "sap/base/Log", "sap/base/assert", "sap/ui/thirdparty/jquery", "sap/base/util/isEmptyObject" ], function( Filter, TreeBinding, ODataTreeBinding, ChangeReason, TreeBindingUtils, uid, Log, assert, jQuery, isEmptyObject ) { "use strict"; /** * 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 = []; // 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 = []; }; /** * Sets the number of expanded levels. */ 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 []; } // make sure the input parameters are not undefined 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; } }; ODataTreeBindingFlat.prototype._calculateRequestParameters = function (aMissing) { var oParent = aMissing[0].parent; var iMissingSkip = aMissing[0].positionInParent; var iMissingLength = Math.min(iMissingSkip + Math.max(this._iThreshold, aMissing.length), oParent.children.length); for (var 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. * @private */ ODataTreeBindingFlat.prototype._retrieveNodeSection = function (iStartIndex, iLength) { return this._bReadOnly ? this._indexRetrieveNodeSection(iStartIndex, iLength) : this._mapRetrieveNodeSection(iStartIndex, iLength); }; ODataTreeBindingFlat.prototype._mapRetrieveNodeSection = function (iStartIndex, iLength) { var iNodeCounter = -1; var aNodes = []; this._map(function (oNode, oRecursionBreaker, sIndexType, iIndex, oParent) { iNodeCounter++; 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; }; 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 */ 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 oRecursionBreaker = {broken: false}; /** * Helper function to iterate all added subtrees of a node. */ var 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} the subtree handle, inside there is either a deep or a flat tree stored */ var 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 */ var 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). */ var 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 { // look into expanded nodes deeper than the initial expand level if (oNode.initiallyCollapsed && oNode.nodeState.expanded) { // 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. */ 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. */ 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 */ 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) { if (!oNode.isDeepOne && !oNode.initiallyCollapsed && oNode.serverIndex < iIndex && oNode.serverIndex + oNode.magnitude >= iIndex) { return true; } }; // $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; var iMagnitude = oEntry[this.oTreeProperties["hierarchy-node-descendant-count-for"]]; // 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."); } var oNode = this._aNodes[iIndex] = this._aNodes[iIndex] || { key: sKey, context: this.oModel.getContext("/" + sKey), 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", 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, 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); } // slection 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. */ 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]; if (this.aApplicationFilters) { aFilters = aFilters.concat(this.aApplicationFilters); } // TODO: Add additional filters to the read call, as soon as back-end implementations support it // Something like this: aFilters = [new sap.ui.model.Filter([hierarchyFilters].concat(this.aFilters))]; var sAbsolutePath = this.oModel.resolve(this.getPath(), this.getContext()); if (sAbsolutePath) { oRequest.oRequestHandle = this.oModel.read(sAbsolutePath, { 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)); }; 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 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 */ 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. */ 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 */ 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); } } }; 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), //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", //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. */ 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); } // TODO: Add additional filters to the read call, as soon as back-end implementations support it // Something like this: aFilters = [new sap.ui.model.Filter([hierarchyFilters].concat(this.aFilters))]; var sAbsolutePath = this.oModel.resolve(this.getPath(), this.getContext()); if (sAbsolutePath) { oRequest.oRequestHandle = this.oModel.read(sAbsolutePath, { 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. */ 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} oParentNode The parent node of the subtree */ 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. */ 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 var oNodeFilter = new Filter(this.oTreeProperties["hierarchy-node-for"], "EQ", oParentNode.context.getProperty(this.oTreeProperties["hierarchy-node-for"])); var oLevelFilter = new Filter(this.oTreeProperties["hierarchy-level-for"], "LE", iLevel); var aFilters = [oNodeFilter, oLevelFilter]; if (this.aApplicationFilters) { aFilters = aFilters.concat(this.aApplicationFilters); } var sAbsolutePath = this.oModel.resolve(this.getPath(), this.getContext()); if (sAbsolutePath) { oRequest.oRequestHandle = this.oModel.read(sAbsolutePath, { 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._aPendingSubtreeRequests.push(oRequest); } }.bind(this)); }; /** * Finds the node object sitting at iRowIndex. * Does not directly correlate to the nodes position in its containing array. */ ODataTreeBindingFlat.prototype.findNode = function (iRowIndex) { return this._bReadOnly ? this._indexFindNode(iRowIndex) : this._mapFindNode(iRowIndex); }; /** * The findNode implementation using the _map algorithm in a WRITE scenario. */ ODataTreeBindingFlat.prototype._mapFindNode = function (iRowIndex) { if (this.isInitial()) { return; } // first make a cache lookup var oFoundNode = this._aNodeCache[iRowIndex]; if (oFoundNode) { return oFoundNode; } // find the node for the given index var iNodeCounter = -1; this._map(function (oNode, oRecursionBreaker, sIndexType, iIndex, oParent) { iNodeCounter++; if (iNodeCounter === iRowIndex) { oFoundNode = oNode; oRecursionBreaker.broken = true; } }); return oFoundNode; }; /** * The findNode implementation using the index-calculation algorithm in a READ scenario. */ ODataTreeBindingFlat.prototype._indexFindNode = function (iRowIndex) { if (this.isInitial()) { return; } // first make a cache lookup var oNode = this._aNodeCache[iRowIndex]; if (oNode) { return oNode; } var oNodeInfo = this.getNodeInfoByRowIndex(iRowIndex), oNode; if (oNodeInfo.parent) { oNode = oNodeInfo.parent.children[oNodeInfo.childIndex]; } else { oNode = this._aNodes[oNodeInfo.index]; } this._aNodeCache[iRowIndex] = oNode; return oNode; }; /** * Toggles a row index between expanded and collapsed. */ ODataTreeBindingFlat.prototype.toggleIndex = function(iRowIndex) { var oToggledNode = this.findNode(iRowIndex); assert(oToggledNode != undefined, "toggleIndex(" + iRowIndex + "): Node not found!"); if (oToggledNode) { if (oToggledNode.nodeState.expanded) { this.collapse(oToggledNode); } else { this.expand(oToggledNode); } } }; /** * Expands a node or index. * @param vRowIndex either an index or a node instance * @param {boolean} bSuppressChange if set to true, no change event will be fired */ ODataTreeBindingFlat.prototype.expand = function (vRowIndex, bSuppressChange) { var oToggledNode = vRowIndex; if (typeof vRowIndex !== "object") { oToggledNode = this.findNode(vRowIndex); assert(oToggledNode != undefined, "expand(" + vRowIndex + "): Node not found!"); } if (oToggledNode.nodeState.expanded) { return; // Nothing to do here } //expand oToggledNode.nodeState.expanded = true; oToggledNode.nodeState.collapsed = false; // remove old tree state from the collapsed array if necessary // they are mutual exclusive var iTreeStateFound = this._aCollapsed.indexOf(oToggledNode); if (iTreeStateFound != -1) { this._aCollapsed.splice(iTreeStateFound, 1); } this._aExpanded.push(oToggledNode); this._sortNodes(this._aExpanded); // keep track of server-indexed node changes if (oToggledNode.serverIndex !== undefined) { this._aNodeChanges[oToggledNode.serverIndex] = true; } if (this._bSelectAll) { this._aExpandedAfterSelectAll.push(oToggledNode); } //trigger loading of the node if it is deeper than our initial level expansion if (oToggledNode.initiallyCollapsed && oToggledNode.childCount == undefined) { this._loadChildren(oTog