diff --git a/examples/vite/router.ts b/examples/vite/router.ts
index a1f4180b3..c28dd5020 100644
--- a/examples/vite/router.ts
+++ b/examples/vite/router.ts
@@ -138,6 +138,14 @@ export const routes: RouterOptions['routes'] = [
path: '/confirm-delete',
component: () => import('./src/ConfirmDelete/ConfirmDeleteExample.vue'),
},
+ {
+ path: '/nesting-flows',
+ component: () => import('./src/NestingFlows/NestingFlows.vue'),
+ },
+ {
+ path: '/recursive-nesting-flows',
+ component: () => import('./src/RecursiveNestingFlows/RecursiveNestingFlows.vue'),
+ },
]
export const router = createRouter({
diff --git a/examples/vite/src/NestingFlows/NestedFlow.vue b/examples/vite/src/NestingFlows/NestedFlow.vue
new file mode 100644
index 000000000..049d770f3
--- /dev/null
+++ b/examples/vite/src/NestingFlows/NestedFlow.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/vite/src/NestingFlows/NestedFlowNode.vue b/examples/vite/src/NestingFlows/NestedFlowNode.vue
new file mode 100644
index 000000000..7f0182f7b
--- /dev/null
+++ b/examples/vite/src/NestingFlows/NestedFlowNode.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
{{ data.label }}
+
+
+
+
+
+
+
diff --git a/examples/vite/src/NestingFlows/NestingFlows.ts b/examples/vite/src/NestingFlows/NestingFlows.ts
new file mode 100644
index 000000000..048d63c31
--- /dev/null
+++ b/examples/vite/src/NestingFlows/NestingFlows.ts
@@ -0,0 +1,4 @@
+let VueFlowInstanceId = 0
+export function newVueFlowInstanceID(): number {
+ return VueFlowInstanceId++
+}
diff --git a/examples/vite/src/NestingFlows/NestingFlows.vue b/examples/vite/src/NestingFlows/NestingFlows.vue
new file mode 100644
index 000000000..7e3bde9bb
--- /dev/null
+++ b/examples/vite/src/NestingFlows/NestingFlows.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/vite/src/RecursiveNestingFlows/NestedFlowNode.vue b/examples/vite/src/RecursiveNestingFlows/NestedFlowNode.vue
new file mode 100644
index 000000000..3b27fbb07
--- /dev/null
+++ b/examples/vite/src/RecursiveNestingFlows/NestedFlowNode.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
{{ data.label }}
+
+
+
+
+
+
+
diff --git a/examples/vite/src/RecursiveNestingFlows/NestingFlows.ts b/examples/vite/src/RecursiveNestingFlows/NestingFlows.ts
new file mode 100644
index 000000000..048d63c31
--- /dev/null
+++ b/examples/vite/src/RecursiveNestingFlows/NestingFlows.ts
@@ -0,0 +1,4 @@
+let VueFlowInstanceId = 0
+export function newVueFlowInstanceID(): number {
+ return VueFlowInstanceId++
+}
diff --git a/examples/vite/src/RecursiveNestingFlows/RecursiveNestingFlows.vue b/examples/vite/src/RecursiveNestingFlows/RecursiveNestingFlows.vue
new file mode 100644
index 000000000..f0a455438
--- /dev/null
+++ b/examples/vite/src/RecursiveNestingFlows/RecursiveNestingFlows.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/core/src/components/ConnectionLine/index.ts b/packages/core/src/components/ConnectionLine/index.ts
index 67227e593..b32576c93 100644
--- a/packages/core/src/components/ConnectionLine/index.ts
+++ b/packages/core/src/components/ConnectionLine/index.ts
@@ -21,6 +21,7 @@ const ConnectionLine = defineComponent({
connectionLineOptions,
connectionStatus,
viewport,
+ ancestorZoom,
findNode,
} = useVueFlow()
@@ -32,8 +33,8 @@ const ConnectionLine = defineComponent({
const toXY = computed(() => {
return {
- x: (connectionPosition.value.x - viewport.value.x) / viewport.value.zoom,
- y: (connectionPosition.value.y - viewport.value.y) / viewport.value.zoom,
+ x: (connectionPosition.value.x - viewport.value.x) / viewport.value.zoom / ancestorZoom.value,
+ y: (connectionPosition.value.y - viewport.value.y) / viewport.value.zoom / ancestorZoom.value,
}
})
diff --git a/packages/core/src/components/Nodes/NodeWrapper.ts b/packages/core/src/components/Nodes/NodeWrapper.ts
index afcd8d8ed..9fa86ad1b 100644
--- a/packages/core/src/components/Nodes/NodeWrapper.ts
+++ b/packages/core/src/components/Nodes/NodeWrapper.ts
@@ -266,7 +266,7 @@ const NodeWrapper = defineComponent({
'vue-flow__node',
`vue-flow__node-${nodeCmp.value === false ? 'default' : node.type || 'default'}`,
{
- [noPanClassName.value]: isDraggable.value,
+ // [noPanClassName.value]: isDraggable.value,
dragging: dragging?.value,
draggable: isDraggable.value,
selected: node.selected,
diff --git a/packages/core/src/composables/useGetPointerPosition.ts b/packages/core/src/composables/useGetPointerPosition.ts
index 4104befc9..6dbeca0ba 100644
--- a/packages/core/src/composables/useGetPointerPosition.ts
+++ b/packages/core/src/composables/useGetPointerPosition.ts
@@ -9,7 +9,7 @@ import type { UseDragEvent } from './useDrag'
* @internal
*/
export function useGetPointerPosition() {
- const { viewport, snapGrid, snapToGrid, vueFlowRef } = useVueFlow()
+ const { viewport, snapGrid, snapToGrid, vueFlowRef, ancestorZoom } = useVueFlow()
// returns the pointer position projected to the VF coordinate system
return (event: UseDragEvent | MouseTouchEvent) => {
@@ -17,7 +17,7 @@ export function useGetPointerPosition() {
const evt = isUseDragEvent(event) ? event.sourceEvent : event
const { x, y } = getEventPosition(evt, containerBounds as DOMRect)
- const pointerPos = pointToRendererPoint({ x, y }, viewport.value)
+ const pointerPos = pointToRendererPoint({ x, y }, viewport.value, undefined, undefined, ancestorZoom.value)
const { x: xSnapped, y: ySnapped } = snapToGrid.value ? snapPosition(pointerPos, snapGrid.value) : pointerPos
// we need the snapped position to be able to skip unnecessary drag events
diff --git a/packages/core/src/composables/useHandle.ts b/packages/core/src/composables/useHandle.ts
index 158b4f395..988188f7a 100644
--- a/packages/core/src/composables/useHandle.ts
+++ b/packages/core/src/composables/useHandle.ts
@@ -68,6 +68,7 @@ export function useHandle({
endConnection,
emits,
viewport,
+ ancestorZoom,
edges,
nodes,
isValidConnection: isValidConnectionProp,
@@ -116,6 +117,8 @@ export function useHandle({
let prevActiveHandle: Element
let connectionPosition = getEventPosition(event, containerBounds)
+ connectionPosition.x += viewport.value.x * (1 - ancestorZoom.value)
+ connectionPosition.y += viewport.value.y * (1 - ancestorZoom.value)
let autoPanStarted = false
// when the user is moving the mouse close to the edge of the canvas while connecting we move the canvas
@@ -177,9 +180,11 @@ export function useHandle({
function onPointerMove(event: MouseTouchEvent) {
connectionPosition = getEventPosition(event, containerBounds)
+ connectionPosition.x += viewport.value.x * (1 - ancestorZoom.value)
+ connectionPosition.y += viewport.value.y * (1 - ancestorZoom.value)
closestHandle = getClosestHandle(
- pointToRendererPoint(connectionPosition, viewport.value, false, [1, 1]),
+ pointToRendererPoint(connectionPosition, viewport.value, false, [1, 1], ancestorZoom.value),
connectionRadius.value,
nodeLookup.value,
fromHandle,
@@ -219,7 +224,7 @@ export function useHandle({
isValid,
to:
result.toHandle && isValid
- ? rendererPointToPoint({ x: result.toHandle.x, y: result.toHandle.y }, viewport.value)
+ ? rendererPointToPoint({ x: result.toHandle.x, y: result.toHandle.y }, viewport.value, ancestorZoom.value)
: connectionPosition,
toHandle: result.toHandle,
toPosition: isValid && result.toHandle ? result.toHandle.position : oppositePosition[fromHandle.position],
@@ -250,6 +255,7 @@ export function useHandle({
y: closestHandle.y,
},
viewport.value,
+ ancestorZoom.value,
)
: connectionPosition,
result.toHandle,
diff --git a/packages/core/src/composables/useViewportHelper.ts b/packages/core/src/composables/useViewportHelper.ts
index 417e95d4b..3b6fabe24 100644
--- a/packages/core/src/composables/useViewportHelper.ts
+++ b/packages/core/src/composables/useViewportHelper.ts
@@ -194,7 +194,7 @@ export function useViewportHelper(state: State) {
y: position.y - domY,
}
- return pointToRendererPoint(correctedPosition, state.viewport, state.snapToGrid, state.snapGrid)
+ return pointToRendererPoint(correctedPosition, state.viewport, state.snapToGrid, state.snapGrid, state.ancestorZoom)
}
return { x: 0, y: 0 }
@@ -208,7 +208,7 @@ export function useViewportHelper(state: State) {
y: position.y + domY,
}
- return rendererPointToPoint(correctedPosition, state.viewport)
+ return rendererPointToPoint(correctedPosition, state.viewport, state.ancestorZoom)
}
return { x: 0, y: 0 }
diff --git a/packages/core/src/container/Viewport/Viewport.vue b/packages/core/src/container/Viewport/Viewport.vue
index 3d98d0fab..b05710572 100644
--- a/packages/core/src/container/Viewport/Viewport.vue
+++ b/packages/core/src/container/Viewport/Viewport.vue
@@ -38,6 +38,7 @@ const {
d3Selection: storeD3Selection,
d3ZoomHandler: storeD3ZoomHandler,
viewport,
+ ancestorZoom,
viewportRef,
paneClickDistance,
} = useVueFlow()
@@ -245,6 +246,7 @@ onMounted(() => {
return (!event.ctrlKey || panKeyPressed.value || event.type === 'wheel') && buttonAllowed
})
+ let prevEventTransform: ZoomTransform = zoomIdentity
watch(
[userSelectionActive, shouldPanOnDrag],
() => {
@@ -252,7 +254,12 @@ onMounted(() => {
d3Zoom.on('zoom', null)
} else if (!userSelectionActive.value) {
d3Zoom.on('zoom', (event: D3ZoomEvent) => {
- viewport.value = { x: event.transform.x, y: event.transform.y, zoom: event.transform.k }
+ viewport.value = {
+ x: viewport.value.x + (event.transform.x - prevEventTransform.x) / ancestorZoom.value,
+ y: viewport.value.y + (event.transform.y - prevEventTransform.y) / ancestorZoom.value,
+ zoom: event.transform.k,
+ }
+ prevEventTransform = event.transform
const flowTransform = eventToFlowTransform(event.transform)
diff --git a/packages/core/src/container/VueFlow/VueFlow.vue b/packages/core/src/container/VueFlow/VueFlow.vue
index f71a823b3..e8064935e 100644
--- a/packages/core/src/container/VueFlow/VueFlow.vue
+++ b/packages/core/src/container/VueFlow/VueFlow.vue
@@ -27,6 +27,7 @@ const props = withDefaults(defineProps(), {
zoomOnDoubleClick: undefined,
panOnScroll: undefined,
panOnDrag: undefined,
+ ancestorZoom: undefined,
applyDefault: undefined,
fitViewOnInit: undefined,
connectOnClick: undefined,
diff --git a/packages/core/src/store/actions.ts b/packages/core/src/store/actions.ts
index ab8616d1a..f0b4ad378 100644
--- a/packages/core/src/store/actions.ts
+++ b/packages/core/src/store/actions.ts
@@ -157,8 +157,8 @@ export function useActions(state: State, nodeLookup: ComputedRef, ed
if (doUpdate) {
const nodeBounds = update.nodeElement.getBoundingClientRect()
node.dimensions = dimensions
- node.handleBounds.source = getHandleBounds('source', update.nodeElement, nodeBounds, zoom, node.id)
- node.handleBounds.target = getHandleBounds('target', update.nodeElement, nodeBounds, zoom, node.id)
+ node.handleBounds.source = getHandleBounds('source', update.nodeElement, nodeBounds, zoom, state.ancestorZoom, node.id)
+ node.handleBounds.target = getHandleBounds('target', update.nodeElement, nodeBounds, zoom, state.ancestorZoom, node.id)
changes.push({
id: node.id,
diff --git a/packages/core/src/store/state.ts b/packages/core/src/store/state.ts
index 2198b9465..a8582102d 100644
--- a/packages/core/src/store/state.ts
+++ b/packages/core/src/store/state.ts
@@ -21,6 +21,7 @@ export function useState(): State {
height: 0,
},
viewport: { x: 0, y: 0, zoom: 1 },
+ ancestorZoom: 1,
d3Zoom: null,
d3Selection: null,
diff --git a/packages/core/src/types/flow.ts b/packages/core/src/types/flow.ts
index 4ea8134aa..734476242 100644
--- a/packages/core/src/types/flow.ts
+++ b/packages/core/src/types/flow.ts
@@ -179,6 +179,7 @@ export interface FlowProps {
panOnDrag?: boolean | number[]
minZoom?: number
maxZoom?: number
+ ancestorZoom?: number
defaultViewport?: Partial
translateExtent?: CoordinateExtent
nodeExtent?: CoordinateExtent | CoordinateExtentRange
diff --git a/packages/core/src/types/store.ts b/packages/core/src/types/store.ts
index 0fd614f82..f923cf7ed 100644
--- a/packages/core/src/types/store.ts
+++ b/packages/core/src/types/store.ts
@@ -67,6 +67,7 @@ export interface State extends Omit {
minZoom: number
/** use setMaxZoom action to change maxZoom */
maxZoom: number
+ ancestorZoom: number
defaultViewport: Partial
/** use setTranslateExtent action to change translateExtent */
translateExtent: CoordinateExtent
diff --git a/packages/core/src/utils/graph.ts b/packages/core/src/utils/graph.ts
index 209709b76..e50554d3f 100644
--- a/packages/core/src/utils/graph.ts
+++ b/packages/core/src/utils/graph.ts
@@ -289,10 +289,14 @@ export function updateEdge(oldEdge: Edge, newConnection: Connection, elements: E
return elements.filter((e) => e.id !== oldEdge.id)
}
-export function rendererPointToPoint({ x, y }: XYPosition, { x: tx, y: ty, zoom: tScale }: ViewportTransform): XYPosition {
+export function rendererPointToPoint(
+ { x, y }: XYPosition,
+ { x: tx, y: ty, zoom: tScale }: ViewportTransform,
+ ancestorZoom: number,
+): XYPosition {
return {
- x: x * tScale + tx,
- y: y * tScale + ty,
+ x: x * tScale * ancestorZoom + tx,
+ y: y * tScale * ancestorZoom + ty,
}
}
export function pointToRendererPoint(
@@ -300,10 +304,11 @@ export function pointToRendererPoint(
{ x: tx, y: ty, zoom: tScale }: ViewportTransform,
snapToGrid: boolean = false,
snapGrid: [snapX: number, snapY: number] = [1, 1],
+ ancestorZoom = 1,
): XYPosition {
const position: XYPosition = {
- x: (x - tx) / tScale,
- y: (y - ty) / tScale,
+ x: (x - tx) / (tScale * ancestorZoom),
+ y: (y - ty) / (tScale * ancestorZoom),
}
return snapToGrid ? snapPosition(position, snapGrid) : position
diff --git a/packages/core/src/utils/node.ts b/packages/core/src/utils/node.ts
index 12855cf17..0961aaded 100644
--- a/packages/core/src/utils/node.ts
+++ b/packages/core/src/utils/node.ts
@@ -8,6 +8,7 @@ export function getHandleBounds(
nodeElement: HTMLDivElement,
nodeBounds: DOMRect,
zoom: number,
+ ancestorZoom: number,
nodeId: string,
): HandleElement[] | null {
const handles = nodeElement.querySelectorAll(`.vue-flow__handle.${type}`)
@@ -24,8 +25,8 @@ export function getHandleBounds(
type,
nodeId,
position: handle.getAttribute('data-handlepos') as unknown as Position,
- x: (handleBounds.left - nodeBounds.left) / zoom,
- y: (handleBounds.top - nodeBounds.top) / zoom,
+ x: (handleBounds.left - nodeBounds.left) / zoom / ancestorZoom,
+ y: (handleBounds.top - nodeBounds.top) / zoom / ancestorZoom,
...getDimensions(handle as HTMLDivElement),
}
})