diff --git a/packages/svelte/src/lib/components/KeyHandler/KeyHandler.d.ts b/packages/svelte/src/lib/components/KeyHandler/KeyHandler.d.ts new file mode 100644 index 00000000..d4fa169e --- /dev/null +++ b/packages/svelte/src/lib/components/KeyHandler/KeyHandler.d.ts @@ -0,0 +1,6 @@ +import { KeyDefinition } from '$lib/types'; + +export type KeyHandlerProps = { + selectionKey?: KeyDefinition; + deleteKey?: KeyDefinition; +}; diff --git a/packages/svelte/src/lib/components/KeyHandler/KeyHandler.svelte b/packages/svelte/src/lib/components/KeyHandler/KeyHandler.svelte new file mode 100644 index 00000000..cb59b3c9 --- /dev/null +++ b/packages/svelte/src/lib/components/KeyHandler/KeyHandler.svelte @@ -0,0 +1,43 @@ + + + selectionKeyPressedStore.set(true) }], + type: 'keydown' + }} + use:shortcut={{ + trigger: [{ key: selectionKeyString, modifier: selectionKeyModifier, callback: () => selectionKeyPressedStore.set(false) }], + type: 'keyup' + }} + use:shortcut={{ + trigger: [{ key: deleteKeyString, modifier: deleteKeyModifier, callback: () => deleteKeyPressedStore.set(true) }], + type: 'keydown' + }} + use:shortcut={{ + trigger: [{ key: deleteKeyString, modifier: deleteKeyModifier, callback: () => deleteKeyPressedStore.set(false) }], + type: 'keyup' + }} +/> diff --git a/packages/svelte/src/lib/components/KeyHandler/index.svelte b/packages/svelte/src/lib/components/KeyHandler/index.svelte deleted file mode 100644 index d55f6eb9..00000000 --- a/packages/svelte/src/lib/components/KeyHandler/index.svelte +++ /dev/null @@ -1,26 +0,0 @@ - - - diff --git a/packages/svelte/src/lib/components/KeyHandler/index.ts b/packages/svelte/src/lib/components/KeyHandler/index.ts new file mode 100644 index 00000000..41481384 --- /dev/null +++ b/packages/svelte/src/lib/components/KeyHandler/index.ts @@ -0,0 +1,2 @@ +export { default as KeyHandler } from './KeyHandler.svelte'; +export type { KeyHandlerProps } from './KeyHandler'; diff --git a/packages/svelte/src/lib/components/nodes/BaseNode.svelte b/packages/svelte/src/lib/components/nodes/BaseNode.svelte index ca21a09c..c5f01795 100644 --- a/packages/svelte/src/lib/components/nodes/BaseNode.svelte +++ b/packages/svelte/src/lib/components/nodes/BaseNode.svelte @@ -23,14 +23,16 @@ export let type: WrapNodeProps['type'] = 'default'; export let sourcePosition: WrapNodeProps['sourcePosition'] = Position.Bottom; export let targetPosition: WrapNodeProps['targetPosition'] = Position.Top; - let className: string = ''; export { className as class }; let nodeRef: HTMLDivElement; - - const { nodesStore, transformStore, updateNodePositions, nodeTypesStore } = useStore(); + + const { nodesStore, transformStore, nodeTypesStore, updateNodePositions, addSelectedNodes } = useStore(); const nodeComponent: typeof SvelteComponentTyped> = $nodeTypesStore[type] || DefaultNode; + const isSelectable = true; + const selectNodesOnDrag = false; + const isDraggable = true; setContext('rf_nodeid', id); @@ -41,6 +43,18 @@ resizeObserver?.unobserve(nodeRef); } }); + + function onSelectNodeHandler(event: MouseEvent) { + if (isSelectable && (!selectNodesOnDrag || !isDraggable)) { + // this handler gets called within the drag start event when selectNodesOnDrag=true + addSelectedNodes([id]); + } + + // if (onClick) { + // const node = store.getState().nodeInternals.get(id)!; + // onClick(event, { ...node }); + // } + };
- {#each $edgesWithDataStore as edge} + {#each $edgesWithDataStore as edge (edge.id)} {/each} diff --git a/packages/svelte/src/lib/container/NodeRenderer/index.svelte b/packages/svelte/src/lib/container/NodeRenderer/index.svelte index 970a362d..4aba3eaf 100644 --- a/packages/svelte/src/lib/container/NodeRenderer/index.svelte +++ b/packages/svelte/src/lib/container/NodeRenderer/index.svelte @@ -1,7 +1,7 @@
- {#each $nodesStore as node} - + {#each $nodesStore as node (node.id)} + {/each}
diff --git a/packages/svelte/src/lib/container/SvelteFlow.svelte b/packages/svelte/src/lib/container/SvelteFlow.svelte index 238f0dbb..c27fe786 100644 --- a/packages/svelte/src/lib/container/SvelteFlow.svelte +++ b/packages/svelte/src/lib/container/SvelteFlow.svelte @@ -10,16 +10,19 @@ import EdgeRenderer from '$lib/container/EdgeRenderer/index.svelte'; import UserSelection from '$lib/components/UserSelection/index.svelte'; import NodeSelection from '$lib/components/NodeSelection/index.svelte'; - import KeyHandler from '$lib/components/KeyHandler/index.svelte'; + import { KeyHandler } from '$lib/components/KeyHandler'; import type { SvelteFlowProps } from '$lib/types'; - interface $$Props extends SvelteFlowProps {} + type $$Props = SvelteFlowProps; export let id: $$Props['id'] = '1'; export let nodes: $$Props['nodes'] = []; export let edges: $$Props['edges'] = []; export let fitView: $$Props['fitView'] = undefined; export let nodeTypes: $$Props['nodeTypes'] = undefined; + export let selectionKey: $$Props['selectionKey'] = undefined; + export let deleteKey: $$Props['deleteKey'] = undefined; + let className: $$Props['class'] = undefined; export { className as class }; @@ -61,7 +64,7 @@ id={id} bind:this={domNode} > - + diff --git a/packages/svelte/src/lib/store/index.ts b/packages/svelte/src/lib/store/index.ts index e417c047..68ceaa35 100644 --- a/packages/svelte/src/lib/store/index.ts +++ b/packages/svelte/src/lib/store/index.ts @@ -13,7 +13,7 @@ import { type SelectionRect, type Node as RFNode } from '@reactflow/system'; -import { fitView, getD3Transition, getDimensions } from '@reactflow/utils'; +import { fitView, getConnectedEdges, getD3Transition, getDimensions } from '@reactflow/utils'; import { getHandleBounds } from '../../utils'; import { @@ -65,6 +65,7 @@ type SvelteFlowStore = { selectionRectModeStore: Writable; selectionMode: Writable; selectionKeyPressedStore: Writable; + deleteKeyPressedStore: Writable; nodeTypesStore: Writable; edgeTypesStore: Writable; setNodes: (nodes: Node[]) => void; @@ -79,6 +80,7 @@ type SvelteFlowStore = { ) => void; updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void; resetSelectedElements: () => void; + addSelectedNodes: (ids: string[]) => void; }; export function createStore({ @@ -102,6 +104,8 @@ export function createStore({ const draggingStore = writable(false); const selectionRectStore = writable(null); const selectionKeyPressedStore = writable(false); + const multiselectionKeyPressedStore = writable(false); + const deleteKeyPressedStore = writable(false); const selectionRectModeStore = writable(null); const selectionMode = writable(SelectionMode.Partial); const nodeTypesStore = writable({ @@ -310,6 +314,66 @@ export function createStore({ edgesStore.update((es) => es.map(resetSelectedItem)); } + deleteKeyPressedStore.subscribe((deleteKeyPressed) => { + if (deleteKeyPressed) { + const nodes = get(nodesStore); + const edges = get(edgesStore); + const selectedNodes = nodes.filter((node) => node.selected); + const selectedEdges = edges.filter((edge) => edge.selected); + + // @todo can we put this stuff into @reactflow/utils? + const nodeIds = selectedNodes.map((node) => node.id); + const edgeIds = selectedEdges.map((edge) => edge.id); + const nodesToRemove = nodes.reduce((res, node) => { + const parentHit = + !nodeIds.includes(node.id) && + node.parentNode && + res.find((n) => n.id === node.parentNode); + const deletable = typeof node.deletable === 'boolean' ? node.deletable : true; + if (deletable && (nodeIds.includes(node.id) || parentHit)) { + res.push(node); + } + + return res; + }, []); + const deletableEdges = edges.filter((e) => + typeof e.deletable === 'boolean' ? e.deletable : true + ); + const initialHitEdges = deletableEdges.filter((e) => edgeIds.includes(e.id)); + + if (nodesToRemove || initialHitEdges) { + const connectedEdges = getConnectedEdges(nodesToRemove as RFNode[], deletableEdges); + const edgesToRemove = [...initialHitEdges, ...connectedEdges]; + const edgeIdsToRemove = edgesToRemove.reduce((res, edge) => { + if (!res.includes(edge.id)) { + res.push(edge.id); + } + return res; + }, []); + + nodesStore.update((nds) => nds.filter((node) => !nodeIds.includes(node.id))); + edgesStore.update((eds) => eds.filter((edge) => !edgeIdsToRemove.includes(edge.id))); + } + } + }); + + function addSelectedNodes(ids: string[]) { + selectionRectStore.set(null); + + if (get(multiselectionKeyPressedStore)) { + // @todo handle multiselection key + } + + nodesStore.update((ns) => + ns.map((node) => { + return { + ...node, + selected: ids.includes(node.id) + }; + }) + ); + } + return { nodesStore, edgesStore, @@ -323,6 +387,7 @@ export function createStore({ draggingStore, selectionRectStore, selectionKeyPressedStore, + deleteKeyPressedStore, selectionRectModeStore, selectionMode, nodeTypesStore, @@ -334,7 +399,8 @@ export function createStore({ zoomIn, zoomOut, fitView: _fitView, - resetSelectedElements + resetSelectedElements, + addSelectedNodes }; } diff --git a/packages/svelte/src/lib/types/edges.ts b/packages/svelte/src/lib/types/edges.ts index 754aa458..217b0efd 100644 --- a/packages/svelte/src/lib/types/edges.ts +++ b/packages/svelte/src/lib/types/edges.ts @@ -9,6 +9,7 @@ export type Edge = { sourceHandle?: string; targetHandle?: string; selected?: boolean; + deletable?: boolean; }; export type EdgeProps = { diff --git a/packages/svelte/src/lib/types/general.ts b/packages/svelte/src/lib/types/general.ts index 3218b914..f19e96d1 100644 --- a/packages/svelte/src/lib/types/general.ts +++ b/packages/svelte/src/lib/types/general.ts @@ -1,9 +1,17 @@ import type { Node, NodeTypes } from './nodes'; import type { Edge } from './edges'; +import type { ShortcutModifierDefinition } from '@svelte-put/shortcut'; + +export type KeyModifier = ShortcutModifierDefinition; +export type KeyDefinitionObject = { key: string; modifier?: KeyModifier }; +export type KeyDefinition = string | KeyDefinitionObject; export type SvelteFlowProps = { nodes: Node[]; edges: Edge[]; + + selectionKey?: KeyDefinition; + deleteKey?: KeyDefinition; nodeTypes?: NodeTypes; fitView?: boolean; class?: string; diff --git a/packages/svelte/src/lib/types/nodes.ts b/packages/svelte/src/lib/types/nodes.ts index a359002d..cc180f12 100644 --- a/packages/svelte/src/lib/types/nodes.ts +++ b/packages/svelte/src/lib/types/nodes.ts @@ -1,6 +1,8 @@ import type { SvelteComponentTyped } from 'svelte'; import type { internalsSymbol, NodeHandleBounds, Position, XYPosition } from '@reactflow/system'; +// @todo: currently the helper function only like Node from '@reactflow/core' +// we need a base node type or helpes that accept Node like types // eslint-disable-next-line @typescript-eslint/no-explicit-any export type Node = { id: string; @@ -16,6 +18,9 @@ export type Node = { selected?: boolean; class?: string; style?: string; + deletable?: boolean; + // not supported yet + parentNode?: string; // only used internally [internalsSymbol]?: {