@itwin/presentation-hierarchies
Version:
A package for creating hierarchies based on data in iTwin.js iModels.
277 lines • 13.4 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import { HierarchyNodeIdentifier } from "./HierarchyNodeIdentifier.js";
import { HierarchyNodeKey } from "./HierarchyNodeKey.js";
var HierarchyFilteringPathOptions;
(function (HierarchyFilteringPathOptions) {
function mergeAutoExpandOptions(lhs, rhs) {
if (rhs === true || lhs === true) {
return true;
}
if (!rhs || !lhs) {
return !!rhs ? rhs : lhs;
}
// eslint-disable-next-line @typescript-eslint/no-deprecated
const lhsDepth = "depth" in lhs ? lhs.depth : "depthInPath" in lhs ? lhs.depthInPath : lhs.depthInHierarchy;
// eslint-disable-next-line @typescript-eslint/no-deprecated
const rhsDepth = "depth" in rhs ? rhs.depth : "depthInPath" in rhs ? rhs.depthInPath : rhs.depthInHierarchy;
const isLhsDepthBasedOnPath =
// eslint-disable-next-line @typescript-eslint/no-deprecated
"depthInPath" in lhs ? true : !("key" in lhs) && !("depthInHierarchy" in lhs) && !lhs.includeGroupingNodes;
const isRhsDepthBasedOnPath =
// eslint-disable-next-line @typescript-eslint/no-deprecated
"depthInPath" in rhs ? true : !("key" in rhs) && !("depthInHierarchy" in rhs) && !rhs.includeGroupingNodes;
if (isLhsDepthBasedOnPath) {
if (isRhsDepthBasedOnPath) {
return lhsDepth > rhsDepth ? lhs : rhs;
}
return lhs;
}
if (isRhsDepthBasedOnPath) {
return rhs;
}
return lhsDepth > rhsDepth ? lhs : rhs;
}
HierarchyFilteringPathOptions.mergeAutoExpandOptions = mergeAutoExpandOptions;
})(HierarchyFilteringPathOptions || (HierarchyFilteringPathOptions = {}));
/** @public */
// eslint-disable-next-line @typescript-eslint/no-redeclare
export var HierarchyFilteringPath;
(function (HierarchyFilteringPath) {
/**
* Normalizes the hierarchy filtering path to the object form.
* @public
*/
function normalize(source) {
if (Array.isArray(source)) {
return { path: source };
}
return source;
}
HierarchyFilteringPath.normalize = normalize;
/**
* Merges two given `HierarchyFilteringPathOptions` objects.
* - if both inputs are `undefined`, `undefined` is returned,
* - else if one of the inputs is `undefined`, the other one is returned.
* - else, merge each option individually.
*
* For the `autoExpand` attribute, the merge chooses to auto-expand as deep as the deepest input:
* - if any one of the inputs is `true`, return `true`,
* - else if only one of the inputs is an object, return it,
* - else if both inputs are falsy, return `false` or `undefined`,
* - else:
* - if only one of the inputs has `includeGroupingNodes` set to `true` or `key` defined or `depthInHierarchy` set, return the other one,
* - else return the one with greater `depth`, `depthInPath` or `depthInHierarchy`.
*
* @public
*/
function mergeOptions(lhs, rhs) {
if (!lhs || !rhs) {
return lhs ?? rhs;
}
return { autoExpand: HierarchyFilteringPathOptions.mergeAutoExpandOptions(lhs.autoExpand, rhs.autoExpand) };
}
HierarchyFilteringPath.mergeOptions = mergeOptions;
})(HierarchyFilteringPath || (HierarchyFilteringPath = {}));
/**
* An utility that extracts filtering properties from given root level filtering props or
* the parent node. Returns `undefined` if filtering props are not present.
* @public
* @deprecated in 1.3. Use `createHierarchyFilteringHelper` instead.
*/
/* v8 ignore start */
export function extractFilteringProps(rootLevelFilteringProps, parentNode) {
return extractFilteringPropsInternal(rootLevelFilteringProps, parentNode);
}
/* v8 ignore stop */
function extractFilteringPropsInternal(rootLevelFilteringProps, parentNode) {
if (!parentNode) {
return rootLevelFilteringProps
? { filteredNodePaths: rootLevelFilteringProps, hasFilterTargetAncestor: false }
: /** v8 ignore next */ undefined;
}
return parentNode.filtering?.filteredChildrenIdentifierPaths
? {
filteredNodePaths: parentNode.filtering.filteredChildrenIdentifierPaths,
hasFilterTargetAncestor: !!parentNode.filtering.hasFilterTargetAncestor || !!parentNode.filtering.isFilterTarget,
}
: undefined;
}
/**
* Creates a set of utilities for making it easier to filter the given hierarchy
* level.
*
* @public
*/
export function createHierarchyFilteringHelper(rootLevelFilteringProps, parentNode) {
const filteringProps = extractFilteringPropsInternal(rootLevelFilteringProps, parentNode);
const hasFilter = !!filteringProps;
return {
/**
* Returns a flag indicating if the hierarchy level is filtered.
*/
hasFilter,
/**
* Returns a flag indicating whether this hierarchy level has an ancestor node
* that is a filter target. That generally means that this and all downstream hierarchy
* levels should be displayed without filter being applied to them, even if filter paths
* say otherwise.
*/
hasFilterTargetAncestor: filteringProps?.hasFilterTargetAncestor ?? false,
/**
* Returns a list of hierarchy node identifiers that apply specifically for this
* hierarchy level. Returns `undefined` if filtering is not applied to this level.
*/
getChildNodeFilteringIdentifiers: () => {
if (!hasFilter) {
return undefined;
}
return filteringProps.filteredNodePaths
.map(HierarchyFilteringPath.normalize)
.filter(({ path }) => path.length > 0)
.map(({ path }) => path[0]);
},
/**
* When a hierarchy node is created for a filtered hierarchy level, it needs some attributes (e.g. `filtering`
* and `autoExpand`) to be set based on the filter paths and filtering options. This function calculates
* these props for a child node based on its key or path matcher.
*
* When using `pathMatcher` prop, callers have more flexibility to decide whether the given `HierarchyNodeIdentifier` applies
* to their node. For example, only some parts of the identifier can be checked for improved performance. Otherwise, the
* `nodeKey` prop can be used to check the whole identifier.
*/
createChildNodeProps: (props) => {
// TODO: MISSING_COVERAGE
/* v8 ignore if -- @preserve */
if (!hasFilter) {
return undefined;
}
const reducer = new MatchingFilteringPathsReducer(filteringProps?.hasFilterTargetAncestor);
filteringProps.filteredNodePaths.forEach((filteredPath) => {
const normalizedPath = HierarchyFilteringPath.normalize(filteredPath);
// TODO: MISSING_COVERAGE
/* v8 ignore start */
if ("nodeKey" in props &&
((HierarchyNodeKey.isGeneric(props.nodeKey) &&
HierarchyNodeIdentifier.equal(normalizedPath.path[0], props.nodeKey)) ||
(HierarchyNodeKey.isInstances(props.nodeKey) &&
props.nodeKey.instanceKeys.some((ik) => HierarchyNodeIdentifier.equal(normalizedPath.path[0], ik))))) {
reducer.accept(normalizedPath);
/* v8 ignore stop */
}
else if ("pathMatcher" in props && props.pathMatcher(normalizedPath.path[0])) {
reducer.accept(normalizedPath);
}
});
return reducer.getNodeProps(parentNode);
},
/**
* Similar to `createChildNodeProps`, but takes an async `pathMatcher` prop.
*/
createChildNodePropsAsync: (props) => {
// TODO: MISSING_COVERAGE
/* v8 ignore if -- @preserve */
if (!hasFilter) {
return undefined;
}
const reducer = new MatchingFilteringPathsReducer(filteringProps?.hasFilterTargetAncestor);
const matchedPathPromises = new Array();
for (const filteredChildrenNodeIdentifierPath of filteringProps.filteredNodePaths) {
const normalizedPath = HierarchyFilteringPath.normalize(filteredChildrenNodeIdentifierPath);
// TODO: MISSING_COVERAGE
/* v8 ignore if -- @preserve */
if (normalizedPath.path.length === 0) {
continue;
}
const matchesPossiblyPromise = props.pathMatcher(normalizedPath.path[0]);
if (matchesPossiblyPromise instanceof Promise) {
matchedPathPromises.push(matchesPossiblyPromise.then((matches) => (matches ? normalizedPath : undefined)));
continue;
}
if (!matchesPossiblyPromise) {
continue;
}
reducer.accept(normalizedPath);
}
if (matchedPathPromises.length === 0) {
return reducer.getNodeProps(parentNode);
}
return Promise.all(matchedPathPromises)
.then((matchedPath) => matchedPath.forEach((normalizedPath) => normalizedPath && reducer.accept(normalizedPath)))
.then(() => reducer.getNodeProps(parentNode));
},
};
}
class MatchingFilteringPathsReducer {
_hasFilterTargetAncestor;
_filteredChildrenIdentifierPaths = new Array();
_isFilterTarget = false;
_filterTargetOptions = undefined;
_autoExpandOption = false;
constructor(_hasFilterTargetAncestor) {
this._hasFilterTargetAncestor = _hasFilterTargetAncestor;
}
accept(normalizedPath) {
const { path, options } = normalizedPath;
// TODO: MISSING_COVERAGE
/* v8 ignore else -- @preserve */
if (path.length === 1) {
this._isFilterTarget = true;
this._filterTargetOptions = HierarchyFilteringPath.mergeOptions(this._filterTargetOptions, options);
}
else if (path.length > 1) {
this._filteredChildrenIdentifierPaths.push({ path: path.slice(1), options });
this._autoExpandOption = HierarchyFilteringPathOptions.mergeAutoExpandOptions(options?.autoExpand, this._autoExpandOption);
}
}
getNeedsAutoExpand(parentNode) {
if (this._autoExpandOption === true) {
return true;
}
if (typeof this._autoExpandOption === "object") {
const parentLength = !parentNode
? 0
: "key" in this._autoExpandOption ||
"depthInHierarchy" in this._autoExpandOption ||
// TODO: MISSING_COVERAGE
/* v8 ignore start */
// eslint-disable-next-line @typescript-eslint/no-deprecated
("includeGroupingNodes" in this._autoExpandOption && this._autoExpandOption.includeGroupingNodes)
? // TODO: MISSING_COVERAGE
/* v8 ignore stop */
1 + parentNode.parentKeys.length
: 1 + parentNode.parentKeys.filter((key) => !HierarchyNodeKey.isGrouping(key)).length;
const depth = "depthInHierarchy" in this._autoExpandOption
? this._autoExpandOption.depthInHierarchy
: "depth" in this._autoExpandOption
? // eslint-disable-next-line @typescript-eslint/no-deprecated
this._autoExpandOption.depth
: // With `depthInPath` option we don't want to expand node that is at the `depthInPath` position
// TODO: MISSING_COVERAGE
/* v8 ignore next */ this._autoExpandOption.depthInPath - 1;
return parentLength < depth;
}
return false;
}
getNodeProps(parentNode) {
return {
...(this._hasFilterTargetAncestor || this._isFilterTarget || this._filteredChildrenIdentifierPaths.length > 0
? {
filtering: {
...(this._hasFilterTargetAncestor ? { hasFilterTargetAncestor: true } : undefined),
...(this._isFilterTarget
? { isFilterTarget: true, filterTargetOptions: this._filterTargetOptions }
: undefined),
...(this._filteredChildrenIdentifierPaths.length > 0
? { filteredChildrenIdentifierPaths: this._filteredChildrenIdentifierPaths }
: undefined),
},
}
: undefined),
...(this.getNeedsAutoExpand(parentNode) ? { autoExpand: true } : undefined),
};
}
}
//# sourceMappingURL=HierarchyFiltering.js.map