feat(utils): add vanilla drag and panzoom (#3108)
* refactor(panzoom): create vanilla helper * feat(svelte): add interaction example * refactor(panzoom): cleanup * Update function-runner.ts * refactor(panzoom): cleanup * refactor(panzoom): rename functions * feat(utils): add vanilla drag helper (#3107) * feat(utils): add vanilla drag helper * refactor(drag): cleanup * refactor(drag): cleanup * chore(packages): cleanup * refactor(panzoom): cleanup and simplify
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
import { getContext } from 'svelte';
|
||||
import { derived, get } from 'svelte/store';
|
||||
import { zoomIdentity } from 'd3-zoom';
|
||||
import {
|
||||
type NodeDragItem,
|
||||
type UpdateNodePositions,
|
||||
type NodeDimensionUpdate,
|
||||
internalsSymbol,
|
||||
type ViewportHelperFunctionOptions,
|
||||
@@ -13,10 +12,10 @@ import {
|
||||
import {
|
||||
createMarkerIds,
|
||||
fitView as fitViewUtil,
|
||||
getD3Transition,
|
||||
getDimensions,
|
||||
getElementsToRemove,
|
||||
getHandleBounds
|
||||
getHandleBounds,
|
||||
infiniteExtent
|
||||
} from '@reactflow/utils';
|
||||
|
||||
import { addEdge as addEdgeUtil } from '$lib/utils';
|
||||
@@ -60,7 +59,7 @@ export function createStore(params: CreateStoreParams): SvelteFlowStore {
|
||||
store.edges.set(addEdgeUtil(edgeParams, edges));
|
||||
}
|
||||
|
||||
function updateNodePositions(nodeDragItems: NodeDragItem[], dragging = false) {
|
||||
const updateNodePositions: UpdateNodePositions = (nodeDragItems, dragging = false) => {
|
||||
store.nodes.update((nds) => {
|
||||
return nds.map((n) => {
|
||||
const nodeDragItem = nodeDragItems.find((ndi) => ndi.id === n.id);
|
||||
@@ -77,7 +76,7 @@ export function createStore(params: CreateStoreParams): SvelteFlowStore {
|
||||
return n;
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function updateNodeDimensions(updates: NodeDimensionUpdate[]) {
|
||||
const viewportNode = document?.querySelector('.svelte-flow__viewport');
|
||||
@@ -123,21 +122,21 @@ export function createStore(params: CreateStoreParams): SvelteFlowStore {
|
||||
return node;
|
||||
});
|
||||
|
||||
const { zoom: d3Zoom, selection: d3Selection } = get(store.d3);
|
||||
const panZoom = get(store.panZoom);
|
||||
|
||||
const fitViewOnInitDone =
|
||||
get(store.fitViewOnInitDone) ||
|
||||
(get(store.fitViewOnInit) && !!d3Zoom && !!d3Selection && fitView({ nodes: nextNodes }));
|
||||
(get(store.fitViewOnInit) && !!panZoom && fitView({ nodes: nextNodes }));
|
||||
|
||||
store.fitViewOnInitDone.set(fitViewOnInitDone);
|
||||
store.nodes.set(nextNodes);
|
||||
}
|
||||
|
||||
function zoomBy(factor: number, options?: ViewportHelperFunctionOptions) {
|
||||
const { zoom: d3Zoom, selection: d3Selection } = get(store.d3);
|
||||
const panZoom = get(store.panZoom);
|
||||
|
||||
if (d3Zoom && d3Selection) {
|
||||
d3Zoom.scaleBy(getD3Transition(d3Selection, options?.duration), factor);
|
||||
if (panZoom) {
|
||||
panZoom.scaleBy(factor, options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,28 +149,36 @@ export function createStore(params: CreateStoreParams): SvelteFlowStore {
|
||||
}
|
||||
|
||||
function setMinZoom(minZoom: number) {
|
||||
const d3Zoom = get(store.d3).zoom;
|
||||
|
||||
if (d3Zoom) {
|
||||
d3Zoom?.scaleExtent([minZoom, get(store.maxZoom)]);
|
||||
const panZoom = get(store.panZoom);
|
||||
|
||||
if (panZoom) {
|
||||
panZoom.setScaleExtent([minZoom, get(store.maxZoom)]);
|
||||
store.minZoom.set(minZoom);
|
||||
}
|
||||
}
|
||||
|
||||
function setMaxZoom(maxZoom: number) {
|
||||
const d3Zoom = get(store.d3).zoom;
|
||||
if (d3Zoom) {
|
||||
d3Zoom?.scaleExtent([get(store.minZoom), maxZoom]);
|
||||
const panZoom = get(store.panZoom);
|
||||
|
||||
if (panZoom) {
|
||||
panZoom.setScaleExtent([get(store.minZoom), maxZoom]);
|
||||
store.maxZoom.set(maxZoom);
|
||||
}
|
||||
}
|
||||
|
||||
function fitView(options?: FitViewOptions) {
|
||||
const { zoom: d3Zoom, selection: d3Selection } = get(store.d3);
|
||||
function setTranslateExtent(extent: CoordinateExtent) {
|
||||
const panZoom = get(store.panZoom);
|
||||
|
||||
if (!d3Zoom || !d3Selection) {
|
||||
if (panZoom) {
|
||||
panZoom.setTranslateExtent(extent);
|
||||
store.translateExtent.set(extent);
|
||||
}
|
||||
}
|
||||
|
||||
function fitView(options?: FitViewOptions) {
|
||||
const panZoom = get(store.panZoom);
|
||||
|
||||
if (!panZoom) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -184,8 +191,7 @@ export function createStore(params: CreateStoreParams): SvelteFlowStore {
|
||||
height: get(store.height),
|
||||
minZoom: 0.2,
|
||||
maxZoom: 2,
|
||||
d3Selection,
|
||||
d3Zoom,
|
||||
panZoom,
|
||||
nodeOrigin: [0, 0]
|
||||
},
|
||||
{}
|
||||
@@ -203,7 +209,7 @@ export function createStore(params: CreateStoreParams): SvelteFlowStore {
|
||||
return item;
|
||||
}
|
||||
|
||||
function resetSelectedElements() {
|
||||
function unselectNodesAndEdges() {
|
||||
store.nodes.update((ns) => ns.map(resetSelectedItem));
|
||||
store.edges.update((es) => es.map(resetSelectedItem));
|
||||
}
|
||||
@@ -267,29 +273,35 @@ export function createStore(params: CreateStoreParams): SvelteFlowStore {
|
||||
}
|
||||
|
||||
function panBy(delta: XYPosition) {
|
||||
const { zoom: d3Zoom, selection: d3Selection } = get(store.d3);
|
||||
const panZoom = get(store.panZoom);
|
||||
const transform = get(store.transform);
|
||||
const width = get(store.width);
|
||||
const height = get(store.height);
|
||||
|
||||
if (!d3Zoom || !d3Selection || (!delta.x && !delta.y)) {
|
||||
return;
|
||||
if (!panZoom || (!delta.x && !delta.y)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const nextTransform = zoomIdentity
|
||||
.translate(transform[0] + delta.x, transform[1] + delta.y)
|
||||
.scale(transform[2]);
|
||||
const nextViewport = panZoom.setViewportConstrained(
|
||||
{
|
||||
x: transform[0] + delta.x,
|
||||
y: transform[1] + delta.y,
|
||||
zoom: transform[2]
|
||||
},
|
||||
[
|
||||
[0, 0],
|
||||
[width, height]
|
||||
],
|
||||
infiniteExtent
|
||||
);
|
||||
|
||||
const extent: CoordinateExtent = [
|
||||
[0, 0],
|
||||
[width, height]
|
||||
];
|
||||
const transformChanged =
|
||||
!!nextViewport &&
|
||||
(nextViewport.x !== transform[0] ||
|
||||
nextViewport.y !== transform[1] ||
|
||||
nextViewport.k !== transform[2]);
|
||||
|
||||
const constrainedTransform = d3Zoom?.constrain()(nextTransform, extent, [
|
||||
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
||||
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY]
|
||||
]);
|
||||
d3Zoom.transform(d3Selection, constrainedTransform);
|
||||
return transformChanged;
|
||||
}
|
||||
|
||||
function updateConnection(connectionUpdate: Partial<ConnectionData> | null) {
|
||||
@@ -319,7 +331,7 @@ export function createStore(params: CreateStoreParams): SvelteFlowStore {
|
||||
store.snapGrid.set(null);
|
||||
store.isValidConnection.set(() => true);
|
||||
|
||||
resetSelectedElements();
|
||||
unselectNodesAndEdges();
|
||||
cancelConnection();
|
||||
}
|
||||
|
||||
@@ -346,7 +358,8 @@ export function createStore(params: CreateStoreParams): SvelteFlowStore {
|
||||
fitView,
|
||||
setMinZoom,
|
||||
setMaxZoom,
|
||||
resetSelectedElements,
|
||||
setTranslateExtent,
|
||||
unselectNodesAndEdges,
|
||||
addSelectedNodes,
|
||||
addSelectedEdges,
|
||||
panBy,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { readable, writable } from 'svelte/store';
|
||||
import {
|
||||
SelectionMode,
|
||||
type D3ZoomInstance,
|
||||
type D3SelectionInstance,
|
||||
ConnectionMode,
|
||||
ConnectionLineType,
|
||||
type SelectionRect,
|
||||
type Transform,
|
||||
type SnapGrid,
|
||||
type MarkerProps
|
||||
type MarkerProps,
|
||||
type PanZoomInstance,
|
||||
type CoordinateExtent
|
||||
} from '@reactflow/system';
|
||||
|
||||
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
|
||||
@@ -27,6 +27,7 @@ import type {
|
||||
Node,
|
||||
IsValidConnection
|
||||
} from '$lib/types';
|
||||
import { infiniteExtent } from '@reactflow/utils';
|
||||
|
||||
export const initConnectionData = {
|
||||
nodeId: null,
|
||||
@@ -58,12 +59,12 @@ export const initialStoreState = {
|
||||
width: writable<number>(500),
|
||||
minZoom: writable<number>(0.5),
|
||||
maxZoom: writable<number>(2),
|
||||
nodeExtent: writable<CoordinateExtent>(infiniteExtent),
|
||||
translateExtent: writable<CoordinateExtent>(infiniteExtent),
|
||||
autoPanOnNodeDrag: writable<boolean>(true),
|
||||
fitViewOnInit: writable<boolean>(false),
|
||||
fitViewOnInitDone: writable<boolean>(false),
|
||||
d3: writable<{ zoom: D3ZoomInstance | null; selection: D3SelectionInstance | null }>({
|
||||
zoom: null,
|
||||
selection: null
|
||||
}),
|
||||
panZoom: writable<PanZoomInstance | null>(null),
|
||||
snapGrid: writable<SnapGrid | null>(null),
|
||||
dragging: writable<boolean>(false),
|
||||
selectionRect: writable<SelectionRect | null>(null),
|
||||
@@ -85,6 +86,7 @@ export const initialStoreState = {
|
||||
nodesDraggable: writable<boolean>(true),
|
||||
nodesConnectable: writable<boolean>(true),
|
||||
elementsSelectable: writable<boolean>(true),
|
||||
selectNodesOnDrag: writable<boolean>(true),
|
||||
markers: readable<MarkerProps[]>([]),
|
||||
defaultMarkerColor: writable<string>('#b1b1b7')
|
||||
};
|
||||
|
||||
@@ -3,7 +3,8 @@ import type {
|
||||
XYPosition,
|
||||
ViewportHelperFunctionOptions,
|
||||
Connection,
|
||||
NodeDragItem
|
||||
UpdateNodePositions,
|
||||
CoordinateExtent
|
||||
} from '@reactflow/system';
|
||||
|
||||
import type { initialStoreState } from './initial-store';
|
||||
@@ -18,17 +19,14 @@ export type SvelteFlowStoreActions = {
|
||||
zoomOut: (options?: ViewportHelperFunctionOptions) => void;
|
||||
setMinZoom: (minZoom: number) => void;
|
||||
setMaxZoom: (maxZoom: number) => void;
|
||||
setTranslateExtent: (extent: CoordinateExtent) => void;
|
||||
fitView: (options?: FitViewOptions) => boolean;
|
||||
updateNodePositions: (
|
||||
nodeDragItems: NodeDragItem[],
|
||||
positionChanged?: boolean,
|
||||
dragging?: boolean
|
||||
) => void;
|
||||
updateNodePositions: UpdateNodePositions;
|
||||
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void;
|
||||
resetSelectedElements: () => void;
|
||||
unselectNodesAndEdges: () => void;
|
||||
addSelectedNodes: (ids: string[]) => void;
|
||||
addSelectedEdges: (ids: string[]) => void;
|
||||
panBy: (delta: XYPosition) => void;
|
||||
panBy: (delta: XYPosition) => boolean;
|
||||
updateConnection: (connection: Partial<ConnectionData>) => void;
|
||||
cancelConnection: () => void;
|
||||
reset(): void;
|
||||
|
||||
Reference in New Issue
Block a user