feat(core): add auto pan on node drag
Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com>
This commit is contained in:
1
packages/core/src/auto-imports.d.ts
vendored
1
packages/core/src/auto-imports.d.ts
vendored
@@ -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']
|
||||
|
||||
@@ -46,7 +46,7 @@ const parentNode = $computed(() => (node.parentNode ? findNode(node.parentNode)
|
||||
|
||||
const connectedEdges = $computed(() => getConnectedEdges([node], edges))
|
||||
|
||||
const nodeElement = ref()
|
||||
const nodeElement = ref<HTMLDivElement>()
|
||||
|
||||
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' },
|
||||
)
|
||||
|
||||
@@ -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<Element>
|
||||
el: Ref<Element | undefined>
|
||||
disabled?: MaybeRef<boolean>
|
||||
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<NodeDragItem[]>()
|
||||
|
||||
let dragItems = $ref<NodeDragItem[]>([])
|
||||
|
||||
let dragHandler = $ref<DragBehavior<Element, unknown, unknown>>()
|
||||
|
||||
let containerBounds = $ref<DOMRect | null>(null)
|
||||
|
||||
let lastPos = $ref<Partial<XYPosition>>({ x: undefined, y: undefined })
|
||||
let dragHandler = $ref<any>()
|
||||
let mousePosition = $ref<XYPosition>({ x: 0, y: 0 })
|
||||
let dragEvent = $ref<MouseEvent | null>(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<HTMLDivElement, null, SubjectPosition>['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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Element>): 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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user