implement onSelectionChanged

This commit is contained in:
peterkogo
2025-04-30 20:32:06 +02:00
parent adb13e261d
commit 47f9118296
13 changed files with 108 additions and 11 deletions

View File

@@ -0,0 +1,2 @@
export const ssr = false;
export const prerender = false;

View File

@@ -1,11 +1,21 @@
<script lang="ts">
import { Handle, Position, type BuiltInNode, type NodeProps } from '@xyflow/svelte';
import {
Handle,
Position,
useSelectionChanged,
type BuiltInNode,
type NodeProps
} from '@xyflow/svelte';
let {
data = { label: 'Node' },
positionAbsoluteX = 0,
positionAbsoluteY = 0
}: NodeProps<BuiltInNode> = $props();
useSelectionChanged(({ nodes, edges }) => {
console.log('on selection changed via hook', { nodes, edges });
});
</script>
<div class="custom">

View File

@@ -216,6 +216,10 @@
connectionMode={ConnectionMode.Strict}
attributionPosition={'top-center'}
deleteKey={['Backspace', 'd']}
onselectionchanged={({ nodes, edges }) => {
console.log('on selection changed via prop', { nodes, edges });
}}
selectNodesOnDrag
>
<Controls orientation="horizontal" {fitViewOptions}>
{#snippet before()}
@@ -240,7 +244,6 @@
}}>left-right</button
>
</Panel>
<InitTracker />
</SvelteFlow>

View File

@@ -4,7 +4,8 @@ import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()],
build: {
sourcemap: true
sourcemap: true,
minify: false
},
server: {
fs: {

View File

@@ -2,9 +2,9 @@
import { portal } from '$lib/actions/portal';
import type { ViewportPortalProps } from './types';
let { moveTo = 'front', children, ...rest }: ViewportPortalProps = $props();
let { target = 'front', children, ...rest }: ViewportPortalProps = $props();
</script>
<div use:portal={`viewport-${moveTo}`} {...rest}>
<div use:portal={`viewport-${target}`} {...rest}>
{@render children?.()}
</div>

View File

@@ -2,6 +2,6 @@ import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
export type ViewportPortalProps = {
moveTo: 'front' | 'back';
target: 'front' | 'back';
children?: Snippet;
} & HTMLAttributes<HTMLDivElement>;

View File

@@ -1,6 +1,6 @@
<script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge">
import type { Edge, Node } from '$lib/types';
import { getContext, setContext, onDestroy } from 'svelte';
import { getContext, setContext, onDestroy, untrack } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import { ConnectionLineType, PanOnScrollMode } from '@xyflow/system';
@@ -110,6 +110,15 @@
}
} satisfies StoreContext<NodeType, EdgeType>);
// handle selection change
$effect(() => {
const params = { nodes: store.selectedNodes, edges: store.selectedEdges };
untrack(() => props.onselectionchanged)?.(params);
for (const handler of store.selectionChangedHandlers.values()) {
handler(params);
}
});
onDestroy(() => {
store.reset();
});

View File

@@ -52,6 +52,7 @@
onclickconnectstart,
onclickconnectend,
oninit,
onselectionchanged,
clickConnect,
fitView,
fitViewOptions,

View File

@@ -36,7 +36,8 @@ import type {
OnBeforeConnect,
OnBeforeDelete,
IsValidConnection,
OnBeforeReconnect
OnBeforeReconnect,
OnSelectionChanged
} from '$lib/types';
import type { Component } from 'svelte';
@@ -391,6 +392,8 @@ export type SvelteFlowProps<
* If you have custom connection logic its preferred to use this callback over the
* `isValidConnection` prop on the handle component for performance reasons.
*/
/** Toggles ability to make connections via clicking the handles */
clickConnect?: boolean;
isValidConnection?: IsValidConnection;
/** This event handler is called when the user begins to pan or zoom the viewport */
onmovestart?: OnMoveStart;
@@ -434,8 +437,8 @@ export type SvelteFlowProps<
onclickconnectstart?: OnConnectStart;
/** A connection is finished by clicking on a handle */
onclickconnectend?: OnConnectEnd;
/** Toggles ability to make connections via clicking the handles */
clickConnect?: boolean;
/** This handler gets called when the flow is finished initializing */
oninit?: () => void;
/** This event handler gets called when the selected nodes & edges change */
onselectionchanged?: OnSelectionChanged<NodeType, EdgeType>;
};

