feat(svelte): selection box
This commit is contained in:
@@ -16,8 +16,9 @@
|
|||||||
".": "./index.js"
|
".": "./index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@reactflow/system": "workspace:11.5.5",
|
"@reactflow/system": "workspace:*",
|
||||||
"@reactflow/utils": "workspace:^11.5.5",
|
"@reactflow/utils": "workspace:*",
|
||||||
|
"@svelte-put/shortcut": "^2.0.0",
|
||||||
"classcat": "^5.0.4",
|
"classcat": "^5.0.4",
|
||||||
"d3-drag": "^3.0.0",
|
"d3-drag": "^3.0.0",
|
||||||
"d3-selection": "^3.0.0",
|
"d3-selection": "^3.0.0",
|
||||||
|
|||||||
@@ -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<Transform>;
|
||||||
|
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<HTMLDivElement, any>) => {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<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,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { getRectOfNodes } from '@reactflow/utils';
|
||||||
|
|
||||||
|
import { useStore } from '$lib/store';
|
||||||
|
import Selection from '$lib/components/Selection/index.svelte';
|
||||||
|
|
||||||
|
const { selectionRectModeStore, nodesStore, nodeOriginStore } = useStore();
|
||||||
|
|
||||||
|
$: selectedNodes = $nodesStore.filter(n => n.selected);
|
||||||
|
$: rect = getRectOfNodes(selectedNodes, $nodeOriginStore);
|
||||||
|
|
||||||
|
// @todo: use zoom action for selection
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if selectedNodes}
|
||||||
|
<Selection
|
||||||
|
isVisible={$selectionRectModeStore === 'nodes'}
|
||||||
|
width={rect.width}
|
||||||
|
height={rect.height}
|
||||||
|
x={rect.x}
|
||||||
|
y={rect.y}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let x: number | null = 0;
|
||||||
|
export let y: number | null = 0;
|
||||||
|
export let width: number | null = 0;
|
||||||
|
export let height: number | null = 0;
|
||||||
|
export let isVisible: boolean = true;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if isVisible}
|
||||||
|
<div
|
||||||
|
class="react-flow__selection"
|
||||||
|
style={`width: ${width}px; height: ${height}px;transform: translate(${x}px, ${y}px)`}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.react-flow__selection {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background: rgba(0, 89, 220, 0.08);
|
||||||
|
border: 1px dotted rgba(0, 89, 220, 0.8);
|
||||||
|
z-index: 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__selection:focus,
|
||||||
|
.react-flow__selection:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { useStore } from '$lib/store';
|
||||||
|
import Selection from '$lib/components/Selection/index.svelte';
|
||||||
|
|
||||||
|
const { selectionRectStore, selectionRectModeStore } = useStore();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Selection
|
||||||
|
isVisible={!!($selectionRectStore && $selectionRectModeStore === 'user')}
|
||||||
|
width={$selectionRectStore?.width}
|
||||||
|
height={$selectionRectStore?.height}
|
||||||
|
x={$selectionRectStore?.x}
|
||||||
|
y={$selectionRectStore?.y}
|
||||||
|
/>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import { onMount, setContext } from 'svelte';
|
import { onMount, setContext } from 'svelte';
|
||||||
import type { XYPosition } from '@reactflow/system';
|
import type { XYPosition } from '@reactflow/system';
|
||||||
|
|
||||||
import drag from '$lib/hooks/drag'
|
import drag from '$lib/actions/drag'
|
||||||
import { useStore } from '$lib/store';
|
import { useStore } from '$lib/store';
|
||||||
import DefaultNode from './DefaultNode.svelte';
|
import DefaultNode from './DefaultNode.svelte';
|
||||||
|
|
||||||
@@ -29,7 +29,6 @@
|
|||||||
resizeObserver?.unobserve(nodeRef);
|
resizeObserver?.unobserve(nodeRef);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -58,6 +57,8 @@
|
|||||||
background-color: white;
|
background-color: white;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
|
pointer-events: all;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dragging {
|
.dragging {
|
||||||
|
|||||||
@@ -31,5 +31,7 @@
|
|||||||
.react-flow__nodes {
|
.react-flow__nodes {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
transform-origin: 0 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { useStore } from '$lib/store';
|
||||||
|
import { SelectionMode, type Node, type Edge } from '@reactflow/system';
|
||||||
|
import { getConnectedEdges, getEventPosition, getNodesInside } from '@reactflow/utils';
|
||||||
|
|
||||||
|
const { nodesStore, edgesStore, transformStore, nodeOriginStore, draggingStore, selectionRectStore, selectionRectModeStore, selectionKeyPressedStore, resetSelectedElements } = useStore();
|
||||||
|
|
||||||
|
const wrapHandler = (
|
||||||
|
handler: (evt: MouseEvent) => void,
|
||||||
|
container: HTMLDivElement
|
||||||
|
): (evt: MouseEvent) => void => {
|
||||||
|
return (event: MouseEvent) => {
|
||||||
|
if (event.target !== container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handler?.(event);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// @todo take from props
|
||||||
|
const elementsSelectable = true;
|
||||||
|
const selectionMode = SelectionMode.Partial
|
||||||
|
|
||||||
|
let container: HTMLDivElement;
|
||||||
|
let containerBounds: DOMRect | null = null
|
||||||
|
let selectedNodes: Node[] = [];
|
||||||
|
|
||||||
|
$: isSelecting = $selectionKeyPressedStore;
|
||||||
|
$: hasActiveSelection = elementsSelectable && (isSelecting || $selectionRectModeStore === 'user');
|
||||||
|
|
||||||
|
|
||||||
|
function onClick (event: MouseEvent) {
|
||||||
|
// onPaneClick?.(event);
|
||||||
|
|
||||||
|
resetSelectedElements();
|
||||||
|
selectionRectModeStore.set(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseDown(event: MouseEvent) {
|
||||||
|
containerBounds = container.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!elementsSelectable ||
|
||||||
|
!isSelecting ||
|
||||||
|
event.button !== 0 ||
|
||||||
|
event.target !== container ||
|
||||||
|
!containerBounds
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { x, y } = getEventPosition(event, containerBounds);
|
||||||
|
|
||||||
|
resetSelectedElements();
|
||||||
|
|
||||||
|
selectionRectStore.set({
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
startX: x,
|
||||||
|
startY: y,
|
||||||
|
x,
|
||||||
|
y
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// onSelectionStart?.(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
function toggleSelected<Item extends Node | Edge>(ids: string[]) {
|
||||||
|
return (item: Item) => {
|
||||||
|
const isSelected = ids.includes(item.id);
|
||||||
|
|
||||||
|
if (item.selected !== isSelected) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
selected: isSelected,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseMove(event: MouseEvent) {
|
||||||
|
if (!isSelecting || !containerBounds || !$selectionRectStore) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const mousePos = getEventPosition(event, containerBounds);
|
||||||
|
const startX = $selectionRectStore.startX ?? 0;
|
||||||
|
const startY = $selectionRectStore.startY ?? 0;
|
||||||
|
const nextUserSelectRect = {
|
||||||
|
...$selectionRectStore,
|
||||||
|
x: mousePos.x < startX ? mousePos.x : startX,
|
||||||
|
y: mousePos.y < startY ? mousePos.y : startY,
|
||||||
|
width: Math.abs(mousePos.x - startX),
|
||||||
|
height: Math.abs(mousePos.y - startY),
|
||||||
|
};
|
||||||
|
|
||||||
|
selectedNodes = getNodesInside(
|
||||||
|
new Map($nodesStore.map(node => [node.id, node])),
|
||||||
|
nextUserSelectRect,
|
||||||
|
$transformStore,
|
||||||
|
selectionMode === SelectionMode.Partial,
|
||||||
|
true,
|
||||||
|
$nodeOriginStore
|
||||||
|
);
|
||||||
|
const selectedEdgeIds = getConnectedEdges(selectedNodes, $edgesStore).map((e) => e.id);
|
||||||
|
const selectedNodeIds = selectedNodes.map((n) => n.id);
|
||||||
|
|
||||||
|
nodesStore.update(nodes => nodes.map(toggleSelected(selectedNodeIds)));
|
||||||
|
edgesStore.update(edges => edges.map(toggleSelected(selectedEdgeIds)));
|
||||||
|
|
||||||
|
selectionRectModeStore.set('user');
|
||||||
|
selectionRectStore.set(nextUserSelectRect);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseUp(event: MouseEvent) {
|
||||||
|
if (event.button !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only want to trigger click functions when in selection mode if
|
||||||
|
// the user did not move the mouse.
|
||||||
|
// if (!userSelectionActive && userSelectionRect && event.target === container.current) {
|
||||||
|
// onClick?.(event);
|
||||||
|
// }
|
||||||
|
selectionRectStore.set(null);
|
||||||
|
|
||||||
|
if (selectedNodes.length > 0) {
|
||||||
|
selectionRectModeStore.set('nodes');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// onSelectionEnd?.(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseLeave = (event: MouseEvent) => {
|
||||||
|
if ($selectionRectModeStore === 'user') {
|
||||||
|
selectionRectModeStore.set(selectedNodes.length > 0 ? 'nodes' : null);
|
||||||
|
// onSelectionEnd?.(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectionRectStore.set(null);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={container}
|
||||||
|
class="react-flow__pane"
|
||||||
|
class:dragging={$draggingStore}
|
||||||
|
class:selection={isSelecting}
|
||||||
|
on:click={hasActiveSelection ? undefined : wrapHandler(onClick, container)}
|
||||||
|
on:mousedown={hasActiveSelection ? onMouseDown : undefined}
|
||||||
|
on:mousemove={hasActiveSelection? onMouseMove : undefined}
|
||||||
|
on:mouseup={hasActiveSelection ? onMouseUp : undefined}
|
||||||
|
on:mouseleave={hasActiveSelection ? onMouseLeave : undefined}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.react-flow__pane {
|
||||||
|
cursor: grab;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragging {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -4,18 +4,23 @@
|
|||||||
import type { Node, Edge } from '@reactflow/system';
|
import type { Node, Edge } from '@reactflow/system';
|
||||||
|
|
||||||
import { key, createStore } from '$lib/store';
|
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 Viewport from '$lib/container/Viewport.svelte';
|
||||||
import NodeRenderer from '$lib/container/NodeRenderer.svelte';
|
import NodeRenderer from '$lib/container/NodeRenderer.svelte';
|
||||||
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 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 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;
|
let domNode: Element;
|
||||||
|
|
||||||
const store = createStore({
|
const store = createStore({
|
||||||
@@ -35,19 +40,24 @@
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
{...props}
|
{...$$restProps}
|
||||||
class={cc(['react-flow', className])}
|
class={cc(['react-flow', className])}
|
||||||
data-testid="rf__wrapper"
|
data-testid="rf__wrapper"
|
||||||
id={id}
|
id={id}
|
||||||
bind:this={domNode}
|
bind:this={domNode}
|
||||||
>
|
>
|
||||||
<Viewport>
|
<KeyHandler />
|
||||||
<NodeRenderer />
|
<Zoom>
|
||||||
<EdgeRenderer />
|
<Pane>
|
||||||
</Viewport>
|
<Viewport>
|
||||||
|
<NodeSelection />
|
||||||
|
<NodeRenderer />
|
||||||
|
<EdgeRenderer />
|
||||||
|
</Viewport>
|
||||||
|
<UserSelection />
|
||||||
|
</Pane>
|
||||||
|
</Zoom>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -59,5 +69,4 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -1,35 +1,25 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import zoom from '$lib/hooks/zoom'
|
|
||||||
import { useStore } from '$lib/store';
|
import { useStore } from '$lib/store';
|
||||||
|
|
||||||
const { transformStore, d3Store } = useStore();
|
const { transformStore} = useStore();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="react-flow__pane" use:zoom={{ transformStore, d3Store }}>
|
<div
|
||||||
<div
|
class="react-flow__viewport"
|
||||||
class="react-flow__viewport"
|
style="transform: translate({$transformStore[0]}px, {$transformStore[1]}px) scale({$transformStore[2]})"
|
||||||
style="transform: translate({$transformStore[0]}px, {$transformStore[1]}px) scale({$transformStore[2]})"
|
>
|
||||||
>
|
<slot />
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.react-flow__pane {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
position: absolute;
|
|
||||||
top:0;
|
|
||||||
left:0;
|
|
||||||
cursor: grab;
|
|
||||||
}
|
|
||||||
|
|
||||||
.react-flow__viewport {
|
.react-flow__viewport {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transform-origin: 0 0;
|
transform-origin: 0 0;
|
||||||
top:0;
|
top: 0;
|
||||||
left:0;
|
left: 0;
|
||||||
|
z-index: 2;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { useStore } from '$lib/store';
|
||||||
|
import zoom from '$lib/actions/zoom';
|
||||||
|
|
||||||
|
const { transformStore, d3Store, selectionKeyPressedStore, selectionRectModeStore } = useStore();
|
||||||
|
|
||||||
|
$: selecting = $selectionKeyPressedStore || $selectionRectModeStore === 'user';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="react-flow__zoom" use:zoom={{ transformStore, d3Store, selecting }}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.react-flow__zoom {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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<Transform>;
|
|
||||||
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<HTMLDivElement, any>) => {
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,8 @@ import {
|
|||||||
type NodeOrigin,
|
type NodeOrigin,
|
||||||
type D3ZoomInstance,
|
type D3ZoomInstance,
|
||||||
type D3SelectionInstance,
|
type D3SelectionInstance,
|
||||||
type ViewportHelperFunctionOptions
|
type ViewportHelperFunctionOptions,
|
||||||
|
type SelectionRect
|
||||||
} from '@reactflow/system';
|
} from '@reactflow/system';
|
||||||
import { fitView, getD3Transition, getDimensions } from '@reactflow/utils';
|
import { fitView, getD3Transition, getDimensions } from '@reactflow/utils';
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
getNodeData,
|
getNodeData,
|
||||||
type EdgePosition
|
type EdgePosition
|
||||||
} from '$lib/container/EdgeRenderer/utils';
|
} from '$lib/container/EdgeRenderer/utils';
|
||||||
|
import { SelectionMode } from 'reactflow';
|
||||||
|
|
||||||
export const key = Symbol();
|
export const key = Symbol();
|
||||||
|
|
||||||
@@ -52,6 +54,11 @@ type SvelteFlowStore = {
|
|||||||
edgesWithDataStore: Readable<EdgeWithData[]>;
|
edgesWithDataStore: Readable<EdgeWithData[]>;
|
||||||
idStore: Writable<string>;
|
idStore: Writable<string>;
|
||||||
nodeOriginStore: Writable<NodeOrigin>;
|
nodeOriginStore: Writable<NodeOrigin>;
|
||||||
|
draggingStore: Writable<boolean>;
|
||||||
|
selectionRectStore: Writable<SelectionRect | null>;
|
||||||
|
selectionRectModeStore: Writable<string | null>;
|
||||||
|
selectionMode: Writable<SelectionMode>;
|
||||||
|
selectionKeyPressedStore: Writable<boolean>;
|
||||||
zoomIn: (options?: ViewportHelperFunctionOptions) => void;
|
zoomIn: (options?: ViewportHelperFunctionOptions) => void;
|
||||||
zoomOut: (options?: ViewportHelperFunctionOptions) => void;
|
zoomOut: (options?: ViewportHelperFunctionOptions) => void;
|
||||||
fitView: (options?: ViewportHelperFunctionOptions) => boolean;
|
fitView: (options?: ViewportHelperFunctionOptions) => boolean;
|
||||||
@@ -61,6 +68,7 @@ type SvelteFlowStore = {
|
|||||||
dragging?: boolean
|
dragging?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void;
|
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void;
|
||||||
|
resetSelectedElements: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createStore({
|
export function createStore({
|
||||||
@@ -81,6 +89,11 @@ export function createStore({
|
|||||||
selection: null
|
selection: null
|
||||||
});
|
});
|
||||||
const idStore = writable(id);
|
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;
|
let fitViewOnInitDone = false;
|
||||||
|
|
||||||
@@ -235,6 +248,22 @@ export function createStore({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetSelectedItem<T extends Node | Edge>(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 {
|
return {
|
||||||
nodesStore,
|
nodesStore,
|
||||||
edgesStore,
|
edgesStore,
|
||||||
@@ -245,11 +274,17 @@ export function createStore({
|
|||||||
edgesWithDataStore,
|
edgesWithDataStore,
|
||||||
idStore,
|
idStore,
|
||||||
nodeOriginStore,
|
nodeOriginStore,
|
||||||
|
draggingStore,
|
||||||
|
selectionRectStore,
|
||||||
|
selectionKeyPressedStore,
|
||||||
|
selectionRectModeStore,
|
||||||
|
selectionMode,
|
||||||
updateNodePositions,
|
updateNodePositions,
|
||||||
updateNodeDimensions,
|
updateNodeDimensions,
|
||||||
zoomIn,
|
zoomIn,
|
||||||
zoomOut,
|
zoomOut,
|
||||||
fitView: _fitView
|
fitView: _fitView,
|
||||||
|
resetSelectedElements
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+8
@@ -312,6 +312,7 @@ importers:
|
|||||||
specifiers:
|
specifiers:
|
||||||
'@reactflow/system': workspace:11.5.5
|
'@reactflow/system': workspace:11.5.5
|
||||||
'@reactflow/utils': workspace:^11.5.5
|
'@reactflow/utils': workspace:^11.5.5
|
||||||
|
'@svelte-put/shortcut': ^2.0.0
|
||||||
'@sveltejs/adapter-auto': ^2.0.0
|
'@sveltejs/adapter-auto': ^2.0.0
|
||||||
'@sveltejs/kit': ^1.5.6
|
'@sveltejs/kit': ^1.5.6
|
||||||
'@sveltejs/package': ^1.0.2
|
'@sveltejs/package': ^1.0.2
|
||||||
@@ -336,6 +337,7 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@reactflow/system': link:../system
|
'@reactflow/system': link:../system
|
||||||
'@reactflow/utils': link:../utils
|
'@reactflow/utils': link:../utils
|
||||||
|
'@svelte-put/shortcut': registry.npmjs.org/@svelte-put/shortcut/2.0.0
|
||||||
classcat: registry.npmjs.org/classcat/5.0.4
|
classcat: registry.npmjs.org/classcat/5.0.4
|
||||||
d3-drag: registry.npmjs.org/d3-drag/3.0.0
|
d3-drag: registry.npmjs.org/d3-drag/3.0.0
|
||||||
d3-selection: registry.npmjs.org/d3-selection/3.0.0
|
d3-selection: registry.npmjs.org/d3-selection/3.0.0
|
||||||
@@ -1964,6 +1966,12 @@ packages:
|
|||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
dev: true
|
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:
|
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}
|
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
|
id: registry.npmjs.org/@sveltejs/adapter-auto/2.0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user