@itwin/presentation-components
Version:
React components based on iTwin.js Presentation library
202 lines • 9.09 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/* eslint-disable @typescript-eslint/no-deprecated */
/** @packageDocumentation
* @module Tree
*/
import "../../common/DisposePolyfill.js";
import { useCallback } from "react";
import { Subject, takeUntil, tap } from "rxjs";
import { TreeEventHandler, } from "@itwin/components-react";
import { Guid } from "@itwin/core-bentley";
import { useDisposable } from "@itwin/core-react";
import { NodeKey } from "@itwin/presentation-common";
import { Presentation, SelectionChangeType, SelectionHelper } from "@itwin/presentation-frontend";
import { isPresentationTreeNodeItem } from "../PresentationTreeNodeItem.js";
import { toRxjsObservable } from "../Utils.js";
/**
* Tree event handler that handles unified selection.
* Extends wrapped tree event handler's functionality by adding, removing or replacing nodes in
* unified selection. It also reacts to unified selection changes and selects/deselects tree nodes
* according changes.
*
* **Note:** conditions used to determine if node is selected and nodes that should be added to
* unified selection can be controlled by overriding 'shouldSelectNode' and 'createKeysForSelection' methods.
*
* @public
* @deprecated in 5.7. All tree-related APIs have been deprecated in favor of the new generation hierarchy
* building APIs (see https://github.com/iTwin/presentation/blob/33e79ee8d77f30580a9bab81a72884bda008db25/README.md#the-packages).
*/
export class UnifiedSelectionTreeEventHandler extends TreeEventHandler {
#dataProvider;
#selectionSourceName;
#listeners = [];
#cancelled = new Subject();
constructor(params) {
super({
...params,
modelSource: params.nodeLoader.modelSource,
});
this.#dataProvider = params.nodeLoader.dataProvider;
this.#selectionSourceName = params.name ?? `Tree_${this.#dataProvider.rulesetId}_${Guid.createValue()}`;
this.#listeners.push(Presentation.selection.selectionChange.addListener((args) => this.onSelectionChanged(args)));
this.#listeners.push(this.modelSource.onModelChanged.addListener((args) => this.selectNodes(args[1])));
this.selectNodes();
}
#dispose() {
super.dispose();
this.#cancelled.next();
this.#listeners.forEach((unregister) => unregister());
}
/** Disposes this event handler */
[Symbol.dispose]() {
this.#dispose();
}
/** @deprecated in 5.7. Use `[Symbol.dispose]` instead. */
dispose() {
this.#dispose();
}
onSelectionModified({ modifications }) {
const withUnifiedSelection = toRxjsObservable(modifications).pipe(takeUntil(this.#cancelled), tap({
next: ({ selectedNodeItems, deselectedNodeItems }) => {
if (selectedNodeItems.length !== 0) {
this.addToSelection(selectedNodeItems);
}
if (deselectedNodeItems.length !== 0) {
this.removeFromSelection(deselectedNodeItems);
}
},
}));
return super.onSelectionModified({ modifications: withUnifiedSelection });
}
onSelectionReplaced({ replacements }) {
this.#cancelled.next();
let firstEmission = true;
const withUnifiedSelection = toRxjsObservable(replacements).pipe(takeUntil(this.#cancelled), tap({
next: ({ selectedNodeItems }) => {
if (selectedNodeItems.length === 0) {
return;
}
if (firstEmission) {
firstEmission = false;
this.replaceSelection(selectedNodeItems);
return;
}
this.addToSelection(selectedNodeItems);
},
}));
return super.onSelectionReplaced({ replacements: withUnifiedSelection });
}
selectNodes(modelChange) {
// when handling model change event only need to update newly added nodes
if (modelChange) {
this.updateAffectedNodes(modelChange);
}
else {
this.updateAllNodes();
}
}
/** @deprecated in 4.0. Use [[isPresentationTreeNodeItem]] and [[PresentationTreeNodeItem.key]] to get [NodeKey]($presentation-common). */
/* c8 ignore start */
getNodeKey(node) {
return this.#dataProvider.getNodeKey(node);
}
/* c8 ignore end */
/**
* Determines if node should be selected.
* Default implementation returns true if node key is in selection
* or node is ECInstance node and instance key is in selection.
*/
shouldSelectNode(node, selection) {
/* c8 ignore next 3 */
if (!isPresentationTreeNodeItem(node)) {
return false;
}
// consider node selected if it's key is in selection
if (selection.has(node.key)) {
return true;
}
// ... or if it's an ECInstances node and any of instance keys is in selection
if (NodeKey.isInstancesNodeKey(node.key) && node.key.instanceKeys.some((instanceKey) => selection.has(instanceKey))) {
return true;
}
return false;
}
/**
* Returns node keys that should be added, removed or used to replace unified selection.
* Default implementation returns keys of supplied nodes.
*/
createKeysForSelection(nodes, _selectionType) {
return this.getKeys(nodes);
}
getKeys(nodes) {
const nodeKeys = nodes
.map((node) => (isPresentationTreeNodeItem(node) ? node.key : /* c8 ignore next */ undefined))
.filter((key) => key !== undefined);
return SelectionHelper.getKeysForSelection(nodeKeys);
}
addToSelection(nodes) {
Presentation.selection.addToSelection(this.#selectionSourceName, this.#dataProvider.imodel, this.createKeysForSelection(nodes, SelectionChangeType.Add), 0, this.#dataProvider.rulesetId);
}
removeFromSelection(nodes) {
Presentation.selection.removeFromSelection(this.#selectionSourceName, this.#dataProvider.imodel, this.createKeysForSelection(nodes, SelectionChangeType.Remove), 0, this.#dataProvider.rulesetId);
}
replaceSelection(nodes) {
Presentation.selection.replaceSelection(this.#selectionSourceName, this.#dataProvider.imodel, this.createKeysForSelection(nodes, SelectionChangeType.Replace), 0, this.#dataProvider.rulesetId);
}
onSelectionChanged(evt) {
if (evt.imodel !== this.#dataProvider.imodel) {
return;
}
if (evt.source !== this.#selectionSourceName && (evt.changeType === SelectionChangeType.Clear || evt.changeType === SelectionChangeType.Replace)) {
this.#cancelled.next();
}
this.selectNodes();
}
updateAllNodes() {
const selection = Presentation.selection.getSelection(this.#dataProvider.imodel);
this.modelSource.modifyModel((model) => {
for (const node of model.iterateTreeModelNodes()) {
this.updateNodeSelectionState(node, selection);
}
});
}
updateAffectedNodes(modelChange) {
const affectedNodeIds = [...modelChange.addedNodeIds, ...modelChange.modifiedNodeIds];
if (affectedNodeIds.length === 0) {
return;
}
const selection = Presentation.selection.getSelection(this.#dataProvider.imodel);
this.modelSource.modifyModel((model) => {
for (const nodeId of affectedNodeIds) {
const node = model.getNode(nodeId);
/* c8 ignore next 3 */
if (!node) {
continue;
}
this.updateNodeSelectionState(node, selection);
}
});
}
updateNodeSelectionState(node, selection) {
const shouldBeSelected = this.shouldSelectNode(node.item, selection);
if (!node.isSelected && shouldBeSelected) {
node.isSelected = true;
}
else if (node.isSelected && !shouldBeSelected) {
node.isSelected = false;
}
}
}
/**
* A custom hook which creates and disposes [[UnifiedSelectionTreeEventHandler]]
* @public
* @deprecated in 4.x. This hook is not compatible with React 18 `StrictMode`. Use [[usePresentationTreeState]] and
* [[UsePresentationTreeProps.eventHandlerFactory]] instead or manually create and dispose [[UnifiedSelectionTreeEventHandler]].
*/
export function useUnifiedSelectionTreeEventHandler(props) {
return useDisposable(useCallback(() => new UnifiedSelectionTreeEventHandler(props), Object.values(props) /* eslint-disable-line react-hooks/exhaustive-deps */ /* want to re-create the handler whenever any prop changes */));
}
//# sourceMappingURL=UseUnifiedSelection.js.map