mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(editor): Migrate moveNodeWorkflow
mixin to useCanvasPanning
composable (#8322)
This commit is contained in:
parent
75d3ecca1c
commit
b6d775768f
|
@ -0,0 +1,97 @@
|
|||
import type { Ref } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import { useCanvasPanning } from '@/composables/useCanvasPanning';
|
||||
import { getMousePosition } from '@/utils/nodeViewUtils';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
|
||||
vi.mock('@/stores/ui.store', () => ({
|
||||
useUIStore: vi.fn(() => ({
|
||||
nodeViewOffsetPosition: [0, 0],
|
||||
nodeViewMoveInProgress: false,
|
||||
isActionActive: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/nodeViewUtils', () => ({
|
||||
getMousePosition: vi.fn(() => [0, 0]),
|
||||
}));
|
||||
|
||||
describe('useCanvasPanning()', () => {
|
||||
let element: HTMLElement;
|
||||
let elementRef: Ref<null | HTMLElement>;
|
||||
|
||||
beforeEach(() => {
|
||||
element = document.createElement('div');
|
||||
element.id = 'node-view';
|
||||
elementRef = ref(element);
|
||||
document.body.appendChild(element);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(element);
|
||||
});
|
||||
|
||||
describe('onMouseDown()', () => {
|
||||
it('should attach mousemove event listener on mousedown', () => {
|
||||
const addEventListenerSpy = vi.spyOn(element, 'addEventListener');
|
||||
const { onMouseDown, onMouseMove } = useCanvasPanning(elementRef);
|
||||
const mouseEvent = new MouseEvent('mousedown', { button: 1 });
|
||||
|
||||
onMouseDown(mouseEvent, true);
|
||||
|
||||
expect(addEventListenerSpy).toHaveBeenCalledWith('mousemove', onMouseMove);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onMouseMove()', () => {
|
||||
it('should update node view position on mousemove', () => {
|
||||
vi.mocked(getMousePosition).mockReturnValueOnce([0, 0]).mockReturnValueOnce([100, 100]);
|
||||
const { onMouseDown, onMouseMove, moveLastPosition } = useCanvasPanning(elementRef);
|
||||
|
||||
expect(moveLastPosition.value).toEqual([0, 0]);
|
||||
|
||||
onMouseDown(new MouseEvent('mousedown', { button: 1 }), true);
|
||||
onMouseMove(new MouseEvent('mousemove', { buttons: 4 }));
|
||||
|
||||
expect(moveLastPosition.value).toEqual([100, 100]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onMouseUp()', () => {
|
||||
it('should remove mousemove event listener on mouseup', () => {
|
||||
vi.mocked(useUIStore).mockReturnValueOnce({
|
||||
nodeViewOffsetPosition: [0, 0],
|
||||
nodeViewMoveInProgress: true,
|
||||
isActionActive: vi.fn(),
|
||||
} as unknown as ReturnType<typeof useUIStore>);
|
||||
|
||||
const removeEventListenerSpy = vi.spyOn(element, 'removeEventListener');
|
||||
const { onMouseDown, onMouseMove, onMouseUp } = useCanvasPanning(elementRef);
|
||||
|
||||
onMouseDown(new MouseEvent('mousedown', { button: 1 }), true);
|
||||
onMouseUp(new MouseEvent('mouseup'));
|
||||
|
||||
expect(removeEventListenerSpy).toHaveBeenCalledWith('mousemove', onMouseMove);
|
||||
});
|
||||
});
|
||||
|
||||
describe('panCanvas()', () => {
|
||||
it('should update node view offset position correctly', () => {
|
||||
vi.mocked(getMousePosition).mockReturnValueOnce([100, 100]);
|
||||
|
||||
const { panCanvas } = useCanvasPanning(elementRef);
|
||||
const [x, y] = panCanvas(new MouseEvent('mousemove'));
|
||||
|
||||
expect(x).toEqual(100);
|
||||
expect(y).toEqual(100);
|
||||
});
|
||||
|
||||
it('should not update offset position if mouse is not moved', () => {
|
||||
const { panCanvas } = useCanvasPanning(elementRef);
|
||||
const [x, y] = panCanvas(new MouseEvent('mousemove'));
|
||||
|
||||
expect(x).toEqual(0);
|
||||
expect(y).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
126
packages/editor-ui/src/composables/useCanvasPanning.ts
Normal file
126
packages/editor-ui/src/composables/useCanvasPanning.ts
Normal file
|
@ -0,0 +1,126 @@
|
|||
import type { Ref } from 'vue';
|
||||
import { ref, unref } from 'vue';
|
||||
|
||||
import { getMousePosition } from '@/utils/nodeViewUtils';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useDeviceSupport } from 'n8n-design-system';
|
||||
import { MOUSE_EVENT_BUTTON, MOUSE_EVENT_BUTTONS } from '@/constants';
|
||||
|
||||
/**
|
||||
* Composable for handling canvas panning interactions - it facilitates the movement of the
|
||||
* canvas element in response to mouse events
|
||||
*/
|
||||
export function useCanvasPanning(
|
||||
elementRef: Ref<null | HTMLElement>,
|
||||
options: {
|
||||
// @TODO To be refactored (unref) when migrating NodeView to composition API
|
||||
onMouseMoveEnd?: Ref<null | ((e: MouseEvent) => void)>;
|
||||
} = {},
|
||||
) {
|
||||
const uiStore = useUIStore();
|
||||
const moveLastPosition = ref([0, 0]);
|
||||
const deviceSupport = useDeviceSupport();
|
||||
|
||||
/**
|
||||
* Updates the canvas offset position based on the mouse movement
|
||||
*/
|
||||
function panCanvas(e: MouseEvent) {
|
||||
const offsetPosition = uiStore.nodeViewOffsetPosition;
|
||||
|
||||
const [x, y] = getMousePosition(e);
|
||||
|
||||
const nodeViewOffsetPositionX = offsetPosition[0] + (x - moveLastPosition.value[0]);
|
||||
const nodeViewOffsetPositionY = offsetPosition[1] + (y - moveLastPosition.value[1]);
|
||||
uiStore.nodeViewOffsetPosition = [nodeViewOffsetPositionX, nodeViewOffsetPositionY];
|
||||
|
||||
// Update the last position
|
||||
moveLastPosition.value = [x, y];
|
||||
|
||||
return [nodeViewOffsetPositionX, nodeViewOffsetPositionY];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates the panning process when specific conditions are met (middle mouse or ctrl key pressed)
|
||||
*/
|
||||
function onMouseDown(e: MouseEvent, moveButtonPressed: boolean) {
|
||||
if (!(deviceSupport.isCtrlKeyPressed(e) || moveButtonPressed)) {
|
||||
// We only care about it when the ctrl key is pressed at the same time.
|
||||
// So we exit when it is not pressed.
|
||||
return;
|
||||
}
|
||||
|
||||
if (uiStore.isActionActive('dragActive')) {
|
||||
// If a node does currently get dragged we do not activate the selection
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent moving canvas on anything but middle button
|
||||
if (e.button !== MOUSE_EVENT_BUTTON.MIDDLE) {
|
||||
uiStore.nodeViewMoveInProgress = true;
|
||||
}
|
||||
|
||||
const [x, y] = getMousePosition(e);
|
||||
|
||||
moveLastPosition.value = [x, y];
|
||||
|
||||
const element = unref(elementRef);
|
||||
element?.addEventListener('mousemove', onMouseMove);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends the panning process and removes the mousemove event listener
|
||||
*/
|
||||
function onMouseUp(_: MouseEvent) {
|
||||
if (!uiStore.nodeViewMoveInProgress) {
|
||||
// If it is not active return directly.
|
||||
// Else normal node dragging will not work.
|
||||
return;
|
||||
}
|
||||
|
||||
const element = unref(elementRef);
|
||||
element?.removeEventListener('mousemove', onMouseMove);
|
||||
|
||||
uiStore.nodeViewMoveInProgress = false;
|
||||
|
||||
// Nothing else to do. Simply leave the node view at the current offset
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the actual movement of the canvas during a mouse drag,
|
||||
* updating the position based on the current mouse position
|
||||
*/
|
||||
function onMouseMove(e: MouseEvent) {
|
||||
const element = unref(elementRef);
|
||||
if (e.target && !(element === e.target || element?.contains(e.target as Node))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (uiStore.isActionActive('dragActive')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Signal that moving canvas is active if middle button is pressed and mouse is moved
|
||||
if (e.buttons === MOUSE_EVENT_BUTTONS.MIDDLE) {
|
||||
uiStore.nodeViewMoveInProgress = true;
|
||||
}
|
||||
|
||||
if (e.buttons === MOUSE_EVENT_BUTTONS.NONE) {
|
||||
// Mouse button is not pressed anymore so stop selection mode
|
||||
// Happens normally when mouse leave the view pressed and then
|
||||
// comes back unpressed.
|
||||
const onMouseMoveEnd = unref(options.onMouseMoveEnd);
|
||||
onMouseMoveEnd?.(e);
|
||||
return;
|
||||
}
|
||||
|
||||
panCanvas(e);
|
||||
}
|
||||
|
||||
return {
|
||||
moveLastPosition,
|
||||
onMouseDown,
|
||||
onMouseUp,
|
||||
onMouseMove,
|
||||
panCanvas,
|
||||
};
|
||||
}
|
|
@ -703,3 +703,37 @@ export const TIME = {
|
|||
};
|
||||
|
||||
export const SUGGESTED_TEMPLATES_FLAG = 'SHOW_N8N_SUGGESTED_TEMPLATES';
|
||||
|
||||
/**
|
||||
* Mouse button codes
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mapping for the MouseEvent.button property that indicates which button was pressed
|
||||
* on the mouse to trigger the event.
|
||||
*
|
||||
* @docs https://www.w3.org/TR/uievents/#dom-mouseevent-button
|
||||
*/
|
||||
export const MOUSE_EVENT_BUTTON = {
|
||||
PRIMARY: 0,
|
||||
MIDDLE: 1,
|
||||
SECONDARY: 2,
|
||||
BROWSER_BACK: 3,
|
||||
BROWSER_FORWARD: 4,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Mapping for the MouseEvent.buttons property that indicates which buttons are pressed
|
||||
* on the mouse when a mouse event is triggered. If multiple buttons are pressed,
|
||||
* the values are added together to produce a new number.
|
||||
*
|
||||
* @docs https://www.w3.org/TR/uievents/#dom-mouseevent-buttons
|
||||
*/
|
||||
export const MOUSE_EVENT_BUTTONS = {
|
||||
NONE: 0,
|
||||
PRIMARY: 1,
|
||||
SECONDARY: 2,
|
||||
MIDDLE: 4,
|
||||
BROWSER_BACK: 8,
|
||||
BROWSER_FORWARD: 16,
|
||||
} as const;
|
||||
|
|
|
@ -1,99 +0,0 @@
|
|||
import { defineComponent } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
|
||||
import { getMousePosition } from '@/utils/nodeViewUtils';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useDeviceSupport } from 'n8n-design-system';
|
||||
|
||||
export const moveNodeWorkflow = defineComponent({
|
||||
data() {
|
||||
return {
|
||||
moveLastPosition: [0, 0],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useUIStore),
|
||||
},
|
||||
methods: {
|
||||
moveWorkflow(e: MouseEvent) {
|
||||
const offsetPosition = this.uiStore.nodeViewOffsetPosition;
|
||||
|
||||
const [x, y] = getMousePosition(e);
|
||||
|
||||
const nodeViewOffsetPositionX = offsetPosition[0] + (x - this.moveLastPosition[0]);
|
||||
const nodeViewOffsetPositionY = offsetPosition[1] + (y - this.moveLastPosition[1]);
|
||||
this.uiStore.nodeViewOffsetPosition = [nodeViewOffsetPositionX, nodeViewOffsetPositionY];
|
||||
|
||||
// Update the last position
|
||||
this.moveLastPosition[0] = x;
|
||||
this.moveLastPosition[1] = y;
|
||||
},
|
||||
mouseDownMoveWorkflow(e: MouseEvent, moveButtonPressed: boolean) {
|
||||
const deviceSupport = useDeviceSupport();
|
||||
|
||||
if (!deviceSupport.isCtrlKeyPressed(e) && !moveButtonPressed) {
|
||||
// We only care about it when the ctrl key is pressed at the same time.
|
||||
// So we exit when it is not pressed.
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.uiStore.isActionActive('dragActive')) {
|
||||
// If a node does currently get dragged we do not activate the selection
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent moving canvas on anything but middle button
|
||||
if (e.button !== 1) {
|
||||
this.uiStore.nodeViewMoveInProgress = true;
|
||||
}
|
||||
|
||||
const [x, y] = getMousePosition(e);
|
||||
|
||||
this.moveLastPosition[0] = x;
|
||||
this.moveLastPosition[1] = y;
|
||||
|
||||
// @ts-ignore
|
||||
this.$el.addEventListener('mousemove', this.mouseMoveNodeWorkflow);
|
||||
},
|
||||
mouseUpMoveWorkflow(e: MouseEvent) {
|
||||
if (!this.uiStore.nodeViewMoveInProgress) {
|
||||
// If it is not active return directly.
|
||||
// Else normal node dragging will not work.
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
this.$el.removeEventListener('mousemove', this.mouseMoveNodeWorkflow);
|
||||
|
||||
this.uiStore.nodeViewMoveInProgress = false;
|
||||
|
||||
// Nothing else to do. Simply leave the node view at the current offset
|
||||
},
|
||||
mouseMoveNodeWorkflow(e: MouseEvent) {
|
||||
// @ts-ignore
|
||||
if (e.target && !e.target.id.includes('node-view')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.uiStore.isActionActive('dragActive')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Signal that moving canvas is active if middle button is pressed and mouse is moved
|
||||
if (e.buttons === 4) {
|
||||
this.uiStore.nodeViewMoveInProgress = true;
|
||||
}
|
||||
|
||||
if (e.buttons === 0) {
|
||||
// Mouse button is not pressed anymore so stop selection mode
|
||||
// Happens normally when mouse leave the view pressed and then
|
||||
// comes back unpressed.
|
||||
// @ts-ignore
|
||||
this.mouseUp(e);
|
||||
return;
|
||||
}
|
||||
|
||||
this.moveWorkflow(e);
|
||||
},
|
||||
},
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div :class="$style['content']">
|
||||
<div :class="$style['content']" ref="nodeViewRootRef">
|
||||
<div
|
||||
id="node-view-root"
|
||||
class="node-view-root do-not-select"
|
||||
|
@ -14,7 +14,7 @@
|
|||
data-test-id="node-view-wrapper"
|
||||
@touchstart="mouseDown"
|
||||
@touchend="mouseUp"
|
||||
@touchmove="mouseMoveNodeWorkflow"
|
||||
@touchmove="canvasPanning.onMouseMove"
|
||||
@mousedown="mouseDown"
|
||||
@mouseup="mouseUp"
|
||||
@contextmenu="contextMenu.open"
|
||||
|
@ -28,7 +28,7 @@
|
|||
/>
|
||||
<div
|
||||
id="node-view"
|
||||
ref="nodeView"
|
||||
ref="nodeViewRef"
|
||||
class="node-view"
|
||||
:style="workflowStyle"
|
||||
data-test-id="node-view"
|
||||
|
@ -197,7 +197,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent, nextTick } from 'vue';
|
||||
import { defineAsyncComponent, defineComponent, nextTick, ref } from 'vue';
|
||||
import { mapStores, storeToRefs } from 'pinia';
|
||||
|
||||
import type {
|
||||
|
@ -244,7 +244,6 @@ import {
|
|||
UPDATE_WEBHOOK_ID_NODE_TYPES,
|
||||
TIME,
|
||||
} from '@/constants';
|
||||
import { moveNodeWorkflow } from '@/mixins/moveNodeWorkflow';
|
||||
|
||||
import useGlobalLinkActions from '@/composables/useGlobalLinkActions';
|
||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||
|
@ -378,6 +377,7 @@ import { usePinnedData } from '@/composables/usePinnedData';
|
|||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import { useDeviceSupport } from 'n8n-design-system';
|
||||
import { useDebounce } from '@/composables/useDebounce';
|
||||
import { useCanvasPanning } from '@/composables/useCanvasPanning';
|
||||
|
||||
interface AddNodeOptions {
|
||||
position?: XYPosition;
|
||||
|
@ -409,7 +409,7 @@ export default defineComponent({
|
|||
ContextMenu,
|
||||
SetupWorkflowCredentialsButton,
|
||||
},
|
||||
mixins: [moveNodeWorkflow, workflowHelpers, workflowRun],
|
||||
mixins: [workflowHelpers, workflowRun],
|
||||
async beforeRouteLeave(to, from, next) {
|
||||
if (
|
||||
getNodeViewTab(to) === MAIN_HEADER_TABS.EXECUTIONS ||
|
||||
|
@ -471,6 +471,10 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
setup(props, ctx) {
|
||||
const nodeViewRootRef = ref(null);
|
||||
const nodeViewRef = ref(null);
|
||||
const onMouseMoveEnd = ref(null);
|
||||
|
||||
const ndvStore = useNDVStore();
|
||||
const externalHooks = useExternalHooks();
|
||||
const locale = useI18n();
|
||||
|
@ -482,6 +486,7 @@ export default defineComponent({
|
|||
const pinnedData = usePinnedData(activeNode);
|
||||
const deviceSupport = useDeviceSupport();
|
||||
const { callDebounced } = useDebounce();
|
||||
const canvasPanning = useCanvasPanning(nodeViewRootRef, { onMouseMoveEnd });
|
||||
|
||||
return {
|
||||
locale,
|
||||
|
@ -492,6 +497,10 @@ export default defineComponent({
|
|||
clipboard,
|
||||
pinnedData,
|
||||
deviceSupport,
|
||||
canvasPanning,
|
||||
nodeViewRootRef,
|
||||
nodeViewRef,
|
||||
onMouseMoveEnd,
|
||||
callDebounced,
|
||||
...useCanvasMouseSelect(),
|
||||
...useGlobalLinkActions(),
|
||||
|
@ -558,7 +567,7 @@ export default defineComponent({
|
|||
this.canvasStore.setRecenteredCanvasAddButtonPosition(this.getNodeViewOffsetPosition);
|
||||
},
|
||||
nodeViewScale(newScale) {
|
||||
const elementRef = this.$refs.nodeView as HTMLDivElement | undefined;
|
||||
const elementRef = this.nodeViewRef as HTMLDivElement | undefined;
|
||||
if (elementRef) {
|
||||
elementRef.style.transform = `scale(${newScale})`;
|
||||
}
|
||||
|
@ -778,8 +787,11 @@ export default defineComponent({
|
|||
};
|
||||
},
|
||||
async mounted() {
|
||||
// To be refactored (unref) when migrating to composition API
|
||||
this.onMouseMoveEnd = this.mouseUp;
|
||||
|
||||
this.resetWorkspace();
|
||||
this.canvasStore.initInstance(this.$refs.nodeView as HTMLElement);
|
||||
this.canvasStore.initInstance(this.nodeViewRef as HTMLElement);
|
||||
this.titleReset();
|
||||
window.addEventListener('message', this.onPostMessageReceived);
|
||||
|
||||
|
@ -1411,7 +1423,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
this.mouseDownMouseSelect(e as MouseEvent, this.moveCanvasKeyPressed);
|
||||
this.mouseDownMoveWorkflow(e as MouseEvent, this.moveCanvasKeyPressed);
|
||||
this.canvasPanning.onMouseDown(e as MouseEvent, this.moveCanvasKeyPressed);
|
||||
|
||||
// Hide the node-creator
|
||||
this.createNodeActive = false;
|
||||
|
@ -1421,7 +1433,7 @@ export default defineComponent({
|
|||
this.moveCanvasKeyPressed = false;
|
||||
}
|
||||
this.mouseUpMouseSelect(e);
|
||||
this.mouseUpMoveWorkflow(e);
|
||||
this.canvasPanning.onMouseUp(e);
|
||||
},
|
||||
keyUp(e: KeyboardEvent) {
|
||||
if (e.key === this.deviceSupport.controlKeyCode) {
|
||||
|
|
Loading…
Reference in a new issue