UNPKG

@gravity-ui/graph

Version:

Modern graph editor component

226 lines (225 loc) 8.6 kB
import { batch, computed, signal } from "@preact/signals-core"; import { isTBlock } from "../../components/canvas/blocks/Block"; import { generateRandomId } from "../../components/canvas/blocks/generate"; import { ESelectionStrategy } from "../../utils/types/types"; import { selectConnectionsByBlockId } from "../connection/selectors"; import { BlockState, mapToBlockId } from "./Block"; export class BlockListStore { constructor(rootStore, graph) { this.rootStore = rootStore; this.graph = graph; this.$blocksMap = signal(new Map()); this.$blocks = signal([]); /** * This signal is used to store blocks in reactive state. * this signal fired for each change of the block state. * * NOTE: Please do not use it before you know what you are doing. */ this.$blocksReactiveState = computed(() => { return Array.from(this.$blocksMap.value.values()); }); this.$selectedBlocks = computed(() => { return this.$blocksReactiveState.value.filter((block) => block.selected); }); this.$selectedAnchor = computed(() => { if (!this.rootStore.settings.getConfigFlag("useBlocksAnchors")) return undefined; const block = this.$blocks.value.find((block) => { return Boolean(block.getSelectedAnchor()); }); if (block) { return block.getSelectedAnchor(); } return undefined; }); } setAnchorSelection(blockId, anchorId, selected) { const blockState = this.$blocksMap.value.get(blockId); if (!blockState) { return; } const anchor = blockState.getAnchorById(anchorId); if (!anchor) { return; } if (selected !== anchor.$selected.value) { this.graph.executеDefaultEventAction("block-anchor-selection-change", { anchor: anchor.asTAnchor(), selected }, () => { const currentSelected = this.$selectedAnchor.value; if (currentSelected && currentSelected !== anchor) { currentSelected.setSelection(false, true); } anchor.setSelection(selected, true); }); } } unsetAnchorsSelection() { this.$selectedAnchor.value?.setSelection(false); } updatePosition(id, nextState) { const blockState = this.$blocksMap.value.get(id); if (!blockState) { return; } this.graph.executеDefaultEventAction("block-change", { block: blockState.asTBlock() }, () => { blockState.updateBlock(nextState); }); } setBlockSelection(block, selected, params) { const blockState = block instanceof BlockState ? block : this.$blocksMap.value.get(block); if (!blockState) { return false; } if (selected !== Boolean(blockState.selected)) { if (!params?.ignoreChanges) { blockState.updateBlock({ selected }); } return true; } return false; } updateBlocksMap(blocks) { this.$blocksMap.value = new Map(blocks); this.$blocks.value = Array.from(this.$blocksMap.value.values()); } addBlock(block) { const id = block.id || generateRandomId("block"); this.$blocksMap.value.set(id, this.getOrCraeateBlockState({ id, ...block, })); this.updateBlocksMap(this.$blocksMap.value); return id; } deleteBlocks(blocks) { const map = new Map(this.$blocksMap.value); blocks.forEach((bId) => { const id = isTBlock(bId) ? bId.id : bId; const block = map.get(id); if (!block) { return; } map.delete(id); }); this.updateBlocksMap(map); } updateBlocks(blocks) { this.updateBlocksMap(blocks.reduce((acc, block) => { const state = this.getOrCraeateBlockState(block); acc.set(block.id, state); return acc; }, this.$blocksMap.value)); } setBlocks(blocks) { const blockStates = blocks.map((block) => this.getOrCraeateBlockState(block)); this.applyBlocksState(blockStates); } getOrCraeateBlockState(block) { const blockState = this.$blocksMap.value.get(block.id); if (blockState) { blockState.updateBlock(block); return blockState; } return BlockState.fromTBlock(this, block); } applyBlocksState(blocks) { this.updateBlocksMap(blocks.map((block) => [block.id, block])); } computeSelectionChange(ids, selected, strategy = ESelectionStrategy.REPLACE) { const list = new Set(this.$selectedBlocks.value); let add; let removed; if (!selected) { removed = new Set(this.getBlockStates(ids).filter((block) => { if (this.setBlockSelection(block.id, false, { ignoreChanges: true })) { list.delete(block); return true; } return false; })); } else { if (strategy === ESelectionStrategy.REPLACE) { removed = new Set(this.$selectedBlocks.value.filter((block) => { return this.setBlockSelection(block.id, false, { ignoreChanges: true }); })); list.clear(); } add = new Set(this.getBlockStates(ids).filter((block) => { if (this.setBlockSelection(block.id, true, { ignoreChanges: true }) || strategy === ESelectionStrategy.REPLACE) { removed?.delete(block); list.add(block); return true; } return false; })); } return { add: Array.from(add || []), removed: Array.from(removed || []), list: Array.from(list) }; } updateBlocksSelection(ids, selected, strategy = ESelectionStrategy.REPLACE) { const { add, removed, list } = this.computeSelectionChange(ids, selected, strategy); if (add.length || removed.length) { this.graph.executеDefaultEventAction("blocks-selection-change", { list: list.map(mapToBlockId), changes: { add: add.map(mapToBlockId), removed: removed.map(mapToBlockId), }, }, () => { batch(() => { this.unsetAnchorsSelection(); /** * Order is important here * If we first add and then remove, we will lose selection */ removed.forEach((block) => { this.setBlockSelection(block.id, false); }); add.forEach((block) => { this.setBlockSelection(block.id, true); }); }); }); } } getBlockConnections(blockId) { return this.rootStore.connectionsList.$connections.value.filter((connection) => [connection.targetBlockId, connection.sourceBlockId].includes(blockId)); } resetSelection() { batch(() => { this.unsetAnchorsSelection(); this.updateBlocksSelection(this.$selectedBlocks.value.map(mapToBlockId), false); }); } deleteSelectedBlocks() { const selectedBlocks = this.$selectedBlocks.value; selectedBlocks.forEach((block) => { this.deleteAllBlockConnections(block.id); }); const newBlocks = this.$blocks.value.filter((block) => !selectedBlocks.includes(block)); this.applyBlocksState(newBlocks); } deleteAllBlockConnections(blockId) { const connections = selectConnectionsByBlockId(this.graph, blockId); this.rootStore.connectionsList.deleteConnections(connections); } reset() { this.applyBlocksState([]); } toJSON() { return this.$blocks.value.map((block) => block.asTBlock()); } getBlockState(id) { return this.$blocksMap.value.get(id); } getBlock(id) { return this.getBlockState(id)?.asTBlock(); } getBlocks(ids) { return this.getBlockStates(ids); } getBlockStates(ids) { return ids.map((id) => this.getBlockState(id)).filter(Boolean); } }