@gravity-ui/graph
Version:
Modern graph editor component
226 lines (225 loc) • 8.6 kB
JavaScript
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);
}
}