@xyflow/svelte
Version:
Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.
305 lines (304 loc) • 11.7 kB
JavaScript
import { panBy as panBySystem, updateNodeInternals as updateNodeInternalsSystem, addEdge as addEdgeUtil, initialConnection, errorMessages, updateAbsolutePositions, snapPosition, calculateNodePosition, getHandlePosition, Position } from '@xyflow/system';
import { initialEdgeTypes, initialNodeTypes, getInitialStore } from './initial-store.svelte';
import {} from './types';
export const key = Symbol();
export { useStore } from '../hooks/useStore';
export function createStore(signals) {
const store = getInitialStore(signals);
function setNodeTypes(nodeTypes) {
store.nodeTypes = {
...initialNodeTypes,
...nodeTypes
};
}
function setEdgeTypes(edgeTypes) {
store.edgeTypes = {
...initialEdgeTypes,
...edgeTypes
};
}
function addEdge(edgeParams) {
store.edges = addEdgeUtil(edgeParams, store.edges);
}
const updateNodePositions = (nodeDragItems, dragging = false) => {
store.nodes = store.nodes.map((node) => {
if (store.connection.inProgress && store.connection.fromNode.id === node.id) {
const internalNode = store.nodeLookup.get(node.id);
if (internalNode) {
store.connection = {
...store.connection,
from: getHandlePosition(internalNode, store.connection.fromHandle, Position.Left, true)
};
}
}
const dragItem = nodeDragItems.get(node.id);
return dragItem ? { ...node, position: dragItem.position, dragging } : node;
});
};
function updateNodeInternals(updates) {
const { changes, updatedInternals } = updateNodeInternalsSystem(updates, store.nodeLookup, store.parentLookup, store.domNode, store.nodeOrigin, store.nodeExtent, store.zIndexMode);
if (!updatedInternals) {
return;
}
updateAbsolutePositions(store.nodeLookup, store.parentLookup, {
nodeOrigin: store.nodeOrigin,
nodeExtent: store.nodeExtent,
zIndexMode: store.zIndexMode
});
if (store.fitViewQueued) {
store.resolveFitView();
}
const newNodes = new Map();
for (const change of changes) {
const userNode = store.nodeLookup.get(change.id)?.internals.userNode;
if (!userNode) {
continue;
}
const node = { ...userNode };
switch (change.type) {
case 'dimensions': {
const measured = { ...node.measured, ...change.dimensions };
if (change.setAttributes) {
node.width = change.dimensions?.width ?? node.width;
node.height = change.dimensions?.height ?? node.height;
}
node.measured = measured;
break;
}
case 'position':
node.position = change.position ?? node.position;
break;
}
newNodes.set(change.id, node);
}
store.nodes = store.nodes.map((node) => newNodes.get(node.id) ?? node);
}
function fitView(options) {
// We either create a new Promise or reuse the existing one
// Even if fitView is called multiple times in a row, we only end up with a single Promise
const fitViewResolver = store.fitViewResolver ?? Promise.withResolvers();
// We schedule a fitView by setting fitViewQueued and triggering a setNodes
store.fitViewQueued = true;
store.fitViewOptions = options;
store.fitViewResolver = fitViewResolver;
// We need to update the nodes so that adoptUserNodes is triggered
store.nodes = [...store.nodes];
return fitViewResolver.promise;
}
async function setCenter(x, y, options) {
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : store.maxZoom;
const currentPanZoom = store.panZoom;
if (!currentPanZoom) {
return Promise.resolve(false);
}
await currentPanZoom.setViewport({
x: store.width / 2 - x * nextZoom,
y: store.height / 2 - y * nextZoom,
zoom: nextZoom
}, { duration: options?.duration, ease: options?.ease, interpolate: options?.interpolate });
return Promise.resolve(true);
}
function zoomBy(factor, options) {
const panZoom = store.panZoom;
if (!panZoom) {
return Promise.resolve(false);
}
return panZoom.scaleBy(factor, options);
}
function zoomIn(options) {
return zoomBy(1.2, options);
}
function zoomOut(options) {
return zoomBy(1 / 1.2, options);
}
function setMinZoom(minZoom) {
const panZoom = store.panZoom;
if (panZoom) {
panZoom.setScaleExtent([minZoom, store.maxZoom]);
store.minZoom = minZoom;
}
}
function setMaxZoom(maxZoom) {
const panZoom = store.panZoom;
if (panZoom) {
panZoom.setScaleExtent([store.minZoom, maxZoom]);
store.maxZoom = maxZoom;
}
}
function setTranslateExtent(extent) {
const panZoom = store.panZoom;
if (panZoom) {
panZoom.setTranslateExtent(extent);
store.translateExtent = extent;
}
}
function deselect(elements, elementsToDeselect = null) {
let deselected = false;
const newElements = elements.map((element) => {
const shouldDeselect = elementsToDeselect ? elementsToDeselect.has(element.id) : true;
if (shouldDeselect && element.selected) {
deselected = true;
return { ...element, selected: false };
}
return element;
});
return [deselected, newElements];
}
function unselectNodesAndEdges(params) {
const nodesToDeselect = params?.nodes ? new Set(params.nodes.map((node) => node.id)) : null;
const [nodesDeselected, newNodes] = deselect(store.nodes, nodesToDeselect);
if (nodesDeselected) {
store.nodes = newNodes;
}
const edgesToDeselect = params?.edges ? new Set(params.edges.map((node) => node.id)) : null;
const [edgesDeselected, newEdges] = deselect(store.edges, edgesToDeselect);
if (edgesDeselected) {
store.edges = newEdges;
}
}
function addSelectedNodes(ids) {
const isMultiSelection = store.multiselectionKeyPressed;
store.nodes = store.nodes.map((node) => {
const nodeWillBeSelected = ids.includes(node.id);
const selected = isMultiSelection ? node.selected || nodeWillBeSelected : nodeWillBeSelected;
if (!!node.selected !== selected) {
return { ...node, selected };
}
return node;
});
if (!isMultiSelection) {
unselectNodesAndEdges({ nodes: [] });
}
}
function addSelectedEdges(ids) {
const isMultiSelection = store.multiselectionKeyPressed;
store.edges = store.edges.map((edge) => {
const edgeWillBeSelected = ids.includes(edge.id);
const selected = isMultiSelection ? edge.selected || edgeWillBeSelected : edgeWillBeSelected;
if (!!edge.selected !== selected) {
return { ...edge, selected };
}
return edge;
});
if (!isMultiSelection) {
unselectNodesAndEdges({ edges: [] });
}
}
function handleNodeSelection(id, unselect, nodeRef) {
const node = store.nodeLookup.get(id);
if (!node) {
console.warn('012', errorMessages['error012'](id));
return;
}
store.selectionRect = null;
store.selectionRectMode = null;
if (!node.selected) {
addSelectedNodes([id]);
}
else if (unselect || (node.selected && store.multiselectionKeyPressed)) {
unselectNodesAndEdges({ nodes: [node], edges: [] });
requestAnimationFrame(() => nodeRef?.blur());
}
}
function handleEdgeSelection(id) {
const edge = store.edgeLookup.get(id);
if (!edge) {
console.warn('012', errorMessages['error012'](id));
return;
}
const selectable = edge.selectable || (store.elementsSelectable && typeof edge.selectable === 'undefined');
if (selectable) {
store.selectionRect = null;
store.selectionRectMode = null;
if (!edge.selected) {
addSelectedEdges([id]);
}
else if (edge.selected && store.multiselectionKeyPressed) {
unselectNodesAndEdges({ nodes: [], edges: [edge] });
}
}
}
function moveSelectedNodes(direction, factor) {
const { nodeExtent, snapGrid, nodeOrigin, nodeLookup, nodesDraggable, onerror } = store;
const nodeUpdates = new Map();
/*
* by default a node moves 5px on each key press
* if snap grid is enabled, we use that for the velocity
*/
const xVelo = snapGrid?.[0] ?? 5;
const yVelo = snapGrid?.[1] ?? 5;
const xDiff = direction.x * xVelo * factor;
const yDiff = direction.y * yVelo * factor;
for (const node of nodeLookup.values()) {
const isSelected = node.selected &&
(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined'));
if (!isSelected) {
continue;
}
let nextPosition = {
x: node.internals.positionAbsolute.x + xDiff,
y: node.internals.positionAbsolute.y + yDiff
};
if (snapGrid) {
nextPosition = snapPosition(nextPosition, snapGrid);
}
const { position, positionAbsolute } = calculateNodePosition({
nodeId: node.id,
nextPosition,
nodeLookup,
nodeExtent,
nodeOrigin,
onError: onerror
});
node.position = position;
node.internals.positionAbsolute = positionAbsolute;
nodeUpdates.set(node.id, node);
}
updateNodePositions(nodeUpdates);
}
function panBy(delta) {
return panBySystem({
delta,
panZoom: store.panZoom,
transform: [store.viewport.x, store.viewport.y, store.viewport.zoom],
translateExtent: store.translateExtent,
width: store.width,
height: store.height
});
}
const updateConnection = (newConnection) => {
store._connection = { ...newConnection };
};
function cancelConnection() {
store._connection = initialConnection;
}
function reset() {
store.resetStoreValues();
unselectNodesAndEdges();
}
const storeWithActions = Object.assign(store, {
setNodeTypes,
setEdgeTypes,
addEdge,
updateNodePositions,
updateNodeInternals,
zoomIn,
zoomOut,
fitView,
setCenter,
setMinZoom,
setMaxZoom,
setTranslateExtent,
unselectNodesAndEdges,
addSelectedNodes,
addSelectedEdges,
handleNodeSelection,
handleEdgeSelection,
moveSelectedNodes,
panBy,
updateConnection,
cancelConnection,
reset
});
return storeWithActions;
}