diff --git a/packages/svelte/package.json b/packages/svelte/package.json index a6ad85db..09e72ce2 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -16,8 +16,9 @@ ".": "./index.js" }, "dependencies": { - "@reactflow/system": "workspace:11.5.5", - "@reactflow/utils": "workspace:^11.5.5", + "@reactflow/system": "workspace:*", + "@reactflow/utils": "workspace:*", + "@svelte-put/shortcut": "^2.0.0", "classcat": "^5.0.4", "d3-drag": "^3.0.0", "d3-selection": "^3.0.0", diff --git a/packages/svelte/src/lib/hooks/drag/index.ts b/packages/svelte/src/lib/actions/drag/index.ts similarity index 100% rename from packages/svelte/src/lib/hooks/drag/index.ts rename to packages/svelte/src/lib/actions/drag/index.ts diff --git a/packages/svelte/src/lib/hooks/drag/utils.ts b/packages/svelte/src/lib/actions/drag/utils.ts similarity index 100% rename from packages/svelte/src/lib/hooks/drag/utils.ts rename to packages/svelte/src/lib/actions/drag/utils.ts diff --git a/packages/svelte/src/lib/actions/zoom/index.ts b/packages/svelte/src/lib/actions/zoom/index.ts new file mode 100644 index 00000000..29d0a106 --- /dev/null +++ b/packages/svelte/src/lib/actions/zoom/index.ts @@ -0,0 +1,119 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { Writable } from 'svelte/store'; +import { select } from 'd3-selection'; +import { zoom as d3Zoom, zoomIdentity } from 'd3-zoom'; +import type { D3ZoomEvent } from 'd3-zoom'; +import type { D3SelectionInstance, D3ZoomInstance, Transform } from '@reactflow/system'; + +const isWrappedWithClass = (event: any, className: string | undefined) => + event.target.closest(`.${className}`); + +function filter(event: any, params: ZoomParams): boolean { + const zoomScroll = true; + const pinchZoom = true; + + if ( + event.button === 1 && + event.type === 'mousedown' && + (isWrappedWithClass(event, 'react-flow__node') || isWrappedWithClass(event, 'react-flow__edge')) + ) { + return true; + } + + // if all interactions are disabled, we prevent all zoom events + // if (!panOnDrag && !zoomScroll && !panOnScroll && !zoomOnDoubleClick && !zoomOnPinch) { + // return false; + // } + // // during a selection we prevent all other interactions + if (params.selecting) { + return false; + } + + // // if zoom on double click is disabled, we prevent the double click event + // if (!zoomOnDoubleClick && event.type === 'dblclick') { + // return false; + // } + + // // if the target element is inside an element with the nowheel class, we prevent zooming + // if (isWrappedWithClass(event, noWheelClassName) && event.type === 'wheel') { + // return false; + // } + + // // if the target element is inside an element with the nopan class, we prevent panning + // if (isWrappedWithClass(event, noPanClassName) && event.type !== 'wheel') { + // return false; + // } + + // if (!zoomOnPinch && event.ctrlKey && event.type === 'wheel') { + // return false; + // } + + // // when there is no scroll handling enabled, we prevent all wheel events + // if (!zoomScroll && !panOnScroll && !pinchZoom && event.type === 'wheel') { + // return false; + // } + + // // if the pane is not movable, we prevent dragging it with mousestart or touchstart + // if (!panOnDrag && (event.type === 'mousedown' || event.type === 'touchstart')) { + // return false; + // } + + // // if the pane is only movable using allowed clicks + // if ( + // Array.isArray(panOnDrag) && + // !panOnDrag.includes(event.button) && + // (event.type === 'mousedown' || event.type === 'touchstart') + // ) { + // return false; + // } + + // // We only allow right clicks if pan on drag is set to right click + // const buttonAllowed = + // (Array.isArray(panOnDrag) && panOnDrag.includes(event.button)) || + // !event.button || + // event.button <= 1; + + // default filter for d3-zoom + return ((!event.ctrlKey || event.type === 'wheel') && !event.button) || event.button <= 1; +} + +type ZoomParams = { + transformStore: Writable; + selecting: boolean; + d3Store: Writable<{ zoom: D3ZoomInstance | null; selection: D3SelectionInstance | null }>; +}; + +export default function zoom(domNode: Element, params: ZoomParams) { + const { transformStore, d3Store } = params; + const d3ZoomInstance = d3Zoom(); + const selection = select(domNode).call(d3ZoomInstance); + const d3ZoomHandler = selection.on('wheel.zoom'); + + d3ZoomInstance.transform(selection, zoomIdentity); + + selection.on('wheel.zoom', function (event: any, d: any) { + if (isWrappedWithClass(event, 'nowheel')) { + return null; + } + + event.preventDefault(); + d3ZoomHandler!.call(this, event, d); + }); + + d3Store.set({ + zoom: d3ZoomInstance, + selection + }); + + d3ZoomInstance.on('zoom', (event: D3ZoomEvent) => { + transformStore.set([event.transform.x, event.transform.y, event.transform.k]); + }); + + d3ZoomInstance.filter((event: any) => filter(event, params)); + + return { + update(params: ZoomParams) { + d3ZoomInstance.filter((event: any) => filter(event, params)); + } + }; +} diff --git a/packages/svelte/src/lib/components/KeyHandler/index.svelte b/packages/svelte/src/lib/components/KeyHandler/index.svelte new file mode 100644 index 00000000..d55f6eb9 --- /dev/null +++ b/packages/svelte/src/lib/components/KeyHandler/index.svelte @@ -0,0 +1,26 @@ + + + diff --git a/packages/svelte/src/lib/components/NodeSelection/index.svelte b/packages/svelte/src/lib/components/NodeSelection/index.svelte new file mode 100644 index 00000000..04295e15 --- /dev/null +++ b/packages/svelte/src/lib/components/NodeSelection/index.svelte @@ -0,0 +1,23 @@ + + +{#if selectedNodes} + +{/if} diff --git a/packages/svelte/src/lib/components/Selection/index.svelte b/packages/svelte/src/lib/components/Selection/index.svelte new file mode 100644 index 00000000..4c8f4946 --- /dev/null +++ b/packages/svelte/src/lib/components/Selection/index.svelte @@ -0,0 +1,30 @@ + + +{#if isVisible} +
+{/if} + + diff --git a/packages/svelte/src/lib/components/UserSelection/index.svelte b/packages/svelte/src/lib/components/UserSelection/index.svelte new file mode 100644 index 00000000..a595a68c --- /dev/null +++ b/packages/svelte/src/lib/components/UserSelection/index.svelte @@ -0,0 +1,14 @@ + + + diff --git a/packages/svelte/src/lib/components/nodes/BaseNode.svelte b/packages/svelte/src/lib/components/nodes/BaseNode.svelte index 40870b1e..cda67257 100644 --- a/packages/svelte/src/lib/components/nodes/BaseNode.svelte +++ b/packages/svelte/src/lib/components/nodes/BaseNode.svelte @@ -2,7 +2,7 @@ import { onMount, setContext } from 'svelte'; import type { XYPosition } from '@reactflow/system'; - import drag from '$lib/hooks/drag' + import drag from '$lib/actions/drag' import { useStore } from '$lib/store'; import DefaultNode from './DefaultNode.svelte'; @@ -29,7 +29,6 @@ resizeObserver?.unobserve(nodeRef); } }); -
\ No newline at end of file diff --git a/packages/svelte/src/lib/container/Pane/index.svelte b/packages/svelte/src/lib/container/Pane/index.svelte new file mode 100644 index 00000000..b4a03386 --- /dev/null +++ b/packages/svelte/src/lib/container/Pane/index.svelte @@ -0,0 +1,180 @@ + + +
+ +
+ + \ No newline at end of file diff --git a/packages/svelte/src/lib/container/SvelteFlow.svelte b/packages/svelte/src/lib/container/SvelteFlow.svelte index 11a659f3..f2617ae4 100644 --- a/packages/svelte/src/lib/container/SvelteFlow.svelte +++ b/packages/svelte/src/lib/container/SvelteFlow.svelte @@ -4,18 +4,23 @@ import type { Node, Edge } from '@reactflow/system'; import { key, createStore } from '$lib/store'; + import Zoom from '$lib/container/Zoom/index.svelte'; + import Pane from '$lib/container/Pane/index.svelte'; import Viewport from '$lib/container/Viewport.svelte'; import NodeRenderer from '$lib/container/NodeRenderer.svelte'; 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'; - export let nodes: Node[]; - export let edges: Edge[]; - export let fitView: boolean; - - export let className: string | null = null; export let id: string = '1'; + export let nodes: Node[] = []; + export let edges: Edge[] = []; + export let fitView: boolean = false; + + let className: string = ''; + export { className as class }; - let props = { ...$$restProps }; let domNode: Element; const store = createStore({ @@ -35,19 +40,24 @@ }) -
- - - - - + + + + + + + + + + +
@@ -59,5 +69,4 @@ position: relative; z-index: 0; } - \ No newline at end of file diff --git a/packages/svelte/src/lib/container/Viewport.svelte b/packages/svelte/src/lib/container/Viewport.svelte index 61a8c944..4be75a58 100644 --- a/packages/svelte/src/lib/container/Viewport.svelte +++ b/packages/svelte/src/lib/container/Viewport.svelte @@ -1,35 +1,25 @@ -
-
- -
+
+
\ No newline at end of file diff --git a/packages/svelte/src/lib/container/Zoom/index.svelte b/packages/svelte/src/lib/container/Zoom/index.svelte new file mode 100644 index 00000000..6f938043 --- /dev/null +++ b/packages/svelte/src/lib/container/Zoom/index.svelte @@ -0,0 +1,23 @@ + + +
+ +
+ + \ No newline at end of file diff --git a/packages/svelte/src/lib/hooks/zoom/index.ts b/packages/svelte/src/lib/hooks/zoom/index.ts deleted file mode 100644 index 352be67d..00000000 --- a/packages/svelte/src/lib/hooks/zoom/index.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import type { Writable } from 'svelte/store'; -import { select } from 'd3-selection'; -import { zoom as d3Zoom, zoomIdentity } from 'd3-zoom'; -import type { D3ZoomEvent } from 'd3-zoom'; -import type { D3SelectionInstance, D3ZoomInstance, Transform } from '@reactflow/system'; - -const isWrappedWithClass = (event: any, className: string | undefined) => - event.target.closest(`.${className}`); - -export default function zoom( - domNode: Element, - { - transformStore, - d3Store - }: { - transformStore: Writable; - d3Store: Writable<{ zoom: D3ZoomInstance | null; selection: D3SelectionInstance | null }>; - } -) { - const d3ZoomInstance = d3Zoom(); - const selection = select(domNode).call(d3ZoomInstance); - const d3ZoomHandler = selection.on('wheel.zoom'); - d3ZoomInstance.transform(selection, zoomIdentity); - - selection.on('wheel.zoom', function (event: any, d: any) { - if (isWrappedWithClass(event, 'nowheel')) { - return null; - } - - event.preventDefault(); - d3ZoomHandler!.call(this, event, d); - }); - - d3Store.set({ - zoom: d3ZoomInstance, - selection - }); - - d3ZoomInstance.on('zoom', (event: D3ZoomEvent) => { - transformStore.set([event.transform.x, event.transform.y, event.transform.k]); - }); - - d3ZoomInstance.filter((event: any) => { - const zoomScroll = true; - const pinchZoom = true; - - if ( - event.button === 1 && - event.type === 'mousedown' && - (isWrappedWithClass(event, 'react-flow__node') || - isWrappedWithClass(event, 'react-flow__edge')) - ) { - return true; - } - - // if all interactions are disabled, we prevent all zoom events - // if (!panOnDrag && !zoomScroll && !panOnScroll && !zoomOnDoubleClick && !zoomOnPinch) { - // return false; - // } - - // // during a selection we prevent all other interactions - // if (userSelectionActive) { - // return false; - // } - - // // if zoom on double click is disabled, we prevent the double click event - // if (!zoomOnDoubleClick && event.type === 'dblclick') { - // return false; - // } - - // // if the target element is inside an element with the nowheel class, we prevent zooming - // if (isWrappedWithClass(event, noWheelClassName) && event.type === 'wheel') { - // return false; - // } - - // // if the target element is inside an element with the nopan class, we prevent panning - // if (isWrappedWithClass(event, noPanClassName) && event.type !== 'wheel') { - // return false; - // } - - // if (!zoomOnPinch && event.ctrlKey && event.type === 'wheel') { - // return false; - // } - - // // when there is no scroll handling enabled, we prevent all wheel events - // if (!zoomScroll && !panOnScroll && !pinchZoom && event.type === 'wheel') { - // return false; - // } - - // // if the pane is not movable, we prevent dragging it with mousestart or touchstart - // if (!panOnDrag && (event.type === 'mousedown' || event.type === 'touchstart')) { - // return false; - // } - - // // if the pane is only movable using allowed clicks - // if ( - // Array.isArray(panOnDrag) && - // !panOnDrag.includes(event.button) && - // (event.type === 'mousedown' || event.type === 'touchstart') - // ) { - // return false; - // } - - // // We only allow right clicks if pan on drag is set to right click - // const buttonAllowed = - // (Array.isArray(panOnDrag) && panOnDrag.includes(event.button)) || - // !event.button || - // event.button <= 1; - - // default filter for d3-zoom - return ((!event.ctrlKey || event.type === 'wheel') && !event.button) || event.button <= 1; - }); -} diff --git a/packages/svelte/src/lib/store/index.ts b/packages/svelte/src/lib/store/index.ts index 86209515..2dd687b3 100644 --- a/packages/svelte/src/lib/store/index.ts +++ b/packages/svelte/src/lib/store/index.ts @@ -11,7 +11,8 @@ import { type NodeOrigin, type D3ZoomInstance, type D3SelectionInstance, - type ViewportHelperFunctionOptions + type ViewportHelperFunctionOptions, + type SelectionRect } from '@reactflow/system'; import { fitView, getD3Transition, getDimensions } from '@reactflow/utils'; @@ -22,6 +23,7 @@ import { getNodeData, type EdgePosition } from '$lib/container/EdgeRenderer/utils'; +import { SelectionMode } from 'reactflow'; export const key = Symbol(); @@ -52,6 +54,11 @@ type SvelteFlowStore = { edgesWithDataStore: Readable; idStore: Writable; nodeOriginStore: Writable; + draggingStore: Writable; + selectionRectStore: Writable; + selectionRectModeStore: Writable; + selectionMode: Writable; + selectionKeyPressedStore: Writable; zoomIn: (options?: ViewportHelperFunctionOptions) => void; zoomOut: (options?: ViewportHelperFunctionOptions) => void; fitView: (options?: ViewportHelperFunctionOptions) => boolean; @@ -61,6 +68,7 @@ type SvelteFlowStore = { dragging?: boolean ) => void; updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void; + resetSelectedElements: () => void; }; export function createStore({ @@ -81,6 +89,11 @@ export function createStore({ selection: null }); const idStore = writable(id); + const draggingStore = writable(false); + const selectionRectStore = writable(null); + const selectionKeyPressedStore = writable(false); + const selectionRectModeStore = writable(null); + const selectionMode = writable(SelectionMode.Partial); let fitViewOnInitDone = false; @@ -235,6 +248,22 @@ export function createStore({ ); } + function resetSelectedItem(item: T) { + if (item.selected) { + return { + ...item, + selected: false + }; + } + + return item; + } + + function resetSelectedElements() { + nodesStore.update((ns) => ns.map(resetSelectedItem)); + edgesStore.update((es) => es.map(resetSelectedItem)); + } + return { nodesStore, edgesStore, @@ -245,11 +274,17 @@ export function createStore({ edgesWithDataStore, idStore, nodeOriginStore, + draggingStore, + selectionRectStore, + selectionKeyPressedStore, + selectionRectModeStore, + selectionMode, updateNodePositions, updateNodeDimensions, zoomIn, zoomOut, - fitView: _fitView + fitView: _fitView, + resetSelectedElements }; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a691607..45c45930 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -312,6 +312,7 @@ importers: specifiers: '@reactflow/system': workspace:11.5.5 '@reactflow/utils': workspace:^11.5.5 + '@svelte-put/shortcut': ^2.0.0 '@sveltejs/adapter-auto': ^2.0.0 '@sveltejs/kit': ^1.5.6 '@sveltejs/package': ^1.0.2 @@ -336,6 +337,7 @@ importers: dependencies: '@reactflow/system': link:../system '@reactflow/utils': link:../utils + '@svelte-put/shortcut': registry.npmjs.org/@svelte-put/shortcut/2.0.0 classcat: registry.npmjs.org/classcat/5.0.4 d3-drag: registry.npmjs.org/d3-drag/3.0.0 d3-selection: registry.npmjs.org/d3-selection/3.0.0 @@ -1964,6 +1966,12 @@ packages: version: 2.0.0 dev: true + registry.npmjs.org/@svelte-put/shortcut/2.0.0: + resolution: {integrity: sha512-dDyxIFlRz+W6wxwQtl2jUen0rX8ofV2VBWaJdz0QehLb0IMDzP3aWM9W4k8cAZQIGmTT0adr1+Gt7HPPUugcpQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@svelte-put/shortcut/-/shortcut-2.0.0.tgz} + name: '@svelte-put/shortcut' + version: 2.0.0 + dev: false + registry.npmjs.org/@sveltejs/adapter-auto/2.0.0_@sveltejs+kit@1.5.6: resolution: {integrity: sha512-b+gkHFZgD771kgV3aO4avHFd7y1zhmMYy9i6xOK7m/rwmwaRO8gnF5zBc0Rgca80B2PMU1bKNxyBTHA14OzUAQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-2.0.0.tgz} id: registry.npmjs.org/@sveltejs/adapter-auto/2.0.0