diff --git a/packages/core/src/components/Edges/EdgeWrapper.ts b/packages/core/src/components/Edges/EdgeWrapper.ts index 4ba3943e..65d11c1b 100644 --- a/packages/core/src/components/Edges/EdgeWrapper.ts +++ b/packages/core/src/components/Edges/EdgeWrapper.ts @@ -44,8 +44,6 @@ const EdgeWrapper = defineComponent({ const handleId = ref(null) - const type = ref('source') - const edgeUpdaterType = ref('source') const edgeEl = ref() @@ -53,17 +51,13 @@ const EdgeWrapper = defineComponent({ provide(EdgeId, props.id) provide(EdgeRef, edgeEl) - const sourceNode = $computed(() => findNode(edge.source)) - - const targetNode = $computed(() => findNode(edge.target)) - const edgeClass = $computed(() => (edge.class instanceof Function ? edge.class(edge) : edge.class)) const edgeStyle = $computed(() => (edge.style instanceof Function ? edge.style(edge) : edge.style)) const { handlePointerDown } = useHandle({ nodeId, handleId, - type, + type: edgeUpdaterType, isValidConnection: isValidConnection.value, edgeUpdaterType, onEdgeUpdate, @@ -71,6 +65,9 @@ const EdgeWrapper = defineComponent({ }) return () => { + const sourceNode = findNode(edge.source) + const targetNode = findNode(edge.target) + if (!sourceNode || !targetNode || !edge) { return null } @@ -248,9 +245,8 @@ const EdgeWrapper = defineComponent({ nodeId.value = isSourceHandle ? edge.target : edge.source handleId.value = (isSourceHandle ? edge.targetHandle : edge.sourceHandle) ?? '' - type.value = isSourceHandle ? 'target' : 'source' - edgeUpdaterType.value = type.value + edgeUpdaterType.value = isSourceHandle ? 'target' : 'source' hooks.emit.updateStart({ event, edge }) @@ -301,6 +297,7 @@ const EdgeWrapper = defineComponent({ if (unselect) { edgeEl.value?.blur() + removeSelectedEdges([findEdge(props.id)!]) } else { addSelectedEdges([findEdge(props.id)!]) diff --git a/packages/core/src/components/Nodes/NodeWrapper.ts b/packages/core/src/components/Nodes/NodeWrapper.ts index 672ae3eb..b778be93 100644 --- a/packages/core/src/components/Nodes/NodeWrapper.ts +++ b/packages/core/src/components/Nodes/NodeWrapper.ts @@ -92,7 +92,7 @@ const NodeWrapper = defineComponent({ return styles }) - const zIndex = computed(() => Number(node.zIndex ?? getStyle.value.zIndex ?? 0)) + const zIndex = () => Number(node.zIndex ?? getStyle.value.zIndex ?? 0) onUpdateNodeInternals((updateIds) => { if (updateIds.includes(props.id)) { @@ -124,18 +124,18 @@ const NodeWrapper = defineComponent({ () => parentNode.value?.computedPosition.x, () => parentNode.value?.computedPosition.y, () => parentNode.value?.computedPosition.z, + () => zIndex(), () => node.selected, () => node.dimensions.height, () => node.dimensions.width, () => parentNode.value?.dimensions.height, () => parentNode.value?.dimensions.width, - zIndex, ], - ([newX, newY, parentX, parentY, parentZ]) => { + ([newX, newY, parentX, parentY, parentZ, nodeZIndex]) => { const xyzPos = { x: newX, y: newY, - z: zIndex.value + (elevateNodesOnSelect ? (node.selected ? 1000 : 0) : 0), + z: nodeZIndex + (elevateNodesOnSelect ? (node.selected ? 1000 : 0) : 0), } if (isNumber(parentX) && isNumber(parentY)) { @@ -187,7 +187,7 @@ const NodeWrapper = defineComponent({ getClass.value, ], 'style': { - zIndex: node.computedPosition.z ?? zIndex.value, + zIndex: node.computedPosition.z ?? zIndex(), transform: `translate(${node.computedPosition.x}px,${node.computedPosition.y}px)`, pointerEvents: props.selectable || props.draggable ? 'all' : 'none', visibility: node.initialized ? 'visible' : 'hidden', diff --git a/packages/core/src/components/NodesSelection/NodesSelection.vue b/packages/core/src/components/NodesSelection/NodesSelection.vue index c3f3615b..861811f7 100644 --- a/packages/core/src/components/NodesSelection/NodesSelection.vue +++ b/packages/core/src/components/NodesSelection/NodesSelection.vue @@ -3,18 +3,18 @@ const { emits, viewport, getSelectedNodes, noPanClassName, disableKeyboardA11y, const updatePositions = useUpdateNodePositions() -const el = ref() +const el = ref() const dragging = useDrag({ el, - onStart(event, node, nodes) { - emits.selectionDragStart({ event, node, nodes }) + onStart(args) { + emits.selectionDragStart(args) }, - onDrag(event, node, nodes) { - emits.selectionDrag({ event, node, nodes }) + onDrag(args) { + emits.selectionDrag(args) }, - onStop(event, node, nodes) { - emits.selectionDragStop({ event, node, nodes }) + onStop(args) { + emits.selectionDragStop(args) }, }) @@ -24,13 +24,13 @@ onMounted(() => { } }) -const selectedNodesBBox = $computed(() => getRectOfNodes(getSelectedNodes)) +const selectedNodesBBox = computed(() => getRectOfNodes(getSelectedNodes)) const innerStyle = computed(() => ({ - width: `${selectedNodesBBox.width}px`, - height: `${selectedNodesBBox.height}px`, - top: `${selectedNodesBBox.y}px`, - left: `${selectedNodesBBox.x}px`, + width: `${selectedNodesBBox.value.width}px`, + height: `${selectedNodesBBox.value.height}px`, + top: `${selectedNodesBBox.value.y}px`, + left: `${selectedNodesBBox.value.x}px`, })) function onContextMenu(event: MouseEvent) { diff --git a/packages/core/src/composables/useDrag.ts b/packages/core/src/composables/useDrag.ts index 051000a2..0921799c 100644 --- a/packages/core/src/composables/useDrag.ts +++ b/packages/core/src/composables/useDrag.ts @@ -18,215 +18,209 @@ interface UseDragParams { } function useDrag(params: UseDragParams) { - const scope = effectScope() + const { + vueFlowRef, + snapToGrid, + snapGrid, + noDragClassName, + nodes, + nodeExtent, + viewport, + autoPanOnNodeDrag, + nodesDraggable, + panBy, + findNode, + multiSelectionActive, + nodesSelectionActive, + selectNodesOnDrag, + removeSelectedElements, + addSelectedNodes, + updateNodePositions, + emits, + } = $(useVueFlow()) - tryOnScopeDispose(scope.stop) + const { onStart, onDrag, onStop, el, disabled, id, selectable } = params - return scope.run(() => { - const { - vueFlowRef, - snapToGrid, - snapGrid, - noDragClassName, - nodes, - nodeExtent, - viewport, - autoPanOnNodeDrag, - nodesDraggable, - panBy, - findNode, - multiSelectionActive, - nodesSelectionActive, - selectNodesOnDrag, - removeSelectedElements, - addSelectedNodes, - updateNodePositions, - emits, - } = $(useVueFlow()) + const dragging = ref(false) - const { onStart, onDrag, onStop, el, disabled, id, selectable } = params + let dragItems = $ref([]) - const dragging = ref(false) + let dragHandler = $ref>() - let dragItems = $ref([]) + let containerBounds = $ref(null) - let dragHandler = $ref>() + let lastPos = $ref>({ x: undefined, y: undefined }) + let mousePosition = $ref({ x: 0, y: 0 }) + let dragEvent = $ref(null) - let containerBounds = $ref(null) + let autoPanId = $ref(0) + let autoPanStarted = $ref(false) - let lastPos = $ref>({ x: undefined, y: undefined }) - let mousePosition = $ref({ x: 0, y: 0 }) - let dragEvent = $ref(null) + const getPointerPosition = useGetPointerPosition() - let autoPanId = $ref(0) - let autoPanStarted = $ref(false) + const updateNodes = ({ x, y }: XYPosition) => { + lastPos = { x, y } - const node = $computed(() => (id ? findNode(id) : undefined)) + let hasChange = false - const getPointerPosition = useGetPointerPosition() + dragItems = dragItems.map((n) => { + const nextPosition = { x: x - n.distance.x, y: y - n.distance.y } - watch([() => resolveUnref(disabled), el], ([isDisabled, nodeEl]) => { - if (nodeEl) { - const selection = select(nodeEl) - - 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, - emits.error, - 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({ event: dragEvent, node: 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 (isDisabled) { - selection.on('.drag', null) - } else { - dragHandler = drag() - .on('start', (event: UseDragEvent) => { - if (!selectNodesOnDrag && !multiSelectionActive && id) { - if (!node?.selected) { - // we need to reset selected nodes when selectNodesOnDrag=false - removeSelectedElements() - } - } - - if (node && resolveUnref(selectable) && selectNodesOnDrag) { - handleNodeClick( - node, - multiSelectionActive, - addSelectedNodes, - removeSelectedElements, - $$(nodesSelectionActive), - false, - nodeEl as HTMLDivElement, - ) - } - - const pointerPos = getPointerPosition(event) - lastPos = pointerPos - dragItems = getDragItems(nodes, nodesDraggable, pointerPos, findNode, id) - - if (dragItems.length) { - const [currentNode, nodes] = getEventHandlerParams({ - id, - dragItems, - findNode, - }) - - onStart({ event: event.sourceEvent, node: currentNode, nodes }) - } - - containerBounds = vueFlowRef?.getBoundingClientRect() || null - mousePosition = getEventPosition(event.sourceEvent, containerBounds!) - }) - .on('drag', (event: UseDragEvent) => { - const pointerPos = getPointerPosition(event) - - if (!autoPanStarted && autoPanOnNodeDrag) { - autoPanStarted = true - autoPan() - } - - // 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!) - - updateNodes(pointerPos) - } - }) - .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: event.sourceEvent, node: currentNode, nodes }) - } - }) - .filter((event: D3DragEvent['sourceEvent']) => { - const target = event.target as HTMLDivElement - return ( - !event.button && - (!noDragClassName || - (!hasSelector(target, `.${noDragClassName}`, nodeEl) && - (!node?.dragHandle || hasSelector(target, node.dragHandle, nodeEl)))) - ) - }) - - selection.call(dragHandler) - } + 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, + emits.error, + 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 }) - return dragging + if (!hasChange) { + return + } + + updateNodePositions(dragItems, true, true) + + dragging.value = true + + if (dragEvent) { + const [currentNode, nodes] = getEventHandlerParams({ + id, + dragItems, + findNode, + }) + + onDrag({ event: dragEvent, node: 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) + } + + watch([() => resolveUnref(disabled), el], ([isDisabled, nodeEl]) => { + if (nodeEl) { + const selection = select(nodeEl) + + if (isDisabled) { + selection.on('.drag', null) + } else { + const node = findNode(id) + + dragHandler = drag() + .on('start', (event: UseDragEvent) => { + if (!selectNodesOnDrag && !multiSelectionActive && node) { + if (!node.selected) { + // we need to reset selected nodes when selectNodesOnDrag=false + removeSelectedElements() + } + } + + if (node && resolveUnref(selectable) && selectNodesOnDrag) { + handleNodeClick( + node, + multiSelectionActive, + addSelectedNodes, + removeSelectedElements, + $$(nodesSelectionActive), + false, + nodeEl as HTMLDivElement, + ) + } + + const pointerPos = getPointerPosition(event) + lastPos = pointerPos + dragItems = getDragItems(nodes, nodesDraggable, pointerPos, findNode, id) + + if (dragItems.length) { + const [currentNode, nodes] = getEventHandlerParams({ + id, + dragItems, + findNode, + }) + + onStart({ event: event.sourceEvent, node: currentNode, nodes }) + } + + containerBounds = vueFlowRef?.getBoundingClientRect() || null + mousePosition = getEventPosition(event.sourceEvent, containerBounds!) + }) + .on('drag', (event: UseDragEvent) => { + const pointerPos = getPointerPosition(event) + + if (!autoPanStarted && autoPanOnNodeDrag) { + autoPanStarted = true + autoPan() + } + + // 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!) + + updateNodes(pointerPos) + } + }) + .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: event.sourceEvent, node: currentNode, nodes }) + } + }) + .filter((event: D3DragEvent['sourceEvent']) => { + const target = event.target as HTMLDivElement + return ( + !event.button && + (!noDragClassName || + (!hasSelector(target, `.${noDragClassName}`, nodeEl) && + (!node?.dragHandle || hasSelector(target, node.dragHandle, nodeEl)))) + ) + }) + + selection.call(dragHandler) + } + } }) + + return dragging } export default useDrag diff --git a/packages/core/src/composables/useVueFlow.ts b/packages/core/src/composables/useVueFlow.ts index 00244010..39bfbe5f 100644 --- a/packages/core/src/composables/useVueFlow.ts +++ b/packages/core/src/composables/useVueFlow.ts @@ -79,6 +79,7 @@ export class Storage { type Injection = VueFlowStore | null | undefined type Scope = (EffectScope & { vueFlowId: string }) | undefined +// todo: maybe replace the storage with a context based solution; This would break calling useVueFlow outside a setup function though, which should be fine export default (options?: FlowProps): VueFlowStore => { const storage = Storage.getInstance() diff --git a/packages/core/src/container/Pane/Pane.vue b/packages/core/src/container/Pane/Pane.vue index 89c10e2c..d6d71b6f 100644 --- a/packages/core/src/container/Pane/Pane.vue +++ b/packages/core/src/container/Pane/Pane.vue @@ -248,7 +248,7 @@ export default { ref="container" :key="`pane-${id}`" class="vue-flow__pane vue-flow__container" - :class="[{ selection: isSelecting }]" + :class="{ selection: isSelecting }" @click="onClick" @contextmenu="onContextMenu" @wheel.passive="onWheel" diff --git a/packages/core/src/store/actions.ts b/packages/core/src/store/actions.ts index 6189f0a8..24e30168 100644 --- a/packages/core/src/store/actions.ts +++ b/packages/core/src/store/actions.ts @@ -29,7 +29,7 @@ export function useActions( ): Actions { let fitViewOnInitDone = false - const viewportHelper = useViewport(state, getters) + const viewportHelper = $(useViewport(state, getters)) const updateNodeInternals: Actions['updateNodeInternals'] = (ids) => { const updateIds = ids ?? nodeIds.value ?? [] diff --git a/packages/node-resizer/src/ResizeControl.vue b/packages/node-resizer/src/ResizeControl.vue index 8a6a4b6d..a7c0b1af 100644 --- a/packages/node-resizer/src/ResizeControl.vue +++ b/packages/node-resizer/src/ResizeControl.vue @@ -185,7 +185,9 @@ watchEffect((onCleanup) => { prevValues.value.height = height } - if (changes.length === 0) return + if (changes.length === 0) { + return + } const direction = getDirection({ width: prevValues.value.width, @@ -200,7 +202,9 @@ watchEffect((onCleanup) => { const callResize = props.shouldResize?.(event, nextValues) - if (callResize === false) return + if (callResize === false) { + return + } emits('resize', { event, params: nextValues }) diff --git a/packages/pathfinding-edge/src/edge/getBoundingBoxes.ts b/packages/pathfinding-edge/src/edge/getBoundingBoxes.ts index febd589d..1f16b3de 100644 --- a/packages/pathfinding-edge/src/edge/getBoundingBoxes.ts +++ b/packages/pathfinding-edge/src/edge/getBoundingBoxes.ts @@ -89,10 +89,18 @@ export function getBoundingBoxes(storeNodes: GraphNode[], nodePadding = 0, graph bottomRight.y = roundUp(bottomRight.y, roundTo) } - if (topLeft.y < yMin) yMin = topLeft.y - if (topLeft.x < xMin) xMin = topLeft.x - if (bottomRight.y > yMax) yMax = bottomRight.y - if (bottomRight.x > xMax) xMax = bottomRight.x + if (topLeft.y < yMin) { + yMin = topLeft.y + } + if (topLeft.x < xMin) { + xMin = topLeft.x + } + if (bottomRight.y > yMax) { + yMax = bottomRight.y + } + if (bottomRight.x > xMax) { + xMax = bottomRight.x + } return { id: node.id,