WIP: migrated store and useSvelteFlow hook to signals & solved state propagation for provider
This commit is contained in:
@@ -1,29 +1,59 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { EdgeWrapper } from '$lib/components/EdgeWrapper';
|
||||
import { CallOnMount } from '$lib/components/CallOnMount';
|
||||
import { MarkerDefinition } from '$lib/container/EdgeRenderer/MarkerDefinition';
|
||||
import { useStore } from '$lib/store';
|
||||
import type { DefaultEdgeOptions, EdgeEvents } from '$lib/types';
|
||||
import type { EdgeEvents, EdgeLayouted } from '$lib/types';
|
||||
import { getEdgePosition, getElevatedEdgeZIndex } from '@xyflow/system';
|
||||
import type { SvelteFlowStore } from '$lib/store/types';
|
||||
|
||||
let {
|
||||
defaultEdgeOptions,
|
||||
store,
|
||||
onedgeclick,
|
||||
onedgecontextmenu,
|
||||
onedgemouseenter,
|
||||
onedgemouseleave
|
||||
}: { defaultEdgeOptions?: DefaultEdgeOptions } & EdgeEvents = $props();
|
||||
}: { store: SvelteFlowStore } & EdgeEvents = $props();
|
||||
|
||||
const {
|
||||
visibleEdges,
|
||||
edgesInitialized,
|
||||
edges: { setDefaultOptions },
|
||||
elementsSelectable
|
||||
} = useStore();
|
||||
const { edges, nodes } = store;
|
||||
|
||||
onMount(() => {
|
||||
// TODO: Remove this when we have a better way to set default options
|
||||
if (defaultEdgeOptions) setDefaultOptions(defaultEdgeOptions);
|
||||
let layoutedEdges = $derived.by(() => {
|
||||
$nodes;
|
||||
const layoutedEdges = $edges.reduce<EdgeLayouted[]>((res, edge) => {
|
||||
const sourceNode = store.nodeLookup.get(edge.source);
|
||||
const targetNode = store.nodeLookup.get(edge.target);
|
||||
|
||||
if (!sourceNode || !targetNode) {
|
||||
return res;
|
||||
}
|
||||
|
||||
const edgePosition = getEdgePosition({
|
||||
id: edge.id,
|
||||
sourceNode,
|
||||
targetNode,
|
||||
sourceHandle: edge.sourceHandle || null,
|
||||
targetHandle: edge.targetHandle || null,
|
||||
connectionMode: store.connectionMode,
|
||||
onError: store.onerror
|
||||
});
|
||||
|
||||
if (edgePosition) {
|
||||
res.push({
|
||||
...edge,
|
||||
zIndex: getElevatedEdgeZIndex({
|
||||
selected: edge.selected,
|
||||
zIndex: edge.zIndex,
|
||||
sourceNode,
|
||||
targetNode,
|
||||
elevateOnSelect: false
|
||||
}),
|
||||
...edgePosition
|
||||
});
|
||||
}
|
||||
|
||||
return res;
|
||||
}, []);
|
||||
|
||||
return layoutedEdges;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -32,8 +62,9 @@
|
||||
<MarkerDefinition />
|
||||
</svg>
|
||||
|
||||
{#each $visibleEdges as edge (edge.id)}
|
||||
{#each layoutedEdges as edge (edge.id)}
|
||||
<EdgeWrapper
|
||||
{store}
|
||||
id={edge.id}
|
||||
source={edge.source}
|
||||
target={edge.target}
|
||||
@@ -41,7 +72,7 @@
|
||||
style={edge.style}
|
||||
animated={edge.animated}
|
||||
selected={edge.selected}
|
||||
selectable={edge.selectable ?? $elementsSelectable}
|
||||
selectable={edge.selectable ?? store.elementsSelectable}
|
||||
deletable={edge.deletable}
|
||||
hidden={edge.hidden}
|
||||
label={edge.label}
|
||||
@@ -68,13 +99,13 @@
|
||||
/>
|
||||
{/each}
|
||||
|
||||
{#if $visibleEdges.length > 0}
|
||||
{#if layoutedEdges.length > 0}
|
||||
<CallOnMount
|
||||
onMount={() => {
|
||||
$edgesInitialized = true;
|
||||
store.edgesInitialized = true;
|
||||
}}
|
||||
onDestroy={() => {
|
||||
$edgesInitialized = false;
|
||||
store.edgesInitialized = false;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
+2
-2
@@ -2,11 +2,11 @@
|
||||
import { useStore } from '$lib/store';
|
||||
import { Marker } from '$lib/container/EdgeRenderer/MarkerDefinition';
|
||||
|
||||
const { markers } = useStore();
|
||||
const store = useStore();
|
||||
</script>
|
||||
|
||||
<defs>
|
||||
{#each $markers as marker (marker.id)}
|
||||
{#each store.markers as marker (marker.id)}
|
||||
<Marker {...marker} />
|
||||
{/each}
|
||||
</defs>
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
|
||||
import type { NodeEvents } from '$lib/types';
|
||||
import { NodeWrapper } from '$lib/components/NodeWrapper';
|
||||
import { useStore } from '$lib/store';
|
||||
import type { SvelteFlowStore } from '$lib/store/types';
|
||||
|
||||
let {
|
||||
store,
|
||||
nodeClickDistance,
|
||||
onnodeclick,
|
||||
onnodecontextmenu,
|
||||
@@ -16,16 +17,9 @@
|
||||
onnodedrag,
|
||||
onnodedragstart,
|
||||
onnodedragstop
|
||||
}: { nodeClickDistance?: number } & NodeEvents = $props();
|
||||
}: { store: SvelteFlowStore; nodeClickDistance?: number } & NodeEvents = $props();
|
||||
|
||||
const {
|
||||
visibleNodes,
|
||||
nodesDraggable,
|
||||
nodesConnectable,
|
||||
elementsSelectable,
|
||||
updateNodeInternals,
|
||||
parentLookup
|
||||
} = useStore();
|
||||
const { nodes } = store;
|
||||
|
||||
const resizeObserver: ResizeObserver | null =
|
||||
typeof ResizeObserver === 'undefined'
|
||||
@@ -43,7 +37,7 @@
|
||||
});
|
||||
});
|
||||
|
||||
updateNodeInternals(updates);
|
||||
store.updateNodeInternals(updates);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
@@ -52,41 +46,46 @@
|
||||
</script>
|
||||
|
||||
<div class="svelte-flow__nodes">
|
||||
{#each $visibleNodes as node (node.id)}
|
||||
{#each $nodes as node (node.id)}
|
||||
{@const internalNode = store.nodeLookup.get(node.id)}
|
||||
<NodeWrapper
|
||||
{node}
|
||||
{store}
|
||||
node={internalNode!}
|
||||
id={node.id}
|
||||
data={node.data}
|
||||
selected={!!node.selected}
|
||||
hidden={!!node.hidden}
|
||||
draggable={!!(node.draggable || ($nodesDraggable && typeof node.draggable === 'undefined'))}
|
||||
draggable={!!(
|
||||
node.draggable ||
|
||||
(store.nodesDraggable && typeof node.draggable === 'undefined')
|
||||
)}
|
||||
selectable={!!(
|
||||
node.selectable ||
|
||||
($elementsSelectable && typeof node.selectable === 'undefined')
|
||||
(store.elementsSelectable && typeof node.selectable === 'undefined')
|
||||
)}
|
||||
connectable={!!(
|
||||
node.connectable ||
|
||||
($nodesConnectable && typeof node.connectable === 'undefined')
|
||||
(store.nodesConnectable && typeof node.connectable === 'undefined')
|
||||
)}
|
||||
deletable={node.deletable ?? true}
|
||||
positionX={node.internals.positionAbsolute.x}
|
||||
positionY={node.internals.positionAbsolute.y}
|
||||
isParent={$parentLookup.has(node.id)}
|
||||
positionX={internalNode!.internals.positionAbsolute.x}
|
||||
positionY={internalNode!.internals.positionAbsolute.y}
|
||||
isParent={store.parentLookup.has(node.id)}
|
||||
style={node.style}
|
||||
class={node.class}
|
||||
type={node.type ?? 'default'}
|
||||
sourcePosition={node.sourcePosition}
|
||||
targetPosition={node.targetPosition}
|
||||
dragging={node.dragging}
|
||||
zIndex={node.internals.z ?? 0}
|
||||
zIndex={internalNode!.internals.z ?? 0}
|
||||
dragHandle={node.dragHandle}
|
||||
initialized={nodeHasDimensions(node)}
|
||||
width={node.width}
|
||||
height={node.height}
|
||||
initialWidth={node.initialWidth}
|
||||
initialHeight={node.initialHeight}
|
||||
measuredWidth={node.measured.width}
|
||||
measuredHeight={node.measured.height}
|
||||
measuredWidth={node.measured?.width ?? 0}
|
||||
measuredHeight={node.measured?.height ?? 0}
|
||||
parentId={node.parentId}
|
||||
{resizeObserver}
|
||||
{nodeClickDistance}
|
||||
|
||||
@@ -37,39 +37,29 @@
|
||||
import type { Node, Edge, InternalNode } from '$lib/types';
|
||||
import type { PaneProps } from './types';
|
||||
let {
|
||||
store,
|
||||
panOnDrag = true,
|
||||
selectionOnDrag,
|
||||
onpaneclick,
|
||||
onpanecontextmenu,
|
||||
nodes,
|
||||
edges,
|
||||
children
|
||||
}: PaneProps = $props();
|
||||
|
||||
const {
|
||||
nodeLookup,
|
||||
viewport,
|
||||
dragging,
|
||||
elementsSelectable,
|
||||
selectionRect,
|
||||
selectionRectMode,
|
||||
selectionKeyPressed,
|
||||
selectionMode,
|
||||
panActivationKeyPressed,
|
||||
unselectNodesAndEdges
|
||||
} = useStore();
|
||||
const { viewport, nodes, edges } = store;
|
||||
|
||||
// svelte-ignore non_reactive_update
|
||||
let container: HTMLDivElement;
|
||||
let containerBounds: DOMRect | null = null;
|
||||
let selectedNodes: InternalNode[] = [];
|
||||
|
||||
let panOnDragActive = $derived($panActivationKeyPressed || panOnDrag);
|
||||
let panOnDragActive = $derived(store.panActivationKeyPressed || panOnDrag);
|
||||
let isSelecting = $derived(
|
||||
$selectionKeyPressed || $selectionRect || (selectionOnDrag && panOnDragActive !== true)
|
||||
store.selectionKeyPressed ||
|
||||
store.selectionRect ||
|
||||
(selectionOnDrag && panOnDragActive !== true)
|
||||
);
|
||||
let hasActiveSelection = $derived(
|
||||
$elementsSelectable && (isSelecting || $selectionRectMode === 'user')
|
||||
store.elementsSelectable && (isSelecting || store.selectionRectMode === 'user')
|
||||
);
|
||||
|
||||
// Used to prevent click events when the user lets go of the selectionKey during a selection
|
||||
@@ -83,15 +73,15 @@
|
||||
}
|
||||
|
||||
onpaneclick?.({ event });
|
||||
unselectNodesAndEdges();
|
||||
selectionRectMode.set(null);
|
||||
store.unselectNodesAndEdges();
|
||||
store.selectionRectMode = null;
|
||||
}
|
||||
|
||||
function onPointerDown(event: PointerEvent) {
|
||||
containerBounds = container.getBoundingClientRect();
|
||||
|
||||
if (
|
||||
!elementsSelectable ||
|
||||
!store.elementsSelectable ||
|
||||
!isSelecting ||
|
||||
event.button !== 0 ||
|
||||
event.target !== container ||
|
||||
@@ -104,32 +94,32 @@
|
||||
|
||||
const { x, y } = getEventPosition(event, containerBounds);
|
||||
|
||||
unselectNodesAndEdges();
|
||||
store.unselectNodesAndEdges();
|
||||
|
||||
selectionRect.set({
|
||||
store.selectionRect = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
startX: x,
|
||||
startY: y,
|
||||
x,
|
||||
y
|
||||
});
|
||||
};
|
||||
|
||||
// onSelectionStart?.(event);
|
||||
}
|
||||
|
||||
function onPointerMove(event: PointerEvent) {
|
||||
if (!isSelecting || !containerBounds || !$selectionRect) {
|
||||
if (!isSelecting || !containerBounds || !store.selectionRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectionInProgress = true;
|
||||
|
||||
const mousePos = getEventPosition(event, containerBounds);
|
||||
const startX = $selectionRect.startX ?? 0;
|
||||
const startY = $selectionRect.startY ?? 0;
|
||||
const startX = store.selectionRect.startX ?? 0;
|
||||
const startY = store.selectionRect.startY ?? 0;
|
||||
const nextUserSelectRect = {
|
||||
...$selectionRect,
|
||||
...store.selectionRect,
|
||||
x: mousePos.x < startX ? mousePos.x : startX,
|
||||
y: mousePos.y < startY ? mousePos.y : startY,
|
||||
width: Math.abs(mousePos.x - startX),
|
||||
@@ -139,10 +129,10 @@
|
||||
const prevSelectedEdgeIds = getConnectedEdges(selectedNodes, $edges).map((e) => e.id);
|
||||
|
||||
selectedNodes = getNodesInside(
|
||||
$nodeLookup,
|
||||
store.nodeLookup,
|
||||
nextUserSelectRect,
|
||||
[$viewport.x, $viewport.y, $viewport.zoom],
|
||||
$selectionMode === SelectionMode.Partial,
|
||||
store.selectionMode === SelectionMode.Partial,
|
||||
true
|
||||
);
|
||||
const selectedEdgeIds = getConnectedEdges(selectedNodes, $edges).map((e) => e.id);
|
||||
@@ -163,8 +153,8 @@
|
||||
edges.update((edges) => edges.map(toggleSelected(selectedEdgeIds)));
|
||||
}
|
||||
|
||||
selectionRectMode.set('user');
|
||||
selectionRect.set(nextUserSelectRect);
|
||||
store.selectionRectMode = 'user';
|
||||
store.selectionRect = nextUserSelectRect;
|
||||
}
|
||||
|
||||
function onPointerUp(event: PointerEvent) {
|
||||
@@ -176,18 +166,18 @@
|
||||
|
||||
// We only want to trigger click functions when in selection mode if
|
||||
// the user did not move the mouse.
|
||||
if (!isSelecting && $selectionRectMode === 'user' && event.target === container) {
|
||||
if (!isSelecting && store.selectionRectMode === 'user' && event.target === container) {
|
||||
onClick?.(event);
|
||||
}
|
||||
selectionRect.set(null);
|
||||
store.selectionRect = null;
|
||||
|
||||
if (selectedNodes.length > 0) {
|
||||
$selectionRectMode = 'nodes';
|
||||
store.selectionRectMode = 'nodes';
|
||||
}
|
||||
|
||||
// If the user kept holding the selectionKey during the selection,
|
||||
// we need to reset the selectionInProgress, so the next click event is not prevented
|
||||
if ($selectionKeyPressed) {
|
||||
if (store.selectionKeyPressed) {
|
||||
selectionInProgress = false;
|
||||
}
|
||||
|
||||
@@ -210,7 +200,7 @@
|
||||
bind:this={container}
|
||||
class="svelte-flow__pane"
|
||||
class:draggable={panOnDrag === true || (Array.isArray(panOnDrag) && panOnDrag.includes(0))}
|
||||
class:dragging={$dragging}
|
||||
class:dragging={store.dragging}
|
||||
class:selection={isSelecting}
|
||||
onclick={hasActiveSelection ? undefined : wrapHandler(onClick, container)}
|
||||
onpointerdown={hasActiveSelection ? onPointerDown : undefined}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { Edge, Node, PaneEvents } from '$lib/types';
|
||||
import type { PaneEvents } from '$lib/types';
|
||||
import type { SvelteFlowStore } from '$lib/store/types';
|
||||
|
||||
export type PaneProps = {
|
||||
store: SvelteFlowStore;
|
||||
panOnDrag?: boolean | number[];
|
||||
selectionOnDrag?: boolean;
|
||||
nodes: Writable<Node[]>;
|
||||
edges: Writable<Edge[]>;
|
||||
children: Snippet;
|
||||
} & PaneEvents;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
...restProps
|
||||
}: PanelProps = $props();
|
||||
|
||||
const { selectionRectMode } = useStore();
|
||||
const store = useStore();
|
||||
|
||||
let positionClasses = $derived(`${position}`.split('-'));
|
||||
</script>
|
||||
@@ -19,7 +19,7 @@
|
||||
<div
|
||||
class={cc(['svelte-flow__panel', className, ...positionClasses])}
|
||||
{style}
|
||||
style:pointer-events={$selectionRectMode ? 'none' : ''}
|
||||
style:pointer-events={store.selectionRectMode ? 'none' : ''}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { onMount, hasContext } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
import { onMount, getContext, setContext } from 'svelte';
|
||||
import cc from 'classcat';
|
||||
import { ConnectionMode, PanOnScrollMode } from '@xyflow/system';
|
||||
import { PanOnScrollMode } from '@xyflow/system';
|
||||
|
||||
import { Zoom } from '$lib/container/Zoom';
|
||||
import { Pane } from '$lib/container/Pane';
|
||||
@@ -14,78 +14,28 @@
|
||||
import { KeyHandler } from '$lib/components/KeyHandler';
|
||||
import { ConnectionLine } from '$lib/components/ConnectionLine';
|
||||
import { Attribution } from '$lib/components/Attribution';
|
||||
import { key, useStore, createStoreContext } from '$lib/store';
|
||||
import { key, createStore } from '$lib/store';
|
||||
import type { SvelteFlowProps } from './types';
|
||||
import { updateStore, updateStoreByKeys, type UpdatableStoreProps } from './utils';
|
||||
import { useColorModeClass } from '$lib/hooks/useColorModeClass';
|
||||
import { type ProviderContext, type ContainerSignals, type StoreContext } from '$lib/store/types';
|
||||
|
||||
let {
|
||||
id = '1',
|
||||
nodes,
|
||||
edges,
|
||||
fitView,
|
||||
fitViewOptions,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
initialViewport,
|
||||
viewport,
|
||||
nodeTypes,
|
||||
edgeTypes,
|
||||
style,
|
||||
class: className,
|
||||
width,
|
||||
height,
|
||||
proOptions,
|
||||
selectionKey,
|
||||
selectionMode,
|
||||
deleteKey,
|
||||
panActivationKey,
|
||||
multiSelectionKey,
|
||||
zoomActivationKey,
|
||||
nodesDraggable,
|
||||
nodesConnectable,
|
||||
nodeDragThreshold,
|
||||
elementsSelectable,
|
||||
snapGrid,
|
||||
deleteKey,
|
||||
connectionRadius,
|
||||
connectionLineType,
|
||||
connectionMode = ConnectionMode.Strict,
|
||||
connectionLineStyle = '',
|
||||
connectionLineContainerStyle = '',
|
||||
onMoveStart,
|
||||
onMove,
|
||||
onMoveEnd,
|
||||
isValidConnection,
|
||||
translateExtent,
|
||||
nodeExtent,
|
||||
onlyRenderVisibleElements,
|
||||
panOnScrollMode = PanOnScrollMode.Free,
|
||||
preventScrolling = true,
|
||||
zoomOnScroll = true,
|
||||
zoomOnDoubleClick = true,
|
||||
zoomOnPinch = true,
|
||||
panOnScroll = false,
|
||||
panOnDrag = true,
|
||||
selectionOnDrag,
|
||||
autoPanOnConnect = true,
|
||||
autoPanOnNodeDrag = true,
|
||||
onerror,
|
||||
ondelete,
|
||||
onedgecreate,
|
||||
attributionPosition,
|
||||
proOptions,
|
||||
defaultEdgeOptions,
|
||||
width,
|
||||
height,
|
||||
colorMode = 'light',
|
||||
onconnect,
|
||||
onconnectstart,
|
||||
onconnectend,
|
||||
onbeforedelete,
|
||||
oninit,
|
||||
nodeOrigin,
|
||||
paneClickDistance = 1,
|
||||
nodeClickDistance = 1,
|
||||
defaultMarkerColor = '#b1b1b7',
|
||||
style,
|
||||
class: className,
|
||||
onMoveStart,
|
||||
onMoveEnd,
|
||||
onMove,
|
||||
connectionLine,
|
||||
children,
|
||||
onnodeclick,
|
||||
onnodecontextmenu,
|
||||
onnodedrag,
|
||||
@@ -102,135 +52,74 @@
|
||||
onedgemouseleave,
|
||||
onpaneclick,
|
||||
onpanecontextmenu,
|
||||
...rest
|
||||
panOnScrollMode = PanOnScrollMode.Free,
|
||||
preventScrolling = true,
|
||||
zoomOnScroll = true,
|
||||
zoomOnDoubleClick = true,
|
||||
zoomOnPinch = true,
|
||||
panOnScroll = false,
|
||||
panOnDrag = true,
|
||||
selectionOnDrag = true,
|
||||
defaultEdgeOptions,
|
||||
connectionLineContainerStyle = '',
|
||||
connectionLineStyle = '',
|
||||
attributionPosition,
|
||||
children,
|
||||
...props
|
||||
}: SvelteFlowProps = $props();
|
||||
|
||||
let domNode = $state<HTMLDivElement>();
|
||||
let clientWidth = $state<number>();
|
||||
let clientHeight = $state<number>();
|
||||
// svelte-ignore perf_avoid_inline_class
|
||||
const container: ContainerSignals = new (class {
|
||||
domNode = $state<HTMLDivElement>();
|
||||
width = $state<number | undefined>(width);
|
||||
height = $state<number | undefined>(height);
|
||||
})();
|
||||
|
||||
const initViewport = $viewport || initialViewport;
|
||||
const store = createStore({
|
||||
props,
|
||||
container
|
||||
});
|
||||
const { viewport } = store;
|
||||
const initialViewport = props.initialViewport ?? get(viewport);
|
||||
|
||||
//TODO SVELTE5
|
||||
store.syncNodeStores(props.nodes);
|
||||
store.syncEdgeStores(props.edges);
|
||||
store.syncViewport(props.viewport);
|
||||
|
||||
const store = hasContext(key)
|
||||
? useStore()
|
||||
: createStoreContext({
|
||||
nodes: get(nodes),
|
||||
edges: get(edges),
|
||||
width,
|
||||
height,
|
||||
fitView,
|
||||
nodeOrigin,
|
||||
nodeExtent
|
||||
});
|
||||
const providerContext = getContext<ProviderContext>(key);
|
||||
if (providerContext) {
|
||||
providerContext.setStore(store);
|
||||
}
|
||||
setContext(key, {
|
||||
provider: false,
|
||||
getStore() {
|
||||
return store;
|
||||
}
|
||||
} satisfies StoreContext);
|
||||
|
||||
onMount(() => {
|
||||
store.width.set(clientWidth!);
|
||||
store.height.set(clientHeight!);
|
||||
store.domNode.set(domNode!);
|
||||
|
||||
store.syncNodeStores(nodes);
|
||||
store.syncEdgeStores(edges);
|
||||
store.syncViewport(viewport);
|
||||
|
||||
if (fitView !== undefined) {
|
||||
store.fitViewOnInit.set(fitView);
|
||||
}
|
||||
|
||||
if (fitViewOptions) {
|
||||
store.fitViewOptions.set(fitViewOptions);
|
||||
}
|
||||
|
||||
updateStore(store, {
|
||||
nodeTypes,
|
||||
edgeTypes,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
translateExtent,
|
||||
paneClickDistance
|
||||
});
|
||||
|
||||
return () => {
|
||||
store.reset();
|
||||
};
|
||||
});
|
||||
|
||||
// Update width & height on resize
|
||||
$effect.pre(() => {
|
||||
if (clientWidth !== undefined && clientHeight !== undefined) {
|
||||
store.width.set(clientWidth);
|
||||
store.height.set(clientHeight);
|
||||
}
|
||||
});
|
||||
|
||||
// Call oninit once when flow is intialized
|
||||
const { initialized } = store;
|
||||
let onInitCalled = $state(false);
|
||||
$effect.pre(() => {
|
||||
if (!onInitCalled && $initialized) {
|
||||
oninit?.();
|
||||
onInitCalled = true;
|
||||
}
|
||||
});
|
||||
|
||||
// this updates the store for simple changes
|
||||
// where the prop names equals the store name
|
||||
$effect(() => {
|
||||
const updatableProps: UpdatableStoreProps = {
|
||||
flowId: id,
|
||||
connectionLineType,
|
||||
connectionRadius,
|
||||
selectionMode,
|
||||
snapGrid,
|
||||
defaultMarkerColor,
|
||||
nodesDraggable,
|
||||
nodesConnectable,
|
||||
elementsSelectable,
|
||||
onlyRenderVisibleElements,
|
||||
isValidConnection,
|
||||
autoPanOnConnect,
|
||||
autoPanOnNodeDrag,
|
||||
onerror,
|
||||
ondelete,
|
||||
onedgecreate,
|
||||
connectionMode,
|
||||
nodeDragThreshold,
|
||||
onconnect,
|
||||
onconnectstart,
|
||||
onconnectend,
|
||||
onbeforedelete,
|
||||
nodeOrigin
|
||||
};
|
||||
|
||||
updateStoreByKeys(store, updatableProps);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
updateStore(store, {
|
||||
nodeTypes,
|
||||
edgeTypes,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
translateExtent,
|
||||
paneClickDistance
|
||||
});
|
||||
});
|
||||
|
||||
let colorModeClass = $derived(useColorModeClass(colorMode));
|
||||
let colorModeClass = $derived(useColorModeClass(store.colorMode));
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={domNode}
|
||||
bind:clientWidth
|
||||
bind:clientHeight
|
||||
bind:this={container.domNode}
|
||||
bind:clientWidth={container.width}
|
||||
bind:clientHeight={container.height}
|
||||
style:width
|
||||
style:height
|
||||
{style}
|
||||
class={cc(['svelte-flow', className, $colorModeClass])}
|
||||
data-testid="svelte-flow__wrapper"
|
||||
{...rest}
|
||||
{...props}
|
||||
role="application"
|
||||
>
|
||||
<KeyHandler
|
||||
{store}
|
||||
{selectionKey}
|
||||
{deleteKey}
|
||||
{panActivationKey}
|
||||
@@ -238,7 +127,8 @@
|
||||
{zoomActivationKey}
|
||||
/>
|
||||
<Zoom
|
||||
initialViewport={initViewport}
|
||||
{store}
|
||||
{initialViewport}
|
||||
{onMoveStart}
|
||||
{onMove}
|
||||
{onMoveEnd}
|
||||
@@ -251,16 +141,17 @@
|
||||
{panOnDrag}
|
||||
{paneClickDistance}
|
||||
>
|
||||
<Pane {nodes} {edges} {onpaneclick} {onpanecontextmenu} {panOnDrag} {selectionOnDrag}>
|
||||
<ViewportComponent>
|
||||
<Pane {store} {onpaneclick} {onpanecontextmenu} {panOnDrag} {selectionOnDrag}>
|
||||
<ViewportComponent viewport={$viewport}>
|
||||
<EdgeRenderer
|
||||
{store}
|
||||
{onedgeclick}
|
||||
{onedgecontextmenu}
|
||||
{onedgemouseenter}
|
||||
{onedgemouseleave}
|
||||
{defaultEdgeOptions}
|
||||
/>
|
||||
<ConnectionLine
|
||||
{store}
|
||||
containerStyle={connectionLineContainerStyle}
|
||||
style={connectionLineStyle}
|
||||
{connectionLine}
|
||||
@@ -268,6 +159,7 @@
|
||||
<div class="svelte-flow__edgelabel-renderer"></div>
|
||||
<div class="svelte-flow__viewport-portal"></div>
|
||||
<NodeRenderer
|
||||
{store}
|
||||
{nodeClickDistance}
|
||||
{onnodeclick}
|
||||
{onnodecontextmenu}
|
||||
@@ -279,6 +171,7 @@
|
||||
{onnodedragstop}
|
||||
/>
|
||||
<NodeSelection
|
||||
{store}
|
||||
{onselectionclick}
|
||||
{onselectioncontextmenu}
|
||||
{onnodedrag}
|
||||
|
||||
@@ -188,6 +188,10 @@ export type SvelteFlowProps = NodeEvents &
|
||||
* @default 'full'
|
||||
*/
|
||||
selectionMode?: SelectionMode;
|
||||
/**
|
||||
* Controls if nodes should be automatically selected when being dragged
|
||||
*/
|
||||
selectNodesOnDrag: boolean;
|
||||
/** Grid all nodes will snap to
|
||||
* @example [20, 20]
|
||||
*/
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { CoordinateExtent } from '@xyflow/system';
|
||||
|
||||
import type { SvelteFlowStore } from '$lib/store/types';
|
||||
import type { EdgeTypes, NodeTypes } from '$lib/types';
|
||||
|
||||
// this is helper function for updating the store
|
||||
// for props where we need to call a specific store action
|
||||
export function updateStore(
|
||||
store: SvelteFlowStore,
|
||||
{
|
||||
nodeTypes,
|
||||
edgeTypes,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
translateExtent,
|
||||
paneClickDistance
|
||||
}: {
|
||||
nodeTypes?: NodeTypes;
|
||||
edgeTypes?: EdgeTypes;
|
||||
minZoom?: number;
|
||||
maxZoom?: number;
|
||||
translateExtent?: CoordinateExtent;
|
||||
paneClickDistance?: number;
|
||||
}
|
||||
) {
|
||||
if (nodeTypes !== undefined) {
|
||||
store.setNodeTypes(nodeTypes);
|
||||
}
|
||||
|
||||
if (edgeTypes !== undefined) {
|
||||
store.setEdgeTypes(edgeTypes);
|
||||
}
|
||||
|
||||
if (minZoom !== undefined) {
|
||||
store.setMinZoom(minZoom);
|
||||
}
|
||||
|
||||
if (maxZoom !== undefined) {
|
||||
store.setMaxZoom(maxZoom);
|
||||
}
|
||||
|
||||
if (translateExtent !== undefined) {
|
||||
store.setTranslateExtent(translateExtent);
|
||||
}
|
||||
|
||||
if (paneClickDistance !== undefined) {
|
||||
store.setPaneClickDistance(paneClickDistance);
|
||||
}
|
||||
}
|
||||
|
||||
const getKeys = <T extends object>(obj: T) => Object.keys(obj) as Array<keyof T>;
|
||||
|
||||
type UnwrapWritable<T> = T extends Writable<infer U> ? U : T;
|
||||
|
||||
// @todo there must be a better way to define the types here..
|
||||
export type UpdatableStoreProps = {
|
||||
flowId?: UnwrapWritable<SvelteFlowStore['flowId']>;
|
||||
connectionLineType?: UnwrapWritable<SvelteFlowStore['connectionLineType']>;
|
||||
connectionRadius?: UnwrapWritable<SvelteFlowStore['connectionRadius']>;
|
||||
selectionMode?: UnwrapWritable<SvelteFlowStore['selectionMode']>;
|
||||
snapGrid?: UnwrapWritable<SvelteFlowStore['snapGrid']>;
|
||||
defaultMarkerColor?: UnwrapWritable<SvelteFlowStore['defaultMarkerColor']>;
|
||||
nodesDraggable?: UnwrapWritable<SvelteFlowStore['nodesDraggable']>;
|
||||
nodesConnectable?: UnwrapWritable<SvelteFlowStore['nodesConnectable']>;
|
||||
elementsSelectable?: UnwrapWritable<SvelteFlowStore['elementsSelectable']>;
|
||||
onlyRenderVisibleElements?: UnwrapWritable<SvelteFlowStore['onlyRenderVisibleElements']>;
|
||||
isValidConnection?: UnwrapWritable<SvelteFlowStore['isValidConnection']>;
|
||||
autoPanOnConnect?: UnwrapWritable<SvelteFlowStore['autoPanOnConnect']>;
|
||||
autoPanOnNodeDrag?: UnwrapWritable<SvelteFlowStore['autoPanOnNodeDrag']>;
|
||||
connectionMode?: UnwrapWritable<SvelteFlowStore['connectionMode']>;
|
||||
onerror?: UnwrapWritable<SvelteFlowStore['onerror']>;
|
||||
ondelete?: UnwrapWritable<SvelteFlowStore['ondelete']>;
|
||||
onedgecreate?: UnwrapWritable<SvelteFlowStore['onedgecreate']>;
|
||||
nodeDragThreshold?: UnwrapWritable<SvelteFlowStore['nodeDragThreshold']>;
|
||||
onconnect?: UnwrapWritable<SvelteFlowStore['onconnect']>;
|
||||
onconnectstart?: UnwrapWritable<SvelteFlowStore['onconnectstart']>;
|
||||
onconnectend?: UnwrapWritable<SvelteFlowStore['onconnectend']>;
|
||||
onbeforedelete?: UnwrapWritable<SvelteFlowStore['onbeforedelete']>;
|
||||
nodeOrigin?: UnwrapWritable<SvelteFlowStore['nodeOrigin']>;
|
||||
};
|
||||
|
||||
export function updateStoreByKeys(store: SvelteFlowStore, keys: UpdatableStoreProps) {
|
||||
getKeys(keys).forEach((prop) => {
|
||||
const update = keys[prop];
|
||||
if (update !== undefined) {
|
||||
// @ts-expect-error @todo: how to fix this TS error?
|
||||
store[prop].set(update);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { useStore } from '$lib/store';
|
||||
import type { Viewport } from '@xyflow/system';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
const { viewport } = useStore();
|
||||
|
||||
let { children }: { children: Snippet } = $props();
|
||||
let { viewport, children }: { viewport: Viewport; children: Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="svelte-flow__viewport xyflow__viewport"
|
||||
style="transform: translate({$viewport.x}px, {$viewport.y}px) scale({$viewport.zoom})"
|
||||
style="transform: translate({viewport.x}px, {viewport.y}px) scale({viewport.zoom})"
|
||||
>
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { PanOnScrollMode, type Transform } from '@xyflow/system';
|
||||
import { PanOnScrollMode, type PanZoomInstance, type Transform } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import zoom from '$lib/actions/zoom';
|
||||
import type { ZoomProps } from './types';
|
||||
|
||||
let {
|
||||
store,
|
||||
initialViewport = { x: 0, y: 0, zoom: 1 },
|
||||
onMoveStart,
|
||||
onMove,
|
||||
@@ -22,40 +23,30 @@
|
||||
children
|
||||
}: ZoomProps = $props();
|
||||
|
||||
const {
|
||||
viewport,
|
||||
panZoom,
|
||||
selectionRect,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
dragging,
|
||||
translateExtent,
|
||||
lib,
|
||||
panActivationKeyPressed,
|
||||
zoomActivationKeyPressed,
|
||||
viewportInitialized
|
||||
} = useStore();
|
||||
|
||||
let panOnDragActive = $derived($panActivationKeyPressed || panOnDrag);
|
||||
let panOnScrollActive = $derived($panActivationKeyPressed || panOnScroll);
|
||||
let panOnDragActive = $derived(store.panActivationKeyPressed || panOnDrag);
|
||||
let panOnScrollActive = $derived(store.panActivationKeyPressed || panOnScroll);
|
||||
|
||||
const onTransformChange = (transform: Transform) =>
|
||||
viewport.set({ x: transform[0], y: transform[1], zoom: transform[2] });
|
||||
store.viewport.set({ x: transform[0], y: transform[1], zoom: transform[2] });
|
||||
|
||||
onMount(() => {
|
||||
$viewportInitialized = true;
|
||||
store.viewportInitialized = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="svelte-flow__zoom"
|
||||
use:zoom={{
|
||||
viewport,
|
||||
minZoom: $minZoom,
|
||||
maxZoom: $maxZoom,
|
||||
viewport: store.viewport,
|
||||
minZoom: store.minZoom,
|
||||
maxZoom: store.maxZoom,
|
||||
initialViewport,
|
||||
dragging,
|
||||
panZoom,
|
||||
onDraggingChange: (dragging: boolean) => {
|
||||
store.dragging = dragging;
|
||||
},
|
||||
setPanZoomInstance: (instance: PanZoomInstance) => {
|
||||
store.panZoom = instance;
|
||||
},
|
||||
onPanZoomStart: onMoveStart,
|
||||
onPanZoom: onMove,
|
||||
onPanZoomEnd: onMoveEnd,
|
||||
@@ -66,13 +57,13 @@
|
||||
panOnDrag: panOnDragActive,
|
||||
panOnScrollSpeed: 0.5,
|
||||
panOnScrollMode: panOnScrollMode || PanOnScrollMode.Free,
|
||||
zoomActivationKeyPressed: $zoomActivationKeyPressed,
|
||||
zoomActivationKeyPressed: store.zoomActivationKeyPressed,
|
||||
preventScrolling: typeof preventScrolling === 'boolean' ? preventScrolling : true,
|
||||
noPanClassName: 'nopan',
|
||||
noWheelClassName: 'nowheel',
|
||||
userSelectionActive: !!$selectionRect,
|
||||
translateExtent: $translateExtent,
|
||||
lib: $lib,
|
||||
userSelectionActive: !!store.selectionRect,
|
||||
translateExtent: store.translateExtent,
|
||||
lib: 'svelte',
|
||||
paneClickDistance,
|
||||
onTransformChange
|
||||
}}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { SvelteFlowStore } from '$lib/store/types';
|
||||
import type { OnMoveStart, OnMove, OnMoveEnd, PanOnScrollMode, Viewport } from '@xyflow/system';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
export type ZoomProps = {
|
||||
store: SvelteFlowStore;
|
||||
initialViewport?: Viewport;
|
||||
panOnScrollMode: PanOnScrollMode;
|
||||
onMove?: OnMove;
|
||||
|
||||
Reference in New Issue
Block a user