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:
@@ -5,6 +5,7 @@
|
||||
'customnode',
|
||||
'drag-n-drop',
|
||||
'edges',
|
||||
'interaction',
|
||||
'overview',
|
||||
'stress',
|
||||
'subflows',
|
||||
|
||||
@@ -1,136 +1,70 @@
|
||||
import { get } from 'svelte/store';
|
||||
import { drag as d3Drag, type D3DragEvent, type SubjectPosition } from 'd3-drag';
|
||||
import { select } from 'd3-selection';
|
||||
import type { XYPosition, CoordinateExtent } from '@reactflow/system';
|
||||
import { XYDrag } from '@reactflow/utils';
|
||||
|
||||
import { getDragItems, hasSelector, calcNextPosition } from './utils';
|
||||
import type { SvelteFlowStore } from '$lib/store/types';
|
||||
|
||||
export type UseDragData = { dx: number; dy: number };
|
||||
export type UseDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>;
|
||||
export type NodeDragItem = {
|
||||
id: string;
|
||||
position: XYPosition;
|
||||
positionAbsolute: XYPosition;
|
||||
// distance from the mouse cursor to the node when start dragging
|
||||
distance: XYPosition;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
extent?: 'parent' | CoordinateExtent;
|
||||
parentNode?: string;
|
||||
dragging?: boolean;
|
||||
};
|
||||
|
||||
type UseDragParams = {
|
||||
store: SvelteFlowStore;
|
||||
disabled?: boolean;
|
||||
noDragClassName?: string;
|
||||
handleSelector?: string;
|
||||
nodeId?: string;
|
||||
updateNodePositions: (dragItems: NodeDragItem[], d: boolean, p: boolean) => void;
|
||||
nodes: SvelteFlowStore['nodes'];
|
||||
transform: SvelteFlowStore['transform'];
|
||||
snapGrid: SvelteFlowStore['snapGrid'];
|
||||
isSelectable?: boolean;
|
||||
};
|
||||
|
||||
export default function drag(
|
||||
nodeRef: Element,
|
||||
{
|
||||
handleSelector,
|
||||
nodeId,
|
||||
updateNodePositions,
|
||||
nodes,
|
||||
transform: transformStore,
|
||||
snapGrid: snapGridStore
|
||||
}: UseDragParams
|
||||
) {
|
||||
let dragging = false;
|
||||
let dragItems: NodeDragItem[] = [];
|
||||
let lastPos: { x: number | null; y: number | null } = { x: null, y: null };
|
||||
export default function drag(domNode: Element, params: UseDragParams) {
|
||||
const dragInstance = XYDrag({
|
||||
domNode,
|
||||
getStoreItems: () => {
|
||||
const { store } = params;
|
||||
const snapGrid = get(store.snapGrid);
|
||||
|
||||
const selection = select(nodeRef);
|
||||
return {
|
||||
nodes: get(store.nodes),
|
||||
edges: get(store.edges),
|
||||
nodeExtent: get(store.nodeExtent),
|
||||
snapGrid: snapGrid ? snapGrid : [0, 0],
|
||||
snapToGrid: !!snapGrid,
|
||||
nodeOrigin: [0, 0],
|
||||
multiSelectionActive: false,
|
||||
domNode: get(store.domNode),
|
||||
transform: get(store.transform),
|
||||
autoPanOnNodeDrag: get(store.autoPanOnNodeDrag),
|
||||
nodesDraggable: get(store.nodesDraggable),
|
||||
selectNodesOnDrag: get(store.selectNodesOnDrag),
|
||||
unselectNodesAndEdges: store.unselectNodesAndEdges,
|
||||
updateNodePositions: store.updateNodePositions,
|
||||
panBy: store.panBy
|
||||
};
|
||||
},
|
||||
onNodeClick: () => {
|
||||
console.log('node click');
|
||||
}
|
||||
});
|
||||
|
||||
const getPointerPosition = ({ sourceEvent }: UseDragEvent) => {
|
||||
const x = sourceEvent.touches ? sourceEvent.touches[0].clientX : sourceEvent.clientX;
|
||||
const y = sourceEvent.touches ? sourceEvent.touches[0].clientY : sourceEvent.clientY;
|
||||
const transform = get(transformStore);
|
||||
const snapGrid = get(snapGridStore);
|
||||
|
||||
const pointerPos = {
|
||||
x: (x - transform[0]) / transform[2],
|
||||
y: (y - transform[1]) / transform[2]
|
||||
};
|
||||
|
||||
// we need the snapped position in order to be able to skip unnecessary drag events
|
||||
return {
|
||||
xSnapped: snapGrid ? snapGrid[0] * Math.round(pointerPos.x / snapGrid[0]) : pointerPos.x,
|
||||
ySnapped: snapGrid ? snapGrid[1] * Math.round(pointerPos.y / snapGrid[1]) : pointerPos.y,
|
||||
...pointerPos
|
||||
};
|
||||
};
|
||||
|
||||
const updateNodes = ({ x, y }: XYPosition) => {
|
||||
let hasChange = false;
|
||||
const snapGrid = get(snapGridStore);
|
||||
|
||||
dragItems = dragItems.map((n) => {
|
||||
const nextPosition = { x: x - n.distance.x, y: y - n.distance.y };
|
||||
|
||||
if (snapGrid) {
|
||||
nextPosition.x = snapGrid[0] * Math.round(nextPosition.x / snapGrid[0]);
|
||||
nextPosition.y = snapGrid[1] * Math.round(nextPosition.y / snapGrid[1]);
|
||||
}
|
||||
|
||||
const updatedPos = calcNextPosition(n, nextPosition, get(nodes));
|
||||
|
||||
// we want to make sure that we only fire a change event when there is a changes
|
||||
hasChange =
|
||||
hasChange ||
|
||||
n.position.x !== updatedPos.position.x ||
|
||||
n.position.y !== updatedPos.position.y;
|
||||
|
||||
n.position = updatedPos.position;
|
||||
n.positionAbsolute = updatedPos.positionAbsolute;
|
||||
|
||||
return n;
|
||||
});
|
||||
|
||||
if (!hasChange) {
|
||||
function updateDrag(domNode: Element, params: UseDragParams) {
|
||||
if (params.disabled) {
|
||||
dragInstance.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
updateNodePositions(dragItems, true, true);
|
||||
dragging = true;
|
||||
};
|
||||
|
||||
const dragHandler = d3Drag()
|
||||
.on('start', (event: UseDragEvent) => {
|
||||
const pointerPos = getPointerPosition(event);
|
||||
lastPos = pointerPos;
|
||||
dragItems = getDragItems(get(nodes), pointerPos, nodeId);
|
||||
})
|
||||
.on('drag', (event: UseDragEvent) => {
|
||||
const pointerPos = getPointerPosition(event);
|
||||
|
||||
// skip events without movement
|
||||
if ((lastPos.x !== pointerPos.xSnapped || lastPos.y !== pointerPos.ySnapped) && dragItems) {
|
||||
lastPos = pointerPos;
|
||||
updateNodes(pointerPos);
|
||||
}
|
||||
})
|
||||
.on('end', (event: UseDragEvent) => {
|
||||
dragging = false;
|
||||
|
||||
if (dragItems) {
|
||||
updateNodePositions(dragItems, false, false);
|
||||
}
|
||||
})
|
||||
.filter((event: MouseEvent) => {
|
||||
const target = event.target as HTMLDivElement;
|
||||
const isDraggable =
|
||||
!event.button &&
|
||||
!hasSelector(target, '.nodrag', nodeRef) &&
|
||||
(!handleSelector || hasSelector(target, handleSelector, nodeRef));
|
||||
|
||||
return isDraggable;
|
||||
dragInstance.update({
|
||||
domNode,
|
||||
noDragClassName: params.noDragClassName,
|
||||
handleSelector: params.handleSelector,
|
||||
nodeId: params.nodeId,
|
||||
isSelectable: params.isSelectable
|
||||
});
|
||||
}
|
||||
|
||||
selection.call(dragHandler);
|
||||
updateDrag(domNode, params);
|
||||
|
||||
return {
|
||||
update(params: UseDragParams) {
|
||||
updateDrag(domNode, params);
|
||||
},
|
||||
destroy() {
|
||||
dragInstance.destroy();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
import {
|
||||
errorMessages,
|
||||
type CoordinateExtent,
|
||||
type NodeDragItem,
|
||||
type NodeOrigin,
|
||||
type XYPosition
|
||||
} from '@reactflow/system';
|
||||
import { clampPosition, devWarn, getNodePositionWithOrigin, isNumeric } from '@reactflow/utils';
|
||||
|
||||
import type { Node } from '$lib/types';
|
||||
|
||||
export function isParentSelected(node: Node, nodes: Node[]): boolean {
|
||||
if (!node.parentNode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parentNode = nodes.find((n) => n.id === node.parentNode);
|
||||
|
||||
if (!parentNode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parentNode.selected) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isParentSelected(parentNode, nodes);
|
||||
}
|
||||
|
||||
export function hasSelector(target: Element, selector: string, domNode: Element): boolean {
|
||||
let current = target;
|
||||
|
||||
do {
|
||||
if (current?.matches(selector)) return true;
|
||||
if (current === domNode) return false;
|
||||
current = current.parentElement as Element;
|
||||
} while (current);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// looks for all selected nodes and created a NodeDragItem for each of them
|
||||
export function getDragItems(nodes: Node[], mousePos: XYPosition, nodeId?: string): NodeDragItem[] {
|
||||
return nodes
|
||||
.filter(
|
||||
(n) =>
|
||||
(n.selected || n.id === nodeId) &&
|
||||
(n.draggable || n.draggable === undefined) &&
|
||||
(!n.parentNode || !isParentSelected(n, nodes))
|
||||
)
|
||||
.map((n) => ({
|
||||
id: n.id,
|
||||
position: n.position ? { ...n.position } : { x: 0, y: 0 },
|
||||
positionAbsolute: n.positionAbsolute ? { ...n.positionAbsolute } : { x: 0, y: 0 },
|
||||
distance: {
|
||||
x: mousePos.x - (n.positionAbsolute?.x ?? 0),
|
||||
y: mousePos.y - (n.positionAbsolute?.y ?? 0)
|
||||
},
|
||||
delta: {
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
extent: n.extent,
|
||||
parentNode: n.parentNode,
|
||||
width: n.width,
|
||||
height: n.height
|
||||
}));
|
||||
}
|
||||
|
||||
export function calcNextPosition(
|
||||
node: NodeDragItem | Node,
|
||||
nextPosition: XYPosition,
|
||||
nodes: Node[],
|
||||
nodeExtent?: CoordinateExtent
|
||||
): { position: XYPosition; positionAbsolute: XYPosition } {
|
||||
let currentExtent = node.extent || nodeExtent;
|
||||
|
||||
if (node.extent === 'parent') {
|
||||
if (node.parentNode && node.width && node.height) {
|
||||
const parent = nodes.find((n) => n.id === node.parentNode);
|
||||
const parentOrigin = parent?.origin || [0, 0];
|
||||
const nodeOrigin = node.origin || [0, 0];
|
||||
const { x: parentX, y: parentY } = getNodePositionWithOrigin(
|
||||
parent,
|
||||
parentOrigin
|
||||
).positionAbsolute;
|
||||
console.log({
|
||||
parentX,
|
||||
parentY,
|
||||
parentW: parent?.width,
|
||||
parentH: parent?.height,
|
||||
nodeW: node.width,
|
||||
nodeH: node.height,
|
||||
parentOrigin: parentOrigin[0]
|
||||
});
|
||||
currentExtent =
|
||||
parent &&
|
||||
isNumeric(parentX) &&
|
||||
isNumeric(parentY) &&
|
||||
isNumeric(parent.width) &&
|
||||
isNumeric(parent.height)
|
||||
? [
|
||||
[parentX + node.width * nodeOrigin[0], parentY + node.height * nodeOrigin[1]],
|
||||
[
|
||||
parentX + parent.width - node.width + node.width * nodeOrigin[0],
|
||||
parentY + parent.height - node.height + node.height * nodeOrigin[1]
|
||||
]
|
||||
]
|
||||
: currentExtent;
|
||||
} else {
|
||||
devWarn('005', errorMessages['error005']());
|
||||
currentExtent = nodeExtent;
|
||||
}
|
||||
} else if (node.extent && node.parentNode) {
|
||||
const parent = nodes.find((n) => n.id === node.parentNode);
|
||||
const { x: parentX, y: parentY } = getNodePositionWithOrigin(
|
||||
parent,
|
||||
parent?.origin || [0, 0]
|
||||
).positionAbsolute;
|
||||
currentExtent = [
|
||||
[node.extent[0][0] + parentX, node.extent[0][1] + parentY],
|
||||
[node.extent[1][0] + parentX, node.extent[1][1] + parentY]
|
||||
];
|
||||
}
|
||||
|
||||
let parentPosition = { x: 0, y: 0 };
|
||||
|
||||
if (node.parentNode) {
|
||||
const parent = nodes.find((n) => n.id === node.parentNode);
|
||||
parentPosition = getNodePositionWithOrigin(parent, parent?.origin || [0, 0]).positionAbsolute;
|
||||
}
|
||||
|
||||
const positionAbsolute = currentExtent
|
||||
? clampPosition(nextPosition, currentExtent as CoordinateExtent)
|
||||
: nextPosition;
|
||||
|
||||
return {
|
||||
position: {
|
||||
x: positionAbsolute.x - parentPosition.x,
|
||||
y: positionAbsolute.y - parentPosition.y
|
||||
},
|
||||
positionAbsolute
|
||||
};
|
||||
}
|
||||
@@ -1,217 +1,62 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type { Writable } from 'svelte/store';
|
||||
import { select } from 'd3-selection';
|
||||
import { zoom as d3Zoom, zoomIdentity, type D3ZoomEvent } from 'd3-zoom';
|
||||
import type {
|
||||
D3SelectionInstance,
|
||||
D3ZoomInstance,
|
||||
OnMove,
|
||||
OnMoveEnd,
|
||||
OnMoveStart,
|
||||
CoordinateExtent,
|
||||
OnPanZoom,
|
||||
PanOnScrollMode,
|
||||
PanZoomInstance,
|
||||
Transform,
|
||||
Viewport
|
||||
} from '@reactflow/system';
|
||||
import { clamp } from '@reactflow/utils';
|
||||
|
||||
const isWrappedWithClass = (event: any, className: string | undefined) =>
|
||||
event.target.closest(`.${className}`);
|
||||
|
||||
const eventToFlowTransform = (eventViewport: any): Viewport => ({
|
||||
x: eventViewport.x,
|
||||
y: eventViewport.y,
|
||||
zoom: eventViewport.k
|
||||
});
|
||||
|
||||
const isRightClickPan = (panOnDrag: FlowRendererProps['panOnDrag'], usedButton: number) =>
|
||||
usedButton === 2 && Array.isArray(panOnDrag) && panOnDrag.includes(2);
|
||||
|
||||
const viewChanged = (prevViewport: Viewport, eventViewport: any): boolean =>
|
||||
prevViewport.x !== eventViewport.x ||
|
||||
prevViewport.y !== eventViewport.y ||
|
||||
prevViewport.zoom !== eventViewport.k;
|
||||
|
||||
function filter(event: any, params: ZoomParams): boolean {
|
||||
const zoomScroll = true;
|
||||
const pinchZoom = true;
|
||||
|
||||
if (
|
||||
event.button === 1 &&
|
||||
event.type === 'mousedown' &&
|
||||
(isWrappedWithClass(event, 'svelte-flow__node') ||
|
||||
isWrappedWithClass(event, 'svelte-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, 'nopan') && 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;
|
||||
}
|
||||
import { XYPanZoom } from '@reactflow/utils';
|
||||
|
||||
type ZoomParams = {
|
||||
transform: Writable<Transform>;
|
||||
selecting: boolean;
|
||||
d3: Writable<{ zoom: D3ZoomInstance | null; selection: D3SelectionInstance | null }>;
|
||||
minZoom: number;
|
||||
maxZoom: number;
|
||||
initialViewport: Viewport;
|
||||
dragging: Writable<boolean>;
|
||||
onMoveStart?: OnMoveStart;
|
||||
onMove?: OnMove;
|
||||
onMoveEnd?: OnMoveEnd;
|
||||
onPanZoomStart?: OnPanZoom;
|
||||
onPanZoom?: OnPanZoom;
|
||||
onPanZoomEnd?: OnPanZoom;
|
||||
onPaneContextMenu?: (event: MouseEvent) => void;
|
||||
translateExtent: CoordinateExtent;
|
||||
panZoom: Writable<PanZoomInstance | null>;
|
||||
zoomOnScroll: boolean;
|
||||
zoomOnPinch: boolean;
|
||||
zoomOnDoubleClick: boolean;
|
||||
panOnScroll: boolean;
|
||||
panOnDrag: boolean | number[];
|
||||
panOnScrollSpeed: number;
|
||||
panOnScrollMode: PanOnScrollMode;
|
||||
zoomActivationKeyPressed: boolean;
|
||||
preventScrolling: boolean;
|
||||
noPanClassName: string;
|
||||
noWheelClassName: string;
|
||||
userSelectionActive: boolean;
|
||||
};
|
||||
|
||||
export default function zoom(domNode: Element, params: ZoomParams) {
|
||||
const { d3, minZoom, maxZoom, initialViewport } = params;
|
||||
const { panZoom, minZoom, maxZoom, initialViewport, transform, dragging, translateExtent } =
|
||||
params;
|
||||
|
||||
const d3ZoomInstance = d3Zoom().scaleExtent([minZoom, maxZoom]);
|
||||
const selection = select(domNode).call(d3ZoomInstance);
|
||||
|
||||
const updatedTransform = zoomIdentity
|
||||
.translate(initialViewport.x, initialViewport.y)
|
||||
.scale(clamp(initialViewport.zoom, minZoom, maxZoom));
|
||||
|
||||
const d3ZoomHandler = selection.on('wheel.zoom');
|
||||
|
||||
let mouseButton = 0;
|
||||
let isZoomingOrPanning = false;
|
||||
let prevTransform: Viewport = { x: 0, y: 0, zoom: 0 };
|
||||
let zoomedWithRightMouseButton = false;
|
||||
let timerId: any;
|
||||
|
||||
function updateZoomHandling(_params: ZoomParams) {
|
||||
const { transform, dragging, onMoveStart, onMove, onMoveEnd } = _params;
|
||||
d3ZoomInstance.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||
// we need to remember it here, because it's always 0 in the "zoom" event
|
||||
mouseButton = event.sourceEvent?.button || 0;
|
||||
|
||||
isZoomingOrPanning = true;
|
||||
|
||||
if (event.sourceEvent?.type === 'mousedown') {
|
||||
dragging.set(true);
|
||||
}
|
||||
|
||||
if (onMoveStart) {
|
||||
const flowTransform = eventToFlowTransform(event.transform);
|
||||
prevTransform = flowTransform;
|
||||
|
||||
onMoveStart?.((event.sourceEvent as MouseEvent | TouchEvent) || null, flowTransform);
|
||||
}
|
||||
});
|
||||
|
||||
d3ZoomInstance.on('zoom', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||
transform.set([event.transform.x, event.transform.y, event.transform.k]);
|
||||
|
||||
if (onMove) {
|
||||
const flowTransform = eventToFlowTransform(event.transform);
|
||||
|
||||
onMove?.((event.sourceEvent as MouseEvent | TouchEvent) || null, flowTransform);
|
||||
}
|
||||
});
|
||||
|
||||
d3ZoomInstance.on('end', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||
isZoomingOrPanning = false;
|
||||
dragging.set(false);
|
||||
|
||||
if (
|
||||
//onPaneContextMenu &&
|
||||
isRightClickPan(panOnDrag, mouseButton ?? 0) &&
|
||||
!zoomedWithRightMouseButton
|
||||
) {
|
||||
// onPaneContextMenu(event.sourceEvent);
|
||||
}
|
||||
zoomedWithRightMouseButton = false;
|
||||
|
||||
if (onMoveEnd && viewChanged(prevTransform, event.transform)) {
|
||||
const flowTransform = eventToFlowTransform(event.transform);
|
||||
prevTransform = flowTransform;
|
||||
|
||||
clearTimeout(timerId);
|
||||
timerId = setTimeout(
|
||||
() => {
|
||||
onMoveEnd?.(event.sourceEvent as MouseEvent | TouchEvent, flowTransform);
|
||||
},
|
||||
panOnScroll ? 150 : 0
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
selection.on('wheel.zoom', function (event: any, d: any) {
|
||||
if (isWrappedWithClass(event, 'nowheel')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
d3ZoomHandler!.call(this, event, d);
|
||||
});
|
||||
|
||||
d3ZoomInstance.filter((event: any) => filter(event, params));
|
||||
}
|
||||
|
||||
d3ZoomInstance.transform(selection, updatedTransform);
|
||||
|
||||
updateZoomHandling(params);
|
||||
|
||||
d3.set({
|
||||
zoom: d3ZoomInstance,
|
||||
selection
|
||||
const panZoomInstance = XYPanZoom({
|
||||
domNode,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
translateExtent,
|
||||
viewport: initialViewport,
|
||||
onTransformChange: transform.set,
|
||||
onDraggingChange: dragging.set
|
||||
});
|
||||
const { x, y, zoom } = panZoomInstance.getViewport();
|
||||
transform.set([x, y, zoom]);
|
||||
panZoom.set(panZoomInstance);
|
||||
|
||||
panZoomInstance.update(params);
|
||||
|
||||
return {
|
||||
update(params: ZoomParams) {
|
||||
updateZoomHandling(params);
|
||||
panZoomInstance.update(params);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
import { Selection } from '$lib/components/Selection';
|
||||
import drag from '$lib/actions/drag';
|
||||
|
||||
const { selectionRectMode, nodes, transform, snapGrid, updateNodePositions } = useStore();
|
||||
const store = useStore();
|
||||
const { selectionRectMode, nodes } = store;
|
||||
|
||||
$: selectedNodes = $nodes.filter((n) => n.selected);
|
||||
$: rect = getRectOfNodes(selectedNodes);
|
||||
@@ -15,7 +16,7 @@
|
||||
<div
|
||||
class="selection-wrapper nopan"
|
||||
style={`width: ${rect.width}px; height: ${rect.height}px; transform: translate(${rect.x}px, ${rect.y}px)`}
|
||||
use:drag={{ nodes, snapGrid, transform, updateNodePositions }}
|
||||
use:drag={{ disabled: false, store }}
|
||||
/>
|
||||
<Selection
|
||||
isVisible={$selectionRectMode === 'nodes'}
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
let className: string = '';
|
||||
export { className as class };
|
||||
|
||||
const { nodes, transform, nodeTypes, snapGrid, updateNodePositions, addSelectedNodes } = useStore();
|
||||
const store = useStore();
|
||||
const { nodes, nodeTypes, addSelectedNodes } = store;
|
||||
|
||||
let nodeRef: HTMLDivElement;
|
||||
const nodeTypeValid = !!$nodeTypes[type!];
|
||||
@@ -74,7 +75,7 @@
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
use:drag={{ nodeId: id, nodes, snapGrid, transform, updateNodePositions }}
|
||||
use:drag={{ nodeId: id, isSelectable: selectable, disabled: false, handleSelector: undefined, noDragClassName: 'nodrag', store }}
|
||||
bind:this={nodeRef}
|
||||
data-id={id}
|
||||
class={cc(['svelte-flow__node', `svelte-flow__node-${type || 'default'}`, className])}
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
selectionRectMode,
|
||||
selectionKeyPressed,
|
||||
selectionMode,
|
||||
resetSelectedElements,
|
||||
unselectNodesAndEdges,
|
||||
} = useStore();
|
||||
|
||||
let container: HTMLDivElement;
|
||||
@@ -61,7 +61,7 @@
|
||||
function onClick(event: MouseEvent) {
|
||||
dispatch('pane:click', event);
|
||||
|
||||
resetSelectedElements();
|
||||
unselectNodesAndEdges();
|
||||
selectionRectMode.set(null);
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
|
||||
const { x, y } = getEventPosition(event, containerBounds);
|
||||
|
||||
resetSelectedElements();
|
||||
unselectNodesAndEdges();
|
||||
|
||||
selectionRect.set({
|
||||
width: 0,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import cc from 'classcat';
|
||||
import { PanOnScrollMode, type CoordinateExtent, type Viewport } from '@reactflow/system';
|
||||
|
||||
import { Zoom } from '$lib/container/Zoom';
|
||||
import { Pane } from '$lib/container/Pane';
|
||||
import { Viewport } from '$lib/container/Viewport';
|
||||
import { Viewport as ViewportComponent } from '$lib/container/Viewport';
|
||||
import { NodeRenderer } from '$lib/container/NodeRenderer';
|
||||
import { EdgeRenderer } from '$lib/container/EdgeRenderer';
|
||||
import { UserSelection } from '$lib/components/UserSelection';
|
||||
@@ -14,6 +15,7 @@
|
||||
import { Attribution } from '$lib/components/Attribution';
|
||||
import { useStore } from '$lib/store';
|
||||
import type { SvelteFlowProps } from './types';
|
||||
import type { EdgeTypes, NodeTypes } from '$lib/types';
|
||||
|
||||
type $$Props = SvelteFlowProps;
|
||||
|
||||
@@ -21,7 +23,7 @@
|
||||
export let fitView: $$Props['fitView'] = undefined;
|
||||
export let minZoom: $$Props['minZoom'] = undefined;
|
||||
export let maxZoom: $$Props['maxZoom'] = undefined;
|
||||
export let initialViewport: $$Props['initialViewport'] = undefined;
|
||||
export let initialViewport: Viewport = { x:0, y: 0, zoom: 1 };
|
||||
export let nodeTypes: $$Props['nodeTypes'] = undefined;
|
||||
export let edgeTypes: $$Props['edgeTypes'] = undefined;
|
||||
export let selectionKey: $$Props['selectionKey'] = undefined;
|
||||
@@ -37,6 +39,15 @@
|
||||
export let onMove: $$Props['onMove'] = undefined;
|
||||
export let onMoveEnd: $$Props['onMoveEnd'] = undefined;
|
||||
export let isValidConnection: $$Props['isValidConnection'] = undefined;
|
||||
export let translateExtent: $$Props['translateExtent'] = undefined;
|
||||
export let panOnScrollMode: PanOnScrollMode = PanOnScrollMode.Free;
|
||||
export let preventScrolling: boolean = true;
|
||||
export let zoomOnScroll: boolean = true;
|
||||
export let zoomOnDoubleClick: boolean = true;
|
||||
export let zoomOnPinch: boolean = true;
|
||||
export let panOnScroll: boolean = false;
|
||||
export let panOnDrag: boolean | number[] = true;
|
||||
|
||||
export let defaultMarkerColor = '#b1b1b7';
|
||||
|
||||
export let style: $$Props['style'] = undefined;
|
||||
@@ -53,6 +64,15 @@
|
||||
store.height.set(height);
|
||||
store.domNode.set(domNode);
|
||||
|
||||
updateStore({
|
||||
nodeTypes,
|
||||
edgeTypes,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
translateExtent,
|
||||
fitView
|
||||
});
|
||||
|
||||
return () => {
|
||||
store.reset();
|
||||
}
|
||||
@@ -81,7 +101,14 @@
|
||||
})
|
||||
}
|
||||
|
||||
$: {
|
||||
function updateStore({ nodeTypes, edgeTypes, minZoom, maxZoom, translateExtent, fitView }: {
|
||||
nodeTypes?: NodeTypes,
|
||||
edgeTypes?: EdgeTypes,
|
||||
minZoom?: number,
|
||||
maxZoom?: number,
|
||||
translateExtent?: CoordinateExtent,
|
||||
fitView?: boolean
|
||||
}) {
|
||||
if (nodeTypes !== undefined) {
|
||||
store.setNodeTypes(nodeTypes);
|
||||
}
|
||||
@@ -98,10 +125,23 @@
|
||||
store.setMaxZoom(maxZoom);
|
||||
}
|
||||
|
||||
if (translateExtent !== undefined) {
|
||||
store.setTranslateExtent(translateExtent)
|
||||
}
|
||||
|
||||
if (fitView !== undefined) {
|
||||
store.fitViewOnInit.set(fitView);
|
||||
}
|
||||
}
|
||||
|
||||
$: updateStore({
|
||||
nodeTypes,
|
||||
edgeTypes,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
translateExtent,
|
||||
fitView
|
||||
})
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -114,9 +154,21 @@
|
||||
{...$$restProps}
|
||||
>
|
||||
<KeyHandler {selectionKey} {deleteKey} />
|
||||
<Zoom {initialViewport} {onMoveStart} {onMove} {onMoveEnd}>
|
||||
<Zoom
|
||||
{initialViewport}
|
||||
{onMoveStart}
|
||||
{onMove}
|
||||
{onMoveEnd}
|
||||
{panOnScrollMode}
|
||||
{preventScrolling}
|
||||
{zoomOnScroll}
|
||||
{zoomOnDoubleClick}
|
||||
{zoomOnPinch}
|
||||
{panOnScroll}
|
||||
{panOnDrag}
|
||||
>
|
||||
<Pane on:pane:click>
|
||||
<Viewport>
|
||||
<ViewportComponent>
|
||||
<EdgeRenderer on:edge:click />
|
||||
<ConnectionLine />
|
||||
<div class="svelte-flow__edgelabel-renderer" />
|
||||
@@ -130,7 +182,7 @@
|
||||
on:connect:end
|
||||
/>
|
||||
<NodeSelection />
|
||||
</Viewport>
|
||||
</ViewportComponent>
|
||||
<UserSelection />
|
||||
</Pane>
|
||||
</Zoom>
|
||||
|
||||
@@ -9,7 +9,9 @@ import type {
|
||||
SnapGrid,
|
||||
OnMoveStart,
|
||||
OnMove,
|
||||
OnMoveEnd
|
||||
OnMoveEnd,
|
||||
CoordinateExtent,
|
||||
PanOnScrollMode
|
||||
} from '@reactflow/system';
|
||||
|
||||
import type {
|
||||
@@ -39,6 +41,14 @@ export type SvelteFlowProps = DOMAttributes<HTMLDivElement> & {
|
||||
nodesDraggable?: boolean;
|
||||
nodesConnectable?: boolean;
|
||||
elementsSelectable?: boolean;
|
||||
translateExtent?: CoordinateExtent;
|
||||
panOnScrollMode?: PanOnScrollMode;
|
||||
preventScrolling?: boolean;
|
||||
zoomOnScroll?: boolean;
|
||||
zoomOnDoubleClick?: boolean;
|
||||
zoomOnPinch?: boolean;
|
||||
panOnScroll?: boolean;
|
||||
panOnDrag?: boolean | number[];
|
||||
|
||||
class?: string;
|
||||
style?: string;
|
||||
|
||||
@@ -1,32 +1,60 @@
|
||||
<script lang="ts">
|
||||
import { PanOnScrollMode } from '@reactflow/system';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import zoom from '$lib/actions/zoom';
|
||||
import type { ZoomProps } from './types';
|
||||
|
||||
type $$Props = ZoomProps;
|
||||
|
||||
export let initialViewport: $$Props['initialViewport'] = undefined;
|
||||
export let initialViewport: $$Props['initialViewport'];
|
||||
export let onMoveStart: $$Props['onMoveStart'] = undefined;
|
||||
export let onMove: $$Props['onMove'] = undefined;
|
||||
export let onMoveEnd: $$Props['onMoveEnd'] = undefined;
|
||||
export let panOnScrollMode: $$Props['panOnScrollMode'];
|
||||
export let preventScrolling: $$Props['preventScrolling'];;
|
||||
export let zoomOnScroll: $$Props['zoomOnScroll'];;
|
||||
export let zoomOnDoubleClick: $$Props['zoomOnDoubleClick'];;
|
||||
export let zoomOnPinch: $$Props['zoomOnPinch'];;
|
||||
export let panOnDrag: $$Props['panOnDrag'];
|
||||
export let panOnScroll: $$Props['panOnScroll'];;
|
||||
|
||||
const { transform, d3, selectionKeyPressed, selectionRectMode, minZoom, maxZoom, dragging } = useStore();
|
||||
const {
|
||||
transform,
|
||||
panZoom,
|
||||
selectionKeyPressed,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
dragging,
|
||||
translateExtent
|
||||
} = useStore();
|
||||
|
||||
$: viewPort = initialViewport || { x: 0, y: 0, zoom: 1 };
|
||||
$: selecting = $selectionKeyPressed || $selectionRectMode === 'user';
|
||||
</script>
|
||||
|
||||
<div class="svelte-flow__zoom" use:zoom={{
|
||||
transform,
|
||||
d3,
|
||||
selecting,
|
||||
minZoom: $minZoom,
|
||||
maxZoom: $maxZoom,
|
||||
initialViewport: viewPort,
|
||||
dragging,
|
||||
onMoveStart,
|
||||
onMove,
|
||||
onMoveEnd
|
||||
panZoom,
|
||||
onPanZoomStart: onMoveStart,
|
||||
onPanZoom: onMove,
|
||||
onPanZoomEnd: onMoveEnd,
|
||||
zoomOnScroll,
|
||||
zoomOnDoubleClick,
|
||||
zoomOnPinch,
|
||||
panOnScroll,
|
||||
panOnDrag,
|
||||
panOnScrollSpeed: 0.5,
|
||||
panOnScrollMode: panOnScrollMode || PanOnScrollMode.Free,
|
||||
zoomActivationKeyPressed: false,
|
||||
preventScrolling: typeof preventScrolling === 'boolean' ? preventScrolling : true,
|
||||
noPanClassName: 'nopan',
|
||||
noWheelClassName: 'nowheel',
|
||||
userSelectionActive: $selectionKeyPressed,
|
||||
translateExtent: $translateExtent
|
||||
}}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import type { OnMove, OnMoveStart, OnMoveEnd, Viewport } from '@reactflow/system';
|
||||
import type { OnMoveStart, OnMove, OnMoveEnd, PanOnScrollMode, Viewport } from '@reactflow/system';
|
||||
|
||||
export type ZoomProps = {
|
||||
initialViewport?: Viewport;
|
||||
initialViewport: Viewport;
|
||||
panOnScrollMode: PanOnScrollMode;
|
||||
onMove?: OnMove;
|
||||
onMoveStart?: OnMoveStart;
|
||||
onMoveEnd?: OnMoveEnd;
|
||||
preventScrolling: boolean;
|
||||
zoomOnScroll: boolean;
|
||||
zoomOnDoubleClick: boolean;
|
||||
zoomOnPinch: boolean;
|
||||
panOnScroll: boolean;
|
||||
panOnDrag: boolean | number[];
|
||||
};
|
||||
|
||||
@@ -7,12 +7,11 @@ import type {
|
||||
XYPosition,
|
||||
ZoomInOut
|
||||
} from '@reactflow/system';
|
||||
import { zoomIdentity } from 'd3-zoom';
|
||||
import { pointToRendererPoint } from '@reactflow/utils';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import type { FitViewOptions } from '$lib/types';
|
||||
import type { SvelteFlowStore } from '$lib/store/types';
|
||||
import { getD3Transition, pointToRendererPoint } from '@reactflow/utils';
|
||||
|
||||
export function useSvelteFlow(): {
|
||||
zoomIn: ZoomInOut;
|
||||
@@ -38,7 +37,7 @@ export function useSvelteFlow(): {
|
||||
width,
|
||||
height,
|
||||
maxZoom,
|
||||
d3,
|
||||
panZoom,
|
||||
nodes,
|
||||
edges
|
||||
} = useStore();
|
||||
@@ -62,23 +61,20 @@ export function useSvelteFlow(): {
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
setZoom: (zoomLevel, options) => {
|
||||
const { zoom, selection } = get(d3);
|
||||
|
||||
if (zoom && selection) {
|
||||
zoom.scaleTo(getD3Transition(selection, options?.duration), zoomLevel);
|
||||
}
|
||||
get(panZoom)?.scaleTo(zoomLevel, { duration: options?.duration });
|
||||
},
|
||||
getZoom: () => get(transform)[2],
|
||||
setViewport: (viewport, options) => {
|
||||
const [x, y, zoom] = get(transform);
|
||||
const { zoom: d3Zoom, selection } = get(d3);
|
||||
|
||||
if (d3Zoom && selection) {
|
||||
const nextTransform = zoomIdentity
|
||||
.translate(viewport.x ?? x, viewport.y ?? y)
|
||||
.scale(viewport.zoom ?? zoom);
|
||||
d3Zoom.transform(getD3Transition(selection, options?.duration), nextTransform);
|
||||
}
|
||||
get(panZoom)?.setViewport(
|
||||
{
|
||||
x: viewport.x ?? x,
|
||||
y: viewport.y ?? y,
|
||||
zoom: viewport.zoom ?? zoom
|
||||
},
|
||||
{ duration: options?.duration }
|
||||
);
|
||||
},
|
||||
getViewport: () => {
|
||||
const [x, y, zoom] = get(transform);
|
||||
@@ -88,16 +84,17 @@ export function useSvelteFlow(): {
|
||||
const _width = get(width);
|
||||
const _height = get(height);
|
||||
const _maxZoom = get(maxZoom);
|
||||
const { zoom, selection } = get(d3);
|
||||
|
||||
if (zoom && selection) {
|
||||
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : _maxZoom;
|
||||
const centerX = _width / 2 - x * nextZoom;
|
||||
const centerY = _height / 2 - y * nextZoom;
|
||||
const transform = zoomIdentity.translate(centerX, centerY).scale(nextZoom);
|
||||
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : _maxZoom;
|
||||
|
||||
zoom.transform(getD3Transition(selection, options?.duration), transform);
|
||||
}
|
||||
get(panZoom)?.setViewport(
|
||||
{
|
||||
x: _width / 2 - x * nextZoom,
|
||||
y: _height / 2 - y * nextZoom,
|
||||
zoom: nextZoom
|
||||
},
|
||||
{ duration: options?.duration }
|
||||
);
|
||||
},
|
||||
fitView,
|
||||
project: (position: XYPosition) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
<script lang="ts">
|
||||
import SvelteFlow, {
|
||||
SvelteFlowProvider,
|
||||
Controls,
|
||||
Panel,
|
||||
createNodes,
|
||||
createEdges,
|
||||
PanOnScrollMode,
|
||||
Minimap,
|
||||
type OnMoveEnd,
|
||||
type Node,
|
||||
type Edge,
|
||||
} from '../../lib/index';
|
||||
|
||||
const nodes = createNodes([
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
data: { label: 'Node 1' },
|
||||
position: { x: 250, y: 5 },
|
||||
},
|
||||
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
|
||||
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
|
||||
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
|
||||
]);
|
||||
|
||||
const edges = createEdges([
|
||||
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
]);
|
||||
|
||||
const onNodeDragStart = (_: MouseEvent, node: Node) => console.log('drag start', node);
|
||||
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
|
||||
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
|
||||
const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge);
|
||||
const onPaneClick = (event: MouseEvent) => console.log('onPaneClick', event);
|
||||
const onPaneScroll = (event?: WheelEvent) => console.log('onPaneScroll', event);
|
||||
const onPaneContextMenu = (event: MouseEvent) => console.log('onPaneContextMenu', event);
|
||||
const onMoveEnd: OnMoveEnd = (_, viewport) => console.log('onMoveEnd', viewport);
|
||||
|
||||
|
||||
let isSelectable = false;
|
||||
let isDraggable = false;
|
||||
let isConnectable = false;
|
||||
let zoomOnScroll = false;
|
||||
let zoomOnPinch = false;
|
||||
let panOnScroll = false;
|
||||
let panOnScrollMode = PanOnScrollMode.Free;
|
||||
let zoomOnDoubleClick = false;
|
||||
let panOnDrag = true;
|
||||
let captureZoomClick = false;
|
||||
let captureZoomScroll = false;
|
||||
let captureElementClick = false;
|
||||
</script>
|
||||
|
||||
|
||||
<SvelteFlowProvider
|
||||
{nodes}
|
||||
{edges}
|
||||
>
|
||||
<SvelteFlow
|
||||
elementsSelectable={isSelectable}
|
||||
nodesConnectable={isConnectable}
|
||||
nodesDraggable={isDraggable}
|
||||
zoomOnScroll={zoomOnScroll}
|
||||
zoomOnPinch={zoomOnPinch}
|
||||
panOnScroll={panOnScroll}
|
||||
panOnScrollMode={panOnScrollMode}
|
||||
zoomOnDoubleClick={zoomOnDoubleClick}
|
||||
panOnDrag={panOnDrag}
|
||||
onMoveEnd={onMoveEnd}
|
||||
>
|
||||
<Minimap />
|
||||
<Controls />
|
||||
|
||||
<Panel position="top-right">
|
||||
<div>
|
||||
<label for="draggable">
|
||||
nodesDraggable
|
||||
<input
|
||||
id="draggable"
|
||||
type="checkbox"
|
||||
bind:checked={isDraggable}
|
||||
class="react-flow__draggable"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="connectable">
|
||||
nodesConnectable
|
||||
<input
|
||||
id="connectable"
|
||||
type="checkbox"
|
||||
bind:checked={isConnectable}
|
||||
class="react-flow__connectable"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="selectable">
|
||||
elementsSelectable
|
||||
<input
|
||||
id="selectable"
|
||||
type="checkbox"
|
||||
bind:checked={isSelectable}
|
||||
class="react-flow__selectable"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="zoomonscroll">
|
||||
zoomOnScroll
|
||||
<input
|
||||
id="zoomonscroll"
|
||||
type="checkbox"
|
||||
bind:checked={zoomOnScroll}
|
||||
class="react-flow__zoomonscroll"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="zoomonpinch">
|
||||
zoomOnPinch
|
||||
<input
|
||||
id="zoomonpinch"
|
||||
type="checkbox"
|
||||
bind:checked={zoomOnPinch}
|
||||
class="react-flow__zoomonpinch"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="panonscroll">
|
||||
panOnScroll
|
||||
<input
|
||||
id="panonscroll"
|
||||
type="checkbox"
|
||||
bind:checked={panOnScroll}
|
||||
class="react-flow__panonscroll"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="panonscrollmode">
|
||||
panOnScrollMode
|
||||
<select
|
||||
id="panonscrollmode"
|
||||
bind:value={panOnScrollMode}
|
||||
on:change={(event) => { panOnScrollMode = PanOnScrollMode.Free }}
|
||||
class="react-flow__panonscrollmode"
|
||||
>
|
||||
<option value="free">free</option>
|
||||
<option value="horizontal">horizontal</option>
|
||||
<option value="vertical">vertical</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="zoomondbl">
|
||||
zoomOnDoubleClick
|
||||
<input
|
||||
id="zoomondbl"
|
||||
type="checkbox"
|
||||
bind:checked={zoomOnDoubleClick}
|
||||
class="react-flow__zoomondbl"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="panondrag">
|
||||
panOnDrag
|
||||
<input
|
||||
id="panondrag"
|
||||
type="checkbox"
|
||||
bind:checked={panOnDrag}
|
||||
class="react-flow__panondrag"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="capturezoompaneclick">
|
||||
capture onPaneClick
|
||||
<input
|
||||
id="capturezoompaneclick"
|
||||
type="checkbox"
|
||||
bind:checked={captureZoomClick}
|
||||
class="react-flow__capturezoompaneclick"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="capturezoompanescroll">
|
||||
capture onPaneScroll
|
||||
<input
|
||||
id="capturezoompanescroll"
|
||||
type="checkbox"
|
||||
bind:checked={captureZoomScroll}
|
||||
class="react-flow__capturezoompanescroll"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="captureelementclick">
|
||||
capture onElementClick
|
||||
<input
|
||||
id="captureelementclick"
|
||||
type="checkbox"
|
||||
bind:checked={captureElementClick}
|
||||
class="react-flow__captureelementclick"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</Panel>
|
||||
</SvelteFlow>
|
||||
</SvelteFlowProvider>
|
||||
@@ -11,8 +11,8 @@
|
||||
type Edge
|
||||
} from '../../lib/index';
|
||||
|
||||
const yNodes = 10;
|
||||
const xNodes = 10;
|
||||
const yNodes = 20;
|
||||
const xNodes = 20;
|
||||
|
||||
const nodeItems: Node[] = [];
|
||||
const edgeItems: Edge[] = [];
|
||||
@@ -55,6 +55,7 @@
|
||||
>
|
||||
<SvelteFlow
|
||||
fitView
|
||||
minZoom={0.2}
|
||||
>
|
||||
<Controls />
|
||||
<Background variant={BackgroundVariant.Dots} />
|
||||
|
||||
Reference in New Issue
Block a user