feat(svelte): make nodes deletable
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
import { KeyDefinition } from '$lib/types';
|
||||||
|
|
||||||
|
export type KeyHandlerProps = {
|
||||||
|
selectionKey?: KeyDefinition;
|
||||||
|
deleteKey?: KeyDefinition;
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { shortcut } from '@svelte-put/shortcut';
|
||||||
|
|
||||||
|
import { useStore } from '$lib/store';
|
||||||
|
import type { KeyHandlerProps } from './KeyHandler'
|
||||||
|
import type { KeyDefinition, KeyDefinitionObject } from '$lib/types';
|
||||||
|
|
||||||
|
const { selectionKeyPressedStore, deleteKeyPressedStore } = useStore();
|
||||||
|
|
||||||
|
type $$Props = KeyHandlerProps;
|
||||||
|
|
||||||
|
export let selectionKey: $$Props['selectionKey'] = 'Shift';
|
||||||
|
export let deleteKey: $$Props['deleteKey'] = 'Backspace';
|
||||||
|
|
||||||
|
function isKeyObject(key?: KeyDefinition): key is KeyDefinitionObject {
|
||||||
|
return typeof key === 'object';
|
||||||
|
}
|
||||||
|
|
||||||
|
$: selectionKeyString = typeof selectionKey === 'string' ? selectionKey : selectionKey!.key;
|
||||||
|
$: selectionKeyModifier = isKeyObject(selectionKey) ? selectionKey?.modifier : [];
|
||||||
|
|
||||||
|
$: deleteKeyString = typeof deleteKey === 'string' ? deleteKey : deleteKey!.key;
|
||||||
|
$: deleteKeyModifier = isKeyObject(deleteKey) ? deleteKey?.modifier : [];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window
|
||||||
|
use:shortcut={{
|
||||||
|
trigger: [{ key: selectionKeyString, modifier: selectionKeyModifier, callback: () => 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'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { shortcut } from '@svelte-put/shortcut';
|
|
||||||
|
|
||||||
import { useStore } from '$lib/store';
|
|
||||||
|
|
||||||
const { selectionKeyPressedStore } = useStore();
|
|
||||||
|
|
||||||
function onSelectionKeyDown() {
|
|
||||||
selectionKeyPressedStore.set(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSelectionKeyUp() {
|
|
||||||
selectionKeyPressedStore.set(false);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:window
|
|
||||||
use:shortcut={{
|
|
||||||
trigger: [{ key: 'Shift', callback: onSelectionKeyDown }],
|
|
||||||
type: 'keydown'
|
|
||||||
}}
|
|
||||||
use:shortcut={{
|
|
||||||
trigger: [{ key: 'Shift', callback: onSelectionKeyUp }],
|
|
||||||
type: 'keyup'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as KeyHandler } from './KeyHandler.svelte';
|
||||||
|
export type { KeyHandlerProps } from './KeyHandler';
|
||||||
@@ -23,14 +23,16 @@
|
|||||||
export let type: WrapNodeProps['type'] = 'default';
|
export let type: WrapNodeProps['type'] = 'default';
|
||||||
export let sourcePosition: WrapNodeProps['sourcePosition'] = Position.Bottom;
|
export let sourcePosition: WrapNodeProps['sourcePosition'] = Position.Bottom;
|
||||||
export let targetPosition: WrapNodeProps['targetPosition'] = Position.Top;
|
export let targetPosition: WrapNodeProps['targetPosition'] = Position.Top;
|
||||||
|
|
||||||
let className: string = '';
|
let className: string = '';
|
||||||
export { className as class };
|
export { className as class };
|
||||||
|
|
||||||
let nodeRef: HTMLDivElement;
|
let nodeRef: HTMLDivElement;
|
||||||
|
|
||||||
const { nodesStore, transformStore, updateNodePositions, nodeTypesStore } = useStore();
|
const { nodesStore, transformStore, nodeTypesStore, updateNodePositions, addSelectedNodes } = useStore();
|
||||||
const nodeComponent: typeof SvelteComponentTyped<Partial<NodeProps>> = $nodeTypesStore[type] || DefaultNode;
|
const nodeComponent: typeof SvelteComponentTyped<Partial<NodeProps>> = $nodeTypesStore[type] || DefaultNode;
|
||||||
|
const isSelectable = true;
|
||||||
|
const selectNodesOnDrag = false;
|
||||||
|
const isDraggable = true;
|
||||||
|
|
||||||
setContext('rf_nodeid', id);
|
setContext('rf_nodeid', id);
|
||||||
|
|
||||||
@@ -41,6 +43,18 @@
|
|||||||
resizeObserver?.unobserve(nodeRef);
|
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 });
|
||||||
|
// }
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -48,7 +62,9 @@
|
|||||||
class={cc(['react-flow__node', `react-flow__node-${type}`, className])}
|
class={cc(['react-flow__node', `react-flow__node-${type}`, className])}
|
||||||
class:initializing={!width && !height}
|
class:initializing={!width && !height}
|
||||||
class:dragging={dragging}
|
class:dragging={dragging}
|
||||||
|
class:selected={selected}
|
||||||
bind:this={nodeRef}
|
bind:this={nodeRef}
|
||||||
|
on:click={onSelectNodeHandler}
|
||||||
style:transform={`translate(${positionAbsolute.x}px, ${positionAbsolute.y}px)`}
|
style:transform={`translate(${positionAbsolute.x}px, ${positionAbsolute.y}px)`}
|
||||||
{style}
|
{style}
|
||||||
data-id={id}
|
data-id={id}
|
||||||
@@ -82,6 +98,11 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 0.5px #1a192b;
|
||||||
|
}
|
||||||
|
|
||||||
.dragging {
|
.dragging {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
width={$widthStore}
|
width={$widthStore}
|
||||||
height={$heightStore}
|
height={$heightStore}
|
||||||
class="react-flow__edges react-flow__container"
|
class="react-flow__edges react-flow__container"
|
||||||
>
|
>
|
||||||
{#each $edgesWithDataStore as edge (edge.id)}
|
{#each $edgesWithDataStore as edge (edge.id)}
|
||||||
<Edge {...edge} />
|
<Edge {...edge} />
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
|
|
||||||
import Node from '$lib/components/nodes/BaseNode.svelte';
|
import BaseNode from '$lib/components/nodes/BaseNode.svelte';
|
||||||
import { useStore } from '$lib/store';
|
import { useStore } from '$lib/store';
|
||||||
|
|
||||||
const { nodesStore, updateNodeDimensions } = useStore();
|
const { nodesStore, updateNodeDimensions } = useStore();
|
||||||
@@ -18,12 +18,11 @@
|
|||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
resizeObserver?.disconnect();
|
resizeObserver?.disconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="react-flow__nodes">
|
<div class="react-flow__nodes">
|
||||||
{#each $nodesStore as node}
|
{#each $nodesStore as node (node.id)}
|
||||||
<Node {...node} {resizeObserver} />
|
<BaseNode {...node} {resizeObserver} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -10,16 +10,19 @@
|
|||||||
import EdgeRenderer from '$lib/container/EdgeRenderer/index.svelte';
|
import EdgeRenderer from '$lib/container/EdgeRenderer/index.svelte';
|
||||||
import UserSelection from '$lib/components/UserSelection/index.svelte';
|
import UserSelection from '$lib/components/UserSelection/index.svelte';
|
||||||
import NodeSelection from '$lib/components/NodeSelection/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';
|
import type { SvelteFlowProps } from '$lib/types';
|
||||||
|
|
||||||
interface $$Props extends SvelteFlowProps {}
|
type $$Props = SvelteFlowProps;
|
||||||
|
|
||||||
export let id: $$Props['id'] = '1';
|
export let id: $$Props['id'] = '1';
|
||||||
export let nodes: $$Props['nodes'] = [];
|
export let nodes: $$Props['nodes'] = [];
|
||||||
export let edges: $$Props['edges'] = [];
|
export let edges: $$Props['edges'] = [];
|
||||||
export let fitView: $$Props['fitView'] = undefined;
|
export let fitView: $$Props['fitView'] = undefined;
|
||||||
export let nodeTypes: $$Props['nodeTypes'] = undefined;
|
export let nodeTypes: $$Props['nodeTypes'] = undefined;
|
||||||
|
export let selectionKey: $$Props['selectionKey'] = undefined;
|
||||||
|
export let deleteKey: $$Props['deleteKey'] = undefined;
|
||||||
|
|
||||||
let className: $$Props['class'] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
|
|
||||||
@@ -61,7 +64,7 @@
|
|||||||
id={id}
|
id={id}
|
||||||
bind:this={domNode}
|
bind:this={domNode}
|
||||||
>
|
>
|
||||||
<KeyHandler />
|
<KeyHandler {selectionKey} {deleteKey} />
|
||||||
<Zoom>
|
<Zoom>
|
||||||
<Pane>
|
<Pane>
|
||||||
<Viewport>
|
<Viewport>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
type SelectionRect,
|
type SelectionRect,
|
||||||
type Node as RFNode
|
type Node as RFNode
|
||||||
} from '@reactflow/system';
|
} from '@reactflow/system';
|
||||||
import { fitView, getD3Transition, getDimensions } from '@reactflow/utils';
|
import { fitView, getConnectedEdges, getD3Transition, getDimensions } from '@reactflow/utils';
|
||||||
|
|
||||||
import { getHandleBounds } from '../../utils';
|
import { getHandleBounds } from '../../utils';
|
||||||
import {
|
import {
|
||||||
@@ -65,6 +65,7 @@ type SvelteFlowStore = {
|
|||||||
selectionRectModeStore: Writable<string | null>;
|
selectionRectModeStore: Writable<string | null>;
|
||||||
selectionMode: Writable<SelectionMode>;
|
selectionMode: Writable<SelectionMode>;
|
||||||
selectionKeyPressedStore: Writable<boolean>;
|
selectionKeyPressedStore: Writable<boolean>;
|
||||||
|
deleteKeyPressedStore: Writable<boolean>;
|
||||||
nodeTypesStore: Writable<NodeTypes>;
|
nodeTypesStore: Writable<NodeTypes>;
|
||||||
edgeTypesStore: Writable<EdgeTypes>;
|
edgeTypesStore: Writable<EdgeTypes>;
|
||||||
setNodes: (nodes: Node[]) => void;
|
setNodes: (nodes: Node[]) => void;
|
||||||
@@ -79,6 +80,7 @@ type SvelteFlowStore = {
|
|||||||
) => void;
|
) => void;
|
||||||
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void;
|
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void;
|
||||||
resetSelectedElements: () => void;
|
resetSelectedElements: () => void;
|
||||||
|
addSelectedNodes: (ids: string[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createStore({
|
export function createStore({
|
||||||
@@ -102,6 +104,8 @@ export function createStore({
|
|||||||
const draggingStore = writable(false);
|
const draggingStore = writable(false);
|
||||||
const selectionRectStore = writable(null);
|
const selectionRectStore = writable(null);
|
||||||
const selectionKeyPressedStore = writable(false);
|
const selectionKeyPressedStore = writable(false);
|
||||||
|
const multiselectionKeyPressedStore = writable(false);
|
||||||
|
const deleteKeyPressedStore = writable(false);
|
||||||
const selectionRectModeStore = writable(null);
|
const selectionRectModeStore = writable(null);
|
||||||
const selectionMode = writable(SelectionMode.Partial);
|
const selectionMode = writable(SelectionMode.Partial);
|
||||||
const nodeTypesStore = writable({
|
const nodeTypesStore = writable({
|
||||||
@@ -310,6 +314,66 @@ export function createStore({
|
|||||||
edgesStore.update((es) => es.map(resetSelectedItem));
|
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<Node[]>((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<string[]>((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 {
|
return {
|
||||||
nodesStore,
|
nodesStore,
|
||||||
edgesStore,
|
edgesStore,
|
||||||
@@ -323,6 +387,7 @@ export function createStore({
|
|||||||
draggingStore,
|
draggingStore,
|
||||||
selectionRectStore,
|
selectionRectStore,
|
||||||
selectionKeyPressedStore,
|
selectionKeyPressedStore,
|
||||||
|
deleteKeyPressedStore,
|
||||||
selectionRectModeStore,
|
selectionRectModeStore,
|
||||||
selectionMode,
|
selectionMode,
|
||||||
nodeTypesStore,
|
nodeTypesStore,
|
||||||
@@ -334,7 +399,8 @@ export function createStore({
|
|||||||
zoomIn,
|
zoomIn,
|
||||||
zoomOut,
|
zoomOut,
|
||||||
fitView: _fitView,
|
fitView: _fitView,
|
||||||
resetSelectedElements
|
resetSelectedElements,
|
||||||
|
addSelectedNodes
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export type Edge = {
|
|||||||
sourceHandle?: string;
|
sourceHandle?: string;
|
||||||
targetHandle?: string;
|
targetHandle?: string;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
|
deletable?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EdgeProps = {
|
export type EdgeProps = {
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
import type { Node, NodeTypes } from './nodes';
|
import type { Node, NodeTypes } from './nodes';
|
||||||
import type { Edge } from './edges';
|
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 = {
|
export type SvelteFlowProps = {
|
||||||
nodes: Node[];
|
nodes: Node[];
|
||||||
edges: Edge[];
|
edges: Edge[];
|
||||||
|
|
||||||
|
selectionKey?: KeyDefinition;
|
||||||
|
deleteKey?: KeyDefinition;
|
||||||
nodeTypes?: NodeTypes;
|
nodeTypes?: NodeTypes;
|
||||||
fitView?: boolean;
|
fitView?: boolean;
|
||||||
class?: string;
|
class?: string;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { SvelteComponentTyped } from 'svelte';
|
import type { SvelteComponentTyped } from 'svelte';
|
||||||
import type { internalsSymbol, NodeHandleBounds, Position, XYPosition } from '@reactflow/system';
|
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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export type Node<NodeData = any> = {
|
export type Node<NodeData = any> = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -16,6 +18,9 @@ export type Node<NodeData = any> = {
|
|||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
class?: string;
|
class?: string;
|
||||||
style?: string;
|
style?: string;
|
||||||
|
deletable?: boolean;
|
||||||
|
// not supported yet
|
||||||
|
parentNode?: string;
|
||||||
|
|
||||||
// only used internally
|
// only used internally
|
||||||
[internalsSymbol]?: {
|
[internalsSymbol]?: {
|
||||||
|
|||||||
Reference in New Issue
Block a user