UNPKG

@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
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; }