From b3fdbd884a14c06212fd4f78538481609a8032ab Mon Sep 17 00:00:00 2001 From: braks <78412429+bcakmakoglu@users.noreply.github.com> Date: Thu, 17 Nov 2022 19:29:17 +0100 Subject: [PATCH] feat(core): support touch for connection creation --- .../core/src/components/Handle/Handle.vue | 3 +- packages/core/src/composables/useHandle.ts | 122 +++++++++++++++++- packages/core/src/types/hooks.ts | 4 +- packages/core/src/types/store.ts | 4 +- 4 files changed, 125 insertions(+), 8 deletions(-) diff --git a/packages/core/src/components/Handle/Handle.vue b/packages/core/src/components/Handle/Handle.vue index aff01a19..4a235d59 100644 --- a/packages/core/src/components/Handle/Handle.vue +++ b/packages/core/src/components/Handle/Handle.vue @@ -15,7 +15,7 @@ const handle = ref() const handleId = $computed(() => id ?? `${nodeId}__handle-${position}`) -const { onMouseDown, onClick } = useHandle({ +const { onMouseDown, onTouchStart, onClick } = useHandle({ nodeId, handleId, isValidConnection, @@ -94,6 +94,7 @@ export default { }, ]" @mousedown="onMouseDown" + @touchstart="onTouchStart" @click="onClick" > diff --git a/packages/core/src/composables/useHandle.ts b/packages/core/src/composables/useHandle.ts index f10f2dfc..c26e9900 100644 --- a/packages/core/src/composables/useHandle.ts +++ b/packages/core/src/composables/useHandle.ts @@ -1,6 +1,6 @@ import type { MaybeRef } from '@vueuse/core' import { isFunction } from '@vueuse/core' -import type { Connection, Getters, GraphEdge, HandleType, ValidConnectionFunc } from '~/types' +import type { Connection, Getters, GraphEdge, HandleType, ValidConnectionFunc, XYPosition } from '~/types' import { ConnectionMode } from '~/types' interface Result { @@ -22,7 +22,7 @@ interface UseHandleProps { // checks if element below mouse is a handle and returns connection in form of an object { source: 123, target: 312 } export const checkElementBelowIsValid = ( - event: MouseEvent, + event: MouseEvent | TouchEvent, connectionMode: ConnectionMode, isTarget: boolean, nodeId: string, @@ -32,7 +32,10 @@ export const checkElementBelowIsValid = ( edges: GraphEdge[], getNode: Getters['getNode'], ) => { - const elementBelow = doc.elementFromPoint(event.clientX, event.clientY) + const clientX = (event as TouchEvent).touches ? (event as TouchEvent).touches[0].clientX : (event as MouseEvent).clientX + const clientY = (event as TouchEvent).touches ? (event as TouchEvent).touches[0].clientY : (event as MouseEvent).clientY + + const elementBelow = doc.elementFromPoint(clientX, clientY) const elementBelowIsTarget = elementBelow?.classList.contains('target') || false const elementBelowIsSource = elementBelow?.classList.contains('source') || false @@ -220,6 +223,118 @@ export default function useHandle({ doc.addEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject) } + const lastTouchPos = ref({ x: 0, y: 0 }) + + const onTouchStart = (event: TouchEvent) => { + const clientX = event.touches[0].clientX + const clientY = event.touches[0].clientY + + const doc = getHostForElement(event.target as HTMLElement) + if (!doc) return + + let validConnectFunc = isValidConnection + + const node = getNode(unref(nodeId)) + + if (node && (typeof node.connectable === 'undefined' ? nodesConnectable : node.connectable) === false) return + + if (!isValidConnection) { + if (node) validConnectFunc = !isTarget ? node.isValidTargetPos : node.isValidSourcePos + } + + const elementBelow = doc.elementFromPoint(clientX, clientY) + const elementBelowIsTarget = elementBelow?.classList.contains('target') + const elementBelowIsSource = elementBelow?.classList.contains('source') + + if (!vueFlowRef || (!elementBelowIsTarget && !elementBelowIsSource && !elementEdgeUpdaterType)) return + + const handleType = elementEdgeUpdaterType ?? (elementBelowIsTarget ? 'target' : 'source') + + const containerBounds = vueFlowRef.getBoundingClientRect() + + startConnection( + { + nodeId: unref(nodeId), + handleId: unref(handleId), + type: unref(handleType), + }, + { + x: clientX - containerBounds.left, + y: clientY - containerBounds.top, + }, + event, + ) + + function onTouchMove(event: TouchEvent) { + updateConnection({ + x: event.touches[0].clientX - containerBounds.left, + y: event.touches[0].clientY - containerBounds.top, + }) + + lastTouchPos.value = { + x: event.touches[0].clientX, + y: event.touches[0].clientY, + } + + const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid( + event, + connectionMode, + unref(isTarget), + unref(nodeId), + unref(handleId), + validConnectFunc, + doc, + edges, + getNode, + ) + + if (!isHoveringHandle) return resetRecentHandle(recentHoveredHandle) + + const isOwnHandle = connection.source === connection.target + + if (!isOwnHandle && elementBelow) { + recentHoveredHandle = elementBelow + elementBelow.classList.add('vue-flow__handle-connecting') + elementBelow.classList.toggle('vue-flow__handle-valid', isValid) + } + } + + function onTouchEnd(event: TouchEvent) { + const { connection, isValid } = checkElementBelowIsValid( + { clientX: lastTouchPos.value.x, clientY: lastTouchPos.value.y } as unknown as MouseEvent, + connectionMode, + unref(isTarget), + unref(nodeId), + unref(handleId), + validConnectFunc, + doc, + edges, + getNode, + ) + + const isOwnHandle = connection.source === connection.target + + if (isValid && !isOwnHandle) { + if (!onEdgeUpdate) emits.connect(connection) + else onEdgeUpdate(connection) + } + + if (elementEdgeUpdaterType) onEdgeUpdateEnd?.() + + resetRecentHandle(recentHoveredHandle) + + endConnection(event) + + lastTouchPos.value = { x: 0, y: 0 } + + doc.removeEventListener('touchmove', onTouchMove as EventListenerOrEventListenerObject) + doc.removeEventListener('touchend', onTouchEnd as EventListenerOrEventListenerObject) + } + + doc.addEventListener('touchmove', onTouchMove as EventListenerOrEventListenerObject) + doc.addEventListener('touchend', onTouchEnd as EventListenerOrEventListenerObject) + } + const onClick = (event: MouseEvent) => { if (!connectOnClick) return @@ -260,6 +375,7 @@ export default function useHandle({ return { onMouseDown, + onTouchStart, onClick, } } diff --git a/packages/core/src/types/hooks.ts b/packages/core/src/types/hooks.ts index 43fd1e72..2ec60382 100644 --- a/packages/core/src/types/hooks.ts +++ b/packages/core/src/types/hooks.ts @@ -54,9 +54,9 @@ export interface FlowEvents { miniMapNodeMouseLeave: NodeMouseEvent connect: Connection connectStart: { - event?: MouseEvent + event?: MouseEvent | TouchEvent } & OnConnectStartParams - connectEnd: MouseEvent | undefined + connectEnd: MouseEvent | TouchEvent | undefined paneReady: VueFlowStore move: { event: D3ZoomEvent; flowTransform: ViewpaneTransform } moveStart: { event: D3ZoomEvent; flowTransform: ViewpaneTransform } diff --git a/packages/core/src/types/store.ts b/packages/core/src/types/store.ts index b22bef6e..22c082a2 100644 --- a/packages/core/src/types/store.ts +++ b/packages/core/src/types/store.ts @@ -220,11 +220,11 @@ export interface Actions extends ViewportFunctions { /** force update node internal data, if handle bounds are incorrect, you might want to use this */ updateNodeInternals: UpdateNodeInternals /** start a connection */ - startConnection: (startHandle: StartHandle, position?: XYPosition, event?: MouseEvent, isClick?: boolean) => void + startConnection: (startHandle: StartHandle, position?: XYPosition, event?: MouseEvent | TouchEvent, isClick?: boolean) => void /** update connection position */ updateConnection: (position: XYPosition) => void /** end (or cancel) a connection */ - endConnection: (event?: MouseEvent, isClick?: boolean) => void + endConnection: (event?: MouseEvent | TouchEvent, isClick?: boolean) => void /** internal position updater, you probably don't want to use this */ updateNodePositions: UpdateNodePosition