refactor: use d3-drag
This commit is contained in:
@@ -37,10 +37,10 @@
|
||||
"vue": "^3.2.25"
|
||||
},
|
||||
"dependencies": {
|
||||
"@braks/revue-draggable": "^0.4.2",
|
||||
"@types/d3": "^7.1.0",
|
||||
"@vueuse/core": "^8.4.2",
|
||||
"d3": "^7.4.4",
|
||||
"d3-drag": "^3.0.0",
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-zoom": "^3.0.0"
|
||||
},
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { useDraggableCore } from '@braks/revue-draggable'
|
||||
import type { CSSProperties } from 'vue'
|
||||
import { useVueFlow } from '../../composables'
|
||||
import type { GraphNode, NodeComponent, SnapGrid } from '../../types'
|
||||
import { CSSProperties } from 'vue'
|
||||
import { useVueFlow, useDrag } from '../../composables'
|
||||
import { GraphNode, NodeComponent, SnapGrid } from '../../types'
|
||||
import { NodeId } from '../../context'
|
||||
import { getConnectedEdges, getHandleBounds, getXYZPos } from '../../utils'
|
||||
|
||||
const { id, type, name, node, draggable, selectable, connectable, snapGrid } = defineProps<{
|
||||
const { id, type, name, node, parentNode, draggable, selectable, connectable, snapGrid } = defineProps<{
|
||||
id: string
|
||||
node: GraphNode
|
||||
parentNode?: GraphNode
|
||||
draggable: boolean
|
||||
selectable: boolean
|
||||
connectable: boolean
|
||||
@@ -31,60 +31,63 @@ const {
|
||||
updateNodeDimensions,
|
||||
getNode,
|
||||
addSelectedNodes,
|
||||
multiSelectionActive,
|
||||
resetSelectedElements,
|
||||
} = $(useVueFlow())
|
||||
|
||||
const nodeElement = ref()
|
||||
|
||||
const { scale, disabled, handle, cancel, grid, onDrag, onDragStart, onDragStop } = useDraggableCore(nodeElement, {
|
||||
handle: node.dragHandle,
|
||||
disabled: !draggable,
|
||||
grid: snapGrid,
|
||||
cancel: `.${noDragClassName}`,
|
||||
enableUserSelectHack: false,
|
||||
scale: viewport.zoom,
|
||||
useDrag({
|
||||
id,
|
||||
el: nodeElement,
|
||||
disabled: computed(() => !draggable),
|
||||
noDragClassName: $$(noDragClassName) as any,
|
||||
handleSelector: node.dragHandle,
|
||||
onStart(event) {
|
||||
if (selectNodesOnDrag && selectable) {
|
||||
setState({
|
||||
nodesSelectionActive: false,
|
||||
})
|
||||
|
||||
if (!node.selected) {
|
||||
addSelectedNodes([node])
|
||||
}
|
||||
} else if (!selectNodesOnDrag && !node.selected && selectable) {
|
||||
if (multiSelectionActive) {
|
||||
addSelectedNodes([node])
|
||||
} else {
|
||||
resetSelectedElements()
|
||||
setState({
|
||||
nodesSelectionActive: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
emits.nodeDragStart({ event: event.sourceEvent, node, connectedEdges: getConnectedEdges([node], edges) })
|
||||
},
|
||||
onDrag(event, { dx, dy }) {
|
||||
updateNodePosition({ id: node.id, diff: { x: dx, y: dy }, dragging: true })
|
||||
emits.nodeDrag({ event: event.sourceEvent, node, connectedEdges: getConnectedEdges([node], edges) })
|
||||
},
|
||||
onStop(event) {
|
||||
if (!node.dragging) {
|
||||
if (selectable && !selectNodesOnDrag && !node.selected) {
|
||||
addSelectedNodes([node])
|
||||
}
|
||||
emits.nodeClick({ event: event.sourceEvent, node, connectedEdges: getConnectedEdges([node], edges) })
|
||||
return
|
||||
}
|
||||
|
||||
updateNodePosition({ id: node.id, dragging: false })
|
||||
|
||||
emits.nodeDragStop({ event: event.sourceEvent, node, connectedEdges: getConnectedEdges([node], edges) })
|
||||
},
|
||||
})
|
||||
|
||||
onBeforeMount(() => {
|
||||
updateNodePosition({ id: node.id, diff: { x: 0, y: 0 } })
|
||||
updateNodePosition({ id: node.id, diff: { x: 0, y: 0 }, dragging: false })
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
debouncedWatch(
|
||||
() => viewport.zoom,
|
||||
() => {
|
||||
scale.value = viewport.zoom
|
||||
},
|
||||
{ debounce: 5, flush: 'post' },
|
||||
)
|
||||
|
||||
watch(
|
||||
() => draggable,
|
||||
() => {
|
||||
disabled.value = !draggable
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => node.dragHandle,
|
||||
() => {
|
||||
if (node.dragHandle) handle.value = node.dragHandle
|
||||
},
|
||||
)
|
||||
|
||||
watch($$(noDragClassName), () => {
|
||||
if (noDragClassName) cancel.value = noDragClassName as any
|
||||
})
|
||||
|
||||
watch(
|
||||
() => snapGrid,
|
||||
() => {
|
||||
if (grid) grid.value = snapGrid
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
const parent = $computed(() => (node.parentNode ? getNode(node.parentNode) : undefined))
|
||||
|
||||
onMounted(() => {
|
||||
const observer = useResizeObserver(nodeElement, () =>
|
||||
updateNodeDimensions([{ id: node.id, nodeElement: nodeElement.value, forceUpdate: true }]),
|
||||
@@ -103,7 +106,7 @@ onMounted(() => {
|
||||
updateNodeDimensions([{ id: node.id, nodeElement: nodeElement.value, forceUpdate: true }])
|
||||
|
||||
watch(
|
||||
[() => node.position, () => parent?.computedPosition, () => node.selected, () => parent?.selected],
|
||||
[() => node.position, () => parentNode?.computedPosition, () => node.selected, () => parentNode?.selected],
|
||||
([pos, parent]) => {
|
||||
const xyzPos = {
|
||||
...pos,
|
||||
@@ -117,7 +120,7 @@ onMounted(() => {
|
||||
graphNode.computedPosition = xyzPos
|
||||
}
|
||||
|
||||
graphNode.handleBounds = getHandleBounds(nodeElement.value, scale.value)
|
||||
graphNode.handleBounds = getHandleBounds(nodeElement.value, viewport.zoom)
|
||||
},
|
||||
{ deep: true, flush: 'post' },
|
||||
)
|
||||
@@ -169,44 +172,6 @@ const onSelectNode = (event: MouseEvent) => {
|
||||
}
|
||||
}
|
||||
|
||||
onDragStart(({ event }) => {
|
||||
addSelectedNodes([])
|
||||
emits.nodeDragStart({ event, node, connectedEdges: getConnectedEdges([node], edges) })
|
||||
|
||||
if (selectNodesOnDrag && selectable) {
|
||||
setState({
|
||||
nodesSelectionActive: false,
|
||||
})
|
||||
|
||||
if (!node.selected) addSelectedNodes([node])
|
||||
} else if (!selectNodesOnDrag && !node.selected && selectable) {
|
||||
setState({
|
||||
nodesSelectionActive: false,
|
||||
})
|
||||
|
||||
addSelectedNodes([])
|
||||
}
|
||||
})
|
||||
|
||||
onDrag(({ event, data: { deltaX, deltaY } }) => {
|
||||
updateNodePosition({ id: node.id, diff: { x: deltaX, y: deltaY }, dragging: true })
|
||||
emits.nodeDrag({ event, node, connectedEdges: getConnectedEdges([node], edges) })
|
||||
})
|
||||
|
||||
onDragStop(({ event, data: { deltaX, deltaY } }) => {
|
||||
// onDragStop also gets called when user just clicks on a node.
|
||||
// Because of that we set dragging to true inside the onDrag handler and handle the click here
|
||||
if (!node.dragging) {
|
||||
if (selectable && !selectNodesOnDrag && !node.selected) {
|
||||
addSelectedNodes([node])
|
||||
}
|
||||
emits.nodeClick({ event, node, connectedEdges: getConnectedEdges([node], edges) })
|
||||
return
|
||||
}
|
||||
updateNodePosition({ id: node.id, diff: { x: deltaX, y: deltaY }, dragging: false })
|
||||
emits.nodeDragStop({ event, node, connectedEdges: getConnectedEdges([node], edges) })
|
||||
})
|
||||
|
||||
const getClass = computed(() => {
|
||||
const extraClass = node.class instanceof Function ? node.class(node) : node.class
|
||||
return [
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
<script lang="ts" setup>
|
||||
import { useDraggableCore } from '@braks/revue-draggable'
|
||||
import { useVueFlow } from '../../composables'
|
||||
import { useVueFlow, useDrag } from '../../composables'
|
||||
import { getRectOfNodes } from '../../utils'
|
||||
|
||||
const { emits, setState, viewport, getSelectedNodes, snapToGrid, snapGrid, updateNodePosition, noPanClassName } = $(useVueFlow())
|
||||
|
||||
const el = templateRef<HTMLDivElement>('el', null)
|
||||
|
||||
useDrag({
|
||||
el,
|
||||
onStart(event) {
|
||||
emits.selectionDragStart({ event: event.sourceEvent, nodes: getSelectedNodes })
|
||||
},
|
||||
onDrag(event, { dx, dy }) {
|
||||
emits.selectionDrag({ event: event.sourceEvent, nodes: getSelectedNodes })
|
||||
updateNodePosition({ diff: { x: dx, y: dy }, dragging: true })
|
||||
},
|
||||
onStop(event) {
|
||||
emits.selectionDragStop({ event: event.sourceEvent, nodes: getSelectedNodes })
|
||||
getSelectedNodes.forEach((node) => (node.dragging = false))
|
||||
},
|
||||
})
|
||||
|
||||
const selectedNodesBBox = $computed(() => getRectOfNodes(getSelectedNodes))
|
||||
|
||||
const innerStyle = computed(() => ({
|
||||
@@ -25,34 +39,6 @@ watch($$(selectedNodesBBox), (v) => {
|
||||
})
|
||||
|
||||
const onContextMenu = (event: MouseEvent) => emits.selectionContextMenu({ event, nodes: getSelectedNodes })
|
||||
|
||||
const { onDragStart, onDrag, onDragStop, scale } = useDraggableCore(el, {
|
||||
grid: snapToGrid ? snapGrid : undefined,
|
||||
enableUserSelectHack: false,
|
||||
scale: viewport.zoom,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
debouncedWatch(
|
||||
() => viewport.zoom,
|
||||
() => {
|
||||
scale.value = viewport.zoom
|
||||
},
|
||||
{ debounce: 5 },
|
||||
)
|
||||
})
|
||||
|
||||
onDragStart(({ event }) => emits.selectionDragStart({ event, nodes: getSelectedNodes }))
|
||||
|
||||
onDrag(({ event, data: { deltaX, deltaY } }) => {
|
||||
emits.selectionDrag({ event, nodes: getSelectedNodes })
|
||||
updateNodePosition({ diff: { x: deltaX, y: deltaY }, dragging: true })
|
||||
})
|
||||
|
||||
onDragStop(({ event }) => {
|
||||
emits.selectionDragStop({ event, nodes: getSelectedNodes })
|
||||
getSelectedNodes.forEach((node) => (node.dragging = false))
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
@@ -3,3 +3,4 @@ export { default as useKeyPress } from './useKeyPress'
|
||||
export { default as useZoomPanHelper } from './useZoomPanHelper'
|
||||
export { default as useWindow } from './useWindow'
|
||||
export { default as useVueFlow } from './useVueFlow'
|
||||
export { default as useDrag } from './useDrag'
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { D3DragEvent, drag, SubjectPosition } from 'd3-drag'
|
||||
import { select } from 'd3-selection'
|
||||
import { Ref } from 'vue'
|
||||
import { MaybeRef } from '@vueuse/core'
|
||||
import useVueFlow from './useVueFlow'
|
||||
import { pointToRendererPoint } from '~/utils'
|
||||
import { GraphNode, XYPosition } from '~/types'
|
||||
|
||||
export type UseDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>
|
||||
export type UseDragData = { dx: number; dy: number }
|
||||
|
||||
type UseDragParams = {
|
||||
onStart: (event: UseDragEvent) => void
|
||||
onDrag: (event: UseDragEvent, data: UseDragData) => void
|
||||
onStop: (event: UseDragEvent) => void
|
||||
el: Ref<Element>
|
||||
disabled?: MaybeRef<boolean>
|
||||
noDragClassName?: MaybeRef<string>
|
||||
handleSelector?: string
|
||||
id?: string
|
||||
}
|
||||
|
||||
function getOffset(event: UseDragEvent, el: Element): XYPosition {
|
||||
const bounds = el.getBoundingClientRect() || { x: 0, y: 0 }
|
||||
|
||||
const parent = (el as HTMLDivElement).offsetParent
|
||||
|
||||
const parentBounds = parent?.getBoundingClientRect() || { x: 0, y: 0 }
|
||||
|
||||
return {
|
||||
x: event.x - (bounds.x - parentBounds.x - (parent?.scrollLeft || 0)),
|
||||
y: event.y - (bounds.y - parentBounds.y - (parent?.scrollTop || 0)),
|
||||
}
|
||||
}
|
||||
|
||||
function getParentNodePosition(parent?: GraphNode): XYPosition {
|
||||
return {
|
||||
x: parent?.computedPosition?.x || 0,
|
||||
y: parent?.computedPosition?.y || 0,
|
||||
}
|
||||
}
|
||||
|
||||
function hasSelector(target: Element, selector: string, node: Ref<Element>): boolean {
|
||||
let current = target
|
||||
|
||||
do {
|
||||
if (current && current.matches(selector)) return true
|
||||
else if (current === node.value) return false
|
||||
|
||||
current = current.parentElement as Element
|
||||
} while (current)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function useDrag(params: UseDragParams) {
|
||||
const { viewport, snapToGrid, snapGrid, getNode } = useVueFlow()
|
||||
const { onStart, onDrag, onStop, el, disabled = false, noDragClassName, id, handleSelector } = $(params)
|
||||
|
||||
let startPos = $ref<XYPosition>({ x: 0, y: 0 })
|
||||
let lastPos = $ref<Partial<XYPosition>>({ x: undefined, y: undefined })
|
||||
let parentPos = $ref<XYPosition>({ x: 0, y: 0 })
|
||||
|
||||
return watch(
|
||||
[() => disabled, () => noDragClassName, () => id, () => el],
|
||||
() => {
|
||||
if (el) {
|
||||
const selection = select(el)
|
||||
const node = id ? getNode.value(id) : undefined
|
||||
|
||||
if (disabled) {
|
||||
selection.on('.drag', null)
|
||||
} else {
|
||||
const dragHandler = drag()
|
||||
.on('start', (event: UseDragEvent) => {
|
||||
const offset = getOffset(event, el)
|
||||
parentPos = getParentNodePosition(node && node.parentNode ? getNode.value(node!.parentNode!) : undefined)
|
||||
|
||||
startPos = {
|
||||
x: offset.x - viewport.value.x,
|
||||
y: offset.y - viewport.value.y,
|
||||
}
|
||||
|
||||
onStart(event)
|
||||
})
|
||||
.on('drag', (event: UseDragEvent) => {
|
||||
const pos = pointToRendererPoint(
|
||||
{
|
||||
x: event.x - startPos.x,
|
||||
y: event.y - startPos.y,
|
||||
},
|
||||
viewport.value,
|
||||
snapToGrid.value,
|
||||
snapGrid.value,
|
||||
)
|
||||
|
||||
pos.x -= parentPos.x
|
||||
pos.y -= parentPos.y
|
||||
|
||||
// skip events without movement
|
||||
if (lastPos.x !== pos.x || lastPos.y !== pos.y) {
|
||||
if (lastPos.x && lastPos.y) {
|
||||
onDrag(event, {
|
||||
dx: pos.x - lastPos.x,
|
||||
dy: pos.y - lastPos.y,
|
||||
})
|
||||
}
|
||||
|
||||
lastPos = pos
|
||||
}
|
||||
})
|
||||
.on('end', onStop)
|
||||
.filter((event: any) => {
|
||||
if (handleSelector) {
|
||||
return !hasSelector(event.sourceEvent.target, handleSelector, $$(el))
|
||||
}
|
||||
return !event.ctrlKey && !event.button && !event.target.className.includes(noDragClassName)
|
||||
})
|
||||
|
||||
selection.call(dragHandler)
|
||||
}
|
||||
}
|
||||
},
|
||||
{ flush: 'post' },
|
||||
)
|
||||
}
|
||||
|
||||
export default useDrag
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { EventHook, EventHookOn, EventHookTrigger } from '@vueuse/core'
|
||||
import type { MouseTouchEvent } from '@braks/revue-draggable'
|
||||
import type { D3ZoomEvent } from 'd3-zoom'
|
||||
import type { FlowInstance } from './flow'
|
||||
import type { GraphEdge } from './edge'
|
||||
@@ -8,6 +7,8 @@ import type { Connection, OnConnectStartParams } from './connection'
|
||||
import type { FlowTransform } from './zoom'
|
||||
import type { EdgeChange, NodeChange } from './changes'
|
||||
|
||||
export type MouseTouchEvent = MouseEvent | TouchEvent | PointerEvent
|
||||
|
||||
export interface NodeMouseEvent {
|
||||
event: MouseTouchEvent
|
||||
node: GraphNode
|
||||
|
||||
Reference in New Issue
Block a user