UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

569 lines (502 loc) 20.1 kB
/*! * 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 the client model implementation of a tree binding sap.ui.define([ "./ChangeReason", "./TreeBinding", "sap/base/util/deepEqual", "sap/base/util/each", "sap/ui/model/FilterProcessor", "sap/ui/model/FilterType", "sap/ui/model/SorterProcessor" ], function(ChangeReason, TreeBinding, deepEqual, each, FilterProcessor, FilterType, SorterProcessor) { "use strict"; /** * Creates a new ClientTreeBinding. * * This constructor should only be called by subclasses or model implementations, not by application or control code. * Such code should use {@link sap.ui.model.Model#bindTree Model#bindTree} on the corresponding model implementation instead. * * @param {sap.ui.model.Model} oModel Model instance that this binding is created for and that it belongs to * @param {string} sPath Binding path pointing to the tree / array that should be bound; syntax is defined by subclasses * @param {sap.ui.model.Context} [oContext=null] Context object for this binding, mandatory when when a relative binding path is given * @param {sap.ui.model.Filter[]|sap.ui.model.Filter} [aApplicationFilters=[]] * The filters to be used initially with type {@link sap.ui.model.FilterType.Application}; call {@link #filter} to * replace them * @param {object} [mParameters=null] Additional model specific parameters as defined by subclasses; this class does not introduce any own parameters * @param {sap.ui.model.Sorter[]|sap.ui.model.Sorter} [aSorters=[]] * The sorters used initially; call {@link #sort} to replace them * @throws {Error} If one of the filters uses an operator that is not supported by the underlying model * implementation or if the {@link sap.ui.model.Filter.NONE} filter instance is contained in * <code>aApplicationFilters</code> together with other filters * * @class * Tree binding implementation for client models. * * Note that a hierarchy's "state" (i.e. the information about expanded, collapsed, selected, and deselected nodes) may become * inconsistent when the structure of the model data is changed at runtime. This is because each node is identified internally by its * index position relative to its parent, plus its parent's ID. Therefore, inserting or removing a node in the model data will likely * lead to a shift in the index positions of other nodes, causing them to lose their state and/or to gain the state of another node. * <b>Note:</b> Tree bindings of client models do neither support * {@link sap.ui.model.Binding#suspend suspend} nor {@link sap.ui.model.Binding#resume resume}. * * @alias sap.ui.model.ClientTreeBinding * @extends sap.ui.model.TreeBinding * @protected */ var ClientTreeBinding = TreeBinding.extend("sap.ui.model.ClientTreeBinding", /** @lends sap.ui.model.ClientTreeBinding.prototype */ { constructor : function(oModel, sPath, oContext, aApplicationFilters, mParameters, aSorters){ TreeBinding.apply(this, arguments); if (!this.oContext) { this.oContext = ""; } this._mLengthsCache = {}; this.filterInfo = { aFilteredContexts : [], iMatches : 0, oParentContext : {} }; this.oCombinedFilter = null; this.mNormalizeCache = FilterProcessor.createNormalizeCache(); this.oTreeData = this.cloneData(this.oModel._getObject(this.sPath, this.oContext)); if (aApplicationFilters) { this.oModel.checkFilter(aApplicationFilters); if (this.oTreeData) { this.filter(aApplicationFilters, FilterType.Application); } } } }); ClientTreeBinding.CannotCloneData = Symbol("CannotCloneData"); /** * Returns a deep clone of the tree data or a symbol indicating that the given tree data cannot be cloned. * * Uses <code>structuredClone</code> to create a deep copy. If cloning fails (e.g., due to functions, DOM nodes, * or other uncloneable values), the symbol <code>ClientTreeBinding.CannotCloneData</code> is returned. * * @param {any} oTreeData * The tree data to clone * @returns {any|sap.ui.model.ClientTreeBinding.CannotCloneData} * A deep clone or the symbol <code>ClientTreeBinding.CannotCloneData</code> if cloning fails * @private */ ClientTreeBinding.prototype.cloneData = function(oTreeData) { try { return structuredClone(oTreeData); } catch { return ClientTreeBinding.CannotCloneData; } }; /** * Return root contexts for the tree. * * @param {int} [iStartIndex=0] the index from which to start the retrieval of contexts * @param {int} [iLength] determines how many contexts to retrieve, beginning from the start index. Defaults to the * model's size limit; see {@link sap.ui.model.Model#setSizeLimit}. * @returns {sap.ui.model.Context[]} the context's array * * @protected */ ClientTreeBinding.prototype.getRootContexts = function(iStartIndex, iLength) { if (!iStartIndex) { iStartIndex = 0; } if (!iLength) { iLength = this.oModel.iSizeLimit; } var sResolvedPath = this.getResolvedPath(), that = this, aContexts, oContext, sContextPath; if (!sResolvedPath) { return []; } if (!this.oModel.isList(sResolvedPath)) { oContext = this.oModel.getContext(sResolvedPath); if (this.bDisplayRootNode) { aContexts = [oContext]; } else { aContexts = this.getNodeContexts(oContext, iStartIndex, iLength); } } else { aContexts = []; sContextPath = this._sanitizePath(sResolvedPath); each(this.oModel._getObject(sContextPath), function(iIndex, oObject) { that._saveSubContext(oObject, aContexts, sContextPath, iIndex); }); this._applySorter(aContexts); this._setLengthCache(sContextPath, aContexts.length); aContexts = aContexts.slice(iStartIndex, iStartIndex + iLength); } return aContexts; }; /** * Return node contexts for the tree. * * @param {sap.ui.model.Context} oContext to use for retrieving the node contexts * @param {int} [iStartIndex=0] the index from which to start the retrieval of contexts * @param {int} [iLength] determines how many contexts to retrieve, beginning from the start index. Defaults to the * model's size limit; see {@link sap.ui.model.Model#setSizeLimit}. * @returns {sap.ui.model.Context[]} the context's array * * @protected */ ClientTreeBinding.prototype.getNodeContexts = function(oContext, iStartIndex, iLength) { if (!iStartIndex) { iStartIndex = 0; } if (!iLength) { iLength = this.oModel.iSizeLimit; } var sContextPath = this._sanitizePath(oContext.getPath()); var aContexts = [], that = this, vNode = this.oModel._getObject(sContextPath), aArrayNames = this.mParameters && this.mParameters.arrayNames, aKeys; if (vNode) { if (Array.isArray(vNode)) { vNode.forEach(function(oSubChild, index) { that._saveSubContext(oSubChild, aContexts, sContextPath, index); }); } else { // vNode is an object aKeys = aArrayNames || Object.keys(vNode); aKeys.forEach(function(sKey) { var oChild = vNode[sKey]; if (oChild) { if (Array.isArray(oChild)) { // vNode is an object containing one or more arrays oChild.forEach(function(oSubChild, sSubName) { that._saveSubContext(oSubChild, aContexts, sContextPath, sKey + "/" + sSubName); }); } else if (typeof oChild == "object") { that._saveSubContext(oChild, aContexts, sContextPath, sKey); } } }); } } this._applySorter(aContexts); this._setLengthCache(sContextPath, aContexts.length); return aContexts.slice(iStartIndex, iStartIndex + iLength); }; /** * Returns if the node has child nodes. * * @param {sap.ui.model.Context} oContext the context element of the node * @returns {boolean} <code>true</code> if the node has children * * @public */ ClientTreeBinding.prototype.hasChildren = function(oContext) { if (oContext == undefined) { return false; } return this.getChildCount(oContext) > 0; }; /** * Retrieves the number of children for the given context. * Makes sure the child count is retrieved from the length cache, and fills the cache if necessary. * Calling it with no arguments or 'null' returns the number of root level nodes. * * @param {sap.ui.model.Context} oContext the context for which the child count should be retrieved * @returns {int} the number of children for the given context * * @public * @override */ ClientTreeBinding.prototype.getChildCount = function(oContext) { //if oContext is null or empty -> root level count is requested var sPath = oContext ? oContext.sPath : this.getPath(); if (this.oContext) { sPath = this.oModel.resolve(sPath, this.oContext); } sPath = this._sanitizePath(sPath); // if the length is not cached, call the get*Contexts functions to fill it if (this._mLengthsCache[sPath] === undefined) { if (oContext) { this.getNodeContexts(oContext); } else { this.getRootContexts(); } } return this._mLengthsCache[sPath]; }; /** * Makes sure the path is prepended and appended with a "/" if necessary. * * @param {string} sContextPath The path to be checked * * @returns {string} The sanitized path */ ClientTreeBinding.prototype._sanitizePath = function (sContextPath) { if (!sContextPath.endsWith("/")) { sContextPath = sContextPath + "/"; } if (!sContextPath.startsWith("/")) { sContextPath = "/" + sContextPath; } return sContextPath; }; ClientTreeBinding.prototype._saveSubContext = function(oNode, aContexts, sContextPath, sName) { // only collect node if it is defined (and not null), because typeof null == "object"! if (oNode && typeof oNode == "object") { var oNodeContext = this.oModel.getContext(sContextPath + sName); // check if there is a filter on this level applied if (this.oCombinedFilter && !this.bIsFiltering) { if (this.filterInfo.aFilteredContexts.indexOf(oNodeContext) != -1) { aContexts.push(oNodeContext); } } else { aContexts.push(oNodeContext); } } }; /** * Filters the tree according to the filter definitions. * * The filtering is applied recursively through the tree. * The parent nodes of filtered child nodes will also be displayed if they don't match the filter conditions. * All filters belonging to a group (=have the same path) are ORed and after that the * results of all groups are ANDed. * * @param {sap.ui.model.Filter[]|sap.ui.model.Filter} [aFilters=[]] * The filters to use; in case of type {@link sap.ui.model.FilterType.Application} this replaces the filters given * in {@link sap.ui.model.ClientModel#bindTree}; a falsy value is treated as an empty array and thus removes all * filters of the specified type * @param {sap.ui.model.FilterType} [sFilterType] * The type of the filter to replace; if no type is given, all filters previously configured with type * {@link sap.ui.model.FilterType.Application} are cleared, and the given filters are used as filters of type * {@link sap.ui.model.FilterType.Control}. Since 1.146.0, you may use * {@link sap.ui.model.FilterType.ApplicationBound} to set bound application filters. * @returns {this} <code>this</code> to facilitate method chaining * @throws {Error} If one of the filters uses an operator that is not supported by the underlying model * implementation or if the {@link sap.ui.model.Filter.NONE} filter instance is contained in * <code>aFilters</code> together with other filters * * @public * @see sap.ui.model.TreeBinding.prototype.filter */ ClientTreeBinding.prototype.filter = function(aFilters, sFilterType){ // The filtering is applied recursively through the tree and stores all filtered contexts and its parent contexts in an array. // wrap single filters in an array if (aFilters && !Array.isArray(aFilters)) { aFilters = [aFilters]; } // check filter integrity this.oModel.checkFilter(aFilters); const bAppFilter = sFilterType === FilterType.Application || sFilterType === FilterType.ApplicationBound; if (bAppFilter) { this.aApplicationFilters = this.computeApplicationFilters(aFilters, sFilterType) || []; } else if (sFilterType == FilterType.Control) { this.aFilters = aFilters || []; } else { //Previous behaviour this.aFilters = aFilters || []; this.aApplicationFilters = []; } this.oCombinedFilter = FilterProcessor.combineFilters(this.aFilters, this.aApplicationFilters); if (this.oCombinedFilter) { this.applyFilter(); } this._mLengthsCache = {}; this._fireChange({reason: ChangeReason.Filter}); /** @deprecated As of version 1.11.0 */ this._fireFilter({filters: aFilters}); return this; }; /** * Apply the current defined filters on the existing dataset. * * @private */ ClientTreeBinding.prototype.applyFilter = function () { // reset previous stored filter contexts this.filterInfo.aFilteredContexts = []; this.filterInfo.iMatches = 0; this.filterInfo.oParentContext = {}; this._applyFilterRecursive(); }; /** * Filters the tree recursively. * Performs the real filtering and stores all filtered contexts and its parent context into an array. * * @param {sap.ui.model.Context} [oParentContext] the context where to start. The children of this node context are * then filtered recursively. * * @private */ ClientTreeBinding.prototype._applyFilterRecursive = function(oParentContext){ var that = this, aFilteredContexts = []; if (!this.oCombinedFilter) { return; } this.bIsFiltering = true; var aUnfilteredContexts; if (oParentContext) { aUnfilteredContexts = this.getNodeContexts(oParentContext, 0, Number.MAX_VALUE); // For client bindings: get *all* available contexts } else { // Root aUnfilteredContexts = this.getRootContexts(0, Number.MAX_VALUE); } this.bIsFiltering = false; if (aUnfilteredContexts.length > 0) { each(aUnfilteredContexts, function(i, oContext){ // Add parentContext reference for later use (currently to calculate correct group IDs in the adapter) oContext._parentContext = oParentContext; that._applyFilterRecursive(oContext); }); aFilteredContexts = FilterProcessor.apply(aUnfilteredContexts, this.oCombinedFilter, function (oContext, sPath) { return that.oModel.getProperty(sPath, oContext); }, this.mNormalizeCache); if (aFilteredContexts.length > 0) { this.filterInfo.aFilteredContexts = this.filterInfo.aFilteredContexts.concat(aFilteredContexts); this.filterInfo.aFilteredContexts.push(oParentContext); this.filterInfo.oParentContext = oParentContext; this.filterInfo.iMatches += aFilteredContexts.length; } // push additionally parentcontexts if any children are already included in filtered contexts if (aUnfilteredContexts.indexOf(this.filterInfo.oParentContext) != -1) { this.filterInfo.aFilteredContexts.push(oParentContext); // set the parent context which was added to be the new parent context this.filterInfo.oParentContext = oParentContext; } } }; /** * Sorts the contexts of this ClientTreeBinding. * The tree will be sorted level by level. So the nodes are NOT sorted absolute, but relative to * their parent node, to keep the hierarchy untouched. * * @param {sap.ui.model.Sorter[]|sap.ui.model.Sorter} [aSorters=[]] * The sorters to use; they replace the sorters given in {@link sap.ui.model.ClientModel#bindTree}; a falsy value * is treated as an empty array and thus removes all sorters * @returns {this} Returns <code>this</code> to facilitate method chaining * * @public */ ClientTreeBinding.prototype.sort = function (aSorters) { aSorters = aSorters || []; this.aSorters = Array.isArray(aSorters) ? aSorters : [aSorters]; this._fireChange({reason: ChangeReason.Sort}); return this; }; /** * Internal function to apply this.aSorters to the given array of contexts. * * @param {sap.ui.model.Context[]} aContexts the context array which should be sorted (inplace) */ ClientTreeBinding.prototype._applySorter = function (aContexts) { var that = this; SorterProcessor.apply(aContexts, this.aSorters, function(oContext, sPath) { return that.oModel.getProperty(sPath, oContext); }, function (oContext) { //the context path is used as a key for internal use in the SortProcessor. return oContext.getPath(); }); }; /** * Sets the length cache. * Called by get*Contexts() to keep track of the child count (after filtering). * * @param {string} sKey The cache entry to set the length for * @param {int} iLength The new length */ ClientTreeBinding.prototype._setLengthCache = function (sKey, iLength) { // keep track of the child count for each context (after filtering) this._mLengthsCache[sKey] = iLength; }; /** * Sets the context for this instance. If the context changes and the binding is relative a change event is fired * with reason {@link sap.ui.model.ChangeReason.Context}. * * @param {sap.ui.model.Context} oContext * The new context object */ ClientTreeBinding.prototype.setContext = function (oContext) { if (this.oContext != oContext) { this.oContext = oContext; if (this.isRelative()) { const oTreeData = this.oModel._getObject(this.sPath, this.oContext); this.oTreeData = this.cloneData(oTreeData); this._fireChange({reason: ChangeReason.Context}); } } }; /** * Check whether this Binding would provide new values and in case it changed, * inform interested parties about this. * * @param {boolean} [bForceUpdate] * Whether the change event will be fired regardless of the bindings state * */ ClientTreeBinding.prototype.checkUpdate = function(bForceUpdate) { const oCurrentTreeData = this.oModel._getObject(this.sPath, this.oContext); // apply filter again this.applyFilter(); this._mLengthsCache = {}; if (bForceUpdate || !deepEqual(this.oTreeData, oCurrentTreeData)) { this.oTreeData = this.cloneData(oCurrentTreeData); this._fireChange({reason: ChangeReason.Change}); } }; /** * Returns the count of entries in the tree, or <code>undefined</code> if it is unknown. If the * tree is filtered, the count of all entries matching the filter conditions is returned. The * entries required only for the tree structure are not counted. * * @returns {number|undefined} The count of entries in the tree, or <code>undefined</code> if * the binding is not resolved * * @public * @since 1.108.0 */ ClientTreeBinding.prototype.getCount = function () { if (!this.isResolved()) { return undefined; } if (this.oCombinedFilter) { return this.filterInfo.iMatches; } return ClientTreeBinding._getTotalNodeCount(this.oModel.getObject(this.getResolvedPath()), this.mParameters && this.mParameters.arrayNames, true); }; /** * Returns the count of objects in the given data by iterating recursively over the given array * names, or if not given over all object keys. * * @param {any} vData * The root of the data to count objects * @param {string[]} [aArrayNames] * The array of property names to consider when counting the child objects in the given data * @param {boolean} [bRoot] * Whether the given data is the root of the tree * @returns {number} * The total count of objects in the given data * * @private */ ClientTreeBinding._getTotalNodeCount = function (vData, aArrayNames, bRoot) { if (vData === null || typeof vData !== "object") { return 0; // null and non-objects do not count } if (Array.isArray(vData)) { return vData.reduce(function (iCount, vItem) { return iCount + ClientTreeBinding._getTotalNodeCount(vItem, aArrayNames); }, 0); } return (aArrayNames || Object.keys(vData)).reduce(function (iCount, sKey) { return iCount + ClientTreeBinding._getTotalNodeCount(vData[sKey], aArrayNames); }, bRoot ? 0 /*root object doesn't count*/ : 1); }; return ClientTreeBinding; });