UNPKG

@itwin/presentation-frontend

Version:

Frontend of iModel.js Presentation library

746 lines • 31.6 kB
"use strict"; /*--------------------------------------------------------------------------------------------- * 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 UnifiedSelection */ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) { if (value !== null && value !== void 0) { if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected."); var dispose, inner; if (async) { if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined."); dispose = value[Symbol.asyncDispose]; } if (dispose === void 0) { if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined."); dispose = value[Symbol.dispose]; if (async) inner = dispose; } if (typeof dispose !== "function") throw new TypeError("Object not disposable."); if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } }; env.stack.push({ value: value, dispose: dispose, async: async }); } else if (async) { env.stack.push({ async: true }); } return value; }; var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) { return function (env) { function fail(e) { env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e; env.hasError = true; } var r, s = 0; function next() { while (r = env.stack.pop()) { try { if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next); if (r.dispose) { var result = r.dispose.call(r.value); if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); }); } else s |= 1; } catch (e) { fail(e); } } if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve(); if (env.hasError) throw env.error; } return next(); }; })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }); Object.defineProperty(exports, "__esModule", { value: true }); exports.ToolSelectionSyncHandler = exports.SelectionManager = void 0; const rxjs_1 = require("rxjs"); const core_bentley_1 = require("@itwin/core-bentley"); const core_frontend_1 = require("@itwin/core-frontend"); const presentation_common_1 = require("@itwin/presentation-common"); const internal_1 = require("@itwin/presentation-common/internal"); const unified_selection_1 = require("@itwin/unified-selection"); const Presentation_js_1 = require("../Presentation.js"); const HiliteSetProvider_js_1 = require("./HiliteSetProvider.js"); const SelectionChangeEvent_js_1 = require("./SelectionChangeEvent.js"); const SelectionScopesManager_js_1 = require("./SelectionScopesManager.js"); /** * The selection manager which stores the overall selection. * @public * @deprecated in 5.0 - will not be removed until after 2026-06-13. Use `SelectionStorage` from [@itwin/unified-selection](https://github.com/iTwin/presentation/blob/master/packages/unified-selection/README.md) package instead. */ class SelectionManager { _imodelKeyFactory; _imodelToolSelectionSyncHandlers = new Map(); _hiliteSetProviders = new Map(); _ownsStorage; _knownIModels = new Set(); _currentSelection = new CurrentSelectionStorage(); _selectionChanges = new rxjs_1.Subject(); _selectionEventsSubscription; _listeners = []; /** * Underlying selection storage used by this selection manager. Ideally, consumers should use * the storage directly instead of using this manager to manipulate selection. */ selectionStorage; /** An event which gets broadcasted on selection changes */ selectionChange; /** Manager for [selection scopes]($docs/presentation/unified-selection/index#selection-scopes) */ scopes; /** * Creates an instance of SelectionManager. */ constructor(props) { this.selectionChange = new SelectionChangeEvent_js_1.SelectionChangeEvent(); this.scopes = props.scopes; this.selectionStorage = props.selectionStorage ?? (0, unified_selection_1.createStorage)(); this._imodelKeyFactory = props.imodelKeyFactory ?? ((imodel) => (imodel.key.length ? imodel.key : imodel.name)); this._ownsStorage = props.selectionStorage === undefined; this.selectionStorage.selectionChangeEvent.addListener((args) => this._selectionChanges.next(args)); this._selectionEventsSubscription = this.streamSelectionEvents(); this._listeners.push(core_frontend_1.IModelConnection.onOpen.addListener((imodel) => { this._knownIModels.add(imodel); })); this._listeners.push(core_frontend_1.IModelConnection.onClose.addListener((imodel) => { this.onConnectionClose(imodel); })); } [Symbol.dispose]() { this._selectionEventsSubscription.unsubscribe(); this._listeners.forEach((dispose) => dispose()); } /** @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [Symbol.dispose] instead. */ /* c8 ignore next 3 */ dispose() { this[Symbol.dispose](); } onConnectionClose(imodel) { const imodelKey = this._imodelKeyFactory(imodel); this._hiliteSetProviders.delete(imodel); this._knownIModels.delete(imodel); this._currentSelection.clear(imodelKey); if (this._ownsStorage) { this.clearSelection("Connection Close Event", imodel); this.selectionStorage.clearStorage({ imodelKey }); } } /** * Request the manager to sync with imodel's tool selection (see `IModelConnection.selectionSet`). */ setSyncWithIModelToolSelection(imodel, sync = true) { const registration = this._imodelToolSelectionSyncHandlers.get(imodel); if (sync) { if (!registration || registration.requestorsCount === 0) { this._imodelToolSelectionSyncHandlers.set(imodel, { requestorsCount: 1, handler: new ToolSelectionSyncHandler(imodel, this) }); } else { this._imodelToolSelectionSyncHandlers.set(imodel, { ...registration, requestorsCount: registration.requestorsCount + 1 }); } } else { if (registration && registration.requestorsCount > 0) { const requestorsCount = registration.requestorsCount - 1; if (requestorsCount > 0) { this._imodelToolSelectionSyncHandlers.set(imodel, { ...registration, requestorsCount }); } else { this._imodelToolSelectionSyncHandlers.delete(imodel); registration.handler[Symbol.dispose](); } } } } /** * Temporarily suspends tool selection synchronization until the returned `Disposable` * is disposed. */ suspendIModelToolSelectionSync(imodel) { const registration = this._imodelToolSelectionSyncHandlers.get(imodel); if (!registration) { const noop = () => { }; return { [Symbol.dispose]: noop, dispose: noop }; } const wasSuspended = registration.handler.isSuspended; registration.handler.isSuspended = true; const doDispose = () => (registration.handler.isSuspended = wasSuspended); return { [Symbol.dispose]: doDispose, dispose: doDispose }; } /** Get the selection levels currently stored in this manager for the specified imodel */ getSelectionLevels(imodel) { const imodelKey = this._imodelKeyFactory(imodel); return this.selectionStorage.getSelectionLevels({ imodelKey }); } /** * Get the selection currently stored in this manager * * @note Calling immediately after `add*`|`replace*`|`remove*`|`clear*` method call does not guarantee * that returned `KeySet` will include latest changes. Listen for `selectionChange` event to get the * latest selection after changes. */ getSelection(imodel, level = 0) { const imodelKey = this._imodelKeyFactory(imodel); return this._currentSelection.getSelection(imodelKey, level); } handleEvent(evt) { const imodelKey = this._imodelKeyFactory(evt.imodel); this._knownIModels.add(evt.imodel); switch (evt.changeType) { case SelectionChangeEvent_js_1.SelectionChangeType.Add: this.selectionStorage.addToSelection({ imodelKey, source: evt.source, level: evt.level, selectables: keysToSelectable(evt.imodel, evt.keys), }); break; case SelectionChangeEvent_js_1.SelectionChangeType.Remove: this.selectionStorage.removeFromSelection({ imodelKey, source: evt.source, level: evt.level, selectables: keysToSelectable(evt.imodel, evt.keys), }); break; case SelectionChangeEvent_js_1.SelectionChangeType.Replace: this.selectionStorage.replaceSelection({ imodelKey, source: evt.source, level: evt.level, selectables: keysToSelectable(evt.imodel, evt.keys), }); break; case SelectionChangeEvent_js_1.SelectionChangeType.Clear: this.selectionStorage.clearSelection({ imodelKey, source: evt.source, level: evt.level }); break; } } /** * Add keys to the selection * @param source Name of the selection source * @param imodel iModel associated with the selection * @param keys Keys to add * @param level Selection level (see [selection levels documentation section]($docs/presentation/unified-selection/index#selection-levels)) * @param rulesetId ID of the ruleset in case the selection was changed from a rules-driven control */ addToSelection(source, imodel, keys, level = 0, rulesetId) { const evt = { source, level, imodel, changeType: SelectionChangeEvent_js_1.SelectionChangeType.Add, keys: new presentation_common_1.KeySet(keys), timestamp: new Date(), rulesetId, }; this.handleEvent(evt); } /** * Remove keys from current selection * @param source Name of the selection source * @param imodel iModel associated with the selection * @param keys Keys to remove * @param level Selection level (see [selection levels documentation section]($docs/presentation/unified-selection/index#selection-levels)) * @param rulesetId ID of the ruleset in case the selection was changed from a rules-driven control */ removeFromSelection(source, imodel, keys, level = 0, rulesetId) { const evt = { source, level, imodel, changeType: SelectionChangeEvent_js_1.SelectionChangeType.Remove, keys: new presentation_common_1.KeySet(keys), timestamp: new Date(), rulesetId, }; this.handleEvent(evt); } /** * Replace current selection * @param source Name of the selection source * @param imodel iModel associated with the selection * @param keys Keys to add * @param level Selection level (see [selection levels documentation section]($docs/presentation/unified-selection/index#selection-levels)) * @param rulesetId ID of the ruleset in case the selection was changed from a rules-driven control */ replaceSelection(source, imodel, keys, level = 0, rulesetId) { const evt = { source, level, imodel, changeType: SelectionChangeEvent_js_1.SelectionChangeType.Replace, keys: new presentation_common_1.KeySet(keys), timestamp: new Date(), rulesetId, }; this.handleEvent(evt); } /** * Clear current selection * @param source Name of the selection source * @param imodel iModel associated with the selection * @param level Selection level (see [selection levels documentation section]($docs/presentation/unified-selection/index#selection-levels)) * @param rulesetId ID of the ruleset in case the selection was changed from a rules-driven control */ clearSelection(source, imodel, level = 0, rulesetId) { const evt = { source, level, imodel, changeType: SelectionChangeEvent_js_1.SelectionChangeType.Clear, keys: new presentation_common_1.KeySet(), timestamp: new Date(), rulesetId, }; this.handleEvent(evt); } /** * Add keys to selection after applying [selection scope]($docs/presentation/unified-selection/index#selection-scopes) on them. * @param source Name of the selection source * @param imodel iModel associated with the selection * @param ids Element IDs to add * @param scope Selection scope to apply * @param level Selection level (see [selection levels documentation section]($docs/presentation/unified-selection/index#selection-levels)) * @param rulesetId ID of the ruleset in case the selection was changed from a rules-driven control */ async addToSelectionWithScope(source, imodel, ids, scope, level = 0, rulesetId) { const scopedKeys = await this.scopes.computeSelection(imodel, ids, scope); this.addToSelection(source, imodel, scopedKeys, level, rulesetId); } /** * Remove keys from current selection after applying [selection scope]($docs/presentation/unified-selection/index#selection-scopes) on them. * @param source Name of the selection source * @param imodel iModel associated with the selection * @param ids Element IDs to remove * @param scope Selection scope to apply * @param level Selection level (see [selection levels documentation section]($docs/presentation/unified-selection/index#selection-levels)) * @param rulesetId ID of the ruleset in case the selection was changed from a rules-driven control */ async removeFromSelectionWithScope(source, imodel, ids, scope, level = 0, rulesetId) { const scopedKeys = await this.scopes.computeSelection(imodel, ids, scope); this.removeFromSelection(source, imodel, scopedKeys, level, rulesetId); } /** * Replace current selection with keys after applying [selection scope]($docs/presentation/unified-selection/index#selection-scopes) on them. * @param source Name of the selection source * @param imodel iModel associated with the selection * @param ids Element IDs to replace with * @param scope Selection scope to apply * @param level Selection level (see [selection levels documentation section]($docs/presentation/unified-selection/index#selection-levels)) * @param rulesetId ID of the ruleset in case the selection was changed from a rules-driven control */ async replaceSelectionWithScope(source, imodel, ids, scope, level = 0, rulesetId) { const scopedKeys = await this.scopes.computeSelection(imodel, ids, scope); this.replaceSelection(source, imodel, scopedKeys, level, rulesetId); } /** * Get the current hilite set for the specified imodel * @public */ async getHiliteSet(imodel) { return this.getHiliteSetProvider(imodel).getHiliteSet(this.getSelection(imodel)); } /** * Get the current hilite set iterator for the specified imodel. * @public */ getHiliteSetIterator(imodel) { return this.getHiliteSetProvider(imodel).getHiliteSetIterator(this.getSelection(imodel)); } getHiliteSetProvider(imodel) { let provider = this._hiliteSetProviders.get(imodel); if (!provider) { provider = HiliteSetProvider_js_1.HiliteSetProvider.create({ imodel }); this._hiliteSetProviders.set(imodel, provider); } return provider; } streamSelectionEvents() { return this._selectionChanges .pipe((0, rxjs_1.mergeMap)((args) => { const currentSelectables = this.selectionStorage.getSelection({ imodelKey: args.imodelKey, level: args.level }); return this._currentSelection.computeSelection(args.imodelKey, args.level, currentSelectables, args.selectables).pipe((0, rxjs_1.mergeMap)(({ level, changedSelection }) => { const imodel = findIModel(this._knownIModels, this._imodelKeyFactory, args.imodelKey); /* c8 ignore next 3 */ if (!imodel) { return rxjs_1.EMPTY; } return (0, rxjs_1.of)({ imodel, keys: changedSelection, level, source: args.source, timestamp: args.timestamp, changeType: getChangeType(args.changeType), }); })); })) .subscribe({ next: (args) => { this.selectionChange.raiseEvent(args, this); }, }); } } exports.SelectionManager = SelectionManager; function findIModel(set, imodelKeyFactory, key) { for (const imodel of set) { if (imodelKeyFactory(imodel) === key) { return imodel; } } return undefined; } /** @internal */ class ToolSelectionSyncHandler { _selectionSourceName = "Tool"; _logicalSelection; _imodel; _imodelToolSelectionListenerDisposeFunc; _asyncsTracker = new internal_1.AsyncTasksTracker(); isSuspended; constructor(imodel, logicalSelection) { this._imodel = imodel; this._logicalSelection = logicalSelection; this._imodelToolSelectionListenerDisposeFunc = imodel.selectionSet.onChanged.addListener(this.onToolSelectionChanged); } [Symbol.dispose]() { this._imodelToolSelectionListenerDisposeFunc(); } /** note: used only it tests */ get pendingAsyncs() { return this._asyncsTracker.pendingAsyncs; } onToolSelectionChanged = async (ev) => { const env_1 = { stack: [], error: void 0, hasError: false }; try { // ignore selection change event if the handler is suspended if (this.isSuspended) { return; } // this component only cares about its own imodel const imodel = ev.set.iModel; if (imodel !== this._imodel) { return; } // determine the level of selection changes // wip: may want to allow selecting at different levels? const selectionLevel = 0; let ids; switch (ev.type) { case core_frontend_1.SelectionSetEventType.Add: ids = ev.additions; break; case core_frontend_1.SelectionSetEventType.Replace: ids = ev.set.active; break; default: ids = ev.removals; break; } // we're always using scoped selection changer even if the scope is set to "element" - that // makes sure we're adding to selection keys with concrete classes and not "BisCore:Element", which // we can't because otherwise our keys compare fails (presentation components load data with // concrete classes) const changer = new ScopedSelectionChanger(this._selectionSourceName, this._imodel, this._logicalSelection, (0, SelectionScopesManager_js_1.createSelectionScopeProps)(this._logicalSelection.scopes.activeScope)); const _r = __addDisposableResource(env_1, this._asyncsTracker.trackAsyncTask(), false); switch (ev.type) { case core_frontend_1.SelectionSetEventType.Add: await changer.add(ids, selectionLevel); break; case core_frontend_1.SelectionSetEventType.Replace: await changer.replace(ids, selectionLevel); break; case core_frontend_1.SelectionSetEventType.Remove: await changer.remove(ids, selectionLevel); break; case core_frontend_1.SelectionSetEventType.Clear: await changer.clear(selectionLevel); break; } } catch (e_1) { env_1.error = e_1; env_1.hasError = true; } finally { __disposeResources(env_1); } }; } exports.ToolSelectionSyncHandler = ToolSelectionSyncHandler; const parseElementIds = (ids) => { let allPersistent = true; let allTransient = true; for (const id of core_bentley_1.Id64.iterable(ids)) { if (core_bentley_1.Id64.isTransient(id)) { allPersistent = false; } else { allTransient = false; } if (!allPersistent && !allTransient) { break; } } // avoid making a copy if ids are only persistent or only transient if (allPersistent) { return { persistent: ids, transient: [] }; } else if (allTransient) { return { persistent: [], transient: ids }; } // if `ids` contain mixed ids, we have to copy.. use Array instead of // a Set for performance const persistentElementIds = []; const transientElementIds = []; for (const id of core_bentley_1.Id64.iterable(ids)) { if (core_bentley_1.Id64.isTransient(id)) { transientElementIds.push(id); } else { persistentElementIds.push(id); } } return { persistent: persistentElementIds, transient: transientElementIds }; }; function addKeys(target, className, ids) { for (const id of core_bentley_1.Id64.iterable(ids)) { target.add({ className, id }); } } class ScopedSelectionChanger { name; imodel; manager; scope; constructor(name, imodel, manager, scope) { this.name = name; this.imodel = imodel; this.manager = manager; this.scope = scope; } async clear(level) { this.manager.clearSelection(this.name, this.imodel, level); } async add(ids, level) { const keys = await this.#computeSelection(ids); this.manager.addToSelection(this.name, this.imodel, keys, level); } async remove(ids, level) { const keys = await this.#computeSelection(ids); this.manager.removeFromSelection(this.name, this.imodel, keys, level); } async replace(ids, level) { const keys = await this.#computeSelection(ids); this.manager.replaceSelection(this.name, this.imodel, keys, level); } async #computeSelection(ids) { let keys = new presentation_common_1.KeySet(); if (ids.elements) { const { persistent, transient } = parseElementIds(ids.elements); keys = await this.manager.scopes.computeSelection(this.imodel, persistent, this.scope); addKeys(keys, unified_selection_1.TRANSIENT_ELEMENT_CLASSNAME, transient); } if (ids.models) { addKeys(keys, "BisCore.Model", ids.models); } if (ids.subcategories) { addKeys(keys, "BisCore.SubCategory", ids.subcategories); } return keys; } } /** Stores current selection in `KeySet` format per iModel. */ class CurrentSelectionStorage { _currentSelection = new Map(); getCurrentSelectionStorage(imodelKey) { let storage = this._currentSelection.get(imodelKey); if (!storage) { storage = new IModelSelectionStorage(); this._currentSelection.set(imodelKey, storage); } return storage; } getSelection(imodelKey, level) { return this.getCurrentSelectionStorage(imodelKey).getSelection(level); } clear(imodelKey) { this._currentSelection.delete(imodelKey); } computeSelection(imodelKey, level, currSelectables, changedSelectables) { return this.getCurrentSelectionStorage(imodelKey).computeSelection(level, currSelectables, changedSelectables); } } /** * Computes and stores current selection in `KeySet` format. * It always stores result of latest resolved call to `computeSelection`. */ class IModelSelectionStorage { _currentSelection = new Map(); getSelection(level) { let entry = this._currentSelection.get(level); if (!entry) { entry = { value: new presentation_common_1.KeySet(), ongoingComputationDisposers: new Set() }; this._currentSelection.set(level, entry); } return entry.value; } clearSelections(level) { const clearedLevels = []; for (const [storedLevel] of this._currentSelection.entries()) { if (storedLevel > level) { clearedLevels.push(storedLevel); } } clearedLevels.forEach((storedLevel) => { const entry = this._currentSelection.get(storedLevel); /* c8 ignore next 3 */ if (!entry) { return; } for (const disposer of entry.ongoingComputationDisposers) { disposer.next(); } this._currentSelection.delete(storedLevel); }); } addDisposer(level, disposer) { const entry = this._currentSelection.get(level); if (!entry) { this._currentSelection.set(level, { value: new presentation_common_1.KeySet(), ongoingComputationDisposers: new Set([disposer]) }); return; } entry.ongoingComputationDisposers.add(disposer); } setSelection(level, keys, disposer) { const currEntry = this._currentSelection.get(level); if (currEntry) { currEntry.ongoingComputationDisposers.delete(disposer); } this._currentSelection.set(level, { value: keys, ongoingComputationDisposers: currEntry?.ongoingComputationDisposers ?? /* c8 ignore next */ new Set(), }); } computeSelection(level, currSelectables, changedSelectables) { this.clearSelections(level); const prevComputationsDisposers = [...(this._currentSelection.get(level)?.ongoingComputationDisposers ?? [])]; const currDisposer = new rxjs_1.Subject(); this.addDisposer(level, currDisposer); return (0, rxjs_1.defer)(async () => { const convertedSelectables = []; const [current, changed] = await Promise.all([ selectablesToKeys(currSelectables, convertedSelectables), selectablesToKeys(changedSelectables, convertedSelectables), ]); const currentSelection = new presentation_common_1.KeySet([...current.keys, ...current.selectableKeys.flatMap((selectable) => selectable.keys)]); const changedSelection = new presentation_common_1.KeySet([...changed.keys, ...changed.selectableKeys.flatMap((selectable) => selectable.keys)]); return { level, currentSelection, changedSelection, }; }).pipe((0, rxjs_1.takeUntil)(currDisposer), (0, rxjs_1.tap)({ next: (val) => { prevComputationsDisposers.forEach((disposer) => disposer.next()); this.setSelection(val.level, val.currentSelection, currDisposer); }, })); } } function keysToSelectable(imodel, keys) { const selectables = []; keys.forEach((key) => { if ("id" in key) { selectables.push(key); return; } const customSelectable = { identifier: key.pathFromRoot.join("/"), data: key, loadInstanceKeys: () => createInstanceKeysIterator(imodel, key), }; selectables.push(customSelectable); }); return selectables; } async function selectablesToKeys(selectables, convertedList) { const keys = []; const selectableKeys = []; for (const [className, ids] of selectables.instanceKeys) { for (const id of ids) { keys.push({ id, className }); } } for (const [_, selectable] of selectables.custom) { if (isNodeKey(selectable.data)) { selectableKeys.push({ identifier: selectable.identifier, keys: [selectable.data] }); continue; } const converted = convertedList.find((con) => con.identifier === selectable.identifier); if (converted) { selectableKeys.push(converted); continue; } const newConverted = { identifier: selectable.identifier, keys: [] }; convertedList.push(newConverted); for await (const instanceKey of selectable.loadInstanceKeys()) { newConverted.keys.push(instanceKey); } selectableKeys.push(newConverted); } return { keys, selectableKeys }; } async function* createInstanceKeysIterator(imodel, nodeKey) { if (presentation_common_1.NodeKey.isInstancesNodeKey(nodeKey)) { for (const key of nodeKey.instanceKeys) { yield key; } return; } const content = await Presentation_js_1.Presentation.presentation.getContentInstanceKeys({ imodel, keys: new presentation_common_1.KeySet([nodeKey]), rulesetOrId: { id: "grouped-instances", rules: [ { ruleType: "Content", specifications: [ { specType: "SelectedNodeInstances", }, ], }, ], }, }); for await (const key of content.items()) { yield key; } } function isNodeKey(data) { const key = data; return key.pathFromRoot !== undefined && key.type !== undefined; } function getChangeType(type) { switch (type) { case "add": return SelectionChangeEvent_js_1.SelectionChangeType.Add; case "remove": return SelectionChangeEvent_js_1.SelectionChangeType.Remove; case "replace": return SelectionChangeEvent_js_1.SelectionChangeType.Replace; case "clear": return SelectionChangeEvent_js_1.SelectionChangeType.Clear; } } //# sourceMappingURL=SelectionManager.js.map