refactor(vue-flow,nodes): use absolute positions instead of deltas

This commit is contained in:
Braks
2022-05-27 23:36:01 +02:00
parent 660c0bceab
commit cec8f4d77e
8 changed files with 265 additions and 201 deletions
@@ -1,8 +1,8 @@
<script lang="ts" setup>
import { useDrag, useVueFlow } from '../../composables'
import type { NodeComponent, SnapGrid, XYZPosition } from '../../types'
import type { NodeComponent, SnapGrid } from '../../types'
import { NodeId } from '../../context'
import { getConnectedEdges, getHandleBounds, getXYZPos } from '../../utils'
import { getConnectedEdges, getHandleBounds, getXYZPos, handleNodeClick } from '../../utils'
const { id, type, name, draggable, selectable, connectable, snapGrid, ...props } = defineProps<{
id: string
@@ -14,8 +14,6 @@ const { id, type, name, draggable, selectable, connectable, snapGrid, ...props }
name: string
}>()
const emit = defineEmits(['update:modelValue'])
provide(NodeId, id)
const {
@@ -26,7 +24,6 @@ const {
emits,
selectNodesOnDrag,
setState,
updateNodePosition,
updateNodeDimensions,
getNode,
addSelectedNodes,
@@ -40,45 +37,20 @@ const parentNode = $computed(() => (node.parentNode ? getNode(node.parentNode) :
const nodeElement = ref()
let dragging = $ref(false)
useDrag({
const dragging = useDrag({
id,
el: nodeElement,
disabled: computed(() => !draggable),
noDragClassName: $$(noDragClassName) as any,
handleSelector: node.dragHandle,
onStart(event) {
removeSelectedElements()
if (selectNodesOnDrag && selectable) {
setState({
nodesSelectionActive: false,
})
if (!node.selected) {
addSelectedNodes([node])
}
} else if (!selectNodesOnDrag && !node.selected && selectable) {
if (multiSelectionActive) {
addSelectedNodes([node])
} else {
removeSelectedElements()
setState({
nodesSelectionActive: false,
})
}
}
emits.nodeDragStart({ event: event.sourceEvent, node, nodes: [] })
onStart(event, node, nodes) {
emits.nodeDragStart({ event, node, nodes })
},
onDrag(event, { dx, dy }) {
dragging = true
updateNodePosition({ id, diff: { x: dx, y: dy } })
emits.nodeDrag({ event: event.sourceEvent, node, nodes: [] })
onDrag(event, node, nodes) {
emits.nodeDrag({ event, node, nodes })
},
onStop(event) {
dragging = false
emits.nodeDragStop({ event: event.sourceEvent, node, nodes: [] })
onStop(event, node, nodes) {
emits.nodeDragStop({ event, node, nodes })
},
})
@@ -156,22 +128,10 @@ const onDoubleClick = (event: MouseEvent) =>
emits.nodeDoubleClick({ event, node, connectedEdges: getConnectedEdges([node], edges) })
const onSelectNode = (event: MouseEvent) => {
if (!dragging) {
if (selectable && !selectNodesOnDrag && !node.selected) {
setState({
nodesSelectionActive: false,
})
if (!node.selected) {
addSelectedNodes([node])
} else if (node.selected && multiSelectionActive) {
// todo add action to unselect nodes/edges easily
getSelectedNodes.splice(getSelectedNodes.indexOf(node), 1)
node.selected = false
}
}
emits.nodeClick({ event, node, connectedEdges: getConnectedEdges([node], edges) })
if (selectable && (!selectNodesOnDrag || !draggable)) {
handleNodeClick(node, multiSelectionActive, addSelectedNodes, removeSelectedElements, setState)
}
emits.nodeClick({ event, node, connectedEdges: getConnectedEdges([node], edges) })
}
const getClass = computed(() => {
+98 -78
View File
@@ -4,19 +4,16 @@ import { select } from 'd3-selection'
import type { Ref } from 'vue'
import type { MaybeRef } from '@vueuse/core'
import useVueFlow from './useVueFlow'
import { pointToRendererPoint } from '~/utils'
import type { GraphNode, XYPosition } from '~/types'
import { handleNodeClick, pointToRendererPoint } from '~/utils'
import type { NodeDragEvent, NodeDragItem, XYPosition } from '~/types'
import { getDragItems, getEventHandlerParams, getParentNodePosition, hasSelector, updatePosition } from '~/utils/drag'
export type UseDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>
export interface UseDragData {
dx: number
dy: number
}
interface UseDragParams {
onStart: (event: UseDragEvent) => void
onDrag: (event: UseDragEvent, data: UseDragData) => void
onStop: (event: UseDragEvent) => void
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>
disabled?: MaybeRef<boolean>
noDragClassName?: MaybeRef<string>
@@ -24,103 +21,124 @@ interface UseDragParams {
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 {
viewport,
snapToGrid,
snapGrid,
nodes,
nodeExtent,
getNode,
multiSelectionActive,
selectNodesOnDrag,
removeSelectedElements,
addSelectedNodes,
setState,
updateNodePositions,
} = $(useVueFlow())
const { onStart, onDrag, onStop, el, disabled = false, noDragClassName, id, handleSelector } = $(params)
let startPos = $ref<XYPosition>({ x: 0, y: 0 })
const dragging = ref(false)
let dragItems = $ref<NodeDragItem[]>()
let lastPos = $ref<Partial<XYPosition>>({ x: undefined, y: undefined })
let parentPos = $ref<XYPosition>({ x: 0, y: 0 })
let dragHandler = $ref<any>()
const handleDrag = (event: UseDragEvent) => {
const pos = pointToRendererPoint(
const getMousePosition = (event: UseDragEvent) => {
const mousePos = pointToRendererPoint(
{
x: event.x - startPos.x,
y: event.y - startPos.y,
x: event.sourceEvent.clientX,
y: event.sourceEvent.clientY,
},
viewport.value,
snapToGrid.value,
snapGrid.value,
viewport,
snapToGrid,
snapGrid,
)
pos.x -= parentPos.x
pos.y -= parentPos.y
mousePos.x -= parentPos.x
mousePos.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
return mousePos
}
return watch(
watch(
[() => disabled, () => noDragClassName, () => id, () => el],
() => {
if (el) {
const selection = select(el)
const node = id ? getNode.value(id) : undefined
const node = id ? getNode(id) : undefined
if (disabled) {
selection.on('.drag', null)
} else {
const dragHandler = drag()
dragHandler = drag()
.on('start', (event: UseDragEvent) => {
const offset = getOffset(event, el)
parentPos = getParentNodePosition(node && node.parentNode ? getNode.value(node!.parentNode!) : undefined)
parentPos = getParentNodePosition(node && node.parentNode ? getNode(node!.parentNode!) : undefined)
startPos = {
x: offset.x - viewport.value.x,
y: offset.y - viewport.value.y,
if (!selectNodesOnDrag && !multiSelectionActive && id) {
if (!node?.selected) {
removeSelectedElements()
}
}
onStart(event)
if (node && !disabled && selectNodesOnDrag) {
handleNodeClick(node, multiSelectionActive, addSelectedNodes, removeSelectedElements, setState)
}
const mousePos = getMousePosition(event)
dragItems = getDragItems(nodes, mousePos, getNode, id)
if (onStart && dragItems) {
const [currentNode, nodes] = getEventHandlerParams({
id,
dragItems,
node: node!,
})
onStart(event.sourceEvent, currentNode, nodes)
}
})
.on('drag', handleDrag)
.on('end', (event: UseDragEvent) => {
if (node) lastPos = node.position
onStop(event)
.on('drag', (event: UseDragEvent) => {
const mousePos = getMousePosition(event)
console.log(node?.label)
// skip events without movement
if ((lastPos.x !== mousePos.x || lastPos.y !== mousePos.y) && dragItems) {
lastPos = mousePos
dragItems = dragItems.map((n) =>
updatePosition(n, mousePos, n.parentNode ? getNode(n.parentNode) : undefined, nodeExtent),
)
updateNodePositions(dragItems)
dragging.value = true
if (onDrag) {
const [currentNode, nodes] = getEventHandlerParams({
id,
dragItems,
node: node!,
})
onDrag(event.sourceEvent, currentNode, nodes)
}
}
event.on('end', (event) => {
if (onStop && dragItems) {
const [currentNode, nodes] = getEventHandlerParams({
id,
dragItems,
node: node!,
})
onStop(event.sourceEvent, currentNode, nodes)
}
})
})
.on('end', (_: UseDragEvent) => {
// if (node) lastPos = node.position
})
.filter((event: D3DragEvent<HTMLDivElement, null, SubjectPosition>['sourceEvent']) => {
const filter = !event.ctrlKey && !event.button && !event.target.className.includes(noDragClassName)
const filter =
!event.ctrlKey && !event.button && (!noDragClassName || !event.target.className.includes(noDragClassName))
if (handleSelector) {
return !hasSelector(event.sourceEvent.target, handleSelector, $$(el)) && filter
@@ -135,6 +153,8 @@ function useDrag(params: UseDragParams) {
},
{ flush: 'post' },
)
return dragging
}
export default useDrag
+12 -21
View File
@@ -16,7 +16,6 @@ import {
applyChanges,
createAdditionChange,
createGraphNodes,
createPositionChange,
createSelectionChange,
getDimensions,
getHandleBounds,
@@ -26,34 +25,26 @@ import {
isGraphEdge,
isGraphNode,
isNode,
isParentSelected,
parseEdge,
updateEdgeAction,
} from '~/utils'
export default (state: State, getters: ComputedGetters): Actions => {
const updateNodePosition: Actions['updateNodePosition'] = ({ id, diff = { x: 0, y: 0 } }) => {
const nodePosPromise = new Promise<NodePositionChange[]>((resolve) => {
const changes: NodePositionChange[] = []
const curr = id ? getters.getNode.value(id)! : undefined
if (curr) {
changes.push(createPositionChange({ node: curr, diff, nodeExtent: state.nodeExtent }, getters.getNode.value))
} else {
getters.getSelectedNodes.value.forEach((node) => {
if (!node.parentNode) {
changes.push(createPositionChange({ node, diff, nodeExtent: state.nodeExtent }, getters.getNode.value))
} else if (!isParentSelected(node, getters.getNode.value)) {
changes.push(createPositionChange({ node, diff, nodeExtent: state.nodeExtent }, getters.getNode.value))
}
})
const updateNodePositions: Actions['updateNodePositions'] = (dragItems) => {
const changes: NodePositionChange[] = []
dragItems.forEach((node) => {
const change: NodePositionChange = {
id: node.id,
type: 'position',
position: node.position,
}
if (changes.length) resolve(changes)
changes.push(change)
})
nodePosPromise.then((changes) => {
if (changes?.length) {
state.hooks.nodesChange.trigger(changes)
})
}
}
const updateNodeDimensions: Actions['updateNodeDimensions'] = (updates) => {
@@ -274,7 +265,7 @@ export default (state: State, getters: ComputedGetters): Actions => {
}
return {
updateNodePosition,
updateNodePositions,
updateNodeDimensions,
setElements,
setNodes,
+13
View File
@@ -2,6 +2,19 @@ import type { Dimensions, ElementData, XYPosition } from './flow'
import type { GraphNode, NodeHandleBounds } from './node'
import type { GraphEdge } from './edge'
export interface NodeDragItem {
id: string
// relative node position
position: XYPosition
// distance from the mouse cursor to the node when start dragging
distance: XYPosition
// delta to previous position
delta: XYPosition
dimensions: Dimensions
extent?: Node['extent']
parentNode?: string
}
export interface NodeDimensionChange {
id: string
type: 'dimensions'
+3 -8
View File
@@ -6,7 +6,7 @@ import type { DefaultEdgeOptions, Edge, GraphEdge } from './edge'
import type { CoordinateExtent, GraphNode, Node } from './node'
import type { D3Selection, D3Zoom, D3ZoomHandler, KeyCode, PanOnScrollMode, Viewport } from './zoom'
import type { FlowHooks, FlowHooksEmit, FlowHooksOn } from './hooks'
import type { EdgeChange, NodeChange } from './changes'
import type { EdgeChange, NodeChange, NodeDragItem } from "./changes";
import type { HandleType, StartHandle } from './handle'
export interface UpdateNodeDimensionsParams {
@@ -15,11 +15,6 @@ export interface UpdateNodeDimensionsParams {
forceUpdate?: boolean
}
export interface UpdateNodePositionsParams {
id?: string
diff?: XYPosition
}
export interface State extends Omit<FlowOptions, 'id' | 'modelValue'> {
/** Event hooks, you can manipulate the triggers at your own peril */
readonly hooks: FlowHooks
@@ -118,7 +113,7 @@ export type SetState = (
| Partial<FlowOptions & Omit<State, 'nodes' | 'edges' | 'modelValue'>>
| ((state: State) => Partial<FlowOptions & Omit<State, 'nodes' | 'edges' | 'modelValue'>>),
) => void
export type UpdateNodePosition = (params: UpdateNodePositionsParams) => void
export type UpdateNodePosition = (dragItems: NodeDragItem[]) => void
export type UpdateNodeDimensions = (updates: UpdateNodeDimensionsParams[]) => void
export interface Actions {
@@ -162,7 +157,7 @@ export interface Actions {
setState: SetState
/** internal position updater, you probably don't want to use this */
updateNodePosition: UpdateNodePosition
updateNodePositions: UpdateNodePosition
/** internal dimensions' updater, you probably don't want to use this */
updateNodeDimensions: UpdateNodeDimensions
+4 -41
View File
@@ -1,6 +1,5 @@
import { clampPosition, isGraphEdge, isGraphNode } from './graph'
import { isGraphEdge, isGraphNode } from './graph'
import type {
CoordinateExtent,
Edge,
EdgeAddChange,
EdgeChange,
@@ -14,18 +13,11 @@ import type {
Node,
NodeAddChange,
NodeChange,
NodePositionChange,
NodeSelectionChange,
XYPosition,
} from '~/types'
interface CreatePositionChangeParams {
node: GraphNode
nodeExtent: CoordinateExtent
diff?: XYPosition
}
function handleParentExpand(updateItem: GraphNode, parent: GraphNode) {
function handleParentExpand(updateItem: GraphNode, curr: GraphNode[]) {
const parent = updateItem.parentNode ? curr.find((el) => el.id === updateItem.parentNode) : undefined
if (parent) {
const extendWidth = updateItem.position.x + updateItem.dimensions.width - parent.dimensions.width
const extendHeight = updateItem.position.y + updateItem.dimensions.height - parent.dimensions.height
@@ -158,39 +150,10 @@ export const createSelectionChange = (id: string, selected: boolean): NodeSelect
selected,
})
export const createPositionChange = (
{ node, diff, nodeExtent }: CreatePositionChangeParams,
getNode: Getters['getNode'],
): NodePositionChange => {
const parent = node.parentNode ? getNode(node.parentNode) : undefined
const change: NodePositionChange = {
id: node.id,
type: 'position',
}
if (diff) {
const nextPosition = { x: node.position.x + diff.x, y: node.position.y + diff.y }
let currentExtent = node.extent === 'parent' || typeof node.extent === 'undefined' ? nodeExtent : node.extent
if (node.extent === 'parent' && parent && node.dimensions.width && node.dimensions.height) {
if (parent.dimensions.width && parent.dimensions.height) {
currentExtent = [
[0, 0],
[parent.dimensions.width - node.dimensions.width, parent.dimensions.height - node.dimensions.height],
]
}
}
change.position = currentExtent ? clampPosition(nextPosition, currentExtent) : nextPosition
}
return change
}
export const createAdditionChange = <
T extends GraphNode | GraphEdge = GraphNode,
C extends NodeAddChange | EdgeAddChange = T extends GraphNode ? NodeAddChange : EdgeAddChange,
>(
>(
item: T,
): C =>
<C>{
+106
View File
@@ -0,0 +1,106 @@
import type { Ref } from 'vue'
import { clampPosition, isParentSelected } from './graph'
import type { CoordinateExtent, Getters, GraphNode, NodeDragItem, XYPosition } from '~/types'
export function getParentNodePosition(parent?: GraphNode): XYPosition {
return {
x: parent?.computedPosition?.x || 0,
y: parent?.computedPosition?.y || 0,
}
}
export 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
}
export function getDragItems(
nodes: GraphNode[],
mousePos: XYPosition,
getNode: Getters['getNode'],
nodeId?: string,
): NodeDragItem[] {
return nodes
.filter((n) => (n.selected || n.id === nodeId) && (!n.parentNode || !isParentSelected(n, getNode)))
.map((n) => ({
id: n.id,
position: n.position,
distance: {
x: mousePos.x - n.position.x,
y: mousePos.y - n.position.y,
},
delta: {
x: 0,
y: 0,
},
extent: n.extent,
parentNode: n.parentNode,
dimensions: n.dimensions,
}))
}
export function getEventHandlerParams({
id,
dragItems,
node,
}: {
id?: string
dragItems: NodeDragItem[]
node: GraphNode
}): [GraphNode, GraphNode[]] {
const extendedDragItems: GraphNode[] = dragItems.map((n) => {
return {
...node,
position: n.position,
computedPosition: {
x: (node.computedPosition?.x || 0) + n.delta.x,
y: (node.computedPosition?.y || 0) + n.delta.y,
z: node.computedPosition.z,
},
}
})
return [id ? extendedDragItems.find((n) => n.id === id)! : extendedDragItems[0], extendedDragItems]
}
export function updatePosition(
dragItem: NodeDragItem,
mousePos: XYPosition,
parent?: GraphNode,
nodeExtent?: CoordinateExtent,
): NodeDragItem {
let currentExtent = dragItem.extent || nodeExtent
let nextPosition = { x: mousePos.x - dragItem.distance.x, y: mousePos.y - dragItem.distance.y }
if (dragItem.extent === 'parent') {
if (dragItem.parentNode && dragItem.dimensions.width && dragItem.dimensions.height) {
currentExtent =
parent?.width && parent?.height
? [
[0, 0],
[parent.dimensions.width - dragItem.dimensions.width, parent.dimensions.height - dragItem.dimensions.height],
]
: currentExtent
} else {
currentExtent = nodeExtent
}
}
nextPosition = currentExtent ? clampPosition(nextPosition, currentExtent as CoordinateExtent) : nextPosition
dragItem.delta = {
x: nextPosition.x - dragItem.position.x,
y: nextPosition.y - dragItem.position.y,
}
dragItem.position = nextPosition
return dragItem
}
+17 -1
View File
@@ -1,5 +1,5 @@
import { getDimensions } from './graph'
import type { HandleElement, Position } from '~/types'
import type { Actions, GraphNode, HandleElement, Position } from '~/types'
export const getHandleBoundsByHandleType = (
selector: string,
@@ -37,3 +37,19 @@ export const getHandleBounds = (nodeElement: HTMLDivElement, scale: number, id?:
target: getHandleBoundsByHandleType(`.target${id ? `.vue-flow__handle-${id}` : ''}`, nodeElement, bounds, scale) ?? undefined,
}
}
export const handleNodeClick = (
node: GraphNode,
multiSelectionActive: boolean,
addSelectedNodes: Actions['addSelectedNodes'],
removeSelectedElements: Actions['removeSelectedElements'],
setState: Actions['setState'],
) => {
setState({ nodesSelectionActive: false })
if (!node.selected) {
addSelectedNodes([node])
} else if (node.selected && multiSelectionActive) {
removeSelectedElements({ nodes: [node] })
}
}