feat: implement workspaces

This commit is contained in:
Braks
2022-04-04 21:42:48 +02:00
parent cc96739c38
commit cd817b7f53
153 changed files with 8970 additions and 1191 deletions
@@ -0,0 +1,89 @@
<script lang="ts" setup>
import { getBezierPath, getSmoothStepPath } from '../Edges/utils'
import { ConnectionLineType, GraphNode, HandleElement, Position } from '../../types'
import { useVueFlow } from '../../composables'
interface ConnectionLineProps {
sourceNode: GraphNode
}
const props = defineProps<ConnectionLineProps>()
const { store } = useVueFlow()
const sourceHandle =
store.connectionHandleId && store.connectionHandleType
? props.sourceNode.handleBounds[store.connectionHandleType]?.find((d: HandleElement) => d.id === store.connectionHandleId)
: store.connectionHandleType && props.sourceNode.handleBounds[store.connectionHandleType ?? 'source']?.[0]
const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : props.sourceNode.dimensions.width / 2
const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : props.sourceNode.dimensions.height
const sourceX = props.sourceNode.computedPosition.x + sourceHandleX
const sourceY = props.sourceNode.computedPosition.y + sourceHandleY
const isRightOrLeft = sourceHandle?.position === Position.Left || sourceHandle?.position === Position.Right
const targetPosition = isRightOrLeft ? Position.Left : Position.Top
const targetX = computed(() => (store.connectionPosition.x - store.transform[0]) / store.transform[2])
const targetY = computed(() => (store.connectionPosition.y - store.transform[1]) / store.transform[2])
const dAttr = computed(() => {
let path = `M${sourceX},${sourceY} ${targetX.value},${targetY.value}`
switch (store.connectionLineType) {
case ConnectionLineType.Bezier:
path = getBezierPath({
sourceX,
sourceY,
sourcePosition: sourceHandle?.position,
targetX: targetX.value,
targetY: targetY.value,
targetPosition,
})
break
case ConnectionLineType.Step:
path = getSmoothStepPath({
sourceX,
sourceY,
sourcePosition: sourceHandle?.position,
targetX: targetX.value,
targetY: targetY.value,
targetPosition,
borderRadius: 0,
})
break
case ConnectionLineType.SmoothStep:
path = getSmoothStepPath({
sourceX,
sourceY,
sourcePosition: sourceHandle?.position,
targetX: targetX.value,
targetY: targetY.value,
targetPosition,
})
break
}
return path
})
</script>
<script lang="ts">
export default {
name: 'ConnectionLine',
}
</script>
<template>
<g class="vue-flow__connection">
<slot
v-bind="{
sourceX,
sourceY,
sourcePosition: sourceHandle?.position,
targetX,
targetY,
targetPosition,
connectionLineType: store.connectionLineType,
connectionLineStyle: store.connectionLineStyle,
nodes: store.getNodes,
sourceNode: props.sourceNode,
sourceHandle,
}"
>
<path :d="dAttr" class="vue-flow__connection-path" :style="store.connectionLineStyle || {}" />
</slot>
</g>
</template>
+45
View File
@@ -0,0 +1,45 @@
<script lang="ts" setup>
import { CSSProperties } from 'vue'
import EdgeText from './EdgeText.vue'
interface Props {
centerX: number
centerY: number
path: string
label?:
| string
| {
props?: any
component: any
}
style?: CSSProperties
labelStyle?: any
labelShowBg?: boolean
labelBgStyle?: any
labelBgPadding?: [number, number]
labelBgBorderRadius?: number
markerStart?: string
markerEnd?: string
}
const props = defineProps<Props>()
</script>
<template>
<path
:style="props.style"
:d="props.path"
class="vue-flow__edge-path"
:marker-end="props.markerEnd"
:marker-start="props.markerStart"
/>
<EdgeText
v-if="props.label"
:x="props.centerX"
:y="props.centerY"
:label="props.label"
:label-style="props.labelStyle"
:label-show-bg="props.labelShowBg"
:label-bg-style="props.labelBgStyle"
:label-bg-padding="props.labelBgPadding"
:label-bg-border-radius="props.labelBgBorderRadius"
/>
</template>
@@ -0,0 +1,62 @@
<script lang="ts" setup>
import { Position } from '../../types/flow'
import type { EdgeProps } from '../../types/edge'
import { getBezierPath, getBezierCenter } from './utils'
import BaseEdge from './BaseEdge.vue'
const props = withDefaults(defineProps<EdgeProps>(), {
selected: false,
sourcePosition: 'bottom' as Position,
targetPosition: 'top' as Position,
labelStyle: () => ({}),
label: () => '',
labelShowBg: true,
labelBgStyle: () => ({}),
})
const centered = computed(() =>
getBezierCenter({
sourceX: props.sourceX,
sourceY: props.sourceY,
targetX: props.targetX,
targetY: props.targetY,
sourcePosition: props.sourcePosition,
targetPosition: props.targetPosition,
curvature: props.curvature,
}),
)
const path = computed(() => {
if (props.sourceX && props.sourceY)
return getBezierPath({
sourceX: props.sourceX,
sourceY: props.sourceY,
targetX: props.targetX,
targetY: props.targetY,
sourcePosition: props.sourcePosition,
targetPosition: props.targetPosition,
curvature: props.curvature,
})
else return ''
})
</script>
<script lang="ts">
export default {
name: 'BezierEdge',
}
</script>
<template>
<BaseEdge
:path="path"
:center-x="centered[0]"
:center-y="centered[1]"
:label="props.label"
:label-style="props.labelStyle"
:label-show-bg="props.labelShowBg"
:label-bg-style="props.labelBgStyle"
:label-bg-padding="props.labelBgPadding"
:label-bg-border-radius="props.labelBgBorderRadius"
:style="props.style"
:marker-end="props.markerEnd"
:marker-start="props.markerStart"
/>
</template>
@@ -0,0 +1,49 @@
<script lang="ts" setup>
import { HTMLAttributes } from 'vue'
import { Position } from '../../types'
const shiftX = (x: number, shift: number, position: Position): number => {
if (position === Position.Left) return x - shift
if (position === Position.Right) return x + shift
return x
}
const shiftY = (y: number, shift: number, position: Position): number => {
if (position === Position.Top) return y - shift
if (position === Position.Bottom) return y + shift
return y
}
interface EdgeAnchorProps extends HTMLAttributes {
position: Position
centerX: number
centerY: number
radius?: number
}
const props = withDefaults(defineProps<EdgeAnchorProps>(), {
radius: 10,
centerX: 0,
centerY: 0,
position: Position.Top,
})
const cx = computed(() => {
const val = shiftX(props.centerX, props.radius, props.position)
if (isNaN(val)) return 0
else return val
})
const cy = computed(() => {
const val = shiftY(props.centerY, props.radius, props.position)
if (isNaN(val)) return 0
else return val
})
</script>
<script lang="ts">
export default {
name: 'EdgeAnchor',
}
</script>
<template>
<circle class="vue-flow__edgeupdater" :cx="cx" :cy="cy" :r="props.radius" stroke="transparent" fill="transparent" />
</template>
+53
View File
@@ -0,0 +1,53 @@
<script lang="ts" setup>
import type { EdgeTextProps } from '../../types/components'
import { Rect } from '../../types'
const props = withDefaults(defineProps<EdgeTextProps>(), {
labelStyle: () => ({}),
labelShowBg: true,
labelBgStyle: () => ({}),
labelBgPadding: () => [2, 4],
labelBgBorderRadius: 2,
})
const edgeRef = templateRef<SVGTextElement>('edge-text', null)
const edgeRefBbox = ref<Rect>({ x: 0, y: 0, width: 0, height: 0 })
onMounted(() => {
edgeRefBbox.value = edgeRef.value.getBBox()
})
const transform = computed(() => `translate(${props.x - edgeRefBbox.value.width / 2} ${props.y - edgeRefBbox.value.height / 2})`)
</script>
<script lang="ts">
export default {
name: 'EdgeText',
}
</script>
<template>
<g :transform="transform" :class="props.class" class="vue-flow__edge-textwrapper">
<rect
v-if="props.labelShowBg"
class="vue-flow__edge-textbg"
:width="edgeRefBbox.width + 2 * props.labelBgPadding[0] + 'px'"
:height="edgeRefBbox.height + 2 * props.labelBgPadding[1] + 'px'"
:x="-props.labelBgPadding[0]"
:y="-props.labelBgPadding[1]"
:style="props.labelBgStyle"
:rx="props.labelBgBorderRadius"
:ry="props.labelBgBorderRadius"
/>
<text ref="edge-text" class="vue-flow__edge-text" :y="edgeRefBbox.height / 2" dy="0.3em" :style="props.labelStyle">
<slot v-bind="props">
<component
:is="props.label?.component"
v-if="typeof props.label !== 'string' && typeof props.label?.component !== 'undefined'"
v-bind="props.label?.props"
/>
<template v-else v-html="props.label">
{{ props.label }}
</template>
</slot>
</text>
</g>
</template>
@@ -0,0 +1,222 @@
<script lang="ts" setup>
import { useHandle, useVueFlow } from '../../composables'
import { ConnectionMode, Position, EdgeComponent } from '../../types'
import { getEdgePositions, getHandle, getMarkerId } from '../../utils'
import EdgeAnchor from './EdgeAnchor.vue'
interface EdgeWrapper {
id: string
selectable?: boolean
updatable?: boolean
}
const props = defineProps<EdgeWrapper>()
const { store } = useVueFlow()
const edge = computed(() => store.getEdge(props.id)!)
if (!edge.value) throw new Error(`Edge with ${props.id} not found!`)
const updating = ref(false)
const { onMouseDown } = useHandle()
const onEdgeClick = (event: MouseEvent) => {
const data = { event, edge: edge.value }
if (props.selectable) {
store.nodesSelectionActive = false
store.addSelectedEdges([edge.value])
}
store.hooks.edgeClick.trigger(data)
}
const onEdgeContextMenu = (event: MouseEvent) => store.hooks.edgeContextMenu.trigger({ event, edge: edge.value })
const onDoubleClick = (event: MouseEvent) => store.hooks.edgeDoubleClick.trigger({ event, edge: edge.value })
const onEdgeMouseEnter = (event: MouseEvent) => store.hooks.edgeMouseEnter.trigger({ event, edge: edge.value })
const onEdgeMouseMove = (event: MouseEvent) => store.hooks.edgeMouseMove.trigger({ event, edge: edge.value })
const onEdgeMouseLeave = (event: MouseEvent) => store.hooks.edgeMouseLeave.trigger({ event, edge: edge.value })
const onEdgeUpdaterMouseEnter = () => (updating.value = true)
const onEdgeUpdaterMouseOut = () => (updating.value = false)
const onEdgeUpdaterSourceMouseDown = (event: MouseEvent) => handleEdgeUpdater(event, true)
const onEdgeUpdaterTargetMouseDown = (event: MouseEvent) => handleEdgeUpdater(event, false)
const handleEdgeUpdater = (event: MouseEvent, isSourceHandle: boolean) => {
const nodeId = isSourceHandle ? edge.value.target : edge.value.source
const handleId = (isSourceHandle ? edge.value.targetHandle : edge.value.sourceHandle) ?? ''
store.hooks.edgeUpdateStart.trigger({ event, edge: edge.value })
onMouseDown(
event,
handleId,
nodeId,
isSourceHandle,
undefined,
isSourceHandle ? 'target' : 'source',
(connection) => store.hooks.edgeUpdate.trigger({ edge: edge.value, connection }),
() => store.hooks.edgeUpdateEnd.trigger({ event, edge: edge.value }),
)
}
// when connection type is loose we can define all handles as sources
const targetNodeHandles = computed(() =>
store.connectionMode === ConnectionMode.Strict
? edge.value.targetNode.handleBounds.target
: edge.value.targetNode.handleBounds.target ?? edge.value.targetNode.handleBounds.source,
)
const sourceHandle = controlledComputed(
() => edge.value.sourceNode.handleBounds,
() => getHandle(edge.value.sourceNode.handleBounds.source, edge.value.sourceHandle),
)
const targetHandle = computed(() => getHandle(targetNodeHandles.value, edge.value.targetHandle))
const sourcePosition = computed(() => (sourceHandle.value ? sourceHandle.value.position : Position.Bottom))
const targetPosition = computed(() => (targetHandle.value ? targetHandle.value.position : Position.Top))
const edgeUpdaterRadius = computed(() => store.edgeUpdaterRadius)
onMounted(() => {
watch(
[
sourcePosition,
targetPosition,
() => edge.value.sourceNode.position,
() => edge.value.targetNode.position,
() => edge.value.sourceNode.computedPosition,
() => edge.value.targetNode.computedPosition,
() => edge.value.sourceNode.dimensions,
() => edge.value.targetNode.dimensions,
],
() => {
const { sourceX, sourceY, targetY, targetX } = getEdgePositions(
edge.value.sourceNode,
sourceHandle.value,
sourcePosition.value,
edge.value.targetNode,
targetHandle.value,
targetPosition.value,
)
if (edge.value.sourceX !== sourceX) edge.value.sourceX = sourceX
if (edge.value.sourceY !== sourceY) edge.value.sourceY = sourceY
if (edge.value.targetX !== targetX) edge.value.targetX = targetX
if (edge.value.targetY !== targetY) edge.value.targetY = targetY
},
{ immediate: true, deep: true },
)
})
const name = ref(edge.value.type ?? 'default')
const type = computed(() => {
let edgeType = store.getEdgeTypes[name.value]
if (typeof edgeType === 'string') edgeType = resolveComponent(name.value, false) as EdgeComponent
if (typeof edgeType !== 'string') return edgeType
const slot = useSlots()?.[name.value]?.({})
if (!slot || !slot[0].key?.toString().includes(name.value)) {
console.warn(`Node type "${edge.value.type}" not found and no slot detected. Using fallback type "default".`)
name.value = 'default'
return store.getEdgeTypes.default
}
})
</script>
<script lang="ts">
export default {
name: 'Edge',
}
</script>
<template>
<g
:key="`edge-${edge.id}`"
:class="[
'vue-flow__edge',
`vue-flow__edge-${name}`,
store.noPanClassName,
{
selected: edge.selected,
animated: edge.animated,
inactive: !props.selectable,
updating,
},
edge.class,
]"
@click="onEdgeClick"
@dblClick="onDoubleClick"
@contextmenu="onEdgeContextMenu"
@mouseenter="onEdgeMouseEnter"
@mousemove="onEdgeMouseMove"
@mouseleave="onEdgeMouseLeave"
>
<slot
v-bind="{
id: edge.id,
sourceNode: edge.sourceNode,
targetNode: edge.targetNode,
source: edge.source,
target: edge.target,
selected: edge.selected,
animated: edge.animated,
updatable: props.updatable,
label: edge.label,
labelStyle: edge.labelStyle,
labelShowBg: edge.labelShowBg,
labelBgStyle: edge.labelBgStyle,
labelBgPadding: edge.labelBgPadding,
labelBgBorderRadius: edge.labelBgBorderRadius,
data: edge.data,
style: edge.style,
sourceX: edge.sourceX,
sourceY: edge.sourceY,
targetX: edge.targetX,
targetY: edge.targetY,
markerStart: `url(#${getMarkerId(edge.markerStart)})`,
markerEnd: `url(#${getMarkerId(edge.markerEnd)})`,
sourcePosition,
targetPosition,
sourceHandleId: edge.sourceHandle,
targetHandleId: edge.targetHandle,
}"
>
<component
:is="type"
v-if="type"
v-bind="{
id: edge.id,
sourceNode: edge.sourceNode,
targetNode: edge.targetNode,
source: edge.source,
target: edge.target,
updatable: props.updatable,
selected: edge.selected,
animated: edge.animated,
label: edge.label,
labelStyle: edge.labelStyle,
labelShowBg: edge.labelShowBg,
labelBgStyle: edge.labelBgStyle,
labelBgPadding: edge.labelBgPadding,
labelBgBorderRadius: edge.labelBgBorderRadius,
data: edge.data,
style: edge.style,
markerStart: `url(#${getMarkerId(edge.markerStart)})`,
markerEnd: `url(#${getMarkerId(edge.markerEnd)})`,
sourcePosition,
targetPosition,
sourceX: edge.sourceX,
sourceY: edge.sourceY,
targetX: edge.targetX,
targetY: edge.targetY,
sourceHandleId: edge.sourceHandle,
targetHandleId: edge.targetHandle,
}"
/>
</slot>
<g
v-if="props.updatable"
@mousedown="onEdgeUpdaterSourceMouseDown"
@mouseenter="onEdgeUpdaterMouseEnter"
@mouseout="onEdgeUpdaterMouseOut"
>
<EdgeAnchor :position="sourcePosition" :center-x="edge.sourceX" :center-y="edge.sourceY" :radius="edgeUpdaterRadius" />
</g>
<g
v-if="props.updatable"
@mousedown="onEdgeUpdaterTargetMouseDown"
@mouseenter="onEdgeUpdaterMouseEnter"
@mouseout="onEdgeUpdaterMouseOut"
>
<EdgeAnchor :position="targetPosition" :center-x="edge.targetX" :center-y="edge.targetY" :radius="edgeUpdaterRadius" />
</g>
</g>
</template>
@@ -0,0 +1,60 @@
<script lang="ts" setup>
import { Position } from '../../types/flow'
import type { EdgeProps } from '../../types/edge'
import { getSimpleBezierPath, getSimpleBezierCenter } from './utils'
import BaseEdge from './BaseEdge.vue'
const props = withDefaults(defineProps<EdgeProps>(), {
selected: false,
sourcePosition: 'bottom' as Position,
targetPosition: 'top' as Position,
labelStyle: () => ({}),
label: () => '',
labelShowBg: true,
labelBgStyle: () => ({}),
})
const centered = computed(() =>
getSimpleBezierCenter({
sourceX: props.sourceX,
sourceY: props.sourceY,
targetX: props.targetX,
targetY: props.targetY,
sourcePosition: props.sourcePosition,
targetPosition: props.targetPosition,
}),
)
const path = computed(() => {
if (props.sourceX && props.sourceY)
return getSimpleBezierPath({
sourceX: props.sourceX,
sourceY: props.sourceY,
targetX: props.targetX,
targetY: props.targetY,
sourcePosition: props.sourcePosition,
targetPosition: props.targetPosition,
})
else return ''
})
</script>
<script lang="ts">
export default {
name: 'SimpleBezierEdge',
}
</script>
<template>
<BaseEdge
:path="path"
:center-x="centered[0]"
:center-y="centered[1]"
:label="props.label"
:label-style="props.labelStyle"
:label-show-bg="props.labelShowBg"
:label-bg-style="props.labelBgStyle"
:label-bg-padding="props.labelBgPadding"
:label-bg-border-radius="props.labelBgBorderRadius"
:style="props.style"
:marker-end="props.markerEnd"
:marker-start="props.markerStart"
/>
</template>
@@ -0,0 +1,62 @@
<script lang="ts" setup>
import { Position } from '../../types'
import type { SmoothStepEdgeProps } from '../../types/edge'
import { getCenter, getSmoothStepPath } from './utils'
import BaseEdge from './BaseEdge.vue'
const props = withDefaults(defineProps<SmoothStepEdgeProps>(), {
selected: false,
sourcePosition: 'bottom' as Position,
targetPosition: 'top' as Position,
label: () => '',
labelStyle: () => ({}),
labelShowBg: true,
labelBgStyle: () => ({}),
})
const centered = computed(() =>
getCenter({
sourceX: props.sourceX,
sourceY: props.sourceY,
targetX: props.targetX,
targetY: props.targetY,
sourcePosition: props.sourcePosition,
targetPosition: props.targetPosition,
}),
)
const path = computed(() => {
if (props.sourceX && props.sourceY)
return getSmoothStepPath({
sourceX: props.sourceX,
sourceY: props.sourceY,
targetX: props.targetX,
targetY: props.targetY,
sourcePosition: props.sourcePosition,
targetPosition: props.targetPosition,
borderRadius: props.borderRadius,
})
else return ''
})
</script>
<script lang="ts">
export default {
name: 'SmoothStepEdge',
}
</script>
<template>
<BaseEdge
:path="path"
:center-x="centered[0]"
:center-y="centered[1]"
:label="props.label"
:label-style="props.labelStyle"
:label-show-bg="props.labelShowBg"
:label-bg-style="props.labelBgStyle"
:label-bg-padding="props.labelBgPadding"
:label-bg-border-radius="props.labelBgBorderRadius"
:style="props.style"
:marker-end="props.markerEnd"
:marker-start="props.markerStart"
/>
</template>
+23
View File
@@ -0,0 +1,23 @@
<script lang="ts" setup>
import { Position } from '../../types'
import type { EdgeProps } from '../../types/edge'
import SmoothStepEdge from './SmoothStepEdge.vue'
const props = withDefaults(defineProps<EdgeProps>(), {
selected: false,
sourcePosition: 'bottom' as Position,
targetPosition: 'top' as Position,
label: () => '',
labelStyle: () => ({}),
labelShowBg: true,
labelBgStyle: () => ({}),
})
</script>
<script lang="ts">
export default {
name: 'StepEdge',
}
</script>
<template>
<SmoothStepEdge v-bind="props" :border-radius="0" />
</template>
@@ -0,0 +1,47 @@
<script lang="ts" setup>
import { Position } from '../../types'
import type { EdgeProps } from '../../types/edge'
import BaseEdge from './BaseEdge.vue'
const props = withDefaults(defineProps<EdgeProps>(), {
selected: false,
sourcePosition: 'bottom' as Position,
targetPosition: 'top' as Position,
label: () => '',
labelStyle: () => ({}),
labelShowBg: true,
labelBgStyle: () => ({}),
})
const centerY = computed(() => {
const yOffset = Math.abs(props.targetY - props.sourceY) / 2
return props.targetY < props.sourceY ? props.targetY + yOffset : props.targetY - yOffset
})
const centerX = computed(() => {
const xOffset = Math.abs(props.targetX - props.sourceX) / 2
return props.targetX < props.sourceX ? props.targetX + xOffset : props.targetX - xOffset
})
const path = computed(() => `M ${props.sourceX},${props.sourceY}L ${props.targetX},${props.targetY}`)
</script>
<script lang="ts">
export default {
name: 'StraightEdge',
}
</script>
<template>
<BaseEdge
:path="path"
:center-x="centerX"
:center-y="centerY"
:label="props.label"
:label-style="props.labelStyle"
:label-show-bg="props.labelShowBg"
:label-bg-style="props.labelBgStyle"
:label-bg-padding="props.labelBgPadding"
:label-bg-border-radius="props.labelBgBorderRadius"
:style="props.style"
:marker-end="props.markerEnd"
:marker-start="props.markerStart"
/>
</template>
+7
View File
@@ -0,0 +1,7 @@
export { default as BezierEdge } from './BezierEdge.vue'
export { default as SimpleBezierEdge } from './SimpleBezierEdge.vue'
export { default as StepEdge } from './StepEdge.vue'
export { default as SmoothStepEdge } from './SmoothStepEdge.vue'
export { default as StraightEdge } from './StraightEdge.vue'
export { default as EdgeAnchor } from './EdgeAnchor.vue'
export { default as EdgeText } from './EdgeText.vue'
@@ -0,0 +1,115 @@
import { Position } from '~/types'
interface GetControlWithCurvatureParams {
pos: Position
x1: number
y1: number
x2: number
y2: number
c: number
}
export interface GetBezierPathParams {
sourceX: number
sourceY: number
sourcePosition?: Position
targetX: number
targetY: number
targetPosition?: Position
curvature?: number
centerX?: number
centerY?: number
}
function calculateControlOffset(distance: number, curvature: number): number {
if (distance >= 0) {
return 0.5 * distance
} else {
return curvature * 25 * Math.sqrt(-distance)
}
}
function getControlWithCurvature({ pos, x1, y1, x2, y2, c }: GetControlWithCurvatureParams): [number, number] {
let ctX: number, ctY: number
switch (pos) {
case Position.Left:
ctX = x1 - calculateControlOffset(x1 - x2, c)
ctY = y1
break
case Position.Right:
ctX = x1 + calculateControlOffset(x2 - x1, c)
ctY = y1
break
case Position.Top:
ctX = x1
ctY = y1 - calculateControlOffset(y1 - y2, c)
break
case Position.Bottom:
ctX = x1
ctY = y1 + calculateControlOffset(y2 - y1, c)
break
}
return [ctX, ctY]
}
export function getBezierPath({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
curvature = 0.25,
}: GetBezierPathParams): string {
const [sourceControlX, sourceControlY] = getControlWithCurvature({
pos: sourcePosition,
x1: sourceX,
y1: sourceY,
x2: targetX,
y2: targetY,
c: curvature,
})
const [targetControlX, targetControlY] = getControlWithCurvature({
pos: targetPosition,
x1: targetX,
y1: targetY,
x2: sourceX,
y2: sourceY,
c: curvature,
})
return `M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`
}
export function getBezierCenter({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
curvature = 0.25,
}: GetBezierPathParams): [number, number, number, number] {
const [sourceControlX, sourceControlY] = getControlWithCurvature({
pos: sourcePosition,
x1: sourceX,
y1: sourceY,
x2: targetX,
y2: targetY,
c: curvature,
})
const [targetControlX, targetControlY] = getControlWithCurvature({
pos: targetPosition,
x1: targetX,
y1: targetY,
x2: sourceX,
y2: sourceY,
c: curvature,
})
// cubic bezier t=0.5 mid point, not the actual mid point, but easy to calculate
// https://stackoverflow.com/questions/67516101/how-to-find-distance-mid-point-of-bezier-curve
const centerX = sourceX * 0.125 + sourceControlX * 0.375 + targetControlX * 0.375 + targetX * 0.125
const centerY = sourceY * 0.125 + sourceControlY * 0.375 + targetControlY * 0.375 + targetY * 0.125
const xOffset = Math.abs(centerX - sourceX)
const yOffset = Math.abs(centerY - sourceY)
return [centerX, centerY, xOffset, yOffset]
}
@@ -0,0 +1,46 @@
import { Position } from '~/types'
export interface GetCenterParams {
sourceX: number
sourceY: number
targetX: number
targetY: number
sourcePosition?: Position
targetPosition?: Position
}
const LeftOrRight = [Position.Left, Position.Right]
export const getCenter = ({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition = Position.Bottom,
targetPosition = Position.Top,
}: GetCenterParams): [number, number, number, number] => {
const sourceIsLeftOrRight = LeftOrRight.includes(sourcePosition)
const targetIsLeftOrRight = LeftOrRight.includes(targetPosition)
// we expect flows to be horizontal or vertical (all handles left or right respectively top or bottom)
// a mixed edge is when one the source is on the left and the target is on the top for example.
const mixedEdge = (sourceIsLeftOrRight && !targetIsLeftOrRight) || (targetIsLeftOrRight && !sourceIsLeftOrRight)
if (mixedEdge) {
const xOffset = sourceIsLeftOrRight ? Math.abs(targetX - sourceX) : 0
const centerX = sourceX > targetX ? sourceX - xOffset : sourceX + xOffset
const yOffset = sourceIsLeftOrRight ? 0 : Math.abs(targetY - sourceY)
const centerY = sourceY < targetY ? sourceY + yOffset : sourceY - yOffset
return [centerX, centerY, xOffset, yOffset]
}
const xOffset = Math.abs(targetX - sourceX) / 2
const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset
const yOffset = Math.abs(targetY - sourceY) / 2
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset
return [centerX, centerY, xOffset, yOffset]
}
@@ -0,0 +1,4 @@
export * from './bezier'
export * from './simple-bezier'
export * from './smoothstep'
export * from './general'
@@ -0,0 +1,95 @@
import { Position } from '~/types'
export interface GetSimpleBezierPathParams {
sourceX: number
sourceY: number
sourcePosition?: Position
targetX: number
targetY: number
targetPosition?: Position
}
interface GetControlParams {
pos: Position
x1: number
y1: number
x2: number
y2: number
}
function getControl({ pos, x1, y1, x2, y2 }: GetControlParams): [number, number] {
let ctX: number, ctY: number
switch (pos) {
case Position.Left:
case Position.Right:
ctX = 0.5 * (x1 + x2)
ctY = y1
break
case Position.Top:
case Position.Bottom:
ctX = x1
ctY = 0.5 * (y1 + y2)
break
}
return [ctX, ctY]
}
export function getSimpleBezierPath({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
}: GetSimpleBezierPathParams): string {
const [sourceControlX, sourceControlY] = getControl({
pos: sourcePosition,
x1: sourceX,
y1: sourceY,
x2: targetX,
y2: targetY,
})
const [targetControlX, targetControlY] = getControl({
pos: targetPosition,
x1: targetX,
y1: targetY,
x2: sourceX,
y2: sourceY,
})
return `M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`
}
// @TODO: this function will recalculate the control points
// one option is to let getXXXPath() return center points
// but will introduce breaking changes
// the getCenter() of other types of edges might need to change, too
export function getSimpleBezierCenter({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
}: GetSimpleBezierPathParams): [number, number, number, number] {
const [sourceControlX, sourceControlY] = getControl({
pos: sourcePosition,
x1: sourceX,
y1: sourceY,
x2: targetX,
y2: targetY,
})
const [targetControlX, targetControlY] = getControl({
pos: targetPosition,
x1: targetX,
y1: targetY,
x2: sourceX,
y2: sourceY,
})
// cubic bezier t=0.5 mid point, not the actual mid point, but easy to calculate
// https://stackoverflow.com/questions/67516101/how-to-find-distance-mid-point-of-bezier-curve
const centerX = sourceX * 0.125 + sourceControlX * 0.375 + targetControlX * 0.375 + targetX * 0.125
const centerY = sourceY * 0.125 + sourceControlY * 0.375 + targetControlY * 0.375 + targetY * 0.125
const xOffset = Math.abs(centerX - sourceX)
const yOffset = Math.abs(centerY - sourceY)
return [centerX, centerY, xOffset, yOffset]
}
@@ -0,0 +1,93 @@
import { getCenter } from './general'
import { Position } from '~/types'
export interface GetSmoothStepPathParams {
sourceX: number
sourceY: number
sourcePosition?: Position
targetX: number
targetY: number
targetPosition?: Position
borderRadius?: number
centerX?: number
centerY?: number
}
// These are some helper methods for drawing the round corners
// The name indicates the direction of the path. "bottomLeftCorner" goes
// from bottom to the left and "leftBottomCorner" goes from left to the bottom.
// We have to consider the direction of the paths because of the animated lines.
const bottomLeftCorner = (x: number, y: number, size: number): string => `L ${x},${y - size}Q ${x},${y} ${x + size},${y}`
const leftBottomCorner = (x: number, y: number, size: number): string => `L ${x + size},${y}Q ${x},${y} ${x},${y - size}`
const bottomRightCorner = (x: number, y: number, size: number): string => `L ${x},${y - size}Q ${x},${y} ${x - size},${y}`
const rightBottomCorner = (x: number, y: number, size: number): string => `L ${x - size},${y}Q ${x},${y} ${x},${y - size}`
const leftTopCorner = (x: number, y: number, size: number): string => `L ${x + size},${y}Q ${x},${y} ${x},${y + size}`
const topLeftCorner = (x: number, y: number, size: number): string => `L ${x},${y + size}Q ${x},${y} ${x + size},${y}`
const topRightCorner = (x: number, y: number, size: number): string => `L ${x},${y + size}Q ${x},${y} ${x - size},${y}`
const rightTopCorner = (x: number, y: number, size: number): string => `L ${x - size},${y}Q ${x},${y} ${x},${y + size}`
export function getSmoothStepPath({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
borderRadius = 5,
centerX,
centerY,
}: GetSmoothStepPathParams): string {
const [_centerX, _centerY, offsetX, offsetY] = getCenter({ sourceX, sourceY, targetX, targetY })
const cornerWidth = Math.min(borderRadius, Math.abs(targetX - sourceX))
const cornerHeight = Math.min(borderRadius, Math.abs(targetY - sourceY))
const cornerSize = Math.min(cornerWidth, cornerHeight, offsetX, offsetY)
const leftAndRight = [Position.Left, Position.Right]
const cX = typeof centerX !== 'undefined' ? centerX : _centerX
const cY = typeof centerY !== 'undefined' ? centerY : _centerY
let firstCornerPath
let secondCornerPath
if (sourceX <= targetX) {
firstCornerPath = sourceY <= targetY ? bottomLeftCorner(sourceX, cY, cornerSize) : topLeftCorner(sourceX, cY, cornerSize)
secondCornerPath = sourceY <= targetY ? rightTopCorner(targetX, cY, cornerSize) : rightBottomCorner(targetX, cY, cornerSize)
} else {
firstCornerPath = sourceY < targetY ? bottomRightCorner(sourceX, cY, cornerSize) : topRightCorner(sourceX, cY, cornerSize)
secondCornerPath = sourceY < targetY ? leftTopCorner(targetX, cY, cornerSize) : leftBottomCorner(targetX, cY, cornerSize)
}
if (leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
if (sourceX <= targetX) {
firstCornerPath = sourceY <= targetY ? rightTopCorner(cX, sourceY, cornerSize) : rightBottomCorner(cX, sourceY, cornerSize)
secondCornerPath = sourceY <= targetY ? bottomLeftCorner(cX, targetY, cornerSize) : topLeftCorner(cX, targetY, cornerSize)
} else if (
(sourcePosition === Position.Right && targetPosition === Position.Left) ||
(sourcePosition === Position.Left && targetPosition === Position.Right) ||
(sourcePosition === Position.Left && targetPosition === Position.Left)
) {
// and sourceX > targetX
firstCornerPath = sourceY <= targetY ? leftTopCorner(cX, sourceY, cornerSize) : leftBottomCorner(cX, sourceY, cornerSize)
secondCornerPath = sourceY <= targetY ? bottomRightCorner(cX, targetY, cornerSize) : topRightCorner(cX, targetY, cornerSize)
}
} else if (leftAndRight.includes(sourcePosition) && !leftAndRight.includes(targetPosition)) {
if (sourceX <= targetX) {
firstCornerPath =
sourceY <= targetY ? rightTopCorner(targetX, sourceY, cornerSize) : rightBottomCorner(targetX, sourceY, cornerSize)
} else {
firstCornerPath =
sourceY <= targetY ? leftTopCorner(targetX, sourceY, cornerSize) : leftBottomCorner(targetX, sourceY, cornerSize)
}
secondCornerPath = ''
} else if (!leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
if (sourceX <= targetX) {
firstCornerPath =
sourceY <= targetY ? bottomLeftCorner(sourceX, targetY, cornerSize) : topLeftCorner(sourceX, targetY, cornerSize)
} else {
firstCornerPath =
sourceY <= targetY ? bottomRightCorner(sourceX, targetY, cornerSize) : topRightCorner(sourceX, targetY, cornerSize)
}
secondCornerPath = ''
}
return `M ${sourceX},${sourceY}${firstCornerPath}${secondCornerPath}L ${targetX},${targetY}`
}
+53
View File
@@ -0,0 +1,53 @@
<script lang="ts" setup>
import { useHandle, useVueFlow } from '../../composables'
import { Position } from '../../types'
import { NodeId } from '../../context'
import type { HandleProps } from '../../types/handle'
const { id, hooks, connectionStartHandle } = useVueFlow()
const props = withDefaults(defineProps<HandleProps>(), {
type: 'source',
position: 'top' as Position,
connectable: true,
})
const nodeId = inject(NodeId, '')
const { onMouseDown, onClick } = useHandle()
const onMouseDownHandler = (event: MouseEvent) =>
onMouseDown(event, props.id ?? null, nodeId, props.type === 'target', props.isValidConnection, undefined, (connection) =>
hooks.value.connect.trigger(connection),
)
const onClickHandler = (event: MouseEvent) => onClick(event, props.id ?? null, nodeId, props.type, props.isValidConnection)
</script>
<script lang="ts">
export default {
name: 'Handle',
}
</script>
<template>
<div
:data-handleid="props.id"
:data-nodeid="nodeId"
:data-handlepos="props.position"
:class="[
'vue-flow__handle',
`vue-flow__handle-${props.position}`,
`vue-flow__handle-${id}`,
'nodrag',
{
source: props.type !== 'target',
target: props.type === 'target',
connectable: props.connectable,
connecting:
connectionStartHandle?.nodeId === nodeId &&
connectionStartHandle?.handleId === props.id &&
connectionStartHandle?.type === props.type,
},
]"
@mousedown="onMouseDownHandler"
@click="onClickHandler"
>
<slot :node-id="nodeId" v-bind="props"></slot>
</div>
</template>
@@ -0,0 +1,33 @@
<script lang="ts" setup>
import Handle from '../Handle/Handle.vue'
import { Position } from '../../types'
import type { NodeProps } from '../../types/node'
const props = withDefaults(defineProps<NodeProps>(), {
data: () => {},
connectable: false,
sourcePosition: 'bottom' as Position,
targetPosition: 'top' as Position,
})
</script>
<script lang="ts">
export default {
name: 'DefaultNode',
}
</script>
<template>
<Handle
type="target"
:position="props.targetPosition"
:is-connectable="props.connectable"
:is-valid-connection="props.isValidTargetPos"
/>
<component :is="props.label.component" v-bind="props.label.props" v-if="typeof props.label !== 'string' && props.label" />
<span v-else v-html="props.label" />
<Handle
type="source"
:position="props.sourcePosition"
:is-connectable="props.connectable"
:is-valid-connection="props.isValidSourcePos"
/>
</template>
@@ -0,0 +1,26 @@
<script lang="ts" setup>
import Handle from '../Handle/Handle.vue'
import { Position } from '../../types'
import type { NodeProps } from '../../types/node'
const props = withDefaults(defineProps<NodeProps>(), {
data: () => {},
connectable: false,
sourcePosition: 'bottom' as Position,
})
</script>
<script lang="ts">
export default {
name: 'InputNode',
}
</script>
<template>
<component :is="props.label.component" v-bind="props.label.props" v-if="typeof props.label !== 'string' && props.label" />
<span v-else v-html="props.label" />
<Handle
type="source"
:position="props.sourcePosition"
:connectable="props.connectable"
:is-valid-connection="props.isValidSourcePos"
/>
</template>
@@ -0,0 +1,219 @@
<script lang="ts" setup>
import { DraggableEventListener, DraggableCore } from '@braks/revue-draggable'
import { useVueFlow } from '../../composables'
import { NodeComponent, SnapGrid } from '../../types'
import { NodeId } from '../../context'
import { getXYZPos } from '../../utils'
interface NodeWrapperProps {
id: string
draggable: boolean
selectable: boolean
connectable: boolean
snapGrid?: SnapGrid
}
const props = defineProps<NodeWrapperProps>()
const { store } = useVueFlow()
const node = computed(() => store.getNode(props.id)!)
if (!node.value) throw new Error(`Node with ${props.id} not found!`)
provide(NodeId, props.id)
const nodeElement = templateRef<HTMLDivElement>('node-element', null)
const onMouseEnterHandler = () =>
node.value.dragging && ((event: MouseEvent) => store.hooks.nodeMouseEnter.trigger({ event, node: node.value }))
const onMouseMoveHandler = () =>
node.value.dragging && ((event: MouseEvent) => store.hooks.nodeMouseMove.trigger({ event, node: node.value }))
const onMouseLeaveHandler = () =>
node.value.dragging && ((event: MouseEvent) => store.hooks.nodeMouseLeave.trigger({ event, node: node.value }))
const onContextMenuHandler = () => (event: MouseEvent) =>
store.hooks.nodeContextMenu.trigger({
event,
node: node.value,
})
const onDoubleClick = () => (event: MouseEvent) => store.hooks.nodeDoubleClick.trigger({ event, node: node.value })
const onSelectNodeHandler = (event: MouseEvent) => {
if (!props.draggable) {
if (props.selectable) {
store.nodesSelectionActive = false
if (!node.value.selected) store.addSelectedNodes([node.value])
}
store.hooks.nodeClick.trigger({ event, node: node.value })
}
}
const onDragStart: DraggableEventListener = ({ event }) => {
store.addSelectedNodes([])
store.hooks.nodeDragStart.trigger({ event, node: node.value })
if (store.selectNodesOnDrag && props.selectable) {
store.nodesSelectionActive = false
if (!node.value.selected) store.addSelectedNodes([node.value])
} else if (!store.selectNodesOnDrag && !node.value.selected && props.selectable) {
store.nodesSelectionActive = false
store.addSelectedNodes([])
}
}
const onDrag: DraggableEventListener = ({ event, data: { deltaX, deltaY } }) => {
nextTick(() => store.updateNodePosition({ id: node.value.id, diff: { x: deltaX, y: deltaY }, dragging: true }))
store.hooks.nodeDrag.trigger({ event, node: node.value })
}
const onDragStop: DraggableEventListener = ({ event, data: { deltaX, deltaY } }) => {
// onDragStop also gets called when user just clicks on a node.
// Because of that we set dragging to true inside the onDrag handler and handle the click here
if (!node.value.dragging) {
if (props.selectable && !store.selectNodesOnDrag && !node.value.selected) {
store.addSelectedNodes([node.value])
}
store.hooks.nodeClick.trigger({ event, node: node.value })
return
}
store.updateNodePosition({ id: node.value.id, diff: { x: deltaX, y: deltaY }, dragging: false })
store.hooks.nodeDragStop.trigger({ event, node: node.value })
}
onMounted(() => {
useResizeObserver(nodeElement, () =>
store.updateNodeDimensions([{ id: node.value.id, nodeElement: nodeElement.value, forceUpdate: true }]),
)
watch([() => node.value.type, () => node.value.sourcePosition, () => node.value.targetPosition], () =>
nextTick(() => store.updateNodeDimensions([{ id: node.value.id, nodeElement: nodeElement.value }])),
)
store.updateNodeDimensions([{ id: node.value.id, nodeElement: nodeElement.value }])
})
const scale = controlledComputed(
() => store.transform[2],
() => store.transform[2],
)
watch(
[() => node.value.position, () => store.getNode(node.value.parentNode!)],
([pos, parent]) => {
const xyzPos = {
...pos,
z: node.value.dragging || node.value.selected ? 1000 : node.value.computedPosition.z,
}
if (parent) {
node.value.computedPosition = getXYZPos(parent, xyzPos)
} else {
node.value.computedPosition = xyzPos
}
},
{ deep: true },
)
store.updateNodePosition({ id: node.value.id, diff: { x: 0, y: 0 } })
const name = ref(node.value.type ?? 'default')
const type = computed(() => {
let nodeType = store.getNodeTypes[name.value]
if (typeof nodeType === 'string') nodeType = resolveComponent(name.value, false) as NodeComponent
if (typeof nodeType !== 'string') return nodeType
const slot = useSlots()?.[name.value]?.({})
if (!slot || !slot[0].key?.toString().includes(name.value)) {
console.warn(`Node type "${node.value.type}" not found and no slot detected. Using fallback type "default".`)
name.value = 'default'
return store.getNodeTypes.default
}
})
</script>
<script lang="ts">
export default {
name: 'Node',
}
</script>
<template>
<DraggableCore
:cancel="`.${store.noDragClassName}`"
:handle="node.dragHandle"
:disabled="!props.draggable"
:scale="scale"
:grid="props.snapGrid"
:enable-user-select-hack="false"
@start="onDragStart"
@move="onDrag"
@stop="onDragStop"
>
<div
ref="node-element"
:key="`node-${node.id}`"
:class="[
'vue-flow__node',
`vue-flow__node-${name}`,
store.noPanClassName,
{
dragging: node.dragging,
selected: node.selected,
selectable: props.selectable,
},
node.class,
]"
:style="{
zIndex: node.computedPosition.z,
transform: `translate(${node.computedPosition.x}px,${node.computedPosition.y}px)`,
pointerEvents: props.selectable || props.draggable ? 'all' : 'none',
...node.style,
}"
:data-id="node.id"
@mouseenter="onMouseEnterHandler"
@mousemove="onMouseMoveHandler"
@mouseleave="onMouseLeaveHandler"
@contextmenu="onContextMenuHandler"
@click="onSelectNodeHandler"
@dblclick="onDoubleClick"
>
<slot
v-bind="{
nodeElement,
id: node.id,
type: node.type,
data: node.data,
selected: !!node.selected,
isConnectable: props.connectable,
position: node.position,
computedPosition: node.computedPosition,
dimensions: node.dimensions,
isValidTargetPos: node.isValidTargetPos,
isValidSourcePos: node.isValidSourcePos,
parentNode: node.parentNode,
dragging: !!node.dragging,
zIndex: node.dragging || node.selected ? 1000 : node.computedPosition.z,
targetPosition: node.targetPosition,
sourcePosition: node.sourcePosition,
label: node.label,
dragHandle: node.dragHandle,
}"
>
<component
:is="type"
v-if="type"
v-bind="{
nodeElement,
id: node.id,
type: node.type,
data: node.data,
selected: !!node.selected,
connectable: props.connectable,
position: node.position,
computedPosition: node.computedPosition,
dimensions: node.dimensions,
isValidTargetPos: node.isValidTargetPos,
isValidSourcePos: node.isValidSourcePos,
parentNode: node.parentNode,
dragging: !!node.dragging,
zIndex: node.dragging || node.selected ? 1000 : node.computedPosition.z,
targetPosition: node.targetPosition,
sourcePosition: node.sourcePosition,
label: node.label,
dragHandle: node.dragHandle,
}"
/>
</slot>
</div>
</DraggableCore>
</template>
@@ -0,0 +1,26 @@
<script lang="ts" setup>
import Handle from '../Handle/Handle.vue'
import { Position } from '../../types'
import type { NodeProps } from '../../types/node'
const props = withDefaults(defineProps<NodeProps>(), {
data: () => {},
connectable: false,
targetPosition: 'top' as Position,
})
</script>
<script lang="ts">
export default {
name: 'OutputNode',
}
</script>
<template>
<component :is="props.label.component" v-bind="props.label.props" v-if="typeof props.label !== 'string' && props.label" />
<span v-else v-html="props.label" />
<Handle
type="source"
:position="props.targetPosition"
:is-connectable="props.connectable"
:is-valid-connection="props.isValidTargetPos"
/>
</template>
+3
View File
@@ -0,0 +1,3 @@
export { default as DefaultNode } from './DefaultNode.vue'
export { default as InputNode } from './InputNode.vue'
export { default as OutputNode } from './OutputNode.vue'
@@ -0,0 +1,51 @@
<script lang="ts" setup>
import { DraggableCore, DraggableEventListener } from '@braks/revue-draggable'
import { useVueFlow } from '../../composables'
import { getRectOfNodes } from '../../utils'
const { id, store } = useVueFlow()
const selectedNodesBBox = computed(() => getRectOfNodes(store.getSelectedNodes))
const innerStyle = computed(() => ({
width: `${selectedNodesBBox.value.width}px`,
height: `${selectedNodesBBox.value.height}px`,
top: `${selectedNodesBBox.value.y}px`,
left: `${selectedNodesBBox.value.x}px`,
}))
watch(selectedNodesBBox, (v) => (store.selectedNodesBbox = v))
const onStart: DraggableEventListener = ({ event }) =>
store.hooks.selectionDragStart.trigger({ event, nodes: store.getSelectedNodes })
const onDrag: DraggableEventListener = ({ event, data: { deltaX, deltaY } }) => {
store.hooks.selectionDrag.trigger({ event, nodes: store.getSelectedNodes })
store.updateNodePosition({ diff: { x: deltaX, y: deltaY }, dragging: true })
}
const onStop: DraggableEventListener = ({ event }) => {
store.hooks.selectionDragStop.trigger({ event, nodes: store.getSelectedNodes })
store.getSelectedNodes.forEach((node) => (node.dragging = false))
}
const onContextMenu = (event: MouseEvent) => store.hooks.selectionContextMenu.trigger({ event, nodes: store.getSelectedNodes })
const transform = computed(() => `translate(${store.transform[0]}px,${store.transform[1]}px) scale(${store.transform[2]})`)
const scale = controlledComputed(
() => store.transform[2],
() => store.transform[2],
)
</script>
<script lang="ts">
export default {
name: 'NodesSelection',
}
</script>
<template>
<div class="vue-flow__nodesselection vue-flow__container" :class="store.noPanClassName" :style="{ transform }">
<DraggableCore
:grid="store.snapToGrid ? store.snapGrid : undefined"
:enable-user-select-hack="false"
:scale="scale"
@start="onStart"
@move="onDrag"
@stop="onStop"
>
<div class="vue-flow__nodesselection-rect" :style="innerStyle" @contextmenu="onContextMenu" />
</DraggableCore>
</div>
</template>
@@ -0,0 +1,25 @@
<script lang="ts" setup>
interface SelectionRectProps {
width: number
height: number
x: number
y: number
}
const props = defineProps<SelectionRectProps>()
</script>
<script lang="ts">
export default {
name: 'SelectionRect',
}
</script>
<template>
<div
class="vue-flow__selection"
:style="{
width: `${props.width}px`,
height: `${props.height}px`,
transform: `translate(${props.x}px, ${props.y}px)`,
}"
/>
</template>
@@ -0,0 +1,96 @@
<script lang="ts" setup>
import { useVueFlow } from '../../composables'
import { SelectionRect as Rect } from '../../types'
import { getConnectedEdges, getNodesInside } from '../../utils'
import SelectionRect from './SelectionRect.vue'
import { getMousePosition } from './utils'
const { store } = useVueFlow()
const el = templateRef('user-selection', null)
const prevNodes = ref(0)
const prevEdges = ref(0)
const initialRect = () => ({
width: 0,
height: 0,
startX: 0,
startY: 0,
x: 0,
y: 0,
draw: false,
})
const rect = ref<Rect>(initialRect())
const reset = () => {
rect.value = initialRect()
store.userSelectionActive = false
prevNodes.value = 0
prevEdges.value = 0
}
const onMouseDown = (event: MouseEvent) => {
const mousePos = getMousePosition(event)
if (!mousePos) return
rect.value = {
width: 0,
height: 0,
startX: mousePos.x,
startY: mousePos.y,
x: mousePos.x,
y: mousePos.y,
draw: true,
}
store.userSelectionActive = true
store.nodesSelectionActive = false
}
const onMouseMove = (event: MouseEvent) => {
if (!store.userSelectionActive) return
const mousePos = getMousePosition(event)
if (!mousePos) return
const startX = rect.value.startX
const startY = rect.value.startY
const nextUserSelectRect: Rect = {
...rect.value,
x: mousePos.x < startX ? mousePos.x : rect.value.x,
y: mousePos.y < startY ? mousePos.y : rect.value.y,
width: Math.abs(mousePos.x - startX),
height: Math.abs(mousePos.y - startY),
}
const selectedNodes = getNodesInside(store.getNodes, rect.value, store.transform)
const selectedEdges = getConnectedEdges(selectedNodes, store.getEdges)
rect.value = nextUserSelectRect
store.addSelectedNodes(selectedNodes)
store.addSelectedEdges(selectedEdges)
prevNodes.value = selectedNodes.length
prevEdges.value = selectedEdges.length
}
const onMouseUp = () => {
store.nodesSelectionActive = prevNodes.value > 0
reset()
}
const onMouseLeave = () => {
store.nodesSelectionActive = false
reset()
}
useEventListener(el, 'mousedown', onMouseDown)
useEventListener(el, 'mousemove', onMouseMove)
useEventListener(el, 'click', onMouseUp)
useEventListener(el, 'mouseup', onMouseUp)
useEventListener(el, 'mouseleave', onMouseLeave)
</script>
<script lang="ts">
export default {
name: 'UserSelection',
}
</script>
<template>
<div ref="user-selection" class="vue-flow__selectionpane vue-flow__container">
<SelectionRect v-if="rect.draw" :width="rect.width" :height="rect.height" :x="rect.x" :y="rect.y" />
</div>
</template>
@@ -0,0 +1,13 @@
import { 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,
}
}
+6
View File
@@ -0,0 +1,6 @@
export * from './Nodes'
export * from './Edges'
export * from './ConnectionLine/ConnectionLine.vue'
export * from './Handle/Handle.vue'
export * from './NodesSelection/NodesSelection.vue'
export * from './UserSelection/UserSelection.vue'