mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 12:44:07 -08:00
fix(editor): Implement canvas zoom UX improvements (#7376)
- Fix pinch-to-zoom - Support command + scroll to zoom - Improve accuracy of zooming (scroll more = zoom more) - Zoom limits - Zoom relative to mouse position
This commit is contained in:
parent
0847623f85
commit
7e06b31a5f
|
@ -14,8 +14,11 @@ const WorkflowPage = new WorkflowPageClass();
|
||||||
const DEFAULT_ZOOM_FACTOR = 1;
|
const DEFAULT_ZOOM_FACTOR = 1;
|
||||||
const ZOOM_IN_X1_FACTOR = 1.25; // Zoom in factor after one click
|
const ZOOM_IN_X1_FACTOR = 1.25; // Zoom in factor after one click
|
||||||
const ZOOM_IN_X2_FACTOR = 1.5625; // Zoom in factor after two clicks
|
const ZOOM_IN_X2_FACTOR = 1.5625; // Zoom in factor after two clicks
|
||||||
const ZOOM_OUT_X1_FACTOR = 0.8;
|
const ZOOM_OUT_X1_FACTOR = 0.75;
|
||||||
const ZOOM_OUT_X2_FACTOR = 0.64;
|
const ZOOM_OUT_X2_FACTOR = 0.5625;
|
||||||
|
|
||||||
|
const PINCH_ZOOM_IN_FACTOR = 1.32;
|
||||||
|
const PINCH_ZOOM_OUT_FACTOR = 0.4752;
|
||||||
const RENAME_NODE_NAME = 'Something else';
|
const RENAME_NODE_NAME = 'Something else';
|
||||||
|
|
||||||
describe('Canvas Node Manipulation and Navigation', () => {
|
describe('Canvas Node Manipulation and Navigation', () => {
|
||||||
|
@ -203,6 +206,26 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should zoom using pinch to zoom', () => {
|
||||||
|
WorkflowPage.actions.pinchToZoom(2, 'zoomIn');
|
||||||
|
WorkflowPage.getters
|
||||||
|
.nodeView()
|
||||||
|
.should(
|
||||||
|
'have.css',
|
||||||
|
'transform',
|
||||||
|
`matrix(${PINCH_ZOOM_IN_FACTOR}, 0, 0, ${PINCH_ZOOM_IN_FACTOR}, 0, 0)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
WorkflowPage.actions.pinchToZoom(4, 'zoomOut');
|
||||||
|
WorkflowPage.getters
|
||||||
|
.nodeView()
|
||||||
|
.should(
|
||||||
|
'have.css',
|
||||||
|
'transform',
|
||||||
|
`matrix(${PINCH_ZOOM_OUT_FACTOR}, 0, 0, ${PINCH_ZOOM_OUT_FACTOR}, 0, 0)`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should reset zoom', () => {
|
it('should reset zoom', () => {
|
||||||
// Reset zoom should not appear until zoom level changed
|
// Reset zoom should not appear until zoom level changed
|
||||||
WorkflowPage.getters.resetZoomButton().should('not.exist');
|
WorkflowPage.getters.resetZoomButton().should('not.exist');
|
||||||
|
|
|
@ -143,11 +143,14 @@ export class WorkflowPage extends BasePage {
|
||||||
this.getters.nodeCreatorSearchBar().type('{enter}');
|
this.getters.nodeCreatorSearchBar().type('{enter}');
|
||||||
if (opts?.action) {
|
if (opts?.action) {
|
||||||
// Expand actions category if it's collapsed
|
// Expand actions category if it's collapsed
|
||||||
nodeCreator.getters.getCategoryItem('Actions').parent().then(($el) => {
|
nodeCreator.getters
|
||||||
if ($el.attr('data-category-collapsed') === 'true') {
|
.getCategoryItem('Actions')
|
||||||
nodeCreator.getters.getCategoryItem('Actions').click();
|
.parent()
|
||||||
}
|
.then(($el) => {
|
||||||
});
|
if ($el.attr('data-category-collapsed') === 'true') {
|
||||||
|
nodeCreator.getters.getCategoryItem('Actions').click();
|
||||||
|
}
|
||||||
|
});
|
||||||
nodeCreator.getters.getCreatorItem(opts.action).click();
|
nodeCreator.getters.getCreatorItem(opts.action).click();
|
||||||
} else if (!opts?.keepNdvOpen) {
|
} else if (!opts?.keepNdvOpen) {
|
||||||
cy.get('body').type('{esc}');
|
cy.get('body').type('{esc}');
|
||||||
|
@ -249,6 +252,17 @@ export class WorkflowPage extends BasePage {
|
||||||
zoomToFit: () => {
|
zoomToFit: () => {
|
||||||
cy.getByTestId('zoom-to-fit').click();
|
cy.getByTestId('zoom-to-fit').click();
|
||||||
},
|
},
|
||||||
|
pinchToZoom: (steps: number, mode: 'zoomIn' | 'zoomOut' = 'zoomIn') => {
|
||||||
|
// Pinch-to-zoom simulates a 'wheel' event with ctrlKey: true (same as zooming by scrolling)
|
||||||
|
this.getters.nodeViewBackground().trigger('wheel', {
|
||||||
|
force: true,
|
||||||
|
bubbles: true,
|
||||||
|
ctrlKey: true,
|
||||||
|
pageX: cy.window().innerWidth / 2,
|
||||||
|
pageY: cy.window().innerHeight / 2,
|
||||||
|
deltaY: mode === 'zoomOut' ? 16 * steps : -16 * steps,
|
||||||
|
});
|
||||||
|
},
|
||||||
hitUndo: () => {
|
hitUndo: () => {
|
||||||
cy.get('body').type(META_KEY, { delay: 500, release: false }).type('z');
|
cy.get('body').type(META_KEY, { delay: 500, release: false }).type('z');
|
||||||
},
|
},
|
||||||
|
@ -311,10 +325,7 @@ export class WorkflowPage extends BasePage {
|
||||||
this.getters.stickies().dblclick().find('textarea').clear().type(content).type('{esc}');
|
this.getters.stickies().dblclick().find('textarea').clear().type(content).type('{esc}');
|
||||||
},
|
},
|
||||||
shouldHaveWorkflowName: (name: string) => {
|
shouldHaveWorkflowName: (name: string) => {
|
||||||
this.getters
|
this.getters.workflowNameInputContainer().invoke('attr', 'title').should('include', name);
|
||||||
.workflowNameInputContainer()
|
|
||||||
.invoke('attr', 'title')
|
|
||||||
.should('include', name);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,6 @@
|
||||||
"luxon": "^3.3.0",
|
"luxon": "^3.3.0",
|
||||||
"n8n-design-system": "workspace:*",
|
"n8n-design-system": "workspace:*",
|
||||||
"n8n-workflow": "workspace:*",
|
"n8n-workflow": "workspace:*",
|
||||||
"normalize-wheel": "^1.0.1",
|
|
||||||
"pinia": "^2.1.6",
|
"pinia": "^2.1.6",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.3",
|
||||||
"qrcode.vue": "^3.3.4",
|
"qrcode.vue": "^3.3.4",
|
||||||
|
|
|
@ -1258,6 +1258,7 @@ export interface IRestApiContext {
|
||||||
export interface IZoomConfig {
|
export interface IZoomConfig {
|
||||||
scale: number;
|
scale: number;
|
||||||
offset: XYPosition;
|
offset: XYPosition;
|
||||||
|
origin?: XYPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBounds {
|
export interface IBounds {
|
||||||
|
|
|
@ -3,12 +3,7 @@ import type { INodeUi, XYPosition } from '@/Interface';
|
||||||
import useDeviceSupport from './useDeviceSupport';
|
import useDeviceSupport from './useDeviceSupport';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import {
|
import { getMousePosition, getRelativePosition } from '@/utils/nodeViewUtils';
|
||||||
getMousePosition,
|
|
||||||
getRelativePosition,
|
|
||||||
SIDEBAR_WIDTH,
|
|
||||||
SIDEBAR_WIDTH_EXPANDED,
|
|
||||||
} from '@/utils/nodeViewUtils';
|
|
||||||
import { ref, onMounted, computed } from 'vue';
|
import { ref, onMounted, computed } from 'vue';
|
||||||
import { useCanvasStore } from '@/stores/canvas.store';
|
import { useCanvasStore } from '@/stores/canvas.store';
|
||||||
|
|
||||||
|
@ -179,18 +174,9 @@ export default function useCanvasMouseSelect() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMousePositionWithinNodeView(event: MouseEvent | TouchEvent): XYPosition {
|
function getMousePositionWithinNodeView(event: MouseEvent | TouchEvent): XYPosition {
|
||||||
const [mouseX, mouseY] = getMousePosition(event);
|
const mousePosition = getMousePosition(event);
|
||||||
|
|
||||||
const sidebarWidth = canvasStore.isDemo
|
const [relativeX, relativeY] = canvasStore.canvasPositionFromPagePosition(mousePosition);
|
||||||
? 0
|
|
||||||
: uiStore.sidebarMenuCollapsed
|
|
||||||
? SIDEBAR_WIDTH
|
|
||||||
: SIDEBAR_WIDTH_EXPANDED;
|
|
||||||
|
|
||||||
const relativeX = mouseX - sidebarWidth;
|
|
||||||
const relativeY = canvasStore.isDemo
|
|
||||||
? mouseY
|
|
||||||
: mouseY - uiStore.bannersHeight - uiStore.headerHeight;
|
|
||||||
const nodeViewScale = canvasStore.nodeViewScale;
|
const nodeViewScale = canvasStore.nodeViewScale;
|
||||||
const nodeViewOffsetPosition = uiStore.nodeViewOffsetPosition;
|
const nodeViewOffsetPosition = uiStore.nodeViewOffsetPosition;
|
||||||
|
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
declare module 'normalize-wheel' {
|
|
||||||
function normalizeWheel(e: WheelEvent): {
|
|
||||||
spinX: number;
|
|
||||||
spinY: number;
|
|
||||||
pixelX: number;
|
|
||||||
pixelY: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export = normalizeWheel;
|
|
||||||
}
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import normalizeWheel from 'normalize-wheel';
|
|
||||||
import {
|
import {
|
||||||
useWorkflowsStore,
|
useWorkflowsStore,
|
||||||
useNodeTypesStore,
|
useNodeTypesStore,
|
||||||
|
@ -10,7 +9,14 @@ import {
|
||||||
useSourceControlStore,
|
useSourceControlStore,
|
||||||
} from '@/stores';
|
} from '@/stores';
|
||||||
import type { INodeUi, XYPosition } from '@/Interface';
|
import type { INodeUi, XYPosition } from '@/Interface';
|
||||||
import { scaleBigger, scaleReset, scaleSmaller } from '@/utils';
|
import {
|
||||||
|
applyScale,
|
||||||
|
getScaleFromWheelEventDelta,
|
||||||
|
normalizeWheelEventDelta,
|
||||||
|
scaleBigger,
|
||||||
|
scaleReset,
|
||||||
|
scaleSmaller,
|
||||||
|
} from '@/utils';
|
||||||
import { START_NODE_TYPE } from '@/constants';
|
import { START_NODE_TYPE } from '@/constants';
|
||||||
import type {
|
import type {
|
||||||
BeforeStartEventParams,
|
BeforeStartEventParams,
|
||||||
|
@ -31,6 +37,9 @@ import {
|
||||||
CONNECTOR_PAINT_STYLE_DEFAULT,
|
CONNECTOR_PAINT_STYLE_DEFAULT,
|
||||||
CONNECTOR_PAINT_STYLE_PRIMARY,
|
CONNECTOR_PAINT_STYLE_PRIMARY,
|
||||||
CONNECTOR_ARROW_OVERLAYS,
|
CONNECTOR_ARROW_OVERLAYS,
|
||||||
|
getMousePosition,
|
||||||
|
SIDEBAR_WIDTH,
|
||||||
|
SIDEBAR_WIDTH_EXPANDED,
|
||||||
} from '@/utils/nodeViewUtils';
|
} from '@/utils/nodeViewUtils';
|
||||||
import type { PointXY } from '@jsplumb/util';
|
import type { PointXY } from '@jsplumb/util';
|
||||||
|
|
||||||
|
@ -64,7 +73,7 @@ export const useCanvasStore = defineStore('canvas', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const setRecenteredCanvasAddButtonPosition = (offset?: XYPosition) => {
|
const setRecenteredCanvasAddButtonPosition = (offset?: XYPosition) => {
|
||||||
const position = getMidCanvasPosition(nodeViewScale.value, offset || [0, 0]);
|
const position = getMidCanvasPosition(nodeViewScale.value, offset ?? [0, 0]);
|
||||||
|
|
||||||
position[0] -= PLACEHOLDER_TRIGGER_NODE_SIZE / 2;
|
position[0] -= PLACEHOLDER_TRIGGER_NODE_SIZE / 2;
|
||||||
position[1] -= PLACEHOLDER_TRIGGER_NODE_SIZE / 2;
|
position[1] -= PLACEHOLDER_TRIGGER_NODE_SIZE / 2;
|
||||||
|
@ -85,6 +94,21 @@ export const useCanvasStore = defineStore('canvas', () => {
|
||||||
const getNodesWithPlaceholderNode = (): INodeUi[] =>
|
const getNodesWithPlaceholderNode = (): INodeUi[] =>
|
||||||
triggerNodes.value.length > 0 ? nodes.value : [getPlaceholderTriggerNodeUI(), ...nodes.value];
|
triggerNodes.value.length > 0 ? nodes.value : [getPlaceholderTriggerNodeUI(), ...nodes.value];
|
||||||
|
|
||||||
|
const canvasPositionFromPagePosition = (position: XYPosition): XYPosition => {
|
||||||
|
const sidebarWidth = isDemo.value
|
||||||
|
? 0
|
||||||
|
: uiStore.sidebarMenuCollapsed
|
||||||
|
? SIDEBAR_WIDTH
|
||||||
|
: SIDEBAR_WIDTH_EXPANDED;
|
||||||
|
|
||||||
|
const relativeX = position[0] - sidebarWidth;
|
||||||
|
const relativeY = isDemo.value
|
||||||
|
? position[1]
|
||||||
|
: position[1] - uiStore.bannersHeight - uiStore.headerHeight;
|
||||||
|
|
||||||
|
return [relativeX, relativeY];
|
||||||
|
};
|
||||||
|
|
||||||
const setZoomLevel = (zoomLevel: number, offset: XYPosition) => {
|
const setZoomLevel = (zoomLevel: number, offset: XYPosition) => {
|
||||||
nodeViewScale.value = zoomLevel;
|
nodeViewScale.value = zoomLevel;
|
||||||
jsPlumbInstanceRef.value?.setZoom(zoomLevel);
|
jsPlumbInstanceRef.value?.setZoom(zoomLevel);
|
||||||
|
@ -95,6 +119,7 @@ export const useCanvasStore = defineStore('canvas', () => {
|
||||||
const { scale, offset } = scaleReset({
|
const { scale, offset } = scaleReset({
|
||||||
scale: nodeViewScale.value,
|
scale: nodeViewScale.value,
|
||||||
offset: uiStore.nodeViewOffsetPosition,
|
offset: uiStore.nodeViewOffsetPosition,
|
||||||
|
origin: canvasPositionFromPagePosition([window.innerWidth / 2, window.innerHeight / 2]),
|
||||||
});
|
});
|
||||||
setZoomLevel(scale, offset);
|
setZoomLevel(scale, offset);
|
||||||
};
|
};
|
||||||
|
@ -103,6 +128,7 @@ export const useCanvasStore = defineStore('canvas', () => {
|
||||||
const { scale, offset } = scaleBigger({
|
const { scale, offset } = scaleBigger({
|
||||||
scale: nodeViewScale.value,
|
scale: nodeViewScale.value,
|
||||||
offset: uiStore.nodeViewOffsetPosition,
|
offset: uiStore.nodeViewOffsetPosition,
|
||||||
|
origin: canvasPositionFromPagePosition([window.innerWidth / 2, window.innerHeight / 2]),
|
||||||
});
|
});
|
||||||
setZoomLevel(scale, offset);
|
setZoomLevel(scale, offset);
|
||||||
};
|
};
|
||||||
|
@ -111,6 +137,7 @@ export const useCanvasStore = defineStore('canvas', () => {
|
||||||
const { scale, offset } = scaleSmaller({
|
const { scale, offset } = scaleSmaller({
|
||||||
scale: nodeViewScale.value,
|
scale: nodeViewScale.value,
|
||||||
offset: uiStore.nodeViewOffsetPosition,
|
offset: uiStore.nodeViewOffsetPosition,
|
||||||
|
origin: canvasPositionFromPagePosition([window.innerWidth / 2, window.innerHeight / 2]),
|
||||||
});
|
});
|
||||||
setZoomLevel(scale, offset);
|
setZoomLevel(scale, offset);
|
||||||
};
|
};
|
||||||
|
@ -125,29 +152,30 @@ export const useCanvasStore = defineStore('canvas', () => {
|
||||||
setZoomLevel(zoomLevel, offset);
|
setZoomLevel(zoomLevel, offset);
|
||||||
};
|
};
|
||||||
|
|
||||||
const wheelMoveWorkflow = (e: WheelEvent) => {
|
const wheelMoveWorkflow = (deltaX: number, deltaY: number, shiftKeyPressed = false) => {
|
||||||
const normalized = normalizeWheel(e);
|
|
||||||
const offsetPosition = uiStore.nodeViewOffsetPosition;
|
const offsetPosition = uiStore.nodeViewOffsetPosition;
|
||||||
const nodeViewOffsetPositionX =
|
const nodeViewOffsetPositionX = offsetPosition[0] - (shiftKeyPressed ? deltaY : deltaX);
|
||||||
offsetPosition[0] - (e.shiftKey ? normalized.pixelY : normalized.pixelX);
|
const nodeViewOffsetPositionY = offsetPosition[1] - (shiftKeyPressed ? deltaX : deltaY);
|
||||||
const nodeViewOffsetPositionY =
|
|
||||||
offsetPosition[1] - (e.shiftKey ? normalized.pixelX : normalized.pixelY);
|
|
||||||
uiStore.nodeViewOffsetPosition = [nodeViewOffsetPositionX, nodeViewOffsetPositionY];
|
uiStore.nodeViewOffsetPosition = [nodeViewOffsetPositionX, nodeViewOffsetPositionY];
|
||||||
};
|
};
|
||||||
|
|
||||||
const wheelScroll = (e: WheelEvent) => {
|
const wheelScroll = (e: WheelEvent) => {
|
||||||
//* Control + scroll zoom
|
// Prevent browser back/forward gesture, default pinch to zoom etc.
|
||||||
if (e.ctrlKey) {
|
e.preventDefault();
|
||||||
if (e.deltaY > 0) {
|
|
||||||
zoomOut();
|
|
||||||
} else {
|
|
||||||
zoomIn();
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
const { deltaX, deltaY } = normalizeWheelEventDelta(e);
|
||||||
|
|
||||||
|
if (e.ctrlKey || e.metaKey) {
|
||||||
|
const scaleFactor = getScaleFromWheelEventDelta(deltaY);
|
||||||
|
const { scale, offset } = applyScale(scaleFactor)({
|
||||||
|
scale: nodeViewScale.value,
|
||||||
|
offset: uiStore.nodeViewOffsetPosition,
|
||||||
|
origin: canvasPositionFromPagePosition(getMousePosition(e)),
|
||||||
|
});
|
||||||
|
setZoomLevel(scale, offset);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
wheelMoveWorkflow(e);
|
wheelMoveWorkflow(deltaX, deltaY, e.shiftKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
function initInstance(container: Element) {
|
function initInstance(container: Element) {
|
||||||
|
@ -268,6 +296,7 @@ export const useCanvasStore = defineStore('canvas', () => {
|
||||||
jsPlumbInstance,
|
jsPlumbInstance,
|
||||||
setRecenteredCanvasAddButtonPosition,
|
setRecenteredCanvasAddButtonPosition,
|
||||||
getNodesWithPlaceholderNode,
|
getNodesWithPlaceholderNode,
|
||||||
|
canvasPositionFromPagePosition,
|
||||||
setZoomLevel,
|
setZoomLevel,
|
||||||
resetZoom,
|
resetZoom,
|
||||||
zoomIn,
|
zoomIn,
|
||||||
|
|
|
@ -13,47 +13,42 @@ import type { Route } from 'vue-router';
|
||||||
'@/utils'.
|
'@/utils'.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const scaleSmaller = ({ scale, offset: [xOffset, yOffset] }: IZoomConfig): IZoomConfig => {
|
const SCALE_INCREASE_FACTOR = 1.25;
|
||||||
scale /= 1.25;
|
const SCALE_DECREASE_FACTOR = 0.75;
|
||||||
xOffset /= 1.25;
|
const MIN_SCALE = 0.2;
|
||||||
yOffset /= 1.25;
|
const MAX_SCALE = 5;
|
||||||
xOffset += window.innerWidth / 10;
|
|
||||||
yOffset += window.innerHeight / 10;
|
|
||||||
|
|
||||||
return {
|
const clamp = (min: number, max: number) => (num: number) => {
|
||||||
scale,
|
return Math.max(min, Math.min(max, num));
|
||||||
offset: [xOffset, yOffset],
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const scaleBigger = ({ scale, offset: [xOffset, yOffset] }: IZoomConfig): IZoomConfig => {
|
const clampScale = clamp(MIN_SCALE, MAX_SCALE);
|
||||||
scale *= 1.25;
|
|
||||||
xOffset -= window.innerWidth / 10;
|
|
||||||
yOffset -= window.innerHeight / 10;
|
|
||||||
xOffset *= 1.25;
|
|
||||||
yOffset *= 1.25;
|
|
||||||
|
|
||||||
return {
|
export const applyScale =
|
||||||
scale,
|
(scale: number) =>
|
||||||
offset: [xOffset, yOffset],
|
({ scale: initialScale, offset: [xOffset, yOffset], origin }: IZoomConfig): IZoomConfig => {
|
||||||
|
const newScale = clampScale(initialScale * scale);
|
||||||
|
const scaleChange = newScale / initialScale;
|
||||||
|
|
||||||
|
const xOrigin = origin?.[0] ?? window.innerWidth / 2;
|
||||||
|
const yOrigin = origin?.[1] ?? window.innerHeight / 2;
|
||||||
|
|
||||||
|
// Calculate the new offsets based on the zoom origin
|
||||||
|
xOffset = xOrigin - scaleChange * (xOrigin - xOffset);
|
||||||
|
yOffset = yOrigin - scaleChange * (yOrigin - yOffset);
|
||||||
|
|
||||||
|
return {
|
||||||
|
scale: newScale,
|
||||||
|
offset: [xOffset, yOffset],
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
export const scaleBigger = applyScale(SCALE_INCREASE_FACTOR);
|
||||||
|
|
||||||
|
export const scaleSmaller = applyScale(SCALE_DECREASE_FACTOR);
|
||||||
|
|
||||||
export const scaleReset = (config: IZoomConfig): IZoomConfig => {
|
export const scaleReset = (config: IZoomConfig): IZoomConfig => {
|
||||||
if (config.scale > 1) {
|
return applyScale(1 / config.scale)(config);
|
||||||
// zoomed in
|
|
||||||
while (config.scale > 1) {
|
|
||||||
config = scaleSmaller(config);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
while (config.scale < 1) {
|
|
||||||
config = scaleBigger(config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.scale = 1;
|
|
||||||
|
|
||||||
return config;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const closestNumberDivisibleBy = (inputNumber: number, divisibleBy: number): number => {
|
export const closestNumberDivisibleBy = (inputNumber: number, divisibleBy: number): number => {
|
||||||
|
@ -112,3 +107,19 @@ export const getConnectionInfo = (
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const normalizeWheelEventDelta = (event: WheelEvent): { deltaX: number; deltaY: number } => {
|
||||||
|
const factorByMode: Record<number, number> = {
|
||||||
|
[WheelEvent.DOM_DELTA_PIXEL]: 1,
|
||||||
|
[WheelEvent.DOM_DELTA_LINE]: 8,
|
||||||
|
[WheelEvent.DOM_DELTA_PAGE]: 24,
|
||||||
|
};
|
||||||
|
|
||||||
|
const factor = factorByMode[event.deltaMode] ?? 1;
|
||||||
|
|
||||||
|
return { deltaX: event.deltaX * factor, deltaY: event.deltaY * factor };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getScaleFromWheelEventDelta = (delta: number): number => {
|
||||||
|
return 1 - delta / 100;
|
||||||
|
};
|
||||||
|
|
|
@ -12,7 +12,6 @@ const vendorChunks = ['vue', 'vue-router'];
|
||||||
const n8nChunks = ['n8n-workflow', 'n8n-design-system'];
|
const n8nChunks = ['n8n-workflow', 'n8n-design-system'];
|
||||||
const ignoreChunks = [
|
const ignoreChunks = [
|
||||||
'@fontsource/open-sans',
|
'@fontsource/open-sans',
|
||||||
'normalize-wheel',
|
|
||||||
'@vueuse/components',
|
'@vueuse/components',
|
||||||
// TODO: remove this. It's currently required by xml2js in NodeErrors
|
// TODO: remove this. It's currently required by xml2js in NodeErrors
|
||||||
'stream-browserify',
|
'stream-browserify',
|
||||||
|
|
|
@ -899,9 +899,6 @@ importers:
|
||||||
n8n-workflow:
|
n8n-workflow:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../workflow
|
version: link:../workflow
|
||||||
normalize-wheel:
|
|
||||||
specifier: ^1.0.1
|
|
||||||
version: 1.0.1
|
|
||||||
pinia:
|
pinia:
|
||||||
specifier: ^2.1.6
|
specifier: ^2.1.6
|
||||||
version: 2.1.6(typescript@5.2.2)(vue@3.3.4)
|
version: 2.1.6(typescript@5.2.2)(vue@3.3.4)
|
||||||
|
@ -16975,10 +16972,6 @@ packages:
|
||||||
resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==}
|
resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/normalize-wheel@1.0.1:
|
|
||||||
resolution: {integrity: sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/now-and-later@2.0.1:
|
/now-and-later@2.0.1:
|
||||||
resolution: {integrity: sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==}
|
resolution: {integrity: sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
|
|
Loading…
Reference in a new issue