View File

@@ -0,0 +1,15 @@
import type { OnSelectionChanged } from '$lib/types';
import { useStore } from '$lib/hooks/useStore';
export function useSelectionChanged(onselectionchanged: OnSelectionChanged) {
const store = $derived(useStore());
const symbol = Symbol();
$effect(() => {
store.selectionChangedHandlers.set(symbol, onselectionchanged);
return () => {
store.selectionChangedHandlers.delete(symbol);
};
});
}

View File

@@ -39,6 +39,7 @@ export * from '$lib/hooks/useNodeConnections.svelte';
export * from '$lib/hooks/useNodesData.svelte';
export * from '$lib/hooks/useInternalNode.svelte';
export * from '$lib/hooks/useInitialized.svelte';
export * from '$lib/hooks/useSelectionChanged.svelte';
//actions
export * from '$lib/actions/portal';

View File

@@ -59,7 +59,8 @@ import type {
Node,
EdgeLayouted,
InternalNode,
OnBeforeReconnect
OnBeforeReconnect,
OnSelectionChanged
} from '$lib/types';
import type { StoreSignals } from './types';
@@ -138,6 +139,52 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
signals.edges = edges;
}
_prevSelectedNodes: NodeType[] = [];
_prevSelectedNodeIds = new Set<string>();
selectedNodes = $derived.by(() => {
const selectedNodesCount = this._prevSelectedNodeIds.size;
const selectedNodeIds = new Set<string>();
const selectedNodes = this.nodes.filter((node) => {
if (node.selected) {
selectedNodeIds.add(node.id);
this._prevSelectedNodeIds.delete(node.id);
}
return node.selected;
});
// Either the number of selected nodes has changed or two nodes changed their selection state
// at the same time. However then the previously selected node will be inside _prevSelectedNodeIds
if (selectedNodesCount !== selectedNodeIds.size || this._prevSelectedNodeIds.size > 0) {
this._prevSelectedNodes = selectedNodes;
}
this._prevSelectedNodeIds = selectedNodeIds;
return this._prevSelectedNodes;
});
_prevSelectedEdges: EdgeType[] = [];
_prevSelectedEdgeIds = new Set<string>();
selectedEdges = $derived.by(() => {
const selectedEdgesCount = this._prevSelectedEdgeIds.size;
const selectedEdgeIds = new Set<string>();
const selectedEdges = this.edges.filter((edge) => {
if (edge.selected) {
selectedEdgeIds.add(edge.id);
this._prevSelectedEdgeIds.delete(edge.id);
}
return edge.selected;
});
// Either the number of selected edges has changed or two edges changed their selection state
// at the same time. However then the previously selected edge will be inside _prevSelectedEdgeIds
if (selectedEdgesCount !== selectedEdgeIds.size || this._prevSelectedEdgeIds.size > 0) {
this._prevSelectedEdges = selectedEdges;
}
this._prevSelectedEdgeIds = selectedEdgeIds;
return this._prevSelectedEdges;
});
selectionChangedHandlers = new Map<symbol, OnSelectionChanged<NodeType, EdgeType>>();
nodeLookup: NodeLookup<InternalNode<NodeType>> = new Map();
parentLookup: ParentLookup<InternalNode<NodeType>> = new Map();
connectionLookup: ConnectionLookup = new Map();

View File

@@ -43,3 +43,8 @@ export type OnBeforeDelete<
export type IsValidConnection<EdgeType extends Edge = Edge> = (
edge: EdgeType | Connection
) => boolean;
export type OnSelectionChanged<
NodeType extends Node = Node,
EdgeType extends Edge = Edge
> = (params: { nodes: NodeType[]; edges: EdgeType[] }) => void;