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
@@ -9,35 +9,37 @@
getStraightPath
} from '@xyflow/system';
import { useStore } from '$lib/store';
import type { SvelteFlowStore } from '$lib/store/types';
let {
store,
containerStyle = '',
style = '',
connectionLine
}: {
store: SvelteFlowStore;
containerStyle: string;
style: string;
connectionLine?: Snippet;
} = $props();
const { width, height, connection, connectionLineType } = useStore();
// $inspect(store.connection);
let path = $derived.by(() => {
if (!$connection.inProgress) {
if (!store.connection.inProgress) {
return '';
}
const pathParams = {
sourceX: $connection.from.x,
sourceY: $connection.from.y,
sourcePosition: $connection.fromPosition,
targetX: $connection.to.x,
targetY: $connection.to.y,
targetPosition: $connection.toPosition
sourceX: store.connection.from.x,
sourceY: store.connection.from.y,
sourcePosition: store.connection.fromPosition,
targetX: store.connection.to.x,
targetY: store.connection.to.y,
targetPosition: store.connection.toPosition
};
switch ($connectionLineType) {
switch (store.connectionLineType) {
case ConnectionLineType.Bezier: {
const [path] = getBezierPath(pathParams);
return path;
@@ -50,7 +52,7 @@
case ConnectionLineType.SmoothStep: {
const [path] = getSmoothStepPath({
...pathParams,
borderRadius: $connectionLineType === ConnectionLineType.Step ? 0 : undefined
borderRadius: store.connectionLineType === ConnectionLineType.Step ? 0 : undefined
});
return path;
}
@@ -58,9 +60,14 @@
});
</script>
{#if $connection.inProgress}
<svg width={$width} height={$height} class="svelte-flow__connectionline" style={containerStyle}>
<g class={cc(['svelte-flow__connection', getConnectionStatus($connection.isValid)])}>
{#if store.connection.inProgress}
<svg
width={store.width}
height={store.height}
class="svelte-flow__connectionline"
style={containerStyle}
>
<g class={cc(['svelte-flow__connection', getConnectionStatus(store.connection.isValid)])}>
{#if connectionLine}
{@render connectionLine()}
{:else}
@@ -3,11 +3,11 @@
import portal from '$lib/actions/portal';
import { useStore } from '$lib/store';
const { domNode } = useStore();
const store = useStore();
let { children }: { children?: Snippet } = $props();
</script>
<div use:portal={{ target: '.svelte-flow__edgelabel-renderer', domNode: $domNode }}>
<div use:portal={{ target: '.svelte-flow__edgelabel-renderer', domNode: store.domNode }}>
{@render children?.()}
</div>
@@ -9,10 +9,12 @@
import { useHandleEdgeSelect } from '$lib/hooks/useHandleEdgeSelect';
import type { EdgeLayouted, Edge, EdgeEvents } from '$lib/types';
import type { SvelteFlowStore } from '$lib/store/types';
const {
id,
type = 'default',
store,
source,
target,
data = {},
@@ -42,24 +44,24 @@
onedgecontextmenu,
onedgemouseenter,
onedgemouseleave
}: EdgeLayouted & EdgeEvents = $props();
}: { store: SvelteFlowStore } & EdgeLayouted & EdgeEvents = $props();
setContext('svelteflow__edge_id', id);
const { edgeLookup, edgeTypes, flowId, elementsSelectable } = useStore();
let edgeType = $derived(type ?? 'default');
let EdgeComponent = $derived($edgeTypes[edgeType] ?? BezierEdgeInternal);
let EdgeComponent = $derived(store.edgeTypes[edgeType] ?? BezierEdgeInternal);
let markerStartUrl = $derived(
markerStart ? `url('#${getMarkerId(markerStart, $flowId)}')` : undefined
markerStart ? `url('#${getMarkerId(markerStart, store.flowId)}')` : undefined
);
let markerEndUrl = $derived(markerEnd ? `url('#${getMarkerId(markerEnd, $flowId)}')` : undefined);
let isSelectable = $derived(selectable ?? $elementsSelectable);
let markerEndUrl = $derived(
markerEnd ? `url('#${getMarkerId(markerEnd, store.flowId)}')` : undefined
);
let isSelectable = $derived(selectable ?? store.elementsSelectable);
const handleEdgeSelect = useHandleEdgeSelect();
function onclick(event: MouseEvent | TouchEvent) {
const edge = $edgeLookup.get(id);
const edge = store.edgeLookup.get(id);
if (edge) {
handleEdgeSelect(id);
@@ -71,7 +73,7 @@
event: MouseEvent,
callback: ({ edge, event }: { edge: Edge; event: MouseEvent }) => void
) {
const edge = $edgeLookup.get(id);
const edge = store.edgeLookup.get(id);
if (edge) {
callback({ event, edge });
@@ -39,28 +39,8 @@
isConnectableProp !== undefined ? isConnectableProp : isConnectableContext.value
);
const {
connectionMode,
domNode,
nodeLookup,
connectionRadius,
viewport,
isValidConnection: isValidConnectionStore,
lib,
addEdge,
onedgecreate,
panBy,
cancelConnection,
updateConnection,
autoPanOnConnect,
edges,
connectionLookup,
onconnect: onConnectAction,
onconnectstart: onConnectStartAction,
onconnectend: onConnectEndAction,
flowId,
connection
} = useStore();
let store = useStore();
let { viewport, edges } = store;
function onPointerDown(event: MouseEvent | TouchEvent) {
const isMouseTriggered = isMouseEvent(event);
@@ -70,39 +50,39 @@
handleId,
nodeId,
isTarget,
connectionRadius: $connectionRadius,
domNode: $domNode,
nodeLookup: $nodeLookup,
connectionMode: $connectionMode,
lib: $lib,
autoPanOnConnect: $autoPanOnConnect,
flowId: $flowId,
isValidConnection: isValidConnectionProp ?? $isValidConnectionStore,
updateConnection,
cancelConnection,
panBy,
connectionRadius: store.connectionRadius,
domNode: store.domNode,
nodeLookup: store.nodeLookup,
connectionMode: store.connectionMode,
lib: 'svelte',
autoPanOnConnect: store.autoPanOnConnect,
flowId: store.flowId,
isValidConnection: isValidConnectionProp ?? store.isValidConnection,
updateConnection: store.updateConnection,
cancelConnection: store.cancelConnection,
panBy: store.panBy,
onConnect: (connection) => {
const edge = $onedgecreate ? $onedgecreate(connection) : connection;
const edge = store.onedgecreate ? store.onedgecreate(connection) : connection;
if (!edge) {
return;
}
addEdge(edge);
$onConnectAction?.(connection);
store.addEdge(edge);
store.onconnect?.(connection);
},
onConnectStart: (event, startParams) => {
$onConnectStartAction?.(event, {
store.onconnectstart?.(event, {
nodeId: startParams.nodeId,
handleId: startParams.handleId,
handleType: startParams.handleType
});
},
onConnectEnd: (event, connectionState) => {
$onConnectEndAction?.(event, connectionState);
store.onconnectend?.(event, connectionState);
},
getTransform: () => [$viewport.x, $viewport.y, $viewport.zoom],
getFromHandle: () => $connection.fromHandle
getFromHandle: () => store.connection.fromHandle
});
}
}
@@ -113,7 +93,7 @@
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
$edges;
if (onconnect || ondisconnect) {
let connections = $connectionLookup.get(`${nodeId}-${type}-${handleId}`);
let connections = store.connectionLookup.get(`${nodeId}-${type}-${handleId}`);
if (prevConnections && !areConnectionMapsEqual(connections, prevConnections)) {
const _connections = connections ?? new Map();
@@ -128,7 +108,7 @@
let [connectionInProcess, connectingFrom, connectingTo, isPossibleEndHandle, valid] = $derived.by(
() => {
const { fromHandle, toHandle, isValid } = $connection;
const { fromHandle, toHandle, isValid } = store.connection;
const connectionInProcess = !!fromHandle;
@@ -139,7 +119,7 @@
toHandle?.nodeId === nodeId && toHandle?.type === type && toHandle?.id === handleId;
const isPossibleEndHandle =
$connectionMode === ConnectionMode.Strict
store.connectionMode === ConnectionMode.Strict
? fromHandle?.type !== type
: nodeId !== fromHandle?.nodeId || handleId !== fromHandle?.id;
@@ -158,7 +138,7 @@ The Handle component is the part of a node that can be used to connect nodes.
data-handleid={handleId}
data-nodeid={nodeId}
data-handlepos={position}
data-id="{$flowId}-{nodeId}-{handleId}-{type}"
data-id="{store.flowId}-{nodeId}-{handleId}-{type}"
class={cc([
'svelte-flow__handle',
`svelte-flow__handle-${position}`,
@@ -12,6 +12,7 @@
import type { KeyDefinition, KeyDefinitionObject } from '$lib/types';
let {
store,
selectionKey = 'Shift',
multiSelectionKey = isMacOs() ? 'Meta' : 'Control',
deleteKey = 'Backspace',
@@ -19,18 +20,7 @@
zoomActivationKey = isMacOs() ? 'Meta' : 'Control'
}: KeyHandlerProps = $props();
const {
selectionKeyPressed,
multiselectionKeyPressed,
deleteKeyPressed,
panActivationKeyPressed,
zoomActivationKeyPressed,
selectionRect,
onbeforedelete,
ondelete,
nodes: _nodes,
edges: _edges
} = useStore();
const { nodes: _nodes, edges: _edges } = store;
function isKeyObject(key?: KeyDefinition | null): key is KeyDefinitionObject {
return key !== null && typeof key === 'object';
@@ -67,12 +57,12 @@
}
function resetKeysAndSelection() {
selectionRect.set(null);
selectionKeyPressed.set(false);
multiselectionKeyPressed.set(false);
deleteKeyPressed.set(false);
panActivationKeyPressed.set(false);
zoomActivationKeyPressed.set(false);
store.selectionRect = null;
store.selectionKeyPressed = false;
store.multiselectionKeyPressed = false;
store.deleteKeyPressed = false;
store.panActivationKeyPressed = false;
store.zoomActivationKeyPressed = false;
}
async function handleDelete() {
@@ -86,14 +76,14 @@
edgesToRemove: selectedEdges,
nodes,
edges,
onBeforeDelete: get(onbeforedelete)
onBeforeDelete: store.onbeforedelete
});
if (matchingNodes.length || matchingEdges.length) {
_nodes.update((nds) => nds.filter((node) => !matchingNodes.some((mN) => mN.id === node.id)));
_edges.update((eds) => eds.filter((edge) => !matchingEdges.some((mE) => mE.id === edge.id)));
get(ondelete)?.({
store.ondelete?.({
nodes: matchingNodes,
edges: matchingEdges
});
@@ -105,19 +95,19 @@
on:blur={resetKeysAndSelection}
on:contextmenu={resetKeysAndSelection}
use:shortcut={{
trigger: getShortcutTrigger(selectionKey, () => selectionKeyPressed.set(true)),
trigger: getShortcutTrigger(selectionKey, () => (store.selectionKeyPressed = true)),
type: 'keydown'
}}
use:shortcut={{
trigger: getShortcutTrigger(selectionKey, () => selectionKeyPressed.set(false)),
trigger: getShortcutTrigger(selectionKey, () => (store.selectionKeyPressed = false)),
type: 'keyup'
}}
use:shortcut={{
trigger: getShortcutTrigger(multiSelectionKey, () => multiselectionKeyPressed.set(true)),
trigger: getShortcutTrigger(multiSelectionKey, () => (store.multiselectionKeyPressed = true)),
type: 'keydown'
}}
use:shortcut={{
trigger: getShortcutTrigger(multiSelectionKey, () => multiselectionKeyPressed.set(false)),
trigger: getShortcutTrigger(multiSelectionKey, () => (store.multiselectionKeyPressed = false)),
type: 'keyup'
}}
use:shortcut={{
@@ -127,30 +117,30 @@
detail.originalEvent.metaKey ||
detail.originalEvent.shiftKey;
if (!isModifierKey && !isInputDOMNode(detail.originalEvent)) {
deleteKeyPressed.set(true);
store.deleteKeyPressed = true;
handleDelete();
}
}),
type: 'keydown'
}}
use:shortcut={{
trigger: getShortcutTrigger(deleteKey, () => deleteKeyPressed.set(false)),
trigger: getShortcutTrigger(deleteKey, () => (store.deleteKeyPressed = false)),
type: 'keyup'
}}
use:shortcut={{
trigger: getShortcutTrigger(panActivationKey, () => panActivationKeyPressed.set(true)),
trigger: getShortcutTrigger(panActivationKey, () => (store.panActivationKeyPressed = true)),
type: 'keydown'
}}
use:shortcut={{
trigger: getShortcutTrigger(panActivationKey, () => panActivationKeyPressed.set(false)),
trigger: getShortcutTrigger(panActivationKey, () => (store.panActivationKeyPressed = false)),
type: 'keyup'
}}
use:shortcut={{
trigger: getShortcutTrigger(zoomActivationKey, () => zoomActivationKeyPressed.set(true)),
trigger: getShortcutTrigger(zoomActivationKey, () => (store.zoomActivationKeyPressed = true)),
type: 'keydown'
}}
use:shortcut={{
trigger: getShortcutTrigger(zoomActivationKey, () => zoomActivationKeyPressed.set(false)),
trigger: getShortcutTrigger(zoomActivationKey, () => (store.zoomActivationKeyPressed = false)),
type: 'keyup'
}}
/>
@@ -1,6 +1,8 @@
import type { SvelteFlowStore } from '$lib/store/types';
import type { KeyDefinition } from '$lib/types';
export type KeyHandlerProps = {
store: SvelteFlowStore;
selectionKey?: KeyDefinition | KeyDefinition[] | null;
multiSelectionKey?: KeyDefinition | KeyDefinition[] | null;
deleteKey?: KeyDefinition | KeyDefinition[] | null;
@@ -1,13 +1,13 @@
<script lang="ts">
import { getInternalNodesBounds, isNumeric, type Rect } from '@xyflow/system';
import { useStore } from '$lib/store';
import { Selection } from '$lib/components/Selection';
import drag from '$lib/actions/drag';
import type { NodeSelectionProps } from './types';
let {
store,
onnodedrag,
onnodedragstart,
onnodedragstop,
@@ -15,14 +15,13 @@
onselectioncontextmenu
}: NodeSelectionProps = $props();
const store = useStore();
const { selectionRectMode, nodes, nodeLookup } = store;
const { nodes } = store;
let bounds: Rect | null = $derived.by(() => {
if ($selectionRectMode === 'nodes') {
if (store.selectionRectMode === 'nodes') {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
$nodes;
return getInternalNodesBounds($nodeLookup, { filter: (node) => !!node.selected });
return getInternalNodesBounds(store.nodeLookup, { filter: (node) => !!node.selected });
}
return null;
});
@@ -38,7 +37,7 @@
}
</script>
{#if $selectionRectMode === 'nodes' && bounds && isNumeric(bounds.x) && isNumeric(bounds.y)}
{#if store.selectionRectMode === 'nodes' && bounds && isNumeric(bounds.x) && isNumeric(bounds.y)}
<div
class="selection-wrapper nopan"
style="width: {bounds.width}px; height: {bounds.height}px; transform: translate({bounds.x}px, {bounds.y}px)"
@@ -1,4 +1,5 @@
import type { SvelteFlowStore } from '$lib/store/types';
import type { NodeEvents, NodeSelectionEvents } from '$lib/types';
export type NodeSelectionProps = NodeSelectionEvents &
export type NodeSelectionProps = { store: SvelteFlowStore } & NodeSelectionEvents &
Pick<NodeEvents, 'onnodedrag' | 'onnodedragstart' | 'onnodedragstop'>;
@@ -1,17 +1,16 @@
<script lang="ts">
import { setContext, onDestroy } from 'svelte';
import { get } from 'svelte/store';
import cc from 'classcat';
import { errorMessages, Position } from '@xyflow/system';
import drag from '$lib/actions/drag';
import { useStore } from '$lib/store';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
import type { ConnectableContext, NodeWrapperProps } from './types';
import { getNodeInlineStyleDimensions } from './utils';
import type { NodeEvents } from '$lib/types';
let {
store,
node,
id,
data = {},
@@ -52,15 +51,6 @@
onnodecontextmenu
}: NodeWrapperProps & NodeEvents = $props();
const store = useStore();
const {
nodeTypes,
nodeDragThreshold,
selectNodesOnDrag,
handleNodeSelection,
updateNodeInternals
} = store;
let nodeRef: HTMLDivElement | null = $state(null);
let prevNodeRef: HTMLDivElement | null = null;
@@ -68,7 +58,7 @@
let prevSourcePosition: Position | undefined;
let prevTargetPosition: Position | undefined;
let NodeComponent = $derived($nodeTypes[type] ?? DefaultNode);
let NodeComponent = $derived(store.nodeTypes[type] ?? DefaultNode);
let connectableContext: ConnectableContext = {
get value() {
@@ -80,7 +70,7 @@
if (process.env.NODE_ENV === 'development') {
$effect(() => {
const valid = !!$nodeTypes[type];
const valid = !!store.nodeTypes[type];
if (!valid) {
console.warn('003', errorMessages['error003'](type!));
}
@@ -109,7 +99,7 @@
if (doUpdate && nodeRef !== null) {
requestAnimationFrame(() => {
if (nodeRef !== null) {
updateNodeInternals(
store.updateNodeInternals(
new Map([
[
id,
@@ -148,10 +138,10 @@
});
function onSelectNodeHandler(event: MouseEvent | TouchEvent) {
if (selectable && (!get(selectNodesOnDrag) || !draggable || get(nodeDragThreshold) > 0)) {
if (selectable && (!store.selectNodesOnDrag || !draggable || store.nodeDragThreshold > 0)) {
// this handler gets called by XYDrag on drag start when selectNodesOnDrag=true
// here we only need to call it when selectNodesOnDrag=false
handleNodeSelection(id);
store.handleNodeSelection(id);
}
onnodeclick?.({ node: node.internals.userNode, event });
@@ -169,7 +159,7 @@
handleSelector: dragHandle,
noDragClass: 'nodrag',
nodeClickDistance,
onNodeMouseDown: handleNodeSelection,
onNodeMouseDown: store.handleNodeSelection,
onDrag: (event, _, targetNode, nodes) => {
onnodedrag?.({ event, targetNode, nodes });
},
@@ -1,3 +1,4 @@
import type { SvelteFlowStore } from '$lib/store/types';
import type { InternalNode, Node } from '$lib/types';
export type ConnectableContext = {
@@ -39,4 +40,5 @@ export type NodeWrapperProps = Pick<
node: InternalNode;
initialized: boolean;
nodeClickDistance?: number;
store: SvelteFlowStore;
};
@@ -3,29 +3,21 @@
import { createStore, key } from '$lib/store';
import type { SvelteFlowProviderProps } from './types';
import type { ProviderContext, SvelteFlowStore } from '$lib/store/types';
let {
initialNodes,
initialEdges,
initialWidth,
initialHeight,
nodeOrigin,
fitView,
children
}: SvelteFlowProviderProps = $props();
let { children }: SvelteFlowProviderProps = $props();
const store = createStore({
nodes: initialNodes,
edges: initialEdges,
width: initialWidth,
height: initialHeight,
nodeOrigin,
fitView
});
let store = $state.raw(createStore({ props: {} }));
setContext(key, {
getStore: () => store
});
provider: true,
getStore() {
return store;
},
setStore: (newStore: SvelteFlowStore) => {
store = newStore;
}
} satisfies ProviderContext);
onDestroy(() => {
store.reset();
@@ -2,13 +2,13 @@
import { useStore } from '$lib/store';
import { Selection } from '$lib/components/Selection';
const { selectionRect, selectionRectMode } = useStore();
const store = useStore();
</script>
<Selection
isVisible={!!($selectionRect && $selectionRectMode === 'user')}
width={$selectionRect?.width}
height={$selectionRect?.height}
x={$selectionRect?.x}
y={$selectionRect?.y}
isVisible={!!(store.selectionRect && store.selectionRectMode === 'user')}
width={store.selectionRect?.width}
height={store.selectionRect?.height}
x={store.selectionRect?.x}
y={store.selectionRect?.y}
/>
@@ -6,9 +6,9 @@
let { children }: { children?: Snippet } = $props();
const { domNode } = useStore();
const store = useStore();
</script>
<div use:portal={{ target: '.svelte-flow__viewport-portal', domNode: $domNode }}>
<div use:portal={{ target: '.svelte-flow__viewport-portal', domNode: store.domNode }}>
{@render children?.()}
</div>