WIP: migrated store and useSvelteFlow hook to signals & solved state propagation for provider
This commit is contained in:
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user