@gravity-ui/graph
Version:
Modern graph editor component
265 lines (264 loc) • 10.6 kB
JavaScript
import { computed, signal } from "@preact/signals-core";
import { ESelectionStrategy } from "./types";
/**
* Service responsible for managing selection across different entity types
* Acts as a central registry and coordinator for ISelectionBucket instances
*
* The SelectionService manages selection state for all graph elements.
* During selection operations, the strategy determines how the selection will change:
* - REPLACE: replaces current selection with new one, affects all entity types
* - APPEND: adds new elements to current selection
* - SUBTRACT: removes elements from current selection
* - TOGGLE: toggles selection state of elements
*
* Supports two APIs:
* 1. Single-type selection: select(entityType, ids, strategy)
* 2. Multi-type selection: select({ type1: ids1, type2: ids2 }, strategy)
*
* @example
* ```typescript
* const selectionService = new SelectionService();
*
* // Single entity type selection
* selectionService.registerBucket(new MultipleSelectionBucket<string>("block"));
* selectionService.select("block", ["1", "2", "3"], ESelectionStrategy.REPLACE);
* selectionService.$selection.value; // { block: ["1", "2", "3"] }
*
* // Multi-entity type selection
* selectionService.registerBucket(new MultipleSelectionBucket<string>("connection"));
* selectionService.select({
* block: ["1", "2"],
* connection: ["4", "5"]
* }, ESelectionStrategy.REPLACE);
* selectionService.$selection.value; // { block: ["1", "2"], connection: ["4", "5"] }
* ```
*/
export class SelectionService {
constructor() {
/**
* Map of entity types to their corresponding selection buckets
*/
this.buckets = signal(new Map());
/**
* Computed signal aggregating selection from all buckets
*/
this.$selection = computed(() => {
const result = new Map();
for (const [type, bucket] of this.buckets.value.entries()) {
// $selected всегда ReadonlySignal<Set<TEntityId>>
result.set(type, bucket.$selected.value);
}
return result;
});
}
/**
* Registers a selection bucket for a specific entity type
* @param bucket The selection bucket to register
* @returns void
*/
registerBucket(bucket) {
if (this.buckets.value.has(bucket.entityType)) {
throw new Error(`Selection bucket for entityType '${bucket.entityType}' is already registered`);
}
const newMap = new Map(this.buckets.value);
newMap.set(bucket.entityType, bucket);
this.buckets.value = newMap;
}
/**
* Unregisters a selection bucket for a specific entity type
*
* @param bucket The selection bucket to unregister
* @returns void
*/
unregisterBucket(bucket) {
const newMap = new Map(this.buckets.value);
newMap.delete(bucket.entityType);
this.buckets.value = newMap;
}
/**
* Retrieves the selection bucket for a specific entity type
*
* @param entityType The entity type to get the bucket for
* @returns {ISelectionBucket | undefined} The selection bucket or undefined if not found
*/
getBucket(entityType) {
return this.buckets.value.get(entityType);
}
getBucketByElement(element) {
return Array.from(this.buckets.value.values()).find((bucket) => bucket.isRelatedElement?.(element));
}
selectRelatedElements(elements, strategy) {
const result = elements.reduce((acc, element) => {
const bucket = this.getBucketByElement(element);
const id = element.getEntityId();
if (bucket) {
if (!acc[bucket.entityType]) {
acc[bucket.entityType] = [];
}
acc[bucket.entityType].push(id);
}
return acc;
}, {});
this.select(result, strategy);
}
/**
* Selects entities using either single-type or multi-type selection API
*
* @param entityTypeOrSelection Either a single entity type or multi-entity selection object
* @param idsOrStrategy Either array of IDs or selection strategy
* @param strategy The selection strategy to apply (optional when using multi-entity API)
* @returns void
*/
select(entityTypeOrSelection, idsOrStrategy, strategy) {
// Detect which API is being used
if (typeof entityTypeOrSelection === "string") {
// Single entity type API
const entityType = entityTypeOrSelection;
const ids = idsOrStrategy;
const finalStrategy = strategy || ESelectionStrategy.REPLACE;
if (finalStrategy === ESelectionStrategy.REPLACE) {
// Reset selection in all other buckets
for (const [type, bucket] of this.buckets.value.entries()) {
if (type !== entityType) {
bucket.reset();
}
}
}
const bucket = this.getBucket(entityType);
if (bucket) {
bucket.updateSelection(ids, true, finalStrategy);
}
}
else {
// Multi-entity selection API
const selection = entityTypeOrSelection;
const finalStrategy = idsOrStrategy;
if (finalStrategy === ESelectionStrategy.REPLACE) {
// Reset selection in all buckets that are not in the selection
const selectedTypes = Object.keys(selection);
for (const [type, bucket] of this.buckets.value.entries()) {
if (!selectedTypes.includes(type)) {
bucket.reset();
}
}
}
// Apply selection to each entity type
for (const [entityType, ids] of Object.entries(selection)) {
const bucket = this.getBucket(entityType);
if (bucket) {
if (finalStrategy === ESelectionStrategy.REPLACE) {
// For REPLACE, clear the bucket first
bucket.updateSelection(ids, true, ESelectionStrategy.REPLACE);
}
else {
// For APPEND and SUBTRACT, we need to get current selection and modify it
const currentIds = bucket.$selected.value;
const newIds = new Set(currentIds);
if (finalStrategy === ESelectionStrategy.APPEND) {
ids.forEach((id) => newIds.add(id));
}
else if (finalStrategy === ESelectionStrategy.SUBTRACT) {
ids.forEach((id) => newIds.delete(id));
}
// Use REPLACE strategy to set the final state
bucket.updateSelection(Array.from(newIds), true, ESelectionStrategy.REPLACE);
}
}
}
}
}
/**
* Deselects entities using either single-type or multi-type deselection API
*
* @param entityTypeOrSelection Either a single entity type or multi-entity selection object
* @param ids Array of IDs to deselect
* @returns void
*/
deselect(entityTypeOrSelection, ids) {
// Detect which API is being used
if (typeof entityTypeOrSelection === "string") {
// Single entity type API
const entityType = entityTypeOrSelection;
const finalIds = ids || [];
const bucket = this.getBucket(entityType);
if (bucket) {
bucket.updateSelection(finalIds, false, ESelectionStrategy.SUBTRACT);
}
}
else {
// Multi-entity deselection API
const selection = entityTypeOrSelection;
// Deselect entities from each entity type
for (const [entityType, entityIds] of Object.entries(selection)) {
const bucket = this.getBucket(entityType);
if (bucket) {
const currentIds = bucket.$selected.value;
const newIds = new Set(currentIds);
entityIds.forEach((id) => newIds.delete(id));
bucket.updateSelection(Array.from(newIds), true, ESelectionStrategy.REPLACE);
}
}
}
}
/**
* Checks selection status for single entity or multiple entities
* @param entityTypeOrQueries Either entity type or multi-entity queries
* @param id ID of entity to check (only used for single entity API)
* @returns Selection status
*/
isSelected(entityTypeOrQueries, id) {
// Detect which API is being used
if (typeof entityTypeOrQueries === "string") {
// Single entity type API
const entityType = entityTypeOrQueries;
const finalId = id;
const bucket = this.getBucket(entityType);
return bucket ? bucket.isSelected(finalId) : false;
}
else {
// Multi-entity queries API
const queries = entityTypeOrQueries;
const results = {};
for (const [entityType, ids] of Object.entries(queries)) {
const bucket = this.getBucket(entityType);
if (bucket) {
results[entityType] = ids.some((id) => bucket.isSelected(id));
}
else {
results[entityType] = false;
}
}
return results;
}
}
/**
* Resets the selection for single entity type or multiple entity types
* @param entityTypeOrTypes Either single entity type or array of entity types
*/
resetSelection(entityTypeOrTypes) {
if (typeof entityTypeOrTypes === "string") {
// Single entity type
const bucket = this.getBucket(entityTypeOrTypes);
if (bucket) {
bucket.reset();
}
}
else {
// Multiple entity types
for (const entityType of entityTypeOrTypes) {
const bucket = this.getBucket(entityType);
if (bucket) {
bucket.reset();
}
}
}
}
/**
* Resets the selection for all registered buckets
*/
resetAllSelections() {
for (const bucket of this.buckets.value.values()) {
bucket.reset();
}
}
}