diff --git a/packages/core/src/auto-imports.d.ts b/packages/core/src/auto-imports.d.ts index 497b319c..b0a146ed 100644 --- a/packages/core/src/auto-imports.d.ts +++ b/packages/core/src/auto-imports.d.ts @@ -28,6 +28,7 @@ declare global { const asyncComputed: typeof import('@vueuse/core')['asyncComputed'] const autoResetRef: typeof import('@vueuse/core')['autoResetRef'] const boxToRect: typeof import('./utils/graph')['boxToRect'] + const calcAutoPan: typeof import('./utils/autopan')['calcAutoPan'] const calcNextPosition: typeof import('./utils/drag')['calcNextPosition'] const checkElementBelowIsValid: typeof import('./composables/useHandleDepr')['checkElementBelowIsValid'] const clamp: typeof import('./utils/graph')['clamp'] diff --git a/packages/core/src/components/Nodes/NodeWrapper.vue b/packages/core/src/components/Nodes/NodeWrapper.vue index 8569252d..c083ca8f 100644 --- a/packages/core/src/components/Nodes/NodeWrapper.vue +++ b/packages/core/src/components/Nodes/NodeWrapper.vue @@ -46,7 +46,7 @@ const parentNode = $computed(() => (node.parentNode ? findNode(node.parentNode) const connectedEdges = $computed(() => getConnectedEdges([node], edges)) -const nodeElement = ref() +const nodeElement = ref() provide(NodeRef, nodeElement) @@ -89,17 +89,17 @@ onUpdateNodeInternals((updateIds) => { }) onMounted(() => { - props.resizeObserver.observe(nodeElement.value) + props.resizeObserver.observe(nodeElement.value as HTMLDivElement) }) onBeforeUnmount(() => { - props.resizeObserver.unobserve(nodeElement.value) + props.resizeObserver.unobserve(nodeElement.value as HTMLDivElement) }) watch( [() => node.type, () => node.sourcePosition, () => node.targetPosition], () => { - updateNodeDimensions([{ id, nodeElement: nodeElement.value, forceUpdate: true }]) + updateNodeDimensions([{ id, nodeElement: nodeElement.value as HTMLDivElement, forceUpdate: true }]) }, { flush: 'pre' }, ) diff --git a/packages/core/src/composables/useDrag.ts b/packages/core/src/composables/useDrag.ts index 342ea27f..5cc30b5f 100644 --- a/packages/core/src/composables/useDrag.ts +++ b/packages/core/src/composables/useDrag.ts @@ -1,4 +1,4 @@ -import type { D3DragEvent, SubjectPosition } from 'd3-drag' +import type { D3DragEvent, DragBehavior, SubjectPosition } from 'd3-drag' import { drag } from 'd3-drag' import { select } from 'd3-selection' import type { Ref } from 'vue' @@ -11,7 +11,7 @@ interface UseDragParams { onStart: (event: NodeDragEvent['event'], currentNode: NodeDragEvent['node'], nodes: NodeDragEvent['nodes']) => void onDrag: (event: NodeDragEvent['event'], currentNode: NodeDragEvent['node'], nodes: NodeDragEvent['nodes']) => void onStop: (event: NodeDragEvent['event'], currentNode: NodeDragEvent['node'], nodes: NodeDragEvent['nodes']) => void - el: Ref + el: Ref disabled?: MaybeRef id?: string } @@ -19,13 +19,19 @@ interface UseDragParams { function useDrag(params: UseDragParams) { const scope = effectScope() - const dragging = scope.run(() => { + tryOnScopeDispose(() => scope.stop()) + + return scope.run(() => { const { + vueFlowRef, snapToGrid, snapGrid, noDragClassName, nodes, nodeExtent, + viewport, + autoPanOnNodeDrag, + panBy, findNode, multiSelectionActive, nodesSelectionActive, @@ -38,9 +44,19 @@ function useDrag(params: UseDragParams) { const { onStart, onDrag, onStop, el, disabled = false, id } = $(params) const dragging = ref(false) - let dragItems = $ref() + + let dragItems = $ref([]) + + let dragHandler = $ref>() + + let containerBounds = $ref(null) + let lastPos = $ref>({ x: undefined, y: undefined }) - let dragHandler = $ref() + let mousePosition = $ref({ x: 0, y: 0 }) + let dragEvent = $ref(null) + + let autoPanId = $ref(0) + let autoPanStarted = $ref(false) const node = $computed(() => (id ? findNode(id) : undefined)) @@ -50,6 +66,68 @@ function useDrag(params: UseDragParams) { if (el) { const selection = select(el) + const updateNodes = ({ x, y }: XYPosition) => { + lastPos = { x, y } + + let hasChange = false + + dragItems = dragItems.map((n) => { + const nextPosition = { x: x - n.distance.x, y: y - n.distance.y } + + if (snapToGrid) { + nextPosition.x = snapGrid[0] * Math.round(nextPosition.x / snapGrid[0]) + nextPosition.y = snapGrid[1] * Math.round(nextPosition.y / snapGrid[1]) + } + + const { computedPosition } = calcNextPosition( + n, + nextPosition, + nodeExtent, + n.parentNode ? findNode(n.parentNode) : undefined, + ) + + // we want to make sure that we only fire a change event when there is a changes + hasChange = hasChange || n.position.x !== computedPosition.x || n.position.y !== computedPosition.y + + n.position = computedPosition + + return n + }) + + if (!hasChange) return + + updateNodePositions(dragItems, true, true) + + dragging.value = true + + if (dragEvent) { + const [currentNode, nodes] = getEventHandlerParams({ + id, + dragItems, + findNode, + }) + + onDrag(dragEvent, currentNode, nodes) + } + } + + const autoPan = (): void => { + if (!containerBounds) return + + const [xMovement, yMovement] = calcAutoPan(mousePosition, containerBounds) + + if (xMovement !== 0 || yMovement !== 0) { + lastPos.x = (lastPos.x ?? 0) - xMovement / viewport.zoom + lastPos.y = (lastPos.y ?? 0) - yMovement / viewport.zoom + + updateNodes(lastPos as XYPosition) + + panBy({ x: xMovement, y: yMovement }) + } + + autoPanId = requestAnimationFrame(autoPan) + } + if (disabled) { selection.on('.drag', null) } else { @@ -57,104 +135,72 @@ function useDrag(params: UseDragParams) { .on('start', (event: UseDragEvent) => { if (!selectNodesOnDrag && !multiSelectionActive && id) { if (!node?.selected) { + // we need to reset selected nodes when selectNodesOnDrag=false removeSelectedElements() } } - if (node && !disabled && selectNodesOnDrag) { + if (node && selectNodesOnDrag) { handleNodeClick(node, multiSelectionActive, addSelectedNodes, removeSelectedElements, $$(nodesSelectionActive)) } - const mousePos = getPointerPosition(event, snapToGrid ? snapGrid : undefined) - dragItems = getDragItems(nodes, mousePos, findNode, id) + const pointerPos = getPointerPosition(event) + lastPos = pointerPos + dragItems = getDragItems(nodes, pointerPos, findNode, id) - if (onStart && dragItems) { + if (dragItems.length) { const [currentNode, nodes] = getEventHandlerParams({ id, dragItems, findNode, }) + onStart(event.sourceEvent, currentNode, nodes) } + + containerBounds = vueFlowRef?.getBoundingClientRect() || null + mousePosition = getEventPosition(event.sourceEvent, containerBounds!) }) .on('drag', (event: UseDragEvent) => { - const mousePos = getPointerPosition(event, snapToGrid ? snapGrid : undefined) + const pointerPos = getPointerPosition(event) - let hasChange = false - - // skip events without movement - if ((lastPos.x !== mousePos.x || lastPos.y !== mousePos.y) && dragItems) { - lastPos = mousePos - - dragItems = dragItems.map((n) => { - const nextPosition = { x: mousePos.x - n.distance.x, y: mousePos.y - n.distance.y } - - if (snapToGrid && snapGrid) { - const [snapX, snapY] = snapGrid - nextPosition.x = snapX * Math.round(nextPosition.x / snapX) - nextPosition.y = snapY * Math.round(nextPosition.y / snapY) - } - - const { computedPosition } = calcNextPosition( - n, - nextPosition, - nodeExtent, - n.parentNode ? findNode(n.parentNode) : undefined, - ) - - // we want to make sure that we only fire a change event when there is a changes - hasChange = hasChange || n.position.x !== nextPosition.x || n.position.y !== nextPosition.y - - n.position = computedPosition - - return n - }) - - if (!hasChange) return - - updateNodePositions(dragItems, true, true) - - if (onDrag) { - const [currentNode, nodes] = getEventHandlerParams({ - id, - dragItems, - findNode, - }) - - dragging.value = true - - onDrag(event.sourceEvent, currentNode, nodes) - } + if (!autoPanStarted && autoPanOnNodeDrag) { + autoPanStarted = true + autoPan() } - event.on('end', (event) => { - dragging.value = false + // skip events without movement + if ((lastPos.x !== pointerPos.xSnapped || lastPos.y !== pointerPos.ySnapped) && dragItems.length) { + dragEvent = event.sourceEvent as MouseEvent + mousePosition = getEventPosition(event.sourceEvent, containerBounds!) - if (onStop && dragItems) { - updateNodePositions(dragItems, false, false) - - const [currentNode, nodes] = getEventHandlerParams({ - id, - dragItems, - findNode, - }) - - dragging.value = false - - onStop(event.sourceEvent, currentNode, nodes) - } - }) + updateNodes(pointerPos) + } }) - .on('end', () => { + .on('end', (event: UseDragEvent) => { dragging.value = false + autoPanStarted = false + cancelAnimationFrame(autoPanId) + + if (dragItems.length) { + updateNodePositions(dragItems, false, false) + + const [currentNode, nodes] = getEventHandlerParams({ + id, + dragItems, + findNode, + }) + + onStop(event.sourceEvent as MouseEvent, currentNode, nodes) + } }) .filter((event: D3DragEvent['sourceEvent']) => { const target = event.target as HTMLDivElement return ( !event.button && (!noDragClassName || - (!hasSelector(target, `.${noDragClassName}`, $$(el)) && - (!node?.dragHandle || hasSelector(target, node.dragHandle, $$(el))))) + (!hasSelector(target, `.${noDragClassName}`, el) && + (!node?.dragHandle || hasSelector(target, node.dragHandle, el)))) ) }) @@ -165,10 +211,6 @@ function useDrag(params: UseDragParams) { return dragging }) - - tryOnScopeDispose(() => scope.stop()) - - return dragging } export default useDrag diff --git a/packages/core/src/composables/useGetPointerPosition.ts b/packages/core/src/composables/useGetPointerPosition.ts index c51a5b1a..a537ae1d 100644 --- a/packages/core/src/composables/useGetPointerPosition.ts +++ b/packages/core/src/composables/useGetPointerPosition.ts @@ -1,15 +1,10 @@ import type { UseDragEvent } from './useDrag' -import type { SnapGrid } from '~/types' export function useGetPointerPosition() { - const { viewport, snapGrid: globalSnapGrid, snapToGrid } = useVueFlow() - - const hasSnapGrid = (sg?: SnapGrid) => (sg ?? snapToGrid.value ? globalSnapGrid.value : undefined) + const { viewport, snapGrid, snapToGrid } = useVueFlow() // returns the pointer position projected to the RF coordinate system - return ({ sourceEvent }: UseDragEvent, snapGrid?: SnapGrid) => { - const currentSnapGrid = unref(hasSnapGrid(snapGrid)) - + return ({ sourceEvent }: UseDragEvent) => { const x = sourceEvent.touches ? sourceEvent.touches[0].clientX : sourceEvent.clientX const y = sourceEvent.touches ? sourceEvent.touches[0].clientY : sourceEvent.clientY @@ -20,8 +15,8 @@ export function useGetPointerPosition() { // we need the snapped position in order to be able to skip unnecessary drag events return { - xSnapped: currentSnapGrid ? currentSnapGrid[0] * Math.round(pointerPos.x / currentSnapGrid[0]) : pointerPos.x, - ySnapped: currentSnapGrid ? currentSnapGrid[1] * Math.round(pointerPos.y / currentSnapGrid[1]) : pointerPos.y, + xSnapped: snapToGrid.value ? snapGrid.value[0] * Math.round(pointerPos.x / snapGrid.value[0]) : pointerPos.x, + ySnapped: snapToGrid.value ? snapGrid.value[1] * Math.round(pointerPos.y / snapGrid.value[1]) : pointerPos.y, ...pointerPos, } } diff --git a/packages/core/src/utils/drag.ts b/packages/core/src/utils/drag.ts index 73b7ac22..e2f340bb 100644 --- a/packages/core/src/utils/drag.ts +++ b/packages/core/src/utils/drag.ts @@ -1,13 +1,12 @@ -import type { Ref } from 'vue' import { isNumber } from '@vueuse/shared' import type { Actions, CoordinateExtent, ExtendedParentExtent, GraphNode, NodeDragItem, XYPosition } from '~/types' -export function hasSelector(target: Element, selector: string, node: Ref): boolean { +export function hasSelector(target: Element, selector: string, node: Element): boolean { let current = target do { if (current && current.matches(selector)) return true - else if (current === node.value) return false + else if (current === node) return false current = current.parentElement as Element } while (current) @@ -28,8 +27,8 @@ export function getDragItems( id: n.id, position: n.position || { x: 0, y: 0 }, distance: { - x: mousePos.x - n.computedPosition?.x || 0, - y: mousePos.y - n.computedPosition?.y || 0, + x: mousePos.x - n.computedPosition.x || 0, + y: mousePos.y - n.computedPosition.y || 0, }, from: n.computedPosition, extent: n.extent, diff --git a/packages/core/src/utils/general.ts b/packages/core/src/utils/general.ts index 3f5d46ec..4484292e 100644 --- a/packages/core/src/utils/general.ts +++ b/packages/core/src/utils/general.ts @@ -2,8 +2,8 @@ export const isMouseEvent = (event: MouseEvent | TouchEvent): event is MouseEven export const getEventPosition = (event: MouseEvent | TouchEvent, bounds?: DOMRect) => { const isMouseTriggered = isMouseEvent(event) - const evtX = isMouseTriggered ? event.clientX : event.touches?.[0].clientX - const evtY = isMouseTriggered ? event.clientY : event.touches?.[0].clientY + const evtX = isMouseTriggered ? event.clientX : event.touches[0].clientX + const evtY = isMouseTriggered ? event.clientY : event.touches[0].clientY return { x: evtX - (bounds?.left ?? 0),