diff --git a/packages/editor-ui/src/components/canvas/Canvas.vue b/packages/editor-ui/src/components/canvas/Canvas.vue index 7037b0a580..94e9c4fbba 100644 --- a/packages/editor-ui/src/components/canvas/Canvas.vue +++ b/packages/editor-ui/src/components/canvas/Canvas.vue @@ -18,7 +18,17 @@ import { Background } from '@vue-flow/background'; import { MiniMap } from '@vue-flow/minimap'; import Node from './elements/nodes/CanvasNode.vue'; import Edge from './elements/edges/CanvasEdge.vue'; -import { computed, onMounted, onUnmounted, provide, ref, toRef, useCssModule, watch } from 'vue'; +import { + computed, + onBeforeUnmount, + onMounted, + onUnmounted, + provide, + ref, + toRef, + useCssModule, + watch, +} from 'vue'; import type { EventBus } from 'n8n-design-system'; import { createEventBus } from 'n8n-design-system'; import { useContextMenu, type ContextMenuAction } from '@/composables/useContextMenu'; @@ -33,6 +43,7 @@ import { onKeyDown, onKeyUp, useDebounceFn } from '@vueuse/core'; import CanvasArrowHeadMarker from './elements/edges/CanvasArrowHeadMarker.vue'; import { CanvasNodeRenderType } from '@/types'; import CanvasBackgroundStripedPattern from './elements/CanvasBackgroundStripedPattern.vue'; +import { useTouchEventsInVueFlow } from '@/composables/useTouchEventsInVueFlow'; const $style = useCssModule(); @@ -95,6 +106,7 @@ const props = withDefaults( }, ); +const vueFlowStore = useVueFlow({ id: props.id, deleteKeyCode: null }); const { getSelectedNodes: selectedNodes, addSelectedNodes, @@ -112,9 +124,11 @@ const { findNode, onNodesInitialized, viewport, -} = useVueFlow({ id: props.id, deleteKeyCode: null }); + onInit, +} = vueFlowStore; const isPaneReady = ref(false); +const { addTouchListeners, removeTouchListeners } = useTouchEventsInVueFlow(vueFlowStore); const classes = computed(() => ({ [$style.canvas]: true, @@ -488,6 +502,14 @@ onNodesInitialized((nodes) => { void onFitView(); }); +onInit(() => { + addTouchListeners(); +}); + +onBeforeUnmount(() => { + removeTouchListeners(); +}); + watch(() => props.readOnly, setReadonly, { immediate: true, }); diff --git a/packages/editor-ui/src/composables/useTouchEventsInVueFlow.ts b/packages/editor-ui/src/composables/useTouchEventsInVueFlow.ts new file mode 100644 index 0000000000..0031ba9623 --- /dev/null +++ b/packages/editor-ui/src/composables/useTouchEventsInVueFlow.ts @@ -0,0 +1,60 @@ +import { ref } from 'vue'; +import type { VueFlowStore } from '@vue-flow/core'; + +export const useTouchEventsInVueFlow = (vueFlowStore: VueFlowStore) => { + const isPanning = ref(false); + const startX = ref(0); + const startY = ref(0); + + const handleTouchStart = (event: TouchEvent) => { + const touch = event.touches[0]; + startX.value = touch.clientX; + startY.value = touch.clientY; + + if (event.touches.length > 1) { + isPanning.value = true; + } + }; + + const handleTouchMove = (event: TouchEvent) => { + event.preventDefault(); + event.stopPropagation(); + if (isPanning.value && vueFlowStore.vueFlowRef.value) { + const touch = event.touches[0]; + const x = touch.clientX - startX.value; + const y = touch.clientY - startY.value; + + vueFlowStore.panBy({ x, y }); + + startX.value = touch.clientX; + startY.value = touch.clientY; + } + }; + + const handleTouchEnd = () => { + isPanning.value = false; + }; + + const addTouchListeners = () => { + const canvas = vueFlowStore.vueFlowRef.value; + if (canvas) { + canvas.addEventListener('touchstart', handleTouchStart); + canvas.addEventListener('touchmove', handleTouchMove); + canvas.addEventListener('touchend', handleTouchEnd); + } + }; + + const removeTouchListeners = () => { + const canvas = vueFlowStore.vueFlowRef.value; + if (canvas) { + canvas.removeEventListener('touchstart', handleTouchStart); + canvas.removeEventListener('touchmove', handleTouchMove); + canvas.removeEventListener('touchend', handleTouchEnd); + } + }; + + return { + addTouchListeners, + removeTouchListeners, + }; +};