feat(core): implement figma controls
Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com>
This commit is contained in:
@@ -1,25 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
const { width, height, x, y } = defineProps<{
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
}>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
export default {
|
|
||||||
name: 'SelectionRect',
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="vue-flow__selection"
|
|
||||||
:style="{
|
|
||||||
width: `${width}px`,
|
|
||||||
height: `${height}px`,
|
|
||||||
transform: `translate(${x}px, ${y}px)`,
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
@@ -1,103 +1,14 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { SelectionRect as Rect } from '../../types'
|
const { userSelectionRect } = useVueFlow()
|
||||||
import SelectionRect from './SelectionRect.vue'
|
|
||||||
import { getMousePosition } from './utils'
|
|
||||||
|
|
||||||
const { userSelectionActive, nodesSelectionActive, getNodes, getEdges, viewport, addSelectedElements } = useVueFlow()
|
|
||||||
|
|
||||||
let prevNodes = $ref(0)
|
|
||||||
|
|
||||||
let prevEdges = $ref(0)
|
|
||||||
|
|
||||||
const initialRect = () => ({
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
startX: 0,
|
|
||||||
startY: 0,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
draw: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
let rect = $ref<Rect>(initialRect())
|
|
||||||
|
|
||||||
const reset = () => {
|
|
||||||
rect = initialRect()
|
|
||||||
prevNodes = 0
|
|
||||||
prevEdges = 0
|
|
||||||
|
|
||||||
userSelectionActive.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const onMouseDown = (event: MouseEvent) => {
|
|
||||||
const mousePos = getMousePosition(event)
|
|
||||||
if (!mousePos) return
|
|
||||||
|
|
||||||
rect = {
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
startX: mousePos.x,
|
|
||||||
startY: mousePos.y,
|
|
||||||
x: mousePos.x,
|
|
||||||
y: mousePos.y,
|
|
||||||
draw: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
userSelectionActive.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const onMouseMove = (event: MouseEvent) => {
|
|
||||||
if (!userSelectionActive || !rect.draw) return
|
|
||||||
|
|
||||||
const mousePos = getMousePosition(event)
|
|
||||||
if (!mousePos) return
|
|
||||||
|
|
||||||
const startX = rect.startX
|
|
||||||
const startY = rect.startY
|
|
||||||
|
|
||||||
const nextUserSelectRect: Rect = {
|
|
||||||
...rect,
|
|
||||||
x: mousePos.x < startX ? mousePos.x : rect.x,
|
|
||||||
y: mousePos.y < startY ? mousePos.y : rect.y,
|
|
||||||
width: Math.abs(mousePos.x - startX),
|
|
||||||
height: Math.abs(mousePos.y - startY),
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedNodes = getNodesInside(getNodes.value, rect, viewport.value)
|
|
||||||
const selectedEdges = getConnectedEdges(selectedNodes, getEdges.value)
|
|
||||||
|
|
||||||
rect = nextUserSelectRect
|
|
||||||
|
|
||||||
addSelectedElements([...selectedNodes, ...selectedEdges])
|
|
||||||
|
|
||||||
prevNodes = selectedNodes.length
|
|
||||||
prevEdges = selectedEdges.length
|
|
||||||
}
|
|
||||||
|
|
||||||
const onMouseUp = () => {
|
|
||||||
rect = initialRect()
|
|
||||||
|
|
||||||
nodesSelectionActive.value = prevNodes > 0
|
|
||||||
userSelectionActive.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
onBeforeUnmount(reset)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
export default {
|
|
||||||
name: 'UserSelection',
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="vue-flow__selectionpane vue-flow__container"
|
class="vue-flow__selection react-flow__container"
|
||||||
@mousedown="onMouseDown"
|
:style="{
|
||||||
@mousemove="onMouseMove"
|
width: `${userSelectionRect?.width}px`,
|
||||||
@click="onMouseUp"
|
height: `${userSelectionRect?.height}px`,
|
||||||
@mouseup="onMouseUp"
|
transform: `translate(${userSelectionRect?.x}px, ${userSelectionRect?.y}px)`,
|
||||||
>
|
}"
|
||||||
<SelectionRect v-if="rect.draw" :width="rect.width" :height="rect.height" :x="rect.x" :y="rect.y" />
|
/>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
import type { XYPosition } from '../../types'
|
|
||||||
|
|
||||||
export function getMousePosition(event: MouseEvent): XYPosition | void {
|
|
||||||
const flowNode = (event.target as Element).closest('.vue-flow')
|
|
||||||
if (!flowNode) return
|
|
||||||
|
|
||||||
const containerBounds = flowNode.getBoundingClientRect()
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: event.clientX - containerBounds.left,
|
|
||||||
y: event.clientY - containerBounds.top,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ import { drag } from 'd3-drag'
|
|||||||
import { select } from 'd3-selection'
|
import { select } from 'd3-selection'
|
||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
import type { MaybeRef } from '@vueuse/core'
|
import type { MaybeRef } from '@vueuse/core'
|
||||||
import type { NodeDragEvent, NodeDragItem, SnapGrid, XYPosition } from '~/types'
|
import type { NodeDragEvent, NodeDragItem, XYPosition } from '~/types'
|
||||||
|
|
||||||
export type UseDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>
|
export type UseDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ function useDrag(params: UseDragParams) {
|
|||||||
const dragging = scope.run(() => {
|
const dragging = scope.run(() => {
|
||||||
const {
|
const {
|
||||||
snapToGrid,
|
snapToGrid,
|
||||||
snapGrid: globalSnapGrid,
|
snapGrid,
|
||||||
noDragClassName,
|
noDragClassName,
|
||||||
nodes,
|
nodes,
|
||||||
nodeExtent,
|
nodeExtent,
|
||||||
@@ -42,8 +42,6 @@ function useDrag(params: UseDragParams) {
|
|||||||
let lastPos = $ref<Partial<XYPosition>>({ x: undefined, y: undefined })
|
let lastPos = $ref<Partial<XYPosition>>({ x: undefined, y: undefined })
|
||||||
let dragHandler = $ref<any>()
|
let dragHandler = $ref<any>()
|
||||||
|
|
||||||
const hasSnapGrid = (sg?: SnapGrid) => (sg ?? snapToGrid ? globalSnapGrid : undefined)
|
|
||||||
|
|
||||||
const getPointerPosition = useGetPointerPosition()
|
const getPointerPosition = useGetPointerPosition()
|
||||||
|
|
||||||
watch([() => disabled, () => el], () => {
|
watch([() => disabled, () => el], () => {
|
||||||
@@ -66,7 +64,7 @@ function useDrag(params: UseDragParams) {
|
|||||||
handleNodeClick(node, multiSelectionActive, addSelectedNodes, removeSelectedElements, $$(nodesSelectionActive))
|
handleNodeClick(node, multiSelectionActive, addSelectedNodes, removeSelectedElements, $$(nodesSelectionActive))
|
||||||
}
|
}
|
||||||
|
|
||||||
const mousePos = getPointerPosition(event, hasSnapGrid(node?.snapGrid) as SnapGrid)
|
const mousePos = getPointerPosition(event, snapToGrid ? snapGrid : undefined)
|
||||||
dragItems = getDragItems(nodes, mousePos, getNode, id)
|
dragItems = getDragItems(nodes, mousePos, getNode, id)
|
||||||
|
|
||||||
if (onStart && dragItems) {
|
if (onStart && dragItems) {
|
||||||
@@ -79,9 +77,7 @@ function useDrag(params: UseDragParams) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on('drag', (event: UseDragEvent) => {
|
.on('drag', (event: UseDragEvent) => {
|
||||||
const snapGrid = hasSnapGrid(node?.snapGrid) as SnapGrid
|
const mousePos = getPointerPosition(event, snapToGrid ? snapGrid : undefined)
|
||||||
|
|
||||||
const mousePos = getPointerPosition(event, snapGrid)
|
|
||||||
|
|
||||||
let hasChange = false
|
let hasChange = false
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import UserSelection from '../../components/UserSelection/UserSelection.vue'
|
||||||
|
import NodesSelection from '../../components/NodesSelection/NodesSelection.vue'
|
||||||
|
import { SelectionMode } from '../../types'
|
||||||
|
import { getMousePosition } from './utils'
|
||||||
|
|
||||||
|
const { isSelecting } = defineProps<{ isSelecting: boolean }>()
|
||||||
|
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
vueFlowRef,
|
||||||
|
getNodes,
|
||||||
|
getEdges,
|
||||||
|
viewport,
|
||||||
|
emits,
|
||||||
|
userSelectionActive,
|
||||||
|
removeSelectedElements,
|
||||||
|
panOnDrag,
|
||||||
|
userSelectionRect,
|
||||||
|
elementsSelectable,
|
||||||
|
nodesSelectionActive,
|
||||||
|
addSelectedElements,
|
||||||
|
selectionMode,
|
||||||
|
} = useVueFlow()
|
||||||
|
|
||||||
|
const container = ref<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
const prevSelectedNodesCount = ref(0)
|
||||||
|
|
||||||
|
const prevSelectedEdgesCount = ref(0)
|
||||||
|
|
||||||
|
const containerBounds = ref<DOMRect>()
|
||||||
|
|
||||||
|
const hasActiveSelection = computed(() => elementsSelectable.value && (isSelecting || userSelectionActive.value))
|
||||||
|
|
||||||
|
const resetUserSelection = () => {
|
||||||
|
userSelectionActive.value = false
|
||||||
|
userSelectionRect.value = null
|
||||||
|
|
||||||
|
prevSelectedNodesCount.value = 0
|
||||||
|
prevSelectedEdgesCount.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClick(event: MouseEvent) {
|
||||||
|
if (event.target !== container.value || hasActiveSelection.value) return
|
||||||
|
|
||||||
|
console.log('click')
|
||||||
|
|
||||||
|
emits.paneClick(event)
|
||||||
|
|
||||||
|
removeSelectedElements()
|
||||||
|
|
||||||
|
nodesSelectionActive.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function onContextMenu(event: MouseEvent) {
|
||||||
|
if (event.target !== container.value) return
|
||||||
|
|
||||||
|
if (Array.isArray(panOnDrag.value) && panOnDrag.value?.includes(2)) {
|
||||||
|
event.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emits.paneContextMenu(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWheel(event: WheelEvent) {
|
||||||
|
if (event.target !== container.value) return
|
||||||
|
|
||||||
|
emits.paneScroll(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseDown(event: MouseEvent) {
|
||||||
|
containerBounds.value = vueFlowRef.value!.getBoundingClientRect()
|
||||||
|
|
||||||
|
if (
|
||||||
|
!hasActiveSelection.value ||
|
||||||
|
!elementsSelectable ||
|
||||||
|
!isSelecting ||
|
||||||
|
event.button !== 0 ||
|
||||||
|
event.target !== container.value ||
|
||||||
|
!containerBounds.value
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { x, y } = getMousePosition(event, containerBounds.value)
|
||||||
|
|
||||||
|
removeSelectedElements()
|
||||||
|
|
||||||
|
userSelectionRect.value = {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
startX: x,
|
||||||
|
startY: y,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
}
|
||||||
|
|
||||||
|
userSelectionActive.value = true
|
||||||
|
|
||||||
|
emits.selectionStart(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseMove(event: MouseEvent) {
|
||||||
|
if (!isSelecting || !containerBounds.value || !userSelectionRect.value) return
|
||||||
|
if (!hasActiveSelection.value) return emits.paneMouseMove(event)
|
||||||
|
|
||||||
|
if (!userSelectionActive.value) userSelectionActive.value = true
|
||||||
|
if (nodesSelectionActive.value) nodesSelectionActive.value = false
|
||||||
|
|
||||||
|
const mousePos = getMousePosition(event, containerBounds.value)
|
||||||
|
const startX = userSelectionRect.value.startX ?? 0
|
||||||
|
const startY = userSelectionRect.value.startY ?? 0
|
||||||
|
|
||||||
|
const nextUserSelectRect = {
|
||||||
|
...userSelectionRect.value,
|
||||||
|
x: mousePos.x < startX ? mousePos.x : startX,
|
||||||
|
y: mousePos.y < startY ? mousePos.y : startY,
|
||||||
|
width: Math.abs(mousePos.x - startX),
|
||||||
|
height: Math.abs(mousePos.y - startY),
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedNodes = getNodesInside(
|
||||||
|
getNodes.value,
|
||||||
|
userSelectionRect.value,
|
||||||
|
viewport.value,
|
||||||
|
selectionMode.value === SelectionMode.Partial,
|
||||||
|
)
|
||||||
|
|
||||||
|
const selectedEdges = getConnectedEdges(selectedNodes, getEdges.value)
|
||||||
|
|
||||||
|
prevSelectedNodesCount.value = selectedNodes.length
|
||||||
|
prevSelectedEdgesCount.value = selectedEdges.length
|
||||||
|
|
||||||
|
userSelectionRect.value = nextUserSelectRect
|
||||||
|
|
||||||
|
addSelectedElements([...selectedNodes, ...selectedEdges])
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseUp(event: MouseEvent) {
|
||||||
|
if (!hasActiveSelection.value) return
|
||||||
|
|
||||||
|
// We only want to trigger click functions when in selection mode if
|
||||||
|
// the user did not move the mouse.
|
||||||
|
if (!userSelectionActive.value && userSelectionRect.value && event.target === container.value) {
|
||||||
|
onClick(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
nodesSelectionActive.value = prevSelectedNodesCount.value > 0
|
||||||
|
|
||||||
|
resetUserSelection()
|
||||||
|
|
||||||
|
emits.selectionEnd(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseLeave(event: MouseEvent) {
|
||||||
|
console.log('mouseleave')
|
||||||
|
if (!hasActiveSelection.value) return emits.paneMouseLeave(event)
|
||||||
|
|
||||||
|
if (userSelectionActive.value) {
|
||||||
|
nodesSelectionActive.value = prevSelectedNodesCount.value > 0
|
||||||
|
emits.selectionEnd?.(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
resetUserSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseEnter(event: MouseEvent) {
|
||||||
|
if (hasActiveSelection.value) return
|
||||||
|
|
||||||
|
emits.paneMouseEnter(event)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
name: 'Pane',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="container"
|
||||||
|
:key="`pane-${id}`"
|
||||||
|
class="vue-flow__pane vue-flow__container"
|
||||||
|
:class="[{ selection: isSelecting }]"
|
||||||
|
@click="onClick"
|
||||||
|
@contextmenu="onContextMenu"
|
||||||
|
@wheel="onWheel"
|
||||||
|
@mouseenter="onMouseEnter"
|
||||||
|
@mousedown="onMouseDown"
|
||||||
|
@mousemove="onMouseMove"
|
||||||
|
@mouseup="onMouseUp"
|
||||||
|
@mouseleave="onMouseLeave"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
<UserSelection v-if="userSelectionActive && userSelectionRect" />
|
||||||
|
<NodesSelection v-if="nodesSelectionActive" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import type { XYPosition } from '~/types'
|
||||||
|
|
||||||
|
export function getMousePosition(event: MouseEvent, containerBounds: DOMRect): XYPosition {
|
||||||
|
return {
|
||||||
|
x: event.clientX - containerBounds.left,
|
||||||
|
y: event.clientY - containerBounds.top,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import type { GraphNode } from '../../types'
|
|
||||||
import { NodesSelection, UserSelection } from '../../components'
|
|
||||||
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
deleteKeyCode,
|
|
||||||
selectionKeyCode,
|
|
||||||
multiSelectionKeyCode,
|
|
||||||
emits,
|
|
||||||
nodesSelectionActive,
|
|
||||||
userSelectionActive,
|
|
||||||
multiSelectionActive,
|
|
||||||
elementsSelectable,
|
|
||||||
getNodes,
|
|
||||||
getSelectedEdges,
|
|
||||||
removeSelectedElements,
|
|
||||||
removeNodes,
|
|
||||||
removeEdges,
|
|
||||||
} = useVueFlow()
|
|
||||||
|
|
||||||
const onClick = (event: MouseEvent) => {
|
|
||||||
emits.paneClick(event)
|
|
||||||
nodesSelectionActive.value = false
|
|
||||||
|
|
||||||
removeSelectedElements()
|
|
||||||
}
|
|
||||||
|
|
||||||
const onContextMenu = (event: MouseEvent) => emits.paneContextMenu(event)
|
|
||||||
|
|
||||||
const onWheel = (event: WheelEvent) => emits.paneScroll(event)
|
|
||||||
|
|
||||||
const onMouseEnter = (event: MouseEvent) => emits.paneMouseEnter(event)
|
|
||||||
|
|
||||||
const onMouseLeave = (event: MouseEvent) => emits.paneMouseLeave(event)
|
|
||||||
|
|
||||||
const onMouseMove = (event: MouseEvent) => emits.paneMouseMove(event)
|
|
||||||
|
|
||||||
useKeyPress(deleteKeyCode, (keyPressed) => {
|
|
||||||
if (!keyPressed) return
|
|
||||||
|
|
||||||
const nodesToRemove = getNodes.value.reduce<GraphNode[]>((res, node) => {
|
|
||||||
if (!node.selected && node.parentNode && res.find((n) => n.id === node.parentNode)) {
|
|
||||||
res.push(node)
|
|
||||||
} else if (node.selected) {
|
|
||||||
res.push(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
if (nodesToRemove || getSelectedEdges.value) {
|
|
||||||
if (getSelectedEdges.value.length > 0) {
|
|
||||||
removeEdges(getSelectedEdges.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nodesToRemove.length > 0) {
|
|
||||||
removeNodes(nodesToRemove)
|
|
||||||
}
|
|
||||||
|
|
||||||
nodesSelectionActive.value = false
|
|
||||||
|
|
||||||
removeSelectedElements()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
useKeyPress(multiSelectionKeyCode, (keyPressed) => {
|
|
||||||
multiSelectionActive.value = keyPressed
|
|
||||||
})
|
|
||||||
|
|
||||||
const selectionKeyPressed = useKeyPress(selectionKeyCode, (keyPressed) => {
|
|
||||||
if (userSelectionActive.value && keyPressed) return
|
|
||||||
|
|
||||||
userSelectionActive.value = keyPressed && elementsSelectable.value
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
export default {
|
|
||||||
name: 'SelectionPane',
|
|
||||||
inheritAttrs: false,
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<UserSelection v-if="selectionKeyPressed && userSelectionActive" />
|
|
||||||
<NodesSelection v-if="nodesSelectionActive" />
|
|
||||||
<div
|
|
||||||
:key="`pane-${id}`"
|
|
||||||
v-bind="$attrs"
|
|
||||||
class="vue-flow__pane vue-flow__container"
|
|
||||||
@click="onClick"
|
|
||||||
@contextmenu="onContextMenu"
|
|
||||||
@wheel="onWheel"
|
|
||||||
@mouseenter="onMouseEnter"
|
|
||||||
@mousemove="onMouseMove"
|
|
||||||
@mouseleave="onMouseLeave"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
@@ -56,6 +56,7 @@ export default {
|
|||||||
<div class="vue-flow__edge-labels" />
|
<div class="vue-flow__edge-labels" />
|
||||||
|
|
||||||
<NodeRenderer />
|
<NodeRenderer />
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
import type { D3ZoomEvent, ZoomTransform } from 'd3-zoom'
|
import type { D3ZoomEvent, ZoomTransform } from 'd3-zoom'
|
||||||
import { zoom, zoomIdentity } from 'd3-zoom'
|
import { zoom, zoomIdentity } from 'd3-zoom'
|
||||||
import { pointer, select } from 'd3-selection'
|
import { pointer, select } from 'd3-selection'
|
||||||
import type { CoordinateExtent, ViewportTransform } from '../../types'
|
import type { CoordinateExtent, FlowOptions, ViewportTransform } from '../../types'
|
||||||
import { PanOnScrollMode } from '../../types'
|
import { PanOnScrollMode } from '../../types'
|
||||||
import SelectionPane from '../SelectionPane/SelectionPane.vue'
|
import Pane from '../Pane/Pane.vue'
|
||||||
import Transform from './Transform.vue'
|
import Transform from './Transform.vue'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -16,6 +16,7 @@ const {
|
|||||||
dimensions,
|
dimensions,
|
||||||
zoomActivationKeyCode,
|
zoomActivationKeyCode,
|
||||||
selectionKeyCode,
|
selectionKeyCode,
|
||||||
|
panActivationKeyCode,
|
||||||
panOnScroll,
|
panOnScroll,
|
||||||
panOnScrollMode,
|
panOnScrollMode,
|
||||||
panOnScrollSpeed,
|
panOnScrollSpeed,
|
||||||
@@ -29,16 +30,30 @@ const {
|
|||||||
setState,
|
setState,
|
||||||
emits,
|
emits,
|
||||||
connectionStartHandle,
|
connectionStartHandle,
|
||||||
|
userSelectionActive,
|
||||||
|
paneDragging,
|
||||||
|
selectionOnDrag,
|
||||||
} = $(useVueFlow())
|
} = $(useVueFlow())
|
||||||
|
|
||||||
const viewportEl = templateRef<HTMLDivElement>('viewport', null)
|
const viewportEl = ref<HTMLDivElement>()
|
||||||
|
|
||||||
|
let selectionKeyPressed = $ref(false)
|
||||||
|
|
||||||
let isZoomingOrPanning = $ref(false)
|
let isZoomingOrPanning = $ref(false)
|
||||||
|
|
||||||
let isDragging = $ref(false)
|
let zoomedWithRightMouseButton = $ref(false)
|
||||||
|
|
||||||
|
const isRightClickPan = (pan: FlowOptions['panOnDrag'], usedButton: number) =>
|
||||||
|
usedButton === 2 && Array.isArray(pan) && pan.includes(2)
|
||||||
|
|
||||||
|
const panKeyPressed = useKeyPress(panActivationKeyCode)
|
||||||
|
|
||||||
const isConnecting = $computed(() => !!connectionStartHandle)
|
const isConnecting = $computed(() => !!connectionStartHandle)
|
||||||
|
|
||||||
|
const shouldPanOnDrag = computed(() => !selectionKeyPressed && panOnDrag && panKeyPressed.value)
|
||||||
|
|
||||||
|
const isSelecting = computed(() => selectionKeyPressed || (selectionOnDrag && shouldPanOnDrag.value !== true))
|
||||||
|
|
||||||
const viewChanged = (prevViewport: ViewportTransform, eventTransform: ZoomTransform): boolean =>
|
const viewChanged = (prevViewport: ViewportTransform, eventTransform: ZoomTransform): boolean =>
|
||||||
(prevViewport.x !== eventTransform.x && !isNaN(eventTransform.x)) ||
|
(prevViewport.x !== eventTransform.x && !isNaN(eventTransform.x)) ||
|
||||||
(prevViewport.y !== eventTransform.y && !isNaN(eventTransform.y)) ||
|
(prevViewport.y !== eventTransform.y && !isNaN(eventTransform.y)) ||
|
||||||
@@ -74,9 +89,10 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const bbox = viewportEl.value.getBoundingClientRect()
|
const viewportElement = viewportEl.value!
|
||||||
|
const bbox = viewportElement.getBoundingClientRect()
|
||||||
const d3Zoom = zoom<HTMLDivElement, any>().scaleExtent([minZoom, maxZoom]).translateExtent(translateExtent)
|
const d3Zoom = zoom<HTMLDivElement, any>().scaleExtent([minZoom, maxZoom]).translateExtent(translateExtent)
|
||||||
const d3Selection = select(viewportEl.value).call(d3Zoom)
|
const d3Selection = select(viewportElement).call(d3Zoom)
|
||||||
const d3ZoomHandler = d3Selection.on('wheel.zoom')
|
const d3ZoomHandler = d3Selection.on('wheel.zoom')
|
||||||
|
|
||||||
const updatedTransform = zoomIdentity
|
const updatedTransform = zoomIdentity
|
||||||
@@ -96,24 +112,30 @@ onMounted(() => {
|
|||||||
d3Selection,
|
d3Selection,
|
||||||
d3ZoomHandler,
|
d3ZoomHandler,
|
||||||
viewport: { x: updatedTransform.x, y: updatedTransform.y, zoom: updatedTransform.k },
|
viewport: { x: updatedTransform.x, y: updatedTransform.y, zoom: updatedTransform.k },
|
||||||
viewportRef: viewportEl.value,
|
viewportRef: viewportElement,
|
||||||
})
|
})
|
||||||
|
|
||||||
const onKeyPress = (keyPress: boolean) => {
|
const onKeyPress = (keyPress: boolean) => {
|
||||||
if (keyPress && !isZoomingOrPanning) {
|
selectionKeyPressed = keyPress
|
||||||
|
|
||||||
|
if (keyPress && userSelectionActive && !isZoomingOrPanning) {
|
||||||
d3Zoom.on('zoom', null)
|
d3Zoom.on('zoom', null)
|
||||||
} else if (!keyPress) {
|
} else if (!keyPress && !userSelectionActive) {
|
||||||
d3Zoom.on('zoom', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
d3Zoom.on('zoom', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||||
setState({ viewport: { x: event.transform.x, y: event.transform.y, zoom: event.transform.k } })
|
setState({ viewport: { x: event.transform.x, y: event.transform.y, zoom: event.transform.k } })
|
||||||
|
|
||||||
const flowTransform = eventToFlowTransform(event.transform)
|
const flowTransform = eventToFlowTransform(event.transform)
|
||||||
|
|
||||||
|
zoomedWithRightMouseButton = isRightClickPan(panOnDrag, event.sourceEvent?.button)
|
||||||
|
|
||||||
|
emits.viewportChange(flowTransform)
|
||||||
emits.move({ event, flowTransform })
|
emits.move({ event, flowTransform })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectionKeyPressed = useKeyPress(selectionKeyCode, onKeyPress)
|
useKeyPress(selectionKeyCode, onKeyPress)
|
||||||
|
|
||||||
// initialize
|
// initialize
|
||||||
onKeyPress(false)
|
onKeyPress(false)
|
||||||
|
|
||||||
@@ -127,11 +149,12 @@ onMounted(() => {
|
|||||||
const flowTransform = eventToFlowTransform(event.transform)
|
const flowTransform = eventToFlowTransform(event.transform)
|
||||||
|
|
||||||
if (event.sourceEvent?.type === 'mousedown') {
|
if (event.sourceEvent?.type === 'mousedown') {
|
||||||
isDragging = true
|
setState({ paneDragging: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
prevTransform = flowTransform
|
prevTransform = flowTransform
|
||||||
|
|
||||||
|
emits.viewportChangeStart(flowTransform)
|
||||||
emits.moveStart({ event, flowTransform })
|
emits.moveStart({ event, flowTransform })
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -139,22 +162,30 @@ onMounted(() => {
|
|||||||
if (!event.sourceEvent) return null
|
if (!event.sourceEvent) return null
|
||||||
|
|
||||||
isZoomingOrPanning = false
|
isZoomingOrPanning = false
|
||||||
isDragging = false
|
|
||||||
|
setState({ paneDragging: false })
|
||||||
|
|
||||||
|
if (isRightClickPan(panOnDrag, event.sourceEvent?.button) && !zoomedWithRightMouseButton) {
|
||||||
|
emits.paneContextMenu(event.sourceEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
zoomedWithRightMouseButton = false
|
||||||
|
|
||||||
if (viewChanged(prevTransform, event.transform)) {
|
if (viewChanged(prevTransform, event.transform)) {
|
||||||
const flowTransform = eventToFlowTransform(event.transform)
|
const flowTransform = eventToFlowTransform(event.transform)
|
||||||
|
|
||||||
prevTransform = flowTransform
|
prevTransform = flowTransform
|
||||||
|
|
||||||
|
emits.viewportChangeEnd(flowTransform)
|
||||||
emits.moveEnd({ event, flowTransform })
|
emits.moveEnd({ event, flowTransform })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
if (panOnScroll && !zoomKeyPressed.value) {
|
if (panOnScroll && !zoomKeyPressed.value && !userSelectionActive) {
|
||||||
d3Selection
|
d3Selection
|
||||||
.on('wheel', (event: WheelEvent) => {
|
.on('wheel', (event: WheelEvent) => {
|
||||||
if (isWrappedWithClass(event, noWheelClassName?.value)) {
|
if (isWrappedWithClass(event, noWheelClassName)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +216,7 @@ onMounted(() => {
|
|||||||
} else if (typeof d3ZoomHandler !== 'undefined') {
|
} else if (typeof d3ZoomHandler !== 'undefined') {
|
||||||
d3Selection
|
d3Selection
|
||||||
.on('wheel', (event: WheelEvent) => {
|
.on('wheel', (event: WheelEvent) => {
|
||||||
if (!preventScrolling || isWrappedWithClass(event, noWheelClassName?.value)) {
|
if (!preventScrolling || isWrappedWithClass(event, noWheelClassName)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,16 +242,16 @@ onMounted(() => {
|
|||||||
if (!panOnDrag && !zoomScroll && !panOnScroll && !zoomOnDoubleClick && !zoomOnPinch) return false
|
if (!panOnDrag && !zoomScroll && !panOnScroll && !zoomOnDoubleClick && !zoomOnPinch) return false
|
||||||
|
|
||||||
// during a selection we prevent all other interactions
|
// during a selection we prevent all other interactions
|
||||||
if (selectionKeyPressed.value && selectionKeyCode !== true) return false
|
if (userSelectionActive) return false
|
||||||
|
|
||||||
// if zoom on double click is disabled, we prevent the double click event
|
// if zoom on double click is disabled, we prevent the double click event
|
||||||
if (!zoomOnDoubleClick && event.type === 'dblclick') return false
|
if (!zoomOnDoubleClick && event.type === 'dblclick') return false
|
||||||
|
|
||||||
// if the target element is inside an element with the nowheel class, we prevent zooming
|
// if the target element is inside an element with the nowheel class, we prevent zooming
|
||||||
if (isWrappedWithClass(event, noWheelClassName as any) && event.type === 'wheel') return false
|
if (isWrappedWithClass(event, noWheelClassName) && event.type === 'wheel') return false
|
||||||
|
|
||||||
// if the target element is inside an element with the nopan class, we prevent panning
|
// if the target element is inside an element with the nopan class, we prevent panning
|
||||||
if (isWrappedWithClass(event, noPanClassName as any) && event.type !== 'wheel') return false
|
if (isWrappedWithClass(event, noPanClassName) && event.type !== 'wheel') return false
|
||||||
|
|
||||||
if (!zoomOnPinch && event.ctrlKey && event.type === 'wheel') return false
|
if (!zoomOnPinch && event.ctrlKey && event.type === 'wheel') return false
|
||||||
|
|
||||||
@@ -230,8 +261,20 @@ onMounted(() => {
|
|||||||
// if the pane is not movable, we prevent dragging it with mousestart or touchstart
|
// if the pane is not movable, we prevent dragging it with mousestart or touchstart
|
||||||
if (!panOnDrag && (event.type === 'mousedown' || event.type === 'touchstart')) return false
|
if (!panOnDrag && (event.type === 'mousedown' || event.type === 'touchstart')) return false
|
||||||
|
|
||||||
|
// if the pane is only movable using allowed clicks
|
||||||
|
if (
|
||||||
|
Array.isArray(panOnDrag) &&
|
||||||
|
!panOnDrag.includes(event.button) &&
|
||||||
|
(event.type === 'mousedown' || event.type === 'touchstart')
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only allow right clicks if pan on drag is set to right-click
|
||||||
|
const buttonAllowed = (Array.isArray(panOnDrag) && panOnDrag.includes(event.button)) || !event.button || event.button <= 1
|
||||||
|
|
||||||
// default filter for d3-zoom
|
// default filter for d3-zoom
|
||||||
return (!event.ctrlKey || event.type === 'wheel') && (!event.button || event.button <= 1)
|
return (!event.ctrlKey || event.type === 'wheel') && buttonAllowed
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -243,10 +286,13 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="viewport" :key="`viewport-${id}`" class="vue-flow__viewport vue-flow__container">
|
<div ref="viewportEl" :key="`viewport-${id}`" class="vue-flow__viewport vue-flow__container">
|
||||||
<Transform>
|
<Pane :is-selecting="isSelecting" :class="{ connecting: isConnecting, dragging: paneDragging, draggable: !!panOnDrag }">
|
||||||
<slot />
|
<Transform>
|
||||||
</Transform>
|
<slot name="zoom-pane" />
|
||||||
<SelectionPane :class="{ connecting: isConnecting, dragging: isDragging, draggable: panOnDrag }" />
|
</Transform>
|
||||||
|
</Pane>
|
||||||
|
|
||||||
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ const props = withDefaults(defineProps<FlowProps>(), {
|
|||||||
disableKeyboardA11y: undefined,
|
disableKeyboardA11y: undefined,
|
||||||
edgesFocusable: undefined,
|
edgesFocusable: undefined,
|
||||||
nodesFocusable: undefined,
|
nodesFocusable: undefined,
|
||||||
|
selectionOnDrag: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -77,6 +78,11 @@ const emit = defineEmits<{
|
|||||||
(event: 'selectionDrag', selectionEvent: NodeDragEvent): void
|
(event: 'selectionDrag', selectionEvent: NodeDragEvent): void
|
||||||
(event: 'selectionDragStop', selectionEvent: NodeDragEvent): void
|
(event: 'selectionDragStop', selectionEvent: NodeDragEvent): void
|
||||||
(event: 'selectionContextMenu', selectionEvent: { event: MouseEvent; nodes: GraphNode[] }): void
|
(event: 'selectionContextMenu', selectionEvent: { event: MouseEvent; nodes: GraphNode[] }): void
|
||||||
|
(event: 'selectionStart', selectionEvent: MouseEvent): void
|
||||||
|
(event: 'selectionEnd', selectionEvent: MouseEvent): void
|
||||||
|
(event: 'viewportChangeStart', viewport: ViewportTransform): void
|
||||||
|
(event: 'viewportChange', viewport: ViewportTransform): void
|
||||||
|
(event: 'viewportChangeEnd', viewport: ViewportTransform): void
|
||||||
(event: 'paneReady', paneEvent: VueFlowStore): void
|
(event: 'paneReady', paneEvent: VueFlowStore): void
|
||||||
(event: 'paneScroll', paneEvent: WheelEvent | undefined): void
|
(event: 'paneScroll', paneEvent: WheelEvent | undefined): void
|
||||||
(event: 'paneClick', paneEvent: MouseEvent): void
|
(event: 'paneClick', paneEvent: MouseEvent): void
|
||||||
@@ -167,10 +173,12 @@ export default {
|
|||||||
<slot name="connection-line" />
|
<slot name="connection-line" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<slot name="zoom-pane" />
|
<template #zoom-pane>
|
||||||
</Viewport>
|
<slot name="zoom-pane" />
|
||||||
|
</template>
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
|
</Viewport>
|
||||||
|
|
||||||
<A11yDescriptions />
|
<A11yDescriptions />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ export { default as Marker } from './EdgeRenderer/Marker.vue'
|
|||||||
export { default as MarkerDefinitions } from './EdgeRenderer/MarkerDefinitions.vue'
|
export { default as MarkerDefinitions } from './EdgeRenderer/MarkerDefinitions.vue'
|
||||||
export { default as NodeRenderer } from './NodeRenderer/NodeRenderer.vue'
|
export { default as NodeRenderer } from './NodeRenderer/NodeRenderer.vue'
|
||||||
export { default as Viewport } from './Viewport/Viewport.vue'
|
export { default as Viewport } from './Viewport/Viewport.vue'
|
||||||
export { default as SelectionPane } from './SelectionPane/SelectionPane.vue'
|
export { default as SelectionPane } from './Pane/SelectionPane.vue'
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ export const createHooks = (): FlowHooks => ({
|
|||||||
selectionDrag: createEventHook(),
|
selectionDrag: createEventHook(),
|
||||||
selectionDragStop: createEventHook(),
|
selectionDragStop: createEventHook(),
|
||||||
selectionContextMenu: createEventHook(),
|
selectionContextMenu: createEventHook(),
|
||||||
|
selectionStart: createEventHook(),
|
||||||
|
selectionEnd: createEventHook(),
|
||||||
|
viewportChangeStart: createEventHook(),
|
||||||
|
viewportChange: createEventHook(),
|
||||||
|
viewportChangeEnd: createEventHook(),
|
||||||
paneScroll: createEventHook(),
|
paneScroll: createEventHook(),
|
||||||
paneClick: createEventHook(),
|
paneClick: createEventHook(),
|
||||||
paneContextMenu: createEventHook(),
|
paneContextMenu: createEventHook(),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { DefaultEdgeTypes, DefaultNodeTypes, FlowOptions, State } from '~/types'
|
import type { DefaultEdgeTypes, DefaultNodeTypes, FlowOptions, State } from '~/types'
|
||||||
import { ConnectionLineType, ConnectionMode, PanOnScrollMode } from '~/types'
|
import { ConnectionLineType, ConnectionMode, PanOnScrollMode, SelectionMode } from '~/types'
|
||||||
import {
|
import {
|
||||||
BezierEdge,
|
BezierEdge,
|
||||||
DefaultNode,
|
DefaultNode,
|
||||||
@@ -56,6 +56,8 @@ const defaultState = (): State => ({
|
|||||||
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
selectionMode: SelectionMode.Full,
|
||||||
|
paneDragging: false,
|
||||||
preventScrolling: true,
|
preventScrolling: true,
|
||||||
zoomOnScroll: true,
|
zoomOnScroll: true,
|
||||||
zoomOnPinch: true,
|
zoomOnPinch: true,
|
||||||
@@ -71,6 +73,8 @@ const defaultState = (): State => ({
|
|||||||
nodesSelectionActive: false,
|
nodesSelectionActive: false,
|
||||||
userSelectionActive: false,
|
userSelectionActive: false,
|
||||||
|
|
||||||
|
userSelectionRect: null,
|
||||||
|
|
||||||
defaultMarkerColor: '#b1b1b7',
|
defaultMarkerColor: '#b1b1b7',
|
||||||
connectionLineStyle: {},
|
connectionLineStyle: {},
|
||||||
connectionLineType: ConnectionLineType.Bezier,
|
connectionLineType: ConnectionLineType.Bezier,
|
||||||
@@ -94,11 +98,13 @@ const defaultState = (): State => ({
|
|||||||
nodesDraggable: true,
|
nodesDraggable: true,
|
||||||
elementsSelectable: true,
|
elementsSelectable: true,
|
||||||
selectNodesOnDrag: true,
|
selectNodesOnDrag: true,
|
||||||
|
selectionOnDrag: false,
|
||||||
multiSelectionActive: false,
|
multiSelectionActive: false,
|
||||||
selectionKeyCode: 'Shift',
|
selectionKeyCode: 'Shift',
|
||||||
multiSelectionKeyCode: 'Meta',
|
multiSelectionKeyCode: 'Meta',
|
||||||
zoomActivationKeyCode: 'Meta',
|
zoomActivationKeyCode: 'Meta',
|
||||||
deleteKeyCode: 'Backspace',
|
deleteKeyCode: 'Backspace',
|
||||||
|
panActivationKeyCode: 'Space',
|
||||||
|
|
||||||
hooks: createHooks(),
|
hooks: createHooks(),
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,10 @@
|
|||||||
&.dragging {
|
&.dragging {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.selection {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.vue-flow__transformationpane {
|
.vue-flow__transformationpane {
|
||||||
@@ -36,8 +40,8 @@
|
|||||||
z-index: 4;
|
z-index: 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vue-flow__selectionpane {
|
.vue-flow__selection {
|
||||||
z-index: 5;
|
z-index: 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vue-flow__edge-labels {
|
.vue-flow__edge-labels {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { KeyFilter } from '@vueuse/core'
|
|||||||
import type { DefaultEdgeOptions, Edge, EdgeUpdatable, GraphEdge } from './edge'
|
import type { DefaultEdgeOptions, Edge, EdgeUpdatable, GraphEdge } from './edge'
|
||||||
import type { CoordinateExtent, GraphNode, Node } from './node'
|
import type { CoordinateExtent, GraphNode, Node } from './node'
|
||||||
import type { ConnectionLineOptions, ConnectionLineType, ConnectionMode, Connector } from './connection'
|
import type { ConnectionLineOptions, ConnectionLineType, ConnectionMode, Connector } from './connection'
|
||||||
import type { PanOnScrollMode, Viewport } from './zoom'
|
import type { PanOnScrollMode, ViewportTransform } from './zoom'
|
||||||
import type { EdgeTypesObject, NodeTypesObject } from './components'
|
import type { EdgeTypesObject, NodeTypesObject } from './components'
|
||||||
import type { CustomEvent } from './hooks'
|
import type { CustomEvent } from './hooks'
|
||||||
|
|
||||||
@@ -88,7 +88,11 @@ export type SnapGrid = [number, number]
|
|||||||
export interface SelectionRect extends Rect {
|
export interface SelectionRect extends Rect {
|
||||||
startX: number
|
startX: number
|
||||||
startY: number
|
startY: number
|
||||||
draw: boolean
|
}
|
||||||
|
|
||||||
|
export enum SelectionMode {
|
||||||
|
Partial = 'partial',
|
||||||
|
Full = 'full',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FlowExportObject {
|
export interface FlowExportObject {
|
||||||
@@ -117,6 +121,7 @@ export interface FlowProps {
|
|||||||
selectionKeyCode?: KeyFilter | null
|
selectionKeyCode?: KeyFilter | null
|
||||||
multiSelectionKeyCode?: KeyFilter | null
|
multiSelectionKeyCode?: KeyFilter | null
|
||||||
zoomActivationKeyCode?: KeyFilter | null
|
zoomActivationKeyCode?: KeyFilter | null
|
||||||
|
panActivationKeyCode?: KeyFilter | null
|
||||||
snapToGrid?: boolean
|
snapToGrid?: boolean
|
||||||
snapGrid?: SnapGrid
|
snapGrid?: SnapGrid
|
||||||
onlyRenderVisibleElements?: boolean
|
onlyRenderVisibleElements?: boolean
|
||||||
@@ -126,10 +131,10 @@ export interface FlowProps {
|
|||||||
elementsSelectable?: boolean
|
elementsSelectable?: boolean
|
||||||
selectNodesOnDrag?: boolean
|
selectNodesOnDrag?: boolean
|
||||||
/** move pane on drag, replaced prop `paneMovable` */
|
/** move pane on drag, replaced prop `paneMovable` */
|
||||||
panOnDrag?: boolean
|
panOnDrag?: boolean | number[]
|
||||||
minZoom?: number
|
minZoom?: number
|
||||||
maxZoom?: number
|
maxZoom?: number
|
||||||
defaultViewport?: Viewport
|
defaultViewport?: ViewportTransform
|
||||||
translateExtent?: CoordinateExtent
|
translateExtent?: CoordinateExtent
|
||||||
nodeExtent?: CoordinateExtent
|
nodeExtent?: CoordinateExtent
|
||||||
defaultMarkerColor?: string
|
defaultMarkerColor?: string
|
||||||
@@ -141,6 +146,8 @@ export interface FlowProps {
|
|||||||
zoomOnDoubleClick?: boolean
|
zoomOnDoubleClick?: boolean
|
||||||
/** enable this to prevent vue flow from scrolling inside the container, i.e. allow for the page to scroll */
|
/** enable this to prevent vue flow from scrolling inside the container, i.e. allow for the page to scroll */
|
||||||
preventScrolling?: boolean
|
preventScrolling?: boolean
|
||||||
|
selectionOnDrag?: boolean
|
||||||
|
selectionMode?: SelectionMode
|
||||||
edgeUpdaterRadius?: number
|
edgeUpdaterRadius?: number
|
||||||
fitViewOnInit?: boolean
|
fitViewOnInit?: boolean
|
||||||
/** allow connection with click handlers, i.e. support touch devices */
|
/** allow connection with click handlers, i.e. support touch devices */
|
||||||
|
|||||||
@@ -65,6 +65,11 @@ export interface FlowEvents {
|
|||||||
selectionDrag: NodeDragEvent
|
selectionDrag: NodeDragEvent
|
||||||
selectionDragStop: NodeDragEvent
|
selectionDragStop: NodeDragEvent
|
||||||
selectionContextMenu: { event: MouseEvent; nodes: GraphNode[] }
|
selectionContextMenu: { event: MouseEvent; nodes: GraphNode[] }
|
||||||
|
selectionStart: MouseEvent
|
||||||
|
selectionEnd: MouseEvent
|
||||||
|
viewportChangeStart: ViewportTransform
|
||||||
|
viewportChange: ViewportTransform
|
||||||
|
viewportChangeEnd: ViewportTransform
|
||||||
paneScroll: WheelEvent | undefined
|
paneScroll: WheelEvent | undefined
|
||||||
paneClick: MouseEvent
|
paneClick: MouseEvent
|
||||||
paneContextMenu: MouseEvent
|
paneContextMenu: MouseEvent
|
||||||
@@ -82,21 +87,6 @@ export interface FlowEvents {
|
|||||||
edgeUpdateEnd: EdgeMouseEvent
|
edgeUpdateEnd: EdgeMouseEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FlowHooks = Readonly<{
|
|
||||||
[key in keyof FlowEvents]: EventHook<FlowEvents[key]>
|
|
||||||
}>
|
|
||||||
|
|
||||||
export type FlowHooksOn = Readonly<{
|
|
||||||
[key in keyof FlowEvents as `on${Capitalize<key>}`]: EventHookOn<FlowEvents[key]>
|
|
||||||
}>
|
|
||||||
|
|
||||||
export type FlowHooksEmit = Readonly<{
|
|
||||||
[key in keyof FlowEvents]: EventHookTrigger<FlowEvents[key]>
|
|
||||||
}>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vue Flow component event emitter definitions
|
|
||||||
*/
|
|
||||||
export interface Emits {
|
export interface Emits {
|
||||||
(event: 'nodesChange', changes: NodeChange[]): void
|
(event: 'nodesChange', changes: NodeChange[]): void
|
||||||
(event: 'edgesChange', changes: EdgeChange[]): void
|
(event: 'edgesChange', changes: EdgeChange[]): void
|
||||||
@@ -119,10 +109,10 @@ export interface Emits {
|
|||||||
(
|
(
|
||||||
event: 'connectStart',
|
event: 'connectStart',
|
||||||
connectionEvent: {
|
connectionEvent: {
|
||||||
event: MouseEvent
|
event?: MouseEvent
|
||||||
} & OnConnectStartParams,
|
} & OnConnectStartParams,
|
||||||
): void
|
): void
|
||||||
(event: 'connectEnd', connectionEvent: MouseEvent): void
|
(event: 'connectEnd', connectionEvent?: MouseEvent): void
|
||||||
(event: 'moveStart', moveEvent: { event: D3ZoomEvent<HTMLDivElement, any>; flowTransform: ViewportTransform }): void
|
(event: 'moveStart', moveEvent: { event: D3ZoomEvent<HTMLDivElement, any>; flowTransform: ViewportTransform }): void
|
||||||
(event: 'move', moveEvent: { event: D3ZoomEvent<HTMLDivElement, any>; flowTransform: ViewportTransform }): void
|
(event: 'move', moveEvent: { event: D3ZoomEvent<HTMLDivElement, any>; flowTransform: ViewportTransform }): void
|
||||||
(event: 'moveEnd', moveEvent: { event: D3ZoomEvent<HTMLDivElement, any>; flowTransform: ViewportTransform }): void
|
(event: 'moveEnd', moveEvent: { event: D3ZoomEvent<HTMLDivElement, any>; flowTransform: ViewportTransform }): void
|
||||||
@@ -130,6 +120,11 @@ export interface Emits {
|
|||||||
(event: 'selectionDrag', selectionEvent: NodeDragEvent): void
|
(event: 'selectionDrag', selectionEvent: NodeDragEvent): void
|
||||||
(event: 'selectionDragStop', selectionEvent: NodeDragEvent): void
|
(event: 'selectionDragStop', selectionEvent: NodeDragEvent): void
|
||||||
(event: 'selectionContextMenu', selectionEvent: { event: MouseEvent; nodes: GraphNode[] }): void
|
(event: 'selectionContextMenu', selectionEvent: { event: MouseEvent; nodes: GraphNode[] }): void
|
||||||
|
(event: 'selectionStart', selectionEvent: MouseEvent): void
|
||||||
|
(event: 'selectionEnd', selectionEvent: MouseEvent): void
|
||||||
|
(event: 'viewportChangeStart', viewport: ViewportTransform): void
|
||||||
|
(event: 'viewportChange', viewport: ViewportTransform): void
|
||||||
|
(event: 'viewportChangeEnd', viewport: ViewportTransform): void
|
||||||
(event: 'paneReady', paneEvent: VueFlowStore): void
|
(event: 'paneReady', paneEvent: VueFlowStore): void
|
||||||
(event: 'paneScroll', paneEvent: WheelEvent | undefined): void
|
(event: 'paneScroll', paneEvent: WheelEvent | undefined): void
|
||||||
(event: 'paneClick', paneEvent: MouseEvent): void
|
(event: 'paneClick', paneEvent: MouseEvent): void
|
||||||
@@ -146,6 +141,7 @@ export interface Emits {
|
|||||||
(event: 'edgeUpdateStart', edgeMouseEvent: EdgeMouseEvent): void
|
(event: 'edgeUpdateStart', edgeMouseEvent: EdgeMouseEvent): void
|
||||||
(event: 'edgeUpdate', edgeUpdateEvent: EdgeUpdateEvent): void
|
(event: 'edgeUpdate', edgeUpdateEvent: EdgeUpdateEvent): void
|
||||||
(event: 'edgeUpdateEnd', edgeMouseEvent: EdgeMouseEvent): void
|
(event: 'edgeUpdateEnd', edgeMouseEvent: EdgeMouseEvent): void
|
||||||
|
(event: 'updateNodeInternals'): void
|
||||||
|
|
||||||
/** v-model event definitions */
|
/** v-model event definitions */
|
||||||
(event: 'update:modelValue', value: FlowElements): void
|
(event: 'update:modelValue', value: FlowElements): void
|
||||||
@@ -153,6 +149,18 @@ export interface Emits {
|
|||||||
(event: 'update:edges', value: GraphEdge[]): void
|
(event: 'update:edges', value: GraphEdge[]): void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FlowHooks = Readonly<{
|
||||||
|
[key in keyof FlowEvents]: EventHook<FlowEvents[key]>
|
||||||
|
}>
|
||||||
|
|
||||||
|
export type FlowHooksOn = Readonly<{
|
||||||
|
[key in keyof FlowEvents as `on${Capitalize<key>}`]: EventHookOn<FlowEvents[key]>
|
||||||
|
}>
|
||||||
|
|
||||||
|
export type FlowHooksEmit = Readonly<{
|
||||||
|
[key in keyof FlowEvents]: EventHookTrigger<FlowEvents[key]>
|
||||||
|
}>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To type `Args` (the event callback arguments) pass an array as argument list as first generic type
|
* To type `Args` (the event callback arguments) pass an array as argument list as first generic type
|
||||||
* To type `Return` (the event callback return value) pass a value to the second generic type
|
* To type `Return` (the event callback return value) pass a value to the second generic type
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Component, VNode } from 'vue'
|
import type { Component, VNode } from 'vue'
|
||||||
import type { ClassFunc, Dimensions, ElementData, Position, SnapGrid, StyleFunc, Styles, XYPosition, XYZPosition } from './flow'
|
import type { ClassFunc, Dimensions, ElementData, Position, StyleFunc, Styles, XYPosition, XYZPosition } from './flow'
|
||||||
import type { DefaultNodeTypes, NodeComponent } from './components'
|
import type { DefaultNodeTypes, NodeComponent } from './components'
|
||||||
import type { HandleConnectable, HandleElement, ValidConnectionFunc } from './handle'
|
import type { HandleConnectable, HandleElement, ValidConnectionFunc } from './handle'
|
||||||
import type { CustomEvent, NodeEventsHandler, NodeEventsOn } from './hooks'
|
import type { CustomEvent, NodeEventsHandler, NodeEventsOn } from './hooks'
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import type {
|
|||||||
FlowExportObject,
|
FlowExportObject,
|
||||||
FlowOptions,
|
FlowOptions,
|
||||||
Rect,
|
Rect,
|
||||||
|
SelectionMode,
|
||||||
|
SelectionRect,
|
||||||
SnapGrid,
|
SnapGrid,
|
||||||
XYPosition,
|
XYPosition,
|
||||||
} from './flow'
|
} from './flow'
|
||||||
@@ -15,7 +17,7 @@ import type { DefaultEdgeTypes, DefaultNodeTypes, EdgeComponent, NodeComponent }
|
|||||||
import type { Connection, ConnectionLineOptions, ConnectionLineType, ConnectionMode, Connector } from './connection'
|
import type { Connection, ConnectionLineOptions, ConnectionLineType, ConnectionMode, Connector } from './connection'
|
||||||
import type { DefaultEdgeOptions, Edge, EdgeUpdatable, GraphEdge } from './edge'
|
import type { DefaultEdgeOptions, Edge, EdgeUpdatable, GraphEdge } from './edge'
|
||||||
import type { CoordinateExtent, GraphNode, Node } from './node'
|
import type { CoordinateExtent, GraphNode, Node } from './node'
|
||||||
import type { D3Selection, D3Zoom, D3ZoomHandler, PanOnScrollMode, Viewport, ViewportFunctions } from './zoom'
|
import type { D3Selection, D3Zoom, D3ZoomHandler, PanOnScrollMode, ViewportFunctions, ViewportTransform } from './zoom'
|
||||||
import type { CustomEvent, FlowHooks, FlowHooksEmit, FlowHooksOn } from './hooks'
|
import type { CustomEvent, FlowHooks, FlowHooksEmit, FlowHooksOn } from './hooks'
|
||||||
import type { EdgeChange, NodeChange, NodeDragItem } from './changes'
|
import type { EdgeChange, NodeChange, NodeDragItem } from './changes'
|
||||||
import type { StartHandle } from './handle'
|
import type { StartHandle } from './handle'
|
||||||
@@ -48,7 +50,7 @@ export interface State extends Omit<FlowOptions, 'id' | 'modelValue'> {
|
|||||||
minZoom: number
|
minZoom: number
|
||||||
/** use setMaxZoom action to change maxZoom */
|
/** use setMaxZoom action to change maxZoom */
|
||||||
maxZoom: number
|
maxZoom: number
|
||||||
defaultViewport: Viewport
|
defaultViewport: ViewportTransform
|
||||||
/** use setTranslateExtent action to change translateExtent */
|
/** use setTranslateExtent action to change translateExtent */
|
||||||
translateExtent: CoordinateExtent
|
translateExtent: CoordinateExtent
|
||||||
nodeExtent: CoordinateExtent
|
nodeExtent: CoordinateExtent
|
||||||
@@ -56,7 +58,7 @@ export interface State extends Omit<FlowOptions, 'id' | 'modelValue'> {
|
|||||||
/** viewport dimensions - do not change! */
|
/** viewport dimensions - do not change! */
|
||||||
readonly dimensions: Dimensions
|
readonly dimensions: Dimensions
|
||||||
/** viewport transform x, y, z - do not change! */
|
/** viewport transform x, y, z - do not change! */
|
||||||
readonly viewport: Viewport
|
readonly viewport: ViewportTransform
|
||||||
/** if true will skip rendering any elements currently not inside viewport until they become visible */
|
/** if true will skip rendering any elements currently not inside viewport until they become visible */
|
||||||
onlyRenderVisibleElements: boolean
|
onlyRenderVisibleElements: boolean
|
||||||
nodesSelectionActive: boolean
|
nodesSelectionActive: boolean
|
||||||
@@ -67,6 +69,7 @@ export interface State extends Omit<FlowOptions, 'id' | 'modelValue'> {
|
|||||||
selectionKeyCode: KeyFilter | null
|
selectionKeyCode: KeyFilter | null
|
||||||
multiSelectionKeyCode: KeyFilter | null
|
multiSelectionKeyCode: KeyFilter | null
|
||||||
zoomActivationKeyCode: KeyFilter | null
|
zoomActivationKeyCode: KeyFilter | null
|
||||||
|
panActivationKeyCode: KeyFilter | null
|
||||||
|
|
||||||
connectionMode: ConnectionMode
|
connectionMode: ConnectionMode
|
||||||
connectionLineOptions: ConnectionLineOptions
|
connectionLineOptions: ConnectionLineOptions
|
||||||
@@ -95,7 +98,10 @@ export interface State extends Omit<FlowOptions, 'id' | 'modelValue'> {
|
|||||||
elementsSelectable: boolean
|
elementsSelectable: boolean
|
||||||
selectNodesOnDrag: boolean
|
selectNodesOnDrag: boolean
|
||||||
|
|
||||||
panOnDrag: boolean
|
userSelectionRect: SelectionRect | null
|
||||||
|
selectionOnDrag: boolean
|
||||||
|
selectionMode: SelectionMode
|
||||||
|
panOnDrag: boolean | number[]
|
||||||
zoomOnScroll: boolean
|
zoomOnScroll: boolean
|
||||||
zoomOnPinch: boolean
|
zoomOnPinch: boolean
|
||||||
panOnScroll: boolean
|
panOnScroll: boolean
|
||||||
@@ -103,16 +109,17 @@ export interface State extends Omit<FlowOptions, 'id' | 'modelValue'> {
|
|||||||
panOnScrollMode: PanOnScrollMode
|
panOnScrollMode: PanOnScrollMode
|
||||||
zoomOnDoubleClick: boolean
|
zoomOnDoubleClick: boolean
|
||||||
preventScrolling: boolean
|
preventScrolling: boolean
|
||||||
|
paneDragging: boolean
|
||||||
|
|
||||||
initialized: boolean
|
initialized: boolean
|
||||||
applyDefault: boolean
|
applyDefault: boolean
|
||||||
autoConnect: boolean | Connector
|
autoConnect: boolean | Connector
|
||||||
|
|
||||||
fitViewOnInit?: boolean
|
fitViewOnInit: boolean
|
||||||
|
|
||||||
noDragClassName?: 'nodrag' | string
|
noDragClassName: 'nodrag' | string
|
||||||
noWheelClassName?: 'nowheel' | string
|
noWheelClassName: 'nowheel' | string
|
||||||
noPanClassName?: 'nopan' | string
|
noPanClassName: 'nopan' | string
|
||||||
|
|
||||||
defaultEdgeOptions?: DefaultEdgeOptions
|
defaultEdgeOptions?: DefaultEdgeOptions
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,6 @@ export type D3Zoom = ZoomBehavior<HTMLDivElement, unknown>
|
|||||||
export type D3Selection = Selection<HTMLDivElement, unknown, any, any>
|
export type D3Selection = Selection<HTMLDivElement, unknown, any, any>
|
||||||
export type D3ZoomHandler = (this: HTMLDivElement, event: any, d: unknown) => void
|
export type D3ZoomHandler = (this: HTMLDivElement, event: any, d: unknown) => void
|
||||||
|
|
||||||
/** Transform x, y, z */
|
|
||||||
export interface Viewport {
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
zoom: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum PanOnScrollMode {
|
export enum PanOnScrollMode {
|
||||||
Free = 'free',
|
Free = 'free',
|
||||||
Vertical = 'vertical',
|
Vertical = 'vertical',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { EdgePositions, Getters, GraphEdge, GraphNode, HandleElement, Rect, Viewport, XYPosition } from '~/types'
|
import type { EdgePositions, Getters, GraphEdge, GraphNode, HandleElement, Rect, ViewportTransform, XYPosition } from '~/types'
|
||||||
import { Position } from '~/types'
|
import { Position } from '~/types'
|
||||||
|
|
||||||
export const getHandlePosition = (position: Position, rect: Rect, handle?: HandleElement): XYPosition => {
|
export const getHandlePosition = (position: Position, rect: Rect, handle?: HandleElement): XYPosition => {
|
||||||
@@ -83,7 +83,7 @@ interface IsEdgeVisibleParams {
|
|||||||
targetHeight: number
|
targetHeight: number
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
viewport: Viewport
|
viewport: ViewportTransform
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isEdgeVisible({
|
export function isEdgeVisible({
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import type {
|
|||||||
GraphNode,
|
GraphNode,
|
||||||
Node,
|
Node,
|
||||||
Rect,
|
Rect,
|
||||||
Viewport,
|
ViewportTransform,
|
||||||
XYPosition,
|
XYPosition,
|
||||||
XYZPosition,
|
XYZPosition,
|
||||||
} from '~/types'
|
} from '~/types'
|
||||||
@@ -222,7 +222,7 @@ export const updateEdge = (oldEdge: Edge, newConnection: Connection, elements: E
|
|||||||
|
|
||||||
export const pointToRendererPoint = (
|
export const pointToRendererPoint = (
|
||||||
{ x, y }: XYPosition,
|
{ x, y }: XYPosition,
|
||||||
{ x: tx, y: ty, zoom: tScale }: Viewport,
|
{ x: tx, y: ty, zoom: tScale }: ViewportTransform,
|
||||||
snapToGrid: boolean,
|
snapToGrid: boolean,
|
||||||
[snapX, snapY]: [number, number],
|
[snapX, snapY]: [number, number],
|
||||||
) => {
|
) => {
|
||||||
@@ -280,7 +280,7 @@ export const getRectOfNodes = (nodes: GraphNode[]) => {
|
|||||||
return boxToRect(box)
|
return boxToRect(box)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const graphPosToZoomedPos = ({ x, y }: XYPosition, { x: tx, y: ty, zoom: tScale }: Viewport): XYPosition => ({
|
export const graphPosToZoomedPos = ({ x, y }: XYPosition, { x: tx, y: ty, zoom: tScale }: ViewportTransform): XYPosition => ({
|
||||||
x: x * tScale + tx,
|
x: x * tScale + tx,
|
||||||
y: y * tScale + ty,
|
y: y * tScale + ty,
|
||||||
})
|
})
|
||||||
@@ -288,7 +288,7 @@ export const graphPosToZoomedPos = ({ x, y }: XYPosition, { x: tx, y: ty, zoom:
|
|||||||
export const getNodesInside = (
|
export const getNodesInside = (
|
||||||
nodes: GraphNode[],
|
nodes: GraphNode[],
|
||||||
rect: Rect,
|
rect: Rect,
|
||||||
{ x: tx, y: ty, zoom: tScale }: Viewport = { x: 0, y: 0, zoom: 1 },
|
{ x: tx, y: ty, zoom: tScale }: ViewportTransform = { x: 0, y: 0, zoom: 1 },
|
||||||
partially = false,
|
partially = false,
|
||||||
// set excludeNonSelectableNodes if you want to pay attention to the nodes "selectable" attribute
|
// set excludeNonSelectableNodes if you want to pay attention to the nodes "selectable" attribute
|
||||||
excludeNonSelectableNodes = false,
|
excludeNonSelectableNodes = false,
|
||||||
@@ -337,7 +337,7 @@ export const getTransformForBounds = (
|
|||||||
x?: number
|
x?: number
|
||||||
y?: number
|
y?: number
|
||||||
} = { x: 0, y: 0 },
|
} = { x: 0, y: 0 },
|
||||||
): Viewport => {
|
): ViewportTransform => {
|
||||||
const xZoom = width / (bounds.width * (1 + padding))
|
const xZoom = width / (bounds.width * (1 + padding))
|
||||||
const yZoom = height / (bounds.height * (1 + padding))
|
const yZoom = height / (bounds.height * (1 + padding))
|
||||||
const zoom = Math.min(xZoom, yZoom)
|
const zoom = Math.min(xZoom, yZoom)
|
||||||
|
|||||||
Reference in New Issue
Block a user