mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-24 23:22:38 +08:00
fix(editor): Fix editor pan / zoom when inserting nodes (#22401)
This commit is contained in:
@@ -727,7 +727,9 @@ async function onClipboardPaste(plainTextData: string): Promise<void> {
|
||||
importTags: false,
|
||||
viewport: viewportBoundaries.value,
|
||||
});
|
||||
selectNodes(result.nodes?.map((node) => node.id) ?? []);
|
||||
const ids = result.nodes?.map((node) => node.id) ?? [];
|
||||
|
||||
canvasRef.value?.ensureNodesAreVisible(ids);
|
||||
}
|
||||
|
||||
async function onCutNodes(ids: string[]) {
|
||||
@@ -747,7 +749,7 @@ async function onDuplicateNodes(ids: string[]) {
|
||||
viewport: viewportBoundaries.value,
|
||||
});
|
||||
|
||||
selectNodes(newIds);
|
||||
canvasRef.value?.ensureNodesAreVisible(newIds);
|
||||
}
|
||||
|
||||
function onPinNodes(ids: string[], source: PinDataSource) {
|
||||
@@ -1042,8 +1044,7 @@ async function onImportWorkflowUrlEvent(data: IDataObject) {
|
||||
viewport: viewportBoundaries.value,
|
||||
});
|
||||
|
||||
fitView();
|
||||
selectNodes(workflowData.nodes?.map((node) => node.id) ?? []);
|
||||
canvasRef.value?.ensureNodesAreVisible(workflowData.nodes?.map((node) => node.id) ?? []);
|
||||
}
|
||||
|
||||
function addImportEventBindings() {
|
||||
|
||||
+75
-10
@@ -1,15 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import Canvas from './Canvas.vue';
|
||||
import { computed, ref, toRef, useCssModule, useTemplateRef } from 'vue';
|
||||
import type { Workflow } from 'n8n-workflow';
|
||||
import type { ContextMenuAction } from '@/features/shared/contextMenu/composables/useContextMenuItems';
|
||||
import type { IWorkflowDb } from '@/Interface';
|
||||
import { useCanvasMapping } from '../composables/useCanvasMapping';
|
||||
import type { EventBus } from '@n8n/utils/event-bus';
|
||||
import { createEventBus } from '@n8n/utils/event-bus';
|
||||
import type { CanvasEventBusEvents } from '../canvas.types';
|
||||
import { useVueFlow } from '@vue-flow/core';
|
||||
import { getRectOfNodes, useVueFlow } from '@vue-flow/core';
|
||||
import { throttledRef } from '@vueuse/core';
|
||||
import type { ContextMenuAction } from '@/features/shared/contextMenu/composables/useContextMenuItems';
|
||||
import type { Workflow } from 'n8n-workflow';
|
||||
import { computed, ref, toRef, useCssModule, useTemplateRef } from 'vue';
|
||||
import type { CanvasEventBusEvents } from '../canvas.types';
|
||||
import { useCanvasMapping } from '../composables/useCanvasMapping';
|
||||
import Canvas from './Canvas.vue';
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
@@ -39,7 +39,7 @@ const props = withDefaults(
|
||||
const canvasRef = useTemplateRef('canvas');
|
||||
const $style = useCssModule();
|
||||
|
||||
const { onNodesInitialized } = useVueFlow(props.id);
|
||||
const { onNodesInitialized, viewport, viewportRef, getNodes, fitBounds } = useVueFlow(props.id);
|
||||
|
||||
const workflow = toRef(props, 'workflow');
|
||||
const workflowObject = toRef(props, 'workflowObject');
|
||||
@@ -58,10 +58,11 @@ const { nodes: mappedNodes, connections: mappedConnections } = useCanvasMapping(
|
||||
});
|
||||
|
||||
const initialFitViewDone = ref(false); // Workaround for https://github.com/bcakmakoglu/vue-flow/issues/1636
|
||||
onNodesInitialized(() => {
|
||||
if (!initialFitViewDone.value || props.showFallbackNodes) {
|
||||
const { off } = onNodesInitialized(() => {
|
||||
if (!initialFitViewDone.value) {
|
||||
props.eventBus.emit('fitView');
|
||||
initialFitViewDone.value = true;
|
||||
off();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -71,6 +72,70 @@ const mappedConnectionsThrottled = throttledRef(mappedConnections, 200);
|
||||
defineExpose({
|
||||
executeContextMenuAction: (action: ContextMenuAction, nodeIds: string[]) =>
|
||||
canvasRef.value?.executeContextMenuAction(action, nodeIds),
|
||||
ensureNodesAreVisible: (ids: string[]) => {
|
||||
const canvasElement = viewportRef.value;
|
||||
|
||||
if (!canvasElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find nodes by IDs
|
||||
const targetNodes = getNodes.value.filter((node) => ids.includes(node.id));
|
||||
|
||||
if (targetNodes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const insertionDone = onNodesInitialized(() => {
|
||||
// Get the current viewport after nodes are initialized
|
||||
const vp = viewport.value;
|
||||
const canvasWidth = canvasElement.clientWidth;
|
||||
const canvasHeight = canvasElement.clientHeight;
|
||||
|
||||
// Get the rect of the newly added nodes
|
||||
const nodesRect = getRectOfNodes(targetNodes);
|
||||
|
||||
// Check if nodes are visible in current viewport
|
||||
const screenX = nodesRect.x * vp.zoom + vp.x;
|
||||
const screenY = nodesRect.y * vp.zoom + vp.y;
|
||||
const screenWidth = nodesRect.width * vp.zoom;
|
||||
const screenHeight = nodesRect.height * vp.zoom;
|
||||
|
||||
const isFullyVisible =
|
||||
screenX >= 0 &&
|
||||
screenY >= 0 &&
|
||||
screenX + screenWidth <= canvasWidth &&
|
||||
screenY + screenHeight <= canvasHeight;
|
||||
|
||||
if (!isFullyVisible) {
|
||||
// Calculate viewport bounds in canvas coordinates
|
||||
const viewportRect = {
|
||||
x: -vp.x / vp.zoom,
|
||||
y: -vp.y / vp.zoom,
|
||||
width: canvasWidth / vp.zoom,
|
||||
height: canvasHeight / vp.zoom,
|
||||
};
|
||||
|
||||
// Combine current viewport with nodes rect
|
||||
const minX = Math.min(viewportRect.x, nodesRect.x);
|
||||
const minY = Math.min(viewportRect.y, nodesRect.y);
|
||||
const maxX = Math.max(viewportRect.x + viewportRect.width, nodesRect.x + nodesRect.width);
|
||||
const maxY = Math.max(viewportRect.y + viewportRect.height, nodesRect.y + nodesRect.height);
|
||||
|
||||
const combinedRect = {
|
||||
x: minX,
|
||||
y: minY,
|
||||
width: maxX - minX,
|
||||
height: maxY - minY,
|
||||
};
|
||||
|
||||
void fitBounds(combinedRect, { padding: 0.15, duration: 100 });
|
||||
}
|
||||
|
||||
props.eventBus.emit('nodes:select', { ids });
|
||||
insertionDone.off();
|
||||
});
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user