WIP: migrated store and useSvelteFlow hook to signals & solved state propagation for provider

This commit is contained in:
peterkogo
2024-12-06 13:53:48 +01:00
parent ed1ead7b5a
commit f29898f0bd
49 changed files with 1616 additions and 1137 deletions
@@ -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,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;