@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
1,437 lines (1,222 loc) • 62.1 kB
JavaScript
/*!
* 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.TreeBindingAdapter
sap.ui.define([
'sap/ui/model/TreeBinding',
'sap/ui/model/TreeAutoExpandMode',
'sap/ui/model/ChangeReason',
'sap/ui/model/TreeBindingUtils',
"sap/base/assert",
"sap/base/Log",
"sap/base/util/each",
"sap/base/util/isEmptyObject"
],
function(
TreeBinding,
TreeAutoExpandMode,
ChangeReason,
TreeBindingUtils,
assert,
Log,
each,
isEmptyObject
) {
"use strict";
/**
* Adapter for TreeBindings to add the ListBinding functionality and use the
* tree structure in list based controls.
*
* @alias sap.ui.model.TreeBindingAdapter
* @class
* @experimental This module is only for experimental and internal use!
* @protected
*/
var TreeBindingAdapter = 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 TreeBindingAdapter.prototype) {
if (TreeBindingAdapter.prototype.hasOwnProperty(fn)) {
this[fn] = TreeBindingAdapter.prototype[fn];
}
}
// make sure we have a parameter object
this.mParameters = this.mParameters || {};
// initialize the contexts
this._aRowIndexMap = [];
//Store length and threshold for all requests
this._iThreshold = 0;
this._iPageSize = 0;
//set the default auto expand mode
this.setAutoExpandMode(this.mParameters.autoExpandMode || TreeAutoExpandMode.Sequential);
//default value for collapse recursive
if (this.mParameters.collapseRecursive === undefined) {
this.bCollapseRecursive = true;
} else {
this.bCollapseRecursive = !!this.mParameters.collapseRecursive;
}
//create general tree structure
this._createTreeState();
this._bIsAdapted = true;
};
/**
* Returns a tree state handle to encapsulate the actual tree state.
* TODO: Encode the list to avoid conflicts with delimiters in the flat expanded list? Re-Check this when back-end support is implemented.
*
* This function is exposed in the sub-classes/adapters (e.g. ODataTreeBindingAdapter) if necessary/possible.
*
* @private
*/
TreeBindingAdapter.prototype.getCurrentTreeState = function () {
var sDelimiter = ";";
//expanded
var mExpandedEntriesGroupIDs = {};
for (var sGroupID in this._mTreeState.expanded) {
mExpandedEntriesGroupIDs[sGroupID] = true;
}
//collapsed
var mCollapsedEntriesGroupIDs = {};
for (var sGroupID in this._mTreeState.collapsed) {
mCollapsedEntriesGroupIDs[sGroupID] = true;
}
//selected
var mSelectedEntriesGroupIDs = {};
for (var sGroupID in this._mTreeState.selected) {
mSelectedEntriesGroupIDs[sGroupID] = true;
}
return {
_getExpandedList: function () {
return Object.keys(mExpandedEntriesGroupIDs).join(sDelimiter);
},
_getCollapsedList: function () {
return Object.keys(mCollapsedEntriesGroupIDs).join(sDelimiter);
},
_getSelectedList: function () {
return Object.keys(mSelectedEntriesGroupIDs).join(sDelimiter);
},
_isExpanded: function (sGroupID) {
return !!mExpandedEntriesGroupIDs[sGroupID];
},
_isCollapsed: function (sGroupID) {
return !!mCollapsedEntriesGroupIDs[sGroupID];
},
_remove: function (sGroupID) {
delete mExpandedEntriesGroupIDs[sGroupID];
delete mCollapsedEntriesGroupIDs[sGroupID];
delete mSelectedEntriesGroupIDs[sGroupID];
}
};
};
/**
* Sets the given as a start point for the tree.
* Only in OperationMode.Client.
* @param oTreeState Only valid tree states from the same binding are accepted
* @private
*/
TreeBindingAdapter.prototype.setTreeState = function (oTreeState) {
this._oInitialTreeState = oTreeState;
};
/**
* Sets the AutoExpand Mode for this Adapter. Default is "Bundled".
* @param {sap.ui.model.TreeAutoExpandMode} sAutoExpandMode
*/
TreeBindingAdapter.prototype.setAutoExpandMode = function (sAutoExpandMode) {
this._autoExpandMode = sAutoExpandMode;
};
/**
* @override
*/
TreeBindingAdapter.prototype.getLength = function() {
if (!this._oRootNode) {
return 0;
}
// The length is the sum of the trees magnitue
return this._oRootNode.magnitude;
};
/**
* @override
*/
TreeBindingAdapter.prototype.getContextByIndex = function (iIndex) {
//step out if the binding is initial (as long as the metadata is not yet loaded)
if (this.isInitial()) {
return;
}
var oNode = this.findNode(iIndex);
return oNode ? oNode.context : undefined;
};
/**
* @override
*/
TreeBindingAdapter.prototype.getNodeByIndex = function(iIndex) {
//step out if the binding is initial (as long as the metadata is not yet loaded)
if (this.isInitial()) {
return;
}
// if the requested index is bigger than the magnitude of the tree, the index can never
// be inside the tree.
if (iIndex >= this.getLength()) {
return undefined;
}
return this.findNode(iIndex);
};
/**
* Find node retrieves an actual tree nodes.
* However if there are sum rows cached (meaning, they are currently displayed), these will also be returned.
*/
TreeBindingAdapter.prototype.findNode = function (vParam) {
//step out if the binding is initial (as long as the metadata is not yet loaded)
if (this.isInitial()) {
return;
}
var sParameterType = typeof vParam;
var oFoundNode;
var aSearchResult = [];
//if the parameter is an index -> first check the cache, and then search the tree if necessary
if (sParameterType === "number") {
oFoundNode = this._aRowIndexMap[vParam];
if (!oFoundNode) {
var iIndexCounter = -1;
this._match(this._oRootNode, aSearchResult, 1, function (oNodeToCheck) {
if (iIndexCounter === vParam) {
return true;
}
iIndexCounter += 1;
});
oFoundNode = aSearchResult[0];
}
}
/*else if (sParameterType === "string" || sParameterType === "object") {
// match auf group id
// oFoundNode = aSearchResult[0];
}*/
return oFoundNode;
};
TreeBindingAdapter.prototype._createTreeState = function (bReset) {
if (!this._mTreeState || bReset) {
//general tree status information, the nodes are referenced by their groupID
this._mTreeState = {
expanded: {}, // a map of all expanded nodes
collapsed: {}, // a map of all collapsed nodes
selected: {}, // a map of all selected nodes
deselected : {} // a map of all deselected nodes (due to user interaction)
};
}
};
TreeBindingAdapter.prototype._updateTreeState = function (mParameters) {
mParameters = mParameters || {};
//get the source and target list
var oTargetStateObject = mParameters.expanded ? this._mTreeState.expanded : this._mTreeState.collapsed;
var oSourceStateObject = mParameters.expanded ? this._mTreeState.collapsed : this._mTreeState.expanded;
// get the current node state, or create a new one
var oNodeStateInSource = this._getNodeState(mParameters.groupID);
// if no node state exists -> create it
if (!oNodeStateInSource) {
oNodeStateInSource = mParameters.fallbackNodeState || this._createNodeState({
groupID: mParameters.groupID,
expanded: mParameters.expanded,
sum: mParameters.sum
});
}
//move from the source state to the target state
delete oSourceStateObject[mParameters.groupID];
oTargetStateObject[mParameters.groupID] = oNodeStateInSource;
//keep track of the expanded status on the node state
oNodeStateInSource.expanded = mParameters.expanded;
return oNodeStateInSource;
};
TreeBindingAdapter.prototype._createNodeState = function (mParameters) {
if (!mParameters.groupID) {
assert(false, "To create a node state a group ID is mandatory!");
return;
}
// check if the tree has an initial expansion state for the given groupID
var bInitiallyExpanded;
var bInitiallyCollapsed;
if (this._oInitialTreeState) {
bInitiallyExpanded = this._oInitialTreeState._isExpanded(mParameters.groupID);
bInitiallyCollapsed = this._oInitialTreeState._isCollapsed(mParameters.groupID);
this._oInitialTreeState._remove(mParameters.groupID);
}
// check the expansion state which should be set
// the given values have precedence over the initially set values, false is the fallback
var bIsExpanded = mParameters.expanded || bInitiallyExpanded || false;
var bIsSelected = mParameters.selected || false;
var oNodeState = {
groupID: mParameters.groupID,
expanded: bIsExpanded,
//a fresh node state has to have a single page with the current pagesize
sections: mParameters.sections || [{startIndex: 0, length: this._iPageSize}],
sum: mParameters.sum || false,
selected: bIsSelected
};
// track initally modified nodes in the global treeState
if (bInitiallyExpanded || bInitiallyCollapsed) {
this._updateTreeState({groupID: mParameters.groupID, fallbackNodeState: oNodeState, expanded: bInitiallyExpanded, collapsed: bInitiallyCollapsed});
}
return oNodeState;
};
TreeBindingAdapter.prototype._getNodeState = function (sGroupID) {
var oExpanded = this._mTreeState.expanded[sGroupID];
var oCollapsed = this._mTreeState.collapsed[sGroupID];
var oSelected = this._mTreeState.selected[sGroupID];
var oDeselected = this._mTreeState.deselected[sGroupID];
//return one or the other
//may be undefined if no sections loaded yet
return oExpanded || oCollapsed || oSelected || oDeselected;
};
/**
* Merges the new section with all currently known sections.
* Makes sure, that only non overlapping sections are kept.
*/
TreeBindingAdapter.prototype._updateNodeSections = function (sGroupID, oNewSection) {
var oNodeState = this._getNodeState(sGroupID);
if (!oNodeState) {
assert(false, "No Node State for Group ID '" + sGroupID + "' found!");
return;
} else if (!oNewSection) {
assert(false, "No Section given!");
return;
} else if (oNewSection.length <= 0) {
assert(false, "The length of the given section must be positive greater than 0.");
return;
} else if (oNewSection.startIndex < 0) {
assert(false, "The sections start index must be greater/equal to 0.");
return;
}
// Iterate over all known/loaded sections of the node
oNodeState.sections = TreeBindingUtils.mergeSections(oNodeState.sections, oNewSection);
return oNodeState.sections;
};
/**
* Increases the section length of all sections of all nodes in the tree. This is necessary in case the page size increases between requests.
* Otherwise unnecessary requests would be performed, because the section length does not match the requested page size.
*/
TreeBindingAdapter.prototype._increaseSections = function() {
var fnIncreaseSections = function (oNode) {
if (!oNode) {
return;
}
var iMaxGroupSize = this._getMaxGroupSize(oNode);
// adapt node sections if the page size increased since the last getcontexts call
// and only if we do not already have a count for the group
var oNodeState = oNode.nodeState;
if (iMaxGroupSize === undefined) {
var aNewSections = [];
for (var i = 0; i < oNodeState.sections.length; i++) {
var oCurrentSection = oNodeState.sections[i];
oCurrentSection.length = Math.max(oCurrentSection.length, this._iPageSize);
aNewSections = TreeBindingUtils.mergeSections(aNewSections, oCurrentSection);
}
oNodeState.sections = aNewSections;
}
};
this._map(this._oRootNode, fnIncreaseSections);
};
/**
* Calculates the maximum possible group-size for a given node.
* Not the same as the direct number of children.
*/
TreeBindingAdapter.prototype._getMaxGroupSize = function (oNode) {
var iMaxGroupSize = 0;
if (oNode.isArtificial) {
// When displaying the root node, the magnitude will always be at least 1:
// Except: if we are bound to a list/collection (e.g. Employees), there will be no single root node
// so we retrieve the regular groupSize instead
var bIsList = this.oModel.isList(this.sPath, this.getContext());
if (this.bDisplayRootNode && !bIsList && !this._bRootMissing) {
iMaxGroupSize = 1;
} else {
iMaxGroupSize = this._getGroupSize(oNode) || 0;
}
} else {
iMaxGroupSize = this.nodeHasChildren(oNode) ? this._getGroupSize(oNode) : 0;
}
return iMaxGroupSize;
};
/**
* 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
*/
TreeBindingAdapter.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
*/
TreeBindingAdapter.prototype._getContextsOrNodes = function (bReturnNodes, iStartIndex,
iLength, iThreshold) {
if (!this.isResolved() || this.isInitial()) {
return [];
}
if (!iLength) {
iLength = this.oModel.iSizeLimit;
}
if (!iThreshold) {
iThreshold = 0;
}
// test if the pagesize has increased -> used to optimise "too small" node sections, see _loadChildContexts
if (iLength > this._iPageSize) {
this._iPageSize = iLength;
this._increaseSections();
}
this._iThreshold = Math.max(this._iThreshold, iThreshold);
// clear the overall rowIndex to tree node map
this._aRowIndexMap = [];
this._buildTree(iStartIndex, iLength);
// retrieve the requested section of nodes from the tree
var aNodes = [];
if (this._oRootNode) {
aNodes = this._retrieveNodeSection(this._oRootNode, iStartIndex, iLength);
}
// keep a map between Table.RowIndex and tree nodes
this._updateRowIndexMap(aNodes, iStartIndex);
//find missing sections
var aContexts = [];
var mMissingSections;
for (var i = 0; i < aNodes.length; i++) {
var oNode = aNodes[i];
// we found a gap because the node is empty (context is undefined)
if (!oNode.context) {
mMissingSections = mMissingSections || {};
// check if we already build a missing section
var oParentNode = oNode.parent;
mMissingSections[oParentNode.groupID] = oParentNode;
this._updateNodeSections(oParentNode.groupID, {startIndex: oNode.positionInParent, length: 1});
}
aContexts.push(oNode.context);
}
// trigger load for nodes with missing sections
if (mMissingSections) {
var that = this;
//if we have a missing section inside a subtree, we need to reload this subtree
each(mMissingSections, function (sGroupID, oNode) {
// reset the root of the subtree
oNode.magnitude = 0;
oNode.numberOfTotals = 0;
that._loadChildContexts(oNode);
});
// try to fill gaps in our return array if we already have new data (thanks to thresholding)
aContexts = [];
for (var j = 0; j < aNodes.length; j++) {
var oNode = aNodes[j];
aContexts.push(oNode.context);
}
}
if (bReturnNodes) {
return aNodes;
} else {
return aContexts;
}
};
/**
* Gets an array of nodes for the requested part of the tree.
*
* @param {number} iStartIndex
* 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
*/
TreeBindingAdapter.prototype.getNodes = function (iStartIndex, iLength, iThreshold) {
return this._getContextsOrNodes(true, iStartIndex, iLength, iThreshold);
};
/**
* Synchronize a node section from the tree with our RowIndex Mapping table.
*/
TreeBindingAdapter.prototype._updateRowIndexMap = function (aNodes, iStartIndex) {
//throw away the old mapping index
this._aRowIndexMap = [];
for (var i = 0; i < aNodes.length; i++) {
this._aRowIndexMap[iStartIndex + i] = aNodes[i];
}
};
/**
* Depth-First traversal of a sub-tree object structure starting with the given node as the root.
* Retrieves all found nodes (including gaps). Gaps will be filled with placeholder nodes.
* These placeholders are later used to automatically update the tree after invalidating and refreshing the
* sub-tree(s) containing the gaps.
* @param {object} oNode the root node of the sub-tree for which the section will be retrieved
* @param {int} iStartIndex the start of the tree section which should be retrieved
* @param {int} iLength the start of the tree section which should be retrieved
* @return {object[]} an array containing all collected nodes, for which the absolute node index is greater than iStartIndex
* the length of the array will be iLength (or less if the tree does not have that many nodes).
* @private
*/
TreeBindingAdapter.prototype._retrieveNodeSection = function (oNode, iStartIndex, iLength) {
var iNodeCounter = -1;
var aNodes = [];
this._match(this._oRootNode, [], iLength, function (oNode, iPositionInParent, oParentNode) {
//make sure to exclude the artificial root node from being counted
if (!oNode || !oNode.isArtificial) {
iNodeCounter++;
}
if (iNodeCounter >= iStartIndex && iNodeCounter < iStartIndex + iLength) {
//node does not exist -> a gap in the tree, fill up with placeholder node
if (!oNode) {
oNode = this._createNode({parent: oParentNode, positionInParent: iPositionInParent});
oParentNode.children[iPositionInParent] = oNode;
}
aNodes.push(oNode);
return true;
}
});
return aNodes;
};
TreeBindingAdapter.prototype._buildTree = function(iStartIndex, iLength) {
//throw away our tree
this._oRootNode = undefined;
//the artificial root hast no context
var oRootContext = null;
var sRootGroupID = this._calculateGroupID({context: oRootContext, parent: null});
var oRootNodeState = this._getNodeState(sRootGroupID);
// create root node state if none exists
if (!oRootNodeState) {
var oRootNodeState = this._createNodeState({
groupID: sRootGroupID,
sum: true,
sections: [{
startIndex: iStartIndex,
length: iLength
}]
});
//the root node is expanded by default under the following conditions:
// 1: root node is artifical/should not be displayed OR we have an autoExpand situation (numberOfExpandedLevels > 0)
// 2: the root node was not previously collapsed by the user
this._updateTreeState({
groupID: oRootNodeState.groupID,
fallbackNodeState: oRootNodeState,
expanded: true
});
}
//create the root node
this._oRootNode = this._createNode({
context: oRootContext,
parent: null,
level: this.bDisplayRootNode && !(oRootContext === null) ? 0 : -1,
nodeState: oRootNodeState,
isLeaf: false,
autoExpand: this.getNumberOfExpandedLevels() + 1
});
//flag the root node as artificial in case we have no real root context (but only children)
this._oRootNode.isArtificial = true;
//expanded the root node if requested
if (this._mTreeState.expanded[this._oRootNode.groupID]) {
this._loadChildContexts(this._oRootNode);
}
};
/**
* Calculate the request length based on the given information
*
* @param {int} iMaxGroupSize the maximum group size
* @param {object} oSection the information of the current section
* @protected
* @deprecated since version 1.52. This method is marked as 'protected' which was meant to be overwritten
* by its subclasses. It may be renamed or deleted and should only be called from this class or its subclasses.
*/
TreeBindingAdapter.prototype._calculateRequestLength = function(iMaxGroupSize, oSection) {
var iRequestedLength;
if (!iMaxGroupSize) {
iRequestedLength = oSection.length;
} else {
//the maximum entries we can request is the groupSize
iRequestedLength = Math.max(Math.min(oSection.length, iMaxGroupSize - oSection.startIndex), 0);
}
return iRequestedLength;
};
TreeBindingAdapter.prototype._loadChildContexts = function (oNode) {
var oNodeState = oNode.nodeState;
// calculate magnitude/groupsize of (artificial) root node seperately
var iMaxGroupSize = this._getMaxGroupSize(oNode);
// make sure the children array gets at least the requested length
if (iMaxGroupSize > 0) {
if (!oNode.children[iMaxGroupSize - 1]) {
oNode.children[iMaxGroupSize - 1] = undefined;
}
oNodeState.leafCount = iMaxGroupSize;
}
// if the binding is running in the OperationMode "Client", make sure the node sections are optimised to load everything
if (this.bClientOperation) {
oNodeState.sections = [{
startIndex: 0,
length: iMaxGroupSize
}];
}
//iterate all loaded (known) sections
for (var i = 0; i < oNodeState.sections.length; i++) {
var oCurrentSection = oNodeState.sections[i];
var iRequestedLength = this._calculateRequestLength(iMaxGroupSize, oCurrentSection);
//if we are in the autoexpand mode "bundled", suppress additional requests during the tree traversal
//paging is handled differently
if (oNode.autoExpand >= 0 && this._autoExpandMode === TreeAutoExpandMode.Bundled) {
iRequestedLength = Math.max(0, iMaxGroupSize);
}
//try to load the contexts for this sections (may be [])
var aChildContexts;
if (oNode.isArtificial) {
aChildContexts = this.getRootContexts(oCurrentSection.startIndex, iRequestedLength, this._iThreshold);
} else {
aChildContexts = this.nodeHasChildren(oNode) ? this.getNodeContexts(oNode.context, oCurrentSection.startIndex, iRequestedLength, this._iThreshold) : [];
}
//for each child context we create a new node
for (var j = 0; j < aChildContexts.length; j++) {
var oChildContext = aChildContexts[j];
// in case the binding does return a gap in the data (a.k.a. undefined) we skip this child
// it will be collected as a missing section later in getContexts()
if (!oChildContext) {
continue;
}
// calculate the index of the child node in the children array
// the offset in the children array is the section start index
var iChildIndex = j + oCurrentSection.startIndex;
var oChildNode = oNode.children[iChildIndex];
//the updated node data after this tree building cycle
var oUpdatedNodeData = {
context: aChildContexts[j],
parent: oNode,
level: oNode.level + 1,
positionInParent: iChildIndex,
autoExpand: Math.max(oNode.autoExpand - 1, -1)
};
// if we already have a child node reuse it, otherwise create a new one
// Using an object reference allows us to automatically update our "snapshot" of the tree, we retrieve in getContexts
if (oChildNode) {
oChildNode.context = oUpdatedNodeData.context;
oChildNode.parent = oUpdatedNodeData.parent;
oChildNode.level = oUpdatedNodeData.level;
oChildNode.positionInParent = oUpdatedNodeData.positionInParent;
oChildNode.magnitude = 0;
oChildNode.numberOfTotals = 0;
oChildNode.autoExpand = oUpdatedNodeData.autoExpand;
//calculate the group id for the given context
//if we reach this point, the binding returned a context from which we can calculate the group id
var sGroupIDForChild;
if (oChildContext) {
sGroupIDForChild = this._calculateGroupID(oChildNode);
}
oChildNode.groupID = sGroupIDForChild;
} else {
//create a node one level deeper (missing a group ID and a context)
oChildNode = this._createNode(oUpdatedNodeData);
}
//retrieve the node state OR create one if necessary
oChildNode.nodeState = this._getNodeState(oChildNode.groupID);
if (!oChildNode.nodeState) {
oChildNode.nodeState = this._createNodeState({
groupID: oChildNode.groupID,
expanded: false // a new node state is never expanded (EXCEPT during auto expand!)
});
}
oChildNode.nodeState.parentGroupID = oNode.groupID;
// if the table is grouped: a leaf is a node 1 level deeper than the number of grouped columns
// otherwise if the table is (fully) ungrouped every node is a leaf
oChildNode.isLeaf = !this.nodeHasChildren(oChildNode);
oNode.children[iChildIndex] = oChildNode;
if (oChildNode.isLeaf) {
oNode.numberOfLeafs += 1;
}
//if the parent node is in selectAllMode, select this child node
if (oChildNode.parent.nodeState.selectAllMode && !this._mTreeState.deselected[oChildNode.groupID]) {
this.setNodeSelection(oChildNode.nodeState, true);
}
// if the child node was previously expanded, it has to be expanded again after we rebuilt our tree
// --> recursion
// but only if we have at least 1 group (otherwise we have a flat list and not a tree)
if ((oChildNode.autoExpand > 0 || oChildNode.nodeState.expanded) && this.isGrouped() ) {
if (!this._mTreeState.collapsed[oChildNode.groupID] && !oChildNode.isLeaf) {
this._updateTreeState({groupID: oChildNode.nodeState.groupID, fallbackNodeState: oChildNode.nodeState , expanded: true});
this._loadChildContexts(oChildNode);
}
// sum up the magnitude/sumRows when moving up in the recursion
oNode.magnitude += Math.max(oChildNode.magnitude || 0, 0);
oNode.numberOfLeafs += oChildNode.numberOfLeafs;
}
}
}
// add up the sum of all sub-tree magnitudes
oNode.magnitude += Math.max(iMaxGroupSize || 0, 0);
};
/**
* Returns if the Binding is grouped, default is true.
* AnalyticalBindings might differ.
*/
TreeBindingAdapter.prototype.isGrouped = function () {
return true;
};
/**
* Hook which needs to be implemented by subclasses
* Calculates a unique group ID for a given node
* @param {Object} oNode Node of which the group ID shall be calculated
* @returns {string} Group ID for oNode
*/
TreeBindingAdapter.prototype._calculateGroupID = function (oNode) {
Log.error("TreeBindingAdapter#_calculateGroupID: Not implemented. Needs to be implemented in respective sub-classes.");
};
/**
* Creates a new tree node with valid default values
* @param {object} mParameters a set of parameters which might differ from the default values
* @returns {object} a newly created tree node
*/
TreeBindingAdapter.prototype._createNode = function (mParameters) {
mParameters = mParameters || {};
var oContext = mParameters.context;
var iLevel = mParameters.level || 0;
var oNode = {
context: oContext,
level: iLevel,
children: mParameters.children || [],
parent: mParameters.parent,
nodeState: mParameters.nodeState,
isLeaf: mParameters.isLeaf || false,
//the relative position of the node inside its parents children array
positionInParent: mParameters.positionInParent,
//the sum of all child nodes in the sub-tree (below this node)
magnitude: mParameters.magnitude || 0,
//the total number of sum rows in the sub-tree
numberOfTotals: mParameters.numberOfTotals || 0,
//the total number of leafs in the sub-tree
numberOfLeafs: mParameters.numberOfLeafs || 0,
autoExpand: mParameters.autoExpand || 0,
absoluteNodeIndex: mParameters.absoluteNodeIndex || 0,
totalNumberOfLeafs: 0
};
//calculate the group id
if (oContext !== undefined) {
oNode.groupID = this._calculateGroupID(oNode);
}
return oNode;
};
/**
* Expand the tree node sitting at the given index.
* @param {int} iIndex the absolute row index
* @param {boolean} bSuppressChange if set to true, no change event will be fired
*/
TreeBindingAdapter.prototype.expand = function(iIndex, bSuppressChange) {
var oNode = this.findNode(iIndex);
if (!oNode) {
assert(false, "No node found for index " + iIndex);
return;
}
this._updateTreeState({groupID: oNode.nodeState.groupID, fallbackNodeState: oNode.nodeState, expanded: true});
if (!bSuppressChange) {
this._fireChange({reason: ChangeReason.Expand});
}
};
/**
* Expands the tree to the given level.
* Change-Event is fired.
* @param {int} iLevel the level to which the data should be expanded
*/
TreeBindingAdapter.prototype.expandToLevel = function (iLevel) {
this._mTreeState.collapsed = {};
this.setNumberOfExpandedLevels(iLevel);
this._fireChange({reason: ChangeReason.Expand});
};
/**
* Retrieves the expanded state of the row sitting at the given index.
* @param {int} iIndex the index for which the expansion state should be retrieved
*/
TreeBindingAdapter.prototype.isExpanded = function (iIndex) {
var oNode = this.findNode(iIndex);
return oNode && oNode.nodeState ? oNode.nodeState.expanded : false;
};
/**
* Collapses the given node, identified via an absolute row index.
* @param {int} vParam the row index of the tree node
* @param {boolean} bSuppressChange if set to true, no change event will be fired
*/
TreeBindingAdapter.prototype.collapse = function(vParam, bSuppressChange) {
var oNodeStateForCollapsingNode;
var that = this;
//check if the Parameter is a node state object
if (typeof vParam === "object") {
oNodeStateForCollapsingNode = vParam;
} else if (typeof vParam === "number") {
var oNode = this.findNode(vParam);
if (!oNode) {
assert(false, "No node found for index " + vParam);
return;
}
oNodeStateForCollapsingNode = oNode.nodeState;
}
this._updateTreeState({groupID: oNodeStateForCollapsingNode.groupID, fallbackNodeState: oNodeStateForCollapsingNode, expanded: false});
// remove selectAllMode if necessary
oNodeStateForCollapsingNode.selectAllMode = false;
if (this.bCollapseRecursive) {
var sGroupIDforCollapsingNode = oNodeStateForCollapsingNode.groupID;
// Collapse all subsequent child nodes, this is determined by a common groupID prefix, e.g.: "/A100-50/" is the parent of "/A100-50/Finance/"
// All expanded nodes which start with 'sGroupIDforCollapsingNode', are basically children of it and also need to be collapsed
each(this._mTreeState.expanded, function (sGroupID, oNodeState) {
if (typeof sGroupIDforCollapsingNode == "string" && sGroupIDforCollapsingNode.length > 0 && sGroupID.startsWith(sGroupIDforCollapsingNode)) {
that._updateTreeState({groupID: sGroupID, expanded: false});
}
});
var aDeselectedNodeIds = [];
// always remove selections from child nodes of the collapsed node
each(this._mTreeState.selected, function (sGroupID, oNodeState) {
if (typeof sGroupIDforCollapsingNode == "string" && sGroupIDforCollapsingNode.length > 0 && sGroupID.startsWith(sGroupIDforCollapsingNode) && sGroupID !== sGroupIDforCollapsingNode) {
//removes the selectAllMode from child nodes
oNodeState.selectAllMode = false;
that.setNodeSelection(oNodeState, false);
aDeselectedNodeIds.push(sGroupID);
}
});
if (aDeselectedNodeIds.length) {
var selectionChangeParams = {
rowIndices: []
};
// Collect the changed indices
var iNodeCounter = -1;
this._map(this._oRootNode, function (oNode) {
if (!oNode || !oNode.isArtificial) {
iNodeCounter++;
}
if (oNode && aDeselectedNodeIds.indexOf(oNode.groupID) !== -1) {
if (oNode.groupID === this._sLeadSelectionGroupID) {
// Lead selection got deselected
selectionChangeParams.oldIndex = iNodeCounter;
selectionChangeParams.leadIndex = -1;
}
selectionChangeParams.rowIndices.push(iNodeCounter);
}
});
this._publishSelectionChanges(selectionChangeParams);
}
}
if (!bSuppressChange) {
this._fireChange({reason: ChangeReason.Collapse});
}
};
/**
* Collape all nodes up to level "iLevel".
* If iLevel is undefined: the tree will be collapsed up to the first child level of the root node.
* @param {int} iLevel the level to which all lower sub-trees should be collapsed
*/
TreeBindingAdapter.prototype.collapseToLevel = function(iLevel) {
//default level is 1, meaning all sub nodes of the root will be collapsed (to the top)
if (!iLevel || iLevel < 0) {
iLevel = 0;
}
//collapse all expanded nodes if they sit on the same level as the one the user wants to collapse to
var that = this;
each(this._mTreeState.expanded, function (sGroupID, oNodeState) {
var iNodeLevel = that._getGroupIdLevel(sGroupID) - 1;
if (iNodeLevel === iLevel) {
that.collapse(oNodeState, true);
}
});
this._fireChange({reason: ChangeReason.Collapse});
};
/**
* Calls a function on every child node in the sub tree with root "oNode".
* Inside the map function "this" is bound to the TreeBindingAdapter instance.
*
* @param {object} oNode the starting node for the function mapping
* @param {function} fnMapFunction the function which should be mapped for each node in the sub-tree
*/
TreeBindingAdapter.prototype._map = function (oNode, fnMapFunction) {
fnMapFunction.call(this, oNode);
//if the node is not defined: there is a missing section in our tree
if (!oNode) {
return;
}
for (var i = 0; i < oNode.children.length; i++) {
var oChildNode = oNode.children[i];
this._map(oChildNode, fnMapFunction);
}
if (this._afterMapHook) {
this._afterMapHook(oNode, fnMapFunction);
}
};
/**
* Calls the given matching function on every child node in the sub tree with root "oNode".
* Inside the map function "this" is bound to the TreeBindingAdapter instance.
* The matching function must return "true" if the node should be collected as a match, and false otherwise.
*
* @param {object} oNode the starting node of the sub-tree which will be traversed, handed to the fnMatchFunction
* @param {array} aResults the collected nodes for which the matching function returns true
* @param {int} iMaxNumberOfMatches the maximum number of matched nodes, _match() will stopp if this boundary is reached
* @param {function} fnMatchFunction the match function is called for every traversed nodes
* @param {int} [iPositionInParent] the relative position of the oNode parameter to its parent nodes children array, handed to the fnMatchFunction
* @param {object} [oParentNode] the parent node of the oNode parameter, handed to the fnMatchFunction
*/
TreeBindingAdapter.prototype._match = function (oNode, aResults, iMaxNumberOfMatches, fnMatchFunction, iPositionInParent, oParentNode) {
// recursion end if max number of matches have been collected
// if iMaxNumberOfMatches is undefined -> the whole tree is searched.
if (aResults.length === iMaxNumberOfMatches) {
return true;
}
// push the node if it matches the criterium
var bNodeMatches = fnMatchFunction.call(this, oNode, iPositionInParent, oParentNode);
if (bNodeMatches) {
aResults.push(oNode);
}
//if the node is not defined: there is a missing section in our tree
if (!oNode) {
return false;
}
for (var i = 0; i < oNode.children.length; i++) {
var oChildNode = oNode.children[i];
var bMaxNumberReached = this._match(oChildNode, aResults, iMaxNumberOfMatches, fnMatchFunction, i, oNode);
//break recursion if enough nodes where collected
if (bMaxNumberReached) {
return true;
}
}
//check if an after match hook is defined on sub-adapters
return this._afterMatchHook ? this._afterMatchHook(oNode, aResults, iMaxNumberOfMatches, fnMatchFunction, iPositionInParent, oParentNode) : false;
};
/**
* Toggles the tree node sitting at the given index.
* @param {int} iIndex the absolute row index
*/
TreeBindingAdapter.prototype.toggleIndex = function(iIndex) {
var oNode = this.findNode(iIndex);
if (!oNode) {
assert(false, "There is no node at index " + iIndex + ".");
return;
}
if (oNode.nodeState.expanded) {
this.collapse(iIndex);
} else {
this.expand(iIndex);
}
};
/**
* A group ID starts and ends with a "/".
* @override
*/
TreeBindingAdapter.prototype._getGroupIdLevel = function (sGroupID) {
if (sGroupID == null) {
Log.warning("assertion failed: no need to determine level of group ID = null");
return -1;
}
return sGroupID.split("/").length - 2;
};
/**
* Determines the size of a group
* @override
*/
TreeBindingAdapter.prototype._getGroupSize = function (oNode) {
return this.getChildCount(oNode.context);
};
//*************************************************
//* Selection-Handling *
//************************************************/
/**
* Sets the selection state of the given node.
* @param {object} oNodeState the node state for which the selection should be changed
* @param {boolean} bIsSelected the selection state for the given node
*/
TreeBindingAdapter.prototype.setNodeSelection = function (oNodeState, bIsSelected) {
if (!oNodeState.groupID) {
assert(false, "NodeState must have a group ID!");
return;
}
oNodeState.selected = bIsSelected;
// toggles the selection state based on bIsSelected
if (bIsSelected) {
this._mTreeState.selected[oNodeState.groupID] = oNodeState;
delete this._mTreeState.deselected[oNodeState.groupID];
} else {
delete this._mTreeState.selected[oNodeState.groupID];
this._mTreeState.deselected[oNodeState.groupID] = oNodeState;
}
};
/**
* Returns the selection state for the node at the given index.
* @param {int} iRowIndex the row index to check for selection state
*/
TreeBindingAdapter.prototype.isIndexSelected = function (iRowIndex) {
var oNode = this.getNodeByIndex(iRowIndex);
return oNode && oNode.nodeState ? oNode.nodeState.selected : false;
};
/**
* Returns if the node at the given index is selectable.
* In the AnalyticalTable only nodes with isLeaf = true are selectable.
* @param {int} iRowIndex the row index which should be checked for "selectability"
*/
TreeBindingAdapter.prototype.isIndexSelectable = function (iRowIndex) {
var oNode = this.getNodeByIndex(iRowIndex);
return this._isNodeSelectable(oNode);
};
/**
* Checks if the given node can be selected. Always true for TreeTable controls, except the node is not defined.
*/
TreeBindingAdapter.prototype._isNodeSelectable = function (oNode) {
return !!oNode && !oNode.isArtificial;
};
/**
* Marks a single TreeTable node sitting on iRowIndex as selected.
* Also sets the lead selection index to this node.
* @param {int} iRowIndex the absolute row index which should be selected
*/
TreeBindingAdapter.prototype.setSelectedIndex = function (iRowIndex) {
var oNode = this.findNode(iRowIndex);
if (oNode && this._isNodeSelectable(oNode)) {
// clear and fetch the changes on the selection
var oChanges = this._clearSelection();
// if the selected row index was already selected before -> remove it from the changed Indices from the clearSection() call
var iChangedIndex = oChanges.rowIndices.indexOf(iRowIndex);
if (iChangedIndex >= 0) {
oChanges.rowIndices.splice(iChangedIndex, 1);
} else {
// the newly selcted index is missing and also has to be propagated via the event params
oChanges.rowIndices.push(iRowIndex);
}
//set the new lead index
oChanges.leadGroupID = oNode.groupID;
oChanges.leadIndex = iRowIndex;
this.setNodeSelection(oNode.nodeState, true);
this._publishSelectionChanges(oChanges);
} else {
Log.warning("TreeBindingAdapter: The selection was ignored. Please make sure to only select rows, for which data has been fetched to the client. For AnalyticalTables, some rows might not be selectable at all.");
}
};
/**
* Retrieves the "Lead-Selection-Index"
* Normally this is the last selected node/table row.
* @return {int} returns the lead selection index or -1 if none is set
*/
TreeBindingAdapter.prototype.getSelectedIndex = function () {
//if we have no nodes selected, the lead selection index is -1
if (!this._sLeadSelectionGroupID || isEmptyObject(this._mTreeState.selected)) {
return -1;
}
// find the first selected entry -> this is our lead selection index
var iNodeCounter = -1;
var nodeFound = false;
var fnMatchFunction = function (oNode) {
if (!oNode || !oNode.isArtificial) {
iNodeCounter++;
}
if (oNode) {
if (oNode.groupID === this._sLeadSelectionGroupID) {
nodeFound = true;
return true;
}
}
};
this._match(this._oRootNode, [], 1, fnMatchFunction);
if (nodeFound) {
return iNodeCounter;
}
// If a parent of the lead selected node has been collapsed,
// we might not be able to find it in the current tree.
// This can only happen if recursive collapse is not active
// (recursive collapse always removes the selection of a collapsed nodes' children)
return -1;
};
/**
* Returns an array with all selected row indices.
* Only absolute row indices for nodes known to the client will can be retrieved this way
* @return {int[]} an array with all selected indices
*/
TreeBindingAdapter.prototype.getSelectedIndices = function () {
var aResultIndices = [];
var that = this;
//if we have no nodes selected, the selection indices are empty
if (isEmptyObject(this._mTreeState.selected)) {
return aResultIndices;
}
// maximum number of possibly selected nodes
var iNumberOfNodesToSelect = Object.keys(this._mTreeState.selected).length;
// collect the indices of all selected nodes
var iNodeCounter = -1;
var fnMatchFunction = function (oNode) {
if (!oNode || !oNode.isArtificial) {
iNodeCounter++;
}
if (oNode) {
if (oNode.nodeState && oNode.nodeState.selected && !oNode.isArtificial) {
aResultIndices.push(iNodeCounter);
// cache the selected node for subsequent findNode/getContextByIndex calls
that._aRowIndexMap[iNodeCounter] = oNode;
return true;
}
}
};
this._match(this._oRootNode, [], iNumberOfNodesToSelect, fnMatchFunction);
return aResultIndices;
};
/**
* Returns the number of selected nodes (including not-yet loaded)
* @private
*/
TreeBindingAdapter.prototype.getSelectedNodesCount = function () {
var iSelectedNodes;
if (this._oRootNode && this._oRootNode.nodeState.selectAllMode) {
var sGroupId, iVisibleDeselectedNodeCount, oParent, oGroupNodeState;
var oContext, aVisibleGroupIds = [];
if (this.filterInfo && this.oCombinedFilter) {
// If we are filtering, we need to map the filtered (visible) contexts to group IDs.
// With that we can check whether a node state is actually a visible node
for (var i = this.filterInfo.aFilteredContexts.length - 1; i >= 0; i--) {
oContext = this.filterInfo.aFilteredContexts[i];
aVisibleGroupIds.push(this._calculateGroupID({
context: oContext
}));
}
}
iVisibleDeselectedNodeCount = 0;
// If we implicitly deselect all nodes under a group node,
// we need to count them as "visible deselected nodes"
for (sGroupId in this._mTreeState.expanded) {
if (!this.oCombinedFilter || aVisibleGroupIds.indexOf(sGroupId) !== -1) { // Not filtering or part of the visible nodes if filtering
oGroupNodeState = this._mTreeState.expanded[sGroupId];
if (!oGroupNodeState.selectAllMode && oGroupNodeState.leafCount !== undefined) {
iVisibleDeselectedNodeCount += oGroupNodeState.leafCount;
}
}
}
// Except those who got explicitly selected after the parent got collapsed
// and expanded again (and while the root is still in select-all mode)
for (sGroupId in this._mTreeState.selected) {
if (!this.oCombinedFilter || aVisibleGroupIds.indexOf(sGroupId) !== -1) { // Not filtering or part of the visible nodes if filtering
oGroupNodeState = this._mTreeState.selected[sGroupId];
oParent = this._mTreeState.expanded[oGroupNodeState.parentGroupID];
if (oParent && !oParent.selectAllMode) {
iVisibleDeselectedNodeCount--;
}
}
}
// Add those which are explicitly deselected and whose parents *are* in selectAllMode (not covered by the above)
for (sGroupId in this._mTreeState.deselected) {
if (!this.oCombinedFilter || aVisibleGroupIds.indexOf(sGroupId) !== -1) { // Not filtering or part of the visible nodes if filtering
oGroupNodeState = this._mTreeState.deselected[sGroupId];
oParent = this._mTreeState.expanded[oGroupNodeState.parentGroupID];
// If parent is expanded check if its in select all mode
if (oParent && oParent.selectAllMode) {
iVisibleDeselectedNodeCount++;
}
}
}
iSelectedNodes = this._getSelectableNodesCount(this._oRootNode) - iVisibleDeselectedNodeCount;
} else {
iSelectedNodes = Object.keys(this._mTreeState.selected).length;
}
return iSelectedNodes;
};
/**
* Returns the number of currently selectable nodes (with respect to the current expand/collapse state).
* @returns {int} Number of currently selectable nodes
*/
TreeBindingAdapter.prototype._getSelectableNodesCount = function (oNode) {
if (oNode) {
return oNode.magnitude;
} else {
return 0;
}
};
/**
* Returns an array containing all selected contexts, ordered by their appearance in the tree.
* @return {sap.ui.model.Context[]} an array containing the binding contexts for all selected nodes
*/
TreeBindingAdapter.prototype.getSelectedContexts = function () {
var aResultContexts = [];
var that = this;
//if we have no nodes selected, the selection indices are empty
if (isEmptyObject(this._mTreeState.selected)) {
return aResultContexts;
}
// maximum number of possibly selected nodes
var iNumberOfNodesToSelect = Object.keys(this._mTreeState.selected).length;
// collect the indices & contexts of all selected nodes
var iNodeCounter = -1;
var fnMatchFunction = function (oNode) {
if (!oNode || !oNode.isArtificial) {
iNodeCounter++;
}
if (oNode) {
if (oNode.nodeState && oNode.nodeState.selected && !oNode.isArtificial) {
aResultContexts.push(oNode.context);
// cache the selected node for subsequent findNode/getContextByIndex calls
that._aRowIndexMap[iNodeCounter] = oNode;
return true;
}
}
};
this._match(this._oRootNode, [], iNumberOfNodesToSelect, fnMatchFunction);
return aResultContexts;
};
/**
* Sets the selection to the range from iFromIndex to iToIndex (including boundaries).
* e.g. setSelectionInterval(1,3) marks the rows 1,2 and 3.
* All currently selected rows will be deselected in the process.
* A selectionChanged event is fired
*/
TreeBindingAdapter.prototype.setSelectionInterval = function (iFromIndex, iToIndex) {
// clears the selection but suppresses the selection change event
var mClearParams = this._clearSelection();
// the addSelectionInterval function takes care of the selection change event
var mSetParams = this._setSelectionInterval(iFromIndex, iToIndex, true);
var mIndicesFound = {};
var aRowIndices = [];
// flag all cleared indices as changed
for (var i = 0; i < mClearParams.rowIndices.length; i++) {
var iIndex = mClearParams.rowIndices[i];
mIndicesFound[iIndex] = true;
}
// now merge the changed indices after clearing with the newly selected
// duplicate indices mean, that the index was previously selected and is now still selected -> remove it from the changes
for (i = 0; i < mSetParams.rowIndices.length; i++) {
iIndex = mSetParams.rowIndices[i];
if (mIndicesFound[iIndex]) {
delete mIndicesFound[iIndex];
} else {
mIndicesFound[iIndex] = true;
}
}
// transform the changed index MAP into a real array of indices
for (iIndex in mIndicesFound) {
if (mIndicesFound[iIndex]) {
aRowIndices.push(parseInt(iIndex));
}
}
//and fire the event
this._publishSelectionChanges({
rowIndices: aRowIndices,
oldIndex: mClearParams.oldIndex,
leadIndex: mSetParams.leadIndex,
leadGroupID: mSetParams.leadGroupID
});
};
/**
* Sets the value inside the given range to the value given with 'bSelectionValue'
* @private
* @param {int} iFromIndex