update(script-setup): Refactor remaining files to script setup style
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
<script lang="ts" setup>
|
||||
import { CSSProperties } from 'vue'
|
||||
import { ConnectionLineType, CustomConnectionLine, HandleElement, Position, RevueFlowStore, Node } from '~/types'
|
||||
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
|
||||
import { getBezierPath, getSmoothStepPath } from '~/components/Edges/utils'
|
||||
|
||||
interface ConnectionLineProps {
|
||||
sourceNode: Node
|
||||
connectionLineType?: ConnectionLineType
|
||||
connectionLineStyle?: CSSProperties
|
||||
customConnectionLine?: CustomConnectionLine
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<ConnectionLineProps>(), {
|
||||
connectionLineType: ConnectionLineType.Bezier,
|
||||
connectionLineStyle: () => ({}),
|
||||
})
|
||||
|
||||
const store = inject<RevueFlowStore>('store')!
|
||||
const hooks = inject<RevueFlowHooks>('hooks')!
|
||||
|
||||
const sourceHandle = computed(() =>
|
||||
store.connectionHandleId && store.connectionHandleType
|
||||
? props.sourceNode.__rf.handleBounds[store.connectionHandleType].find((d: HandleElement) => d.id === store.connectionHandleId)
|
||||
: store.connectionHandleType && props.sourceNode.__rf.handleBounds[store.connectionHandleType][0],
|
||||
)
|
||||
const sourceHandleX = computed(() =>
|
||||
sourceHandle.value ? sourceHandle.value.x + sourceHandle.value.width / 2 : (props.sourceNode.__rf.width as number) / 2,
|
||||
)
|
||||
const sourceHandleY = computed(() =>
|
||||
sourceHandle.value ? sourceHandle.value.y + sourceHandle.value.height / 2 : props.sourceNode.__rf.height,
|
||||
)
|
||||
const sourceX = computed(() => props.sourceNode.__rf.position.x + sourceHandleX.value)
|
||||
const sourceY = computed(() => props.sourceNode.__rf.position.y + sourceHandleY.value)
|
||||
|
||||
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 isRightOrLeft = computed(
|
||||
() => sourceHandle.value?.position === Position.Left || sourceHandle.value?.position === Position.Right,
|
||||
)
|
||||
const targetPosition = computed(() => (isRightOrLeft.value ? Position.Left : Position.Top))
|
||||
|
||||
let dAttr = computed(() => `M${sourceX.value},${sourceY.value} ${targetX.value},${targetY.value}`)
|
||||
|
||||
if (props.connectionLineType === ConnectionLineType.Bezier) {
|
||||
dAttr = computed(() =>
|
||||
getBezierPath({
|
||||
sourceX: sourceX.value,
|
||||
sourceY: sourceY.value,
|
||||
sourcePosition: sourceHandle.value?.position,
|
||||
targetX: targetX.value,
|
||||
targetY: targetY.value,
|
||||
targetPosition: targetPosition.value,
|
||||
}),
|
||||
)
|
||||
} else if (props.connectionLineType === ConnectionLineType.Step) {
|
||||
dAttr = computed(() =>
|
||||
getSmoothStepPath({
|
||||
sourceX: sourceX.value,
|
||||
sourceY: sourceY.value,
|
||||
sourcePosition: sourceHandle.value?.position,
|
||||
targetX: targetX.value,
|
||||
targetY: targetY.value,
|
||||
targetPosition: targetPosition.value,
|
||||
borderRadius: 0,
|
||||
}),
|
||||
)
|
||||
} else if (props.connectionLineType === ConnectionLineType.SmoothStep) {
|
||||
dAttr = computed(() =>
|
||||
getSmoothStepPath({
|
||||
sourceX: sourceX.value,
|
||||
sourceY: sourceY.value,
|
||||
sourcePosition: sourceHandle.value?.position,
|
||||
targetX: targetX.value,
|
||||
targetY: targetY.value,
|
||||
targetPosition: targetPosition.value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
watch(dAttr, () => console.log(dAttr.value))
|
||||
</script>
|
||||
<template>
|
||||
<g>
|
||||
<component
|
||||
:is="props.customConnectionLine"
|
||||
v-if="props.customConnectionLine"
|
||||
class="revue-flow__connection"
|
||||
v-bind="{
|
||||
sourceX: sourceX,
|
||||
sourceY: sourceY,
|
||||
sourcePosition: sourceHandle.position,
|
||||
targetX: targetX,
|
||||
targetY: targetY,
|
||||
targetPosition: targetPosition,
|
||||
connectionLineType: props.connectionLineType,
|
||||
connectionLineStyle: props.connectionLineStyle,
|
||||
}"
|
||||
/>
|
||||
<path v-else :d="dAttr" class="revue-flow__connection-path" :style="props.connectionLineStyle" />
|
||||
</g>
|
||||
</template>
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ref, defineComponent, CSSProperties, PropType, computed, inject, h } from 'vue'
|
||||
|
||||
import { getBezierPath } from '../Edges/BezierEdge'
|
||||
import { getSmoothStepPath } from '../Edges/SmoothStepEdge'
|
||||
import { getBezierPath } from '../Edges/BezierEdgeDepr'
|
||||
import { getSmoothStepPath } from '../Edges/SmoothStepEdgeDepr'
|
||||
import { Node, HandleElement, Position, ConnectionLineType, ConnectionLineComponent, RevueFlowStore } from '../../types'
|
||||
|
||||
interface ConnectionLineProps {
|
||||
@@ -15,18 +15,18 @@ export default defineComponent({
|
||||
connectionLineStyle: {
|
||||
type: Object as PropType<ConnectionLineProps['connectionLineStyle']>,
|
||||
required: false,
|
||||
default: () => {}
|
||||
default: () => {},
|
||||
},
|
||||
connectionLineType: {
|
||||
type: String as PropType<ConnectionLineProps['connectionLineType']>,
|
||||
required: false,
|
||||
default: ConnectionLineType.Bezier
|
||||
default: ConnectionLineType.Bezier,
|
||||
},
|
||||
customConnectionLine: {
|
||||
type: Object as PropType<ConnectionLineProps['customConnectionLine']>,
|
||||
required: false,
|
||||
default: undefined
|
||||
}
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const store = inject<RevueFlowStore>('store')!
|
||||
@@ -36,15 +36,15 @@ export default defineComponent({
|
||||
const sourceHandle = computed(() =>
|
||||
store.connectionHandleId && store.connectionHandleType
|
||||
? sourceNode.value?.__rf.handleBounds[store.connectionHandleType].find(
|
||||
(d: HandleElement) => d.id === store.connectionHandleId
|
||||
(d: HandleElement) => d.id === store.connectionHandleId,
|
||||
)
|
||||
: store.connectionHandleType && sourceNode.value?.__rf.handleBounds[store.connectionHandleType][0]
|
||||
: store.connectionHandleType && sourceNode.value?.__rf.handleBounds[store.connectionHandleType][0],
|
||||
)
|
||||
const sourceHandleX = computed(() =>
|
||||
sourceHandle.value ? sourceHandle.value.x + sourceHandle.value.width / 2 : (sourceNode.value?.__rf.width as number) / 2
|
||||
sourceHandle.value ? sourceHandle.value.x + sourceHandle.value.width / 2 : (sourceNode.value?.__rf.width as number) / 2,
|
||||
)
|
||||
const sourceHandleY = computed(() =>
|
||||
sourceHandle.value ? sourceHandle.value.y + sourceHandle.value.height / 2 : sourceNode.value?.__rf.height
|
||||
sourceHandle.value ? sourceHandle.value.y + sourceHandle.value.height / 2 : sourceNode.value?.__rf.height,
|
||||
)
|
||||
const sourceX = computed(() => sourceNode.value?.__rf.position.x + sourceHandleX.value)
|
||||
const sourceY = computed(() => sourceNode.value?.__rf.position.y + sourceHandleY.value)
|
||||
@@ -53,7 +53,7 @@ export default defineComponent({
|
||||
const targetY = computed(() => (store.connectionPosition.y - store.transform[1]) / store.transform[2])
|
||||
|
||||
const isRightOrLeft = computed(
|
||||
() => sourceHandle.value?.position === Position.Left || sourceHandle.value?.position === Position.Right
|
||||
() => sourceHandle.value?.position === Position.Left || sourceHandle.value?.position === Position.Right,
|
||||
)
|
||||
const targetPosition = computed(() => (isRightOrLeft.value ? Position.Left : Position.Top))
|
||||
|
||||
@@ -67,8 +67,8 @@ export default defineComponent({
|
||||
sourcePosition: sourceHandle.value?.position,
|
||||
targetX: targetX.value,
|
||||
targetY: targetY.value,
|
||||
targetPosition: targetPosition.value
|
||||
})
|
||||
targetPosition: targetPosition.value,
|
||||
}),
|
||||
)
|
||||
} else if (props.connectionLineType === ConnectionLineType.Step) {
|
||||
dAttr = computed(() =>
|
||||
@@ -79,8 +79,8 @@ export default defineComponent({
|
||||
targetX: targetX.value,
|
||||
targetY: targetY.value,
|
||||
targetPosition: targetPosition.value,
|
||||
borderRadius: 0
|
||||
})
|
||||
borderRadius: 0,
|
||||
}),
|
||||
)
|
||||
} else if (props.connectionLineType === ConnectionLineType.SmoothStep) {
|
||||
dAttr = computed(() =>
|
||||
@@ -90,14 +90,14 @@ export default defineComponent({
|
||||
sourcePosition: sourceHandle.value?.position,
|
||||
targetX: targetX.value,
|
||||
targetY: targetY.value,
|
||||
targetPosition: targetPosition.value
|
||||
})
|
||||
targetPosition: targetPosition.value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
if (props.customConnectionLine) {
|
||||
return () => (
|
||||
<g class="revue-flow__connection">
|
||||
<g className="revue-flow__connection">
|
||||
{props.customConnectionLine &&
|
||||
h(props.customConnectionLine, {
|
||||
sourceX: sourceX.value,
|
||||
@@ -107,7 +107,7 @@ export default defineComponent({
|
||||
targetY: targetY.value,
|
||||
targetPosition: targetPosition.value,
|
||||
connectionLineType: props.connectionLineType,
|
||||
connectionLineStyle: props.connectionLineStyle
|
||||
connectionLineStyle: props.connectionLineStyle,
|
||||
})}
|
||||
</g>
|
||||
)
|
||||
@@ -115,11 +115,11 @@ export default defineComponent({
|
||||
|
||||
return () =>
|
||||
nodesConnectable.value && sourceNode.value ? (
|
||||
<g class="revue-flow__connection">
|
||||
<path d={dAttr.value} class="revue-flow__connection-path" style={props.connectionLineStyle} />
|
||||
<g className="revue-flow__connection">
|
||||
<path d={dAttr.value} className="revue-flow__connection-path" style={props.connectionLineStyle} />
|
||||
</g>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import { defineComponent } from 'vue'
|
||||
import EdgeText from './EdgeText'
|
||||
import { getMarkerEnd, getCenter, GetCenterParams, DefaultEdgeProps } from './utils'
|
||||
import { ArrowHeadType, EdgeType, Position } from '../../types'
|
||||
import { reactify } from '@vueuse/core'
|
||||
|
||||
interface GetBezierPathParams {
|
||||
sourceX: number
|
||||
sourceY: number
|
||||
sourcePosition?: Position
|
||||
targetX: number
|
||||
targetY: number
|
||||
targetPosition?: Position
|
||||
centerX?: number
|
||||
centerY?: number
|
||||
}
|
||||
|
||||
export function getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition = Position.Bottom,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition = Position.Top,
|
||||
centerX,
|
||||
centerY
|
||||
}: GetBezierPathParams): string {
|
||||
const [_centerX, _centerY] = getCenter({ sourceX, sourceY, targetX, targetY })
|
||||
const leftAndRight = [Position.Left, Position.Right]
|
||||
|
||||
const cX = typeof centerX !== 'undefined' ? centerX : _centerX
|
||||
const cY = typeof centerY !== 'undefined' ? centerY : _centerY
|
||||
|
||||
let path = `M${sourceX},${sourceY} C${sourceX},${cY} ${targetX},${cY} ${targetX},${targetY}`
|
||||
|
||||
if (leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
|
||||
path = `M${sourceX},${sourceY} C${cX},${sourceY} ${cX},${targetY} ${targetX},${targetY}`
|
||||
} else if (leftAndRight.includes(targetPosition)) {
|
||||
path = `M${sourceX},${sourceY} C${sourceX},${targetY} ${sourceX},${targetY} ${targetX},${targetY}`
|
||||
} else if (leftAndRight.includes(sourcePosition)) {
|
||||
path = `M${sourceX},${sourceY} C${targetX},${sourceY} ${targetX},${sourceY} ${targetX},${targetY}`
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
...DefaultEdgeProps
|
||||
},
|
||||
setup(props) {
|
||||
const centered = reactify(({ sourceX, sourceY, targetX, targetY, targetPosition, sourcePosition }: GetCenterParams) => {
|
||||
return getCenter({
|
||||
sourceX: sourceX,
|
||||
sourceY: sourceY,
|
||||
targetX: targetX,
|
||||
targetY: targetY,
|
||||
sourcePosition: sourcePosition,
|
||||
targetPosition: targetPosition
|
||||
})
|
||||
})
|
||||
const path = reactify(({ sourceX, sourceY, targetX, targetY, targetPosition, sourcePosition }: GetBezierPathParams) => {
|
||||
return getBezierPath({
|
||||
sourceX: sourceX,
|
||||
sourceY: sourceY,
|
||||
targetX: targetX,
|
||||
targetY: targetY,
|
||||
targetPosition: targetPosition,
|
||||
sourcePosition: sourcePosition
|
||||
})
|
||||
})
|
||||
|
||||
const markerEnd = reactify((arrowHeadType?: ArrowHeadType, markerEndId?: string) => getMarkerEnd(arrowHeadType, markerEndId))
|
||||
|
||||
return () => (
|
||||
<>
|
||||
<path
|
||||
class="revue-flow__edge-path"
|
||||
style={props.style}
|
||||
d={path({ ...props }).value}
|
||||
marker-end={markerEnd(props.arrowHeadType, props.markerEndId).value}
|
||||
/>
|
||||
{props.label ? (
|
||||
<EdgeText
|
||||
x={centered({ ...props }).value[0]}
|
||||
y={centered({ ...props }).value[1]}
|
||||
label={props.label}
|
||||
labelStyle={props.labelStyle}
|
||||
labelShowBg={props.labelShowBg}
|
||||
labelBgStyle={props.labelBgStyle}
|
||||
labelBgPadding={props.labelBgPadding}
|
||||
labelBgBorderRadius={props.labelBgBorderRadius}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}) as EdgeType
|
||||
@@ -0,0 +1,67 @@
|
||||
<script lang="ts" setup>
|
||||
import { CSSProperties, VNode } from 'vue'
|
||||
import EdgeText from './EdgeText.vue'
|
||||
import { ArrowHeadType, ElementId, Position } from '~/types'
|
||||
import { getCenter, getMarkerEnd, getBezierPath } from '~/components/Edges/utils'
|
||||
|
||||
interface BezierEdgeProps<T = any> {
|
||||
id: ElementId
|
||||
source: ElementId
|
||||
target: ElementId
|
||||
sourceX: number
|
||||
sourceY: number
|
||||
targetX: number
|
||||
targetY: number
|
||||
selected?: boolean
|
||||
animated?: boolean
|
||||
sourcePosition?: Position
|
||||
targetPosition?: Position
|
||||
label?: string | VNode
|
||||
labelStyle?: any
|
||||
labelShowBg?: boolean
|
||||
labelBgStyle?: any
|
||||
labelBgPadding?: [number, number]
|
||||
labelBgBorderRadius?: number
|
||||
style?: CSSProperties
|
||||
arrowHeadType?: ArrowHeadType
|
||||
markerEndId?: string
|
||||
data?: T
|
||||
sourceHandleId?: ElementId
|
||||
targetHandleId?: ElementId
|
||||
}
|
||||
const props = withDefaults(defineProps<BezierEdgeProps>(), {
|
||||
selected: false,
|
||||
sourcePosition: Position.Bottom,
|
||||
targetPosition: Position.Top,
|
||||
labelStyle: () => ({}),
|
||||
labelShowBg: true,
|
||||
labelBgStyle: () => ({}),
|
||||
})
|
||||
|
||||
const centered = computed(() =>
|
||||
getCenter({
|
||||
...props,
|
||||
}),
|
||||
)
|
||||
const path = computed(() =>
|
||||
getBezierPath({
|
||||
...props,
|
||||
}),
|
||||
)
|
||||
|
||||
const markerEnd = computed(() => getMarkerEnd(props.arrowHeadType, props.markerEndId))
|
||||
</script>
|
||||
<template>
|
||||
<path class="revue-flow__edge-path" :style="props.style" :d="path" :marker-end="markerEnd" />
|
||||
<EdgeText
|
||||
v-if="props.label"
|
||||
:x="centered[0]"
|
||||
: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"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,248 +0,0 @@
|
||||
import { computed, defineComponent, h, inject, PropType, ref, watchPostEffect } from 'vue'
|
||||
import EdgeAnchor from './EdgeAnchor'
|
||||
import { ConnectionMode, Edge, Elements, Node, Position, RevueFlowStore, Transform } from '../../types'
|
||||
import { RevueFlowHooks } from '../../hooks/RevueFlowHooks'
|
||||
import { getEdgePositions, getHandle, getSourceTargetNodes, isEdgeVisible } from '../../container/EdgeRenderer/utils'
|
||||
import { isEdge } from '../../utils/graph'
|
||||
import { onMouseDown } from '../Handle/handler'
|
||||
import { controlledRef } from '@vueuse/core'
|
||||
|
||||
interface EdgeProps {
|
||||
edge: Edge
|
||||
nodes: Node[]
|
||||
selectedElements: Elements | null
|
||||
elementsSelectable: boolean
|
||||
transform: Transform
|
||||
width: number
|
||||
height: number
|
||||
onlyRenderVisibleElements: boolean
|
||||
connectionMode?: ConnectionMode
|
||||
markerEndId?: string
|
||||
edgeUpdaterRadius?: number
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
components: { EdgeAnchor },
|
||||
props: {
|
||||
edge: {
|
||||
type: Object as PropType<EdgeProps['edge']>,
|
||||
required: true
|
||||
},
|
||||
markerEndId: {
|
||||
type: Object as PropType<EdgeProps['markerEndId']>,
|
||||
required: false,
|
||||
default: undefined
|
||||
},
|
||||
edgeUpdaterRadius: {
|
||||
type: Number as PropType<EdgeProps['edgeUpdaterRadius']>,
|
||||
required: false,
|
||||
default: undefined
|
||||
},
|
||||
type: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
onlyRenderVisibleElements: {
|
||||
type: Boolean as PropType<EdgeProps['onlyRenderVisibleElements']>,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
connectionMode: {
|
||||
type: String as PropType<EdgeProps['connectionMode']>,
|
||||
required: false,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const store = inject<RevueFlowStore>('store')!
|
||||
const hooks = inject<RevueFlowHooks>('hooks')!
|
||||
const nodes = controlledRef(getSourceTargetNodes(props.edge, store.nodes), {
|
||||
onBeforeChange: (value, oldValue) => {
|
||||
if (JSON.stringify(value) === JSON.stringify(oldValue)) return false
|
||||
}
|
||||
})
|
||||
|
||||
watchPostEffect(() => {
|
||||
nodes.value = getSourceTargetNodes(props.edge, store.nodes)
|
||||
})
|
||||
|
||||
hooks.connect.on((connection) => {
|
||||
hooks.edgeUpdate.trigger({ edge: props.edge, connection })
|
||||
})
|
||||
|
||||
if (!nodes.value.sourceNode) {
|
||||
console.warn(`couldn't create edge for source id: ${props.edge.source}; edge id: ${props.edge.id}`)
|
||||
}
|
||||
|
||||
if (!nodes.value.targetNode) {
|
||||
console.warn(`couldn't create edge for target id: ${props.edge.target}; edge id: ${props.edge.id}`)
|
||||
}
|
||||
|
||||
// when connection type is loose we can define all handles as sources
|
||||
const targetNodeHandles =
|
||||
props.connectionMode === ConnectionMode.Strict
|
||||
? nodes.value.targetNode?.__rf.handleBounds.target
|
||||
: nodes.value.targetNode?.__rf.handleBounds.target || nodes.value.targetNode?.__rf.handleBounds.source
|
||||
|
||||
const sourceHandle =
|
||||
nodes.value.sourceNode && getHandle(nodes.value.sourceNode.__rf.handleBounds.source, props.edge.sourceHandle || null)
|
||||
const targetHandle = getHandle(targetNodeHandles, props.edge.targetHandle || null)
|
||||
const sourcePosition = sourceHandle ? sourceHandle.position : Position.Bottom
|
||||
const targetPosition = targetHandle ? targetHandle.position : Position.Top
|
||||
|
||||
const isVisible = ({ sourceX, sourceY, targetX, targetY }: Record<string, number>) => {
|
||||
return props.onlyRenderVisibleElements
|
||||
? isEdgeVisible({
|
||||
sourcePos: { x: sourceX, y: sourceY },
|
||||
targetPos: { x: targetX, y: targetY },
|
||||
width: store.width || 0,
|
||||
height: store.height || 0,
|
||||
transform: store.transform
|
||||
})
|
||||
: true
|
||||
}
|
||||
|
||||
const isSelected = computed(() => store.selectedElements?.some((elm) => isEdge(elm) && elm.id === props.edge.id) || false)
|
||||
|
||||
const edgeElement = () => {
|
||||
const el: Edge = {
|
||||
id: props.edge.id || '',
|
||||
source: props.edge.source,
|
||||
target: props.edge.target,
|
||||
type: props.edge.type
|
||||
}
|
||||
|
||||
if (props.edge.sourceHandle) {
|
||||
el.sourceHandle = props.edge.sourceHandle
|
||||
}
|
||||
|
||||
if (props.edge.targetHandle) {
|
||||
el.targetHandle = props.edge.targetHandle
|
||||
}
|
||||
|
||||
if (typeof props.edge.data !== 'undefined') {
|
||||
el.data = props.edge.data
|
||||
}
|
||||
|
||||
return el
|
||||
}
|
||||
|
||||
const onEdgeClick = (event: MouseEvent) => {
|
||||
if (store.elementsSelectable) {
|
||||
store.unsetNodesSelection()
|
||||
store.addSelectedElements(edgeElement() as any)
|
||||
}
|
||||
|
||||
hooks.edgeClick.trigger({ event, edge: edgeElement() })
|
||||
}
|
||||
|
||||
const onEdgeContextMenu = (event: MouseEvent) =>
|
||||
hooks.edgeContextMenu.trigger({
|
||||
event,
|
||||
edge: edgeElement()
|
||||
})
|
||||
|
||||
const onEdgeMouseEnter = (event: MouseEvent) => hooks.edgeMouseEnter.trigger({ event, edge: edgeElement() })
|
||||
|
||||
const onEdgeMouseMove = (event: MouseEvent) => hooks.edgeMouseMove.trigger({ event, edge: edgeElement() })
|
||||
|
||||
const onEdgeMouseLeave = (event: MouseEvent) => hooks.edgeMouseLeave.trigger({ event, edge: edgeElement() })
|
||||
|
||||
const handleEdgeUpdater = (event: MouseEvent, isSourceHandle: boolean) => {
|
||||
const nodeId = isSourceHandle ? props.edge.target : props.edge.source
|
||||
const handleId = isSourceHandle ? props.edge.targetHandle : props.edge.sourceHandle
|
||||
const isValidConnection = () => true
|
||||
const isTarget = isSourceHandle
|
||||
|
||||
hooks.edgeUpdateStart.trigger({ event, edge: edgeElement() })
|
||||
onMouseDown(
|
||||
event,
|
||||
store,
|
||||
hooks,
|
||||
handleId as string,
|
||||
nodeId,
|
||||
isTarget,
|
||||
isValidConnection,
|
||||
isSourceHandle ? 'target' : 'source'
|
||||
)
|
||||
}
|
||||
|
||||
const onEdgeUpdaterSourceMouseDown = (event: MouseEvent) => {
|
||||
handleEdgeUpdater(event, true)
|
||||
}
|
||||
|
||||
const onEdgeUpdaterTargetMouseDown = (event: MouseEvent) => {
|
||||
handleEdgeUpdater(event, false)
|
||||
}
|
||||
|
||||
const updating = ref<boolean>(false)
|
||||
const onEdgeUpdaterMouseEnter = () => (updating.value = true)
|
||||
const onEdgeUpdaterMouseOut = () => (updating.value = false)
|
||||
|
||||
return () => {
|
||||
const { targetX, targetY, sourceX, sourceY } = getEdgePositions(
|
||||
nodes.value.sourceNode as Node,
|
||||
sourceHandle,
|
||||
sourcePosition,
|
||||
nodes.value.targetNode as Node,
|
||||
targetHandle,
|
||||
targetPosition
|
||||
)
|
||||
return !props.edge.isHidden && isVisible({ targetX, targetY, sourceX, sourceY }) ? (
|
||||
<g
|
||||
class={[
|
||||
'revue-flow__edge',
|
||||
`revue-flow__edge-${props.type}`,
|
||||
{
|
||||
selected: isSelected.value,
|
||||
animated: props.edge.animated,
|
||||
inactive: !store.elementsSelectable,
|
||||
updating: updating.value
|
||||
}
|
||||
]}
|
||||
onClick={onEdgeClick}
|
||||
onContextmenu={onEdgeContextMenu}
|
||||
onMouseenter={onEdgeMouseEnter}
|
||||
onMousemove={onEdgeMouseMove}
|
||||
onMouseleave={onEdgeMouseLeave}
|
||||
>
|
||||
{h(
|
||||
props.type,
|
||||
{
|
||||
id: props.edge.id,
|
||||
source: props.edge.source,
|
||||
target: props.edge.target,
|
||||
selected: isSelected.value,
|
||||
animated: props.edge.animated,
|
||||
label: props.edge.label,
|
||||
labelStyle: props.edge.labelStyle,
|
||||
labelShowBg: props.edge.labelShowBg,
|
||||
labelBgStyle: props.edge.labelBgStyle,
|
||||
labelBgPadding: props.edge.labelBgPadding,
|
||||
labelBgBorderRadius: props.edge.labelBgBorderRadius,
|
||||
data: props.edge.data,
|
||||
style: props.edge.style,
|
||||
arrowHeadType: props.edge.arrowHeadType,
|
||||
sourceX: sourceX,
|
||||
sourceY: sourceY,
|
||||
targetX: targetX,
|
||||
targetY: targetY,
|
||||
markerEndId: props.markerEndId,
|
||||
sourceHandleId: props.edge.sourceHandle,
|
||||
targetHandleId: props.edge.targetHandle
|
||||
},
|
||||
{}
|
||||
)}
|
||||
<g onMousedown={onEdgeUpdaterSourceMouseDown} onMouseenter={onEdgeUpdaterMouseEnter} onMouseout={onEdgeUpdaterMouseOut}>
|
||||
<EdgeAnchor position={sourcePosition} centerX={sourceX} centerY={sourceY} radius={props.edgeUpdaterRadius} />
|
||||
</g>
|
||||
<g onMousedown={onEdgeUpdaterTargetMouseDown} onMouseenter={onEdgeUpdaterMouseEnter} onMouseout={onEdgeUpdaterMouseOut}>
|
||||
<EdgeAnchor position={sourcePosition} centerX={sourceX} centerY={sourceY} radius={props.edgeUpdaterRadius} />
|
||||
</g>
|
||||
</g>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,210 @@
|
||||
<script lang="ts" setup>
|
||||
import EdgeAnchor from './EdgeAnchor.vue'
|
||||
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
|
||||
import { getEdgePositions, getHandle, getSourceTargetNodes, isEdgeVisible } from '~/container/EdgeRenderer/utils'
|
||||
import { isEdge } from '~/utils/graph'
|
||||
import { ConnectionMode, Dimensions, Edge, EdgeType, Elements, Position, RevueFlowStore, Transform } from '~/types'
|
||||
import { onMouseDown } from '~/components/Handle/handler'
|
||||
|
||||
interface EdgeProps {
|
||||
type: EdgeType
|
||||
edge: Edge
|
||||
nodes: ReturnType<typeof getSourceTargetNodes>
|
||||
dimensions: Dimensions
|
||||
transform: Transform
|
||||
selectedElements?: Elements | null
|
||||
elementsSelectable?: boolean
|
||||
onlyRenderVisibleElements?: boolean
|
||||
connectionMode?: ConnectionMode
|
||||
markerEndId?: string
|
||||
edgeUpdaterRadius?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<EdgeProps>(), {
|
||||
elementsSelectable: true,
|
||||
onlyRenderVisibleElements: false,
|
||||
selectedElements: () => null,
|
||||
})
|
||||
|
||||
const store = inject<RevueFlowStore>('store')!
|
||||
const hooks = inject<RevueFlowHooks>('hooks')!
|
||||
|
||||
hooks.connect.on((connection) => {
|
||||
hooks.edgeUpdate.trigger({ edge: props.edge, connection })
|
||||
})
|
||||
|
||||
if (!props.nodes.value.sourceNode) {
|
||||
console.warn(`couldn't create edge for source id: ${props.edge.source}; edge id: ${props.edge.id}`)
|
||||
}
|
||||
|
||||
if (!props.nodes.value.targetNode) {
|
||||
console.warn(`couldn't create edge for target id: ${props.edge.target}; edge id: ${props.edge.id}`)
|
||||
}
|
||||
|
||||
// when connection type is loose we can define all handles as sources
|
||||
const targetNodeHandles =
|
||||
props.connectionMode === ConnectionMode.Strict
|
||||
? props.nodes.value.targetNode?.__rf.handleBounds.target
|
||||
: props.nodes.value.targetNode?.__rf.handleBounds.target || props.nodes.value.targetNode?.__rf.handleBounds.source
|
||||
|
||||
const sourceHandle =
|
||||
props.nodes.value.sourceNode &&
|
||||
getHandle(props.nodes.value.sourceNode.__rf.handleBounds.source, props.edge.sourceHandle || null)
|
||||
const targetHandle = getHandle(targetNodeHandles, props.edge.targetHandle || null)
|
||||
const sourcePosition = sourceHandle ? sourceHandle.position : Position.Bottom
|
||||
const targetPosition = targetHandle ? targetHandle.position : Position.Top
|
||||
|
||||
const isSelected = computed(() => props.selectedElements?.some((elm) => isEdge(elm) && elm.id === props.edge.id) || false)
|
||||
|
||||
const edgeElement = computed(() => {
|
||||
const el: Edge = {
|
||||
id: props.edge.id || '',
|
||||
source: props.edge.source,
|
||||
target: props.edge.target,
|
||||
type: props.edge.type,
|
||||
}
|
||||
|
||||
if (props.edge.sourceHandle) {
|
||||
el.sourceHandle = props.edge.sourceHandle
|
||||
}
|
||||
|
||||
if (props.edge.targetHandle) {
|
||||
el.targetHandle = props.edge.targetHandle
|
||||
}
|
||||
|
||||
if (typeof props.edge.data !== 'undefined') {
|
||||
el.data = props.edge.data
|
||||
}
|
||||
|
||||
return el
|
||||
})
|
||||
|
||||
const onEdgeClick = (event: MouseEvent) => {
|
||||
if (props.elementsSelectable) {
|
||||
store.unsetNodesSelection()
|
||||
store.addSelectedElements([edgeElement.value])
|
||||
}
|
||||
|
||||
hooks.edgeClick.trigger({ event, edge: edgeElement.value })
|
||||
}
|
||||
|
||||
const onEdgeContextMenu = (event: MouseEvent) =>
|
||||
hooks.edgeContextMenu.trigger({
|
||||
event,
|
||||
edge: edgeElement.value,
|
||||
})
|
||||
|
||||
const onEdgeMouseEnter = (event: MouseEvent) => hooks.edgeMouseEnter.trigger({ event, edge: edgeElement.value })
|
||||
|
||||
const onEdgeMouseMove = (event: MouseEvent) => hooks.edgeMouseMove.trigger({ event, edge: edgeElement.value })
|
||||
|
||||
const onEdgeMouseLeave = (event: MouseEvent) => hooks.edgeMouseLeave.trigger({ event, edge: edgeElement.value })
|
||||
|
||||
const handleEdgeUpdater = (event: MouseEvent, isSourceHandle: boolean) => {
|
||||
const nodeId = isSourceHandle ? props.edge.target : props.edge.source
|
||||
const handleId = isSourceHandle ? props.edge.targetHandle : props.edge.sourceHandle
|
||||
const isValidConnection = () => true
|
||||
const isTarget = isSourceHandle
|
||||
|
||||
hooks.edgeUpdateStart.trigger({ event, edge: edgeElement.value })
|
||||
handleId &&
|
||||
onMouseDown(event, store, hooks, handleId, nodeId, isTarget, isValidConnection, isSourceHandle ? 'target' : 'source')
|
||||
}
|
||||
|
||||
const onEdgeUpdaterSourceMouseDown = (event: MouseEvent) => {
|
||||
handleEdgeUpdater(event, true)
|
||||
}
|
||||
|
||||
const onEdgeUpdaterTargetMouseDown = (event: MouseEvent) => {
|
||||
handleEdgeUpdater(event, false)
|
||||
}
|
||||
|
||||
const updating = ref<boolean>(false)
|
||||
const onEdgeUpdaterMouseEnter = () => (updating.value = true)
|
||||
const onEdgeUpdaterMouseOut = () => (updating.value = false)
|
||||
const edgePos = computed(() =>
|
||||
getEdgePositions(
|
||||
props.nodes.value.sourceNode,
|
||||
sourceHandle,
|
||||
sourcePosition,
|
||||
props.nodes.value.targetNode,
|
||||
targetHandle,
|
||||
targetPosition,
|
||||
),
|
||||
)
|
||||
const isVisible = ({ sourceX, sourceY, targetX, targetY }: ReturnType<typeof getEdgePositions>) => {
|
||||
return props.onlyRenderVisibleElements
|
||||
? isEdgeVisible({
|
||||
sourcePos: { x: sourceX, y: sourceY },
|
||||
targetPos: { x: targetX, y: targetY },
|
||||
width: props.dimensions.width,
|
||||
height: props.dimensions.height,
|
||||
transform: props.transform,
|
||||
})
|
||||
: true
|
||||
}
|
||||
const visible = computed(() => !props.edge.isHidden && isVisible(edgePos.value))
|
||||
</script>
|
||||
<template>
|
||||
<g
|
||||
v-if="visible"
|
||||
:class="[
|
||||
'revue-flow__edge',
|
||||
`revue-flow__edge-${props.type.name || 'default'}`,
|
||||
{
|
||||
selected: isSelected.value,
|
||||
animated: props.edge.animated,
|
||||
inactive: !store.elementsSelectable,
|
||||
updating: updating.value,
|
||||
},
|
||||
]"
|
||||
@click="onEdgeClick"
|
||||
@contextmenu="onEdgeContextMenu"
|
||||
@mouseenter="onEdgeMouseEnter"
|
||||
@mousemove="onEdgeMouseMove"
|
||||
@mouseleave="onEdgeMouseLeave"
|
||||
>
|
||||
<component
|
||||
:is="props.type"
|
||||
v-bind="{
|
||||
id: props.edge.id,
|
||||
source: props.edge.source,
|
||||
target: props.edge.target,
|
||||
selected: isSelected.value,
|
||||
animated: props.edge.animated,
|
||||
label: props.edge.label,
|
||||
labelStyle: props.edge.labelStyle,
|
||||
labelShowBg: props.edge.labelShowBg,
|
||||
labelBgStyle: props.edge.labelBgStyle,
|
||||
labelBgPadding: props.edge.labelBgPadding,
|
||||
labelBgBorderRadius: props.edge.labelBgBorderRadius,
|
||||
data: props.edge.data,
|
||||
style: props.edge.style,
|
||||
arrowHeadType: props.edge.arrowHeadType,
|
||||
sourceX: edgePos.sourceX,
|
||||
sourceY: edgePos.sourceY,
|
||||
targetX: edgePos.targetX,
|
||||
targetY: edgePos.targetY,
|
||||
markerEndId: props.markerEndId,
|
||||
sourceHandleId: props.edge.sourceHandle,
|
||||
targetHandleId: props.edge.targetHandle,
|
||||
}"
|
||||
/>
|
||||
<g @mousedown="onEdgeUpdaterSourceMouseDown" @mouseenter="onEdgeUpdaterMouseEnter" @mouseout="onEdgeUpdaterMouseOut">
|
||||
<EdgeAnchor
|
||||
:position="sourcePosition"
|
||||
:center-x="edgePos.sourceX"
|
||||
:center-y="edgePos.sourceY"
|
||||
:radius="props.edgeUpdaterRadius"
|
||||
/>
|
||||
</g>
|
||||
<g @mousedown="onEdgeUpdaterTargetMouseDown" @mouseenter="onEdgeUpdaterMouseEnter" @mouseout="onEdgeUpdaterMouseOut">
|
||||
<EdgeAnchor
|
||||
:position="sourcePosition"
|
||||
:center-x="edgePos.sourceX"
|
||||
:center-y="edgePos.sourceY"
|
||||
:radius="props.edgeUpdaterRadius"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</template>
|
||||
@@ -1,55 +0,0 @@
|
||||
import { Position } from '../../types'
|
||||
import { defineComponent, HTMLAttributes, PropType } from 'vue'
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
export interface EdgeAnchorProps extends HTMLAttributes {
|
||||
position: Position
|
||||
centerX: number
|
||||
centerY: number
|
||||
radius?: number
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
position: {
|
||||
type: String as PropType<EdgeAnchorProps['position']>,
|
||||
required: true
|
||||
},
|
||||
centerX: {
|
||||
type: Number as PropType<EdgeAnchorProps['centerX']>,
|
||||
required: true
|
||||
},
|
||||
centerY: {
|
||||
type: Number as PropType<EdgeAnchorProps['centerY']>,
|
||||
required: true
|
||||
},
|
||||
radius: {
|
||||
type: Number as PropType<EdgeAnchorProps['radius']>,
|
||||
required: false,
|
||||
default: 10
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
return () => (
|
||||
<circle
|
||||
class="revue-flow__edgeupdater"
|
||||
cx={shiftX(props.centerX, props.radius || 10, props.position)}
|
||||
cy={shiftY(props.centerY, props.radius || 10, props.position)}
|
||||
r={props.radius || 10}
|
||||
stroke="transparent"
|
||||
fill="transparent"
|
||||
/>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,37 @@
|
||||
<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,
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<circle
|
||||
class="revue-flow__edgeupdater"
|
||||
:cx="shiftX(props.centerX, props.radius, props.position)"
|
||||
:cy="shiftY(props.centerY, props.radius, props.position)"
|
||||
:r="props.radius"
|
||||
stroke="transparent"
|
||||
fill="transparent"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,80 +0,0 @@
|
||||
import { EdgeTextProps, Rect } from '../../types'
|
||||
import { defineComponent, PropType, ref, watchEffect } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
x: {
|
||||
type: Number as PropType<EdgeTextProps['x']>,
|
||||
required: true
|
||||
},
|
||||
y: {
|
||||
type: Number as PropType<EdgeTextProps['y']>,
|
||||
required: true
|
||||
},
|
||||
label: {
|
||||
type: [String, Object] as PropType<EdgeTextProps['label']>,
|
||||
required: true
|
||||
},
|
||||
labelStyle: {
|
||||
type: Object as PropType<EdgeTextProps['labelStyle']>,
|
||||
default: () => ({})
|
||||
},
|
||||
labelShowBg: {
|
||||
type: Boolean as PropType<EdgeTextProps['labelShowBg']>,
|
||||
default: true
|
||||
},
|
||||
labelBgStyle: {
|
||||
type: Object as PropType<EdgeTextProps['labelBgStyle']>,
|
||||
default: () => ({})
|
||||
},
|
||||
labelBgPadding: {
|
||||
type: Array as unknown as PropType<[number, number]>,
|
||||
default: () => [2, 4]
|
||||
},
|
||||
labelBgBorderRadius: {
|
||||
type: Number as PropType<EdgeTextProps['labelBgBorderRadius']>,
|
||||
default: 2
|
||||
}
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
const edgeRef = ref<SVGTextElement | null>(null)
|
||||
const edgeTextBox = ref<Rect>({ x: 0, y: 0, width: 0, height: 0 })
|
||||
|
||||
watchEffect(() => {
|
||||
const textBbox = edgeRef.value?.getBBox()
|
||||
|
||||
if (textBbox) {
|
||||
edgeTextBox.value = {
|
||||
x: textBbox.x,
|
||||
y: textBbox.y,
|
||||
width: textBbox.width,
|
||||
height: textBbox.height
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return () => (
|
||||
<g
|
||||
transform={`translate(${props.x - edgeTextBox.value.width / 2} ${props.y - edgeTextBox.value.height / 2})`}
|
||||
class="revue-flow__edge-textwrapper"
|
||||
>
|
||||
{props.labelShowBg && (
|
||||
<rect
|
||||
width={edgeTextBox.value.width + 2 * props.labelBgPadding[0] + 'px'}
|
||||
height={edgeTextBox.value.height + 2 * props.labelBgPadding[1] + 'px'}
|
||||
x={-props.labelBgPadding[0]}
|
||||
y={-props.labelBgPadding[1]}
|
||||
class="revue-flow__edge-textbg"
|
||||
style={props.labelBgStyle}
|
||||
rx={props.labelBgBorderRadius}
|
||||
ry={props.labelBgBorderRadius}
|
||||
/>
|
||||
)}
|
||||
<text ref={edgeRef} class="revue-flow__edge-text" y={edgeTextBox.value.height / 2} dy="0.3em" style={props.labelStyle}>
|
||||
{props.label}
|
||||
</text>
|
||||
{slots.default ? slots.default() : ''}
|
||||
</g>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
<script lang="ts" setup>
|
||||
import { HTMLAttributes, ref, VNode, watchEffect } from 'vue'
|
||||
import { Rect } from '~/types'
|
||||
|
||||
interface EdgeTextProps extends HTMLAttributes {
|
||||
x: number
|
||||
y: number
|
||||
label?: string | VNode
|
||||
labelStyle?: any
|
||||
labelShowBg?: boolean
|
||||
labelBgStyle?: any
|
||||
labelBgPadding?: [number, number]
|
||||
labelBgBorderRadius?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<EdgeTextProps>(), {
|
||||
labelStyle: () => ({}),
|
||||
labelShowBg: true,
|
||||
labelBgStyle: () => ({}),
|
||||
labelBgPadding: () => [2, 4],
|
||||
labelBgBorderRadius: 2,
|
||||
})
|
||||
|
||||
const edgeRef = templateRef<SVGTextElement>('edge-text', null)
|
||||
const edgeTextBox = ref<Rect>({ x: 0, y: 0, width: 0, height: 0 })
|
||||
|
||||
watchEffect(() => {
|
||||
const textBbox = edgeRef.value?.getBBox()
|
||||
|
||||
if (textBbox) {
|
||||
edgeTextBox.value = {
|
||||
x: textBbox.x,
|
||||
y: textBbox.y,
|
||||
width: textBbox.width,
|
||||
height: textBbox.height,
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<g
|
||||
:transform="`translate(${props.x - edgeTextBox.width / 2} ${props.y - edgeTextBox.height / 2})`"
|
||||
class="revue-flow__edge-textwrapper"
|
||||
>
|
||||
<rect
|
||||
v-if="props.labelShowBg"
|
||||
:width="edgeTextBox.width + 2 * props.labelBgPadding[0] + 'px'"
|
||||
:height="edgeTextBox.height + 2 * props.labelBgPadding[1] + 'px'"
|
||||
:x="-props.labelBgPadding[0]"
|
||||
:y="-props.labelBgPadding[1]"
|
||||
class="revue-flow__edge-textbg"
|
||||
:style="props.labelBgStyle"
|
||||
:rx="props.labelBgBorderRadius"
|
||||
:ry="props.labelBgBorderRadius"
|
||||
/>
|
||||
<text ref="edge-text" class="revue-flow__edge-text" :y="edgeTextBox.height / 2" dy="0.3em" :style="props.labelStyle">
|
||||
{{ props.label }}
|
||||
</text>
|
||||
<slot></slot>
|
||||
</g>
|
||||
</template>
|
||||
@@ -1,147 +0,0 @@
|
||||
import { defineComponent } from 'vue'
|
||||
import EdgeText from './EdgeText'
|
||||
import { getMarkerEnd, getCenter, EdgeSmoothProps, GetCenterParams } from './utils'
|
||||
import { ArrowHeadType, EdgeType, Position } from '../../types'
|
||||
import { reactify } from '@vueuse/core'
|
||||
import { getBezierPath } from './BezierEdge'
|
||||
|
||||
// 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}`
|
||||
|
||||
interface GetSmoothStepPathParams {
|
||||
sourceX: number
|
||||
sourceY: number
|
||||
sourcePosition?: Position
|
||||
targetX: number
|
||||
targetY: number
|
||||
targetPosition?: Position
|
||||
borderRadius?: number
|
||||
centerX?: number
|
||||
centerY?: number
|
||||
}
|
||||
|
||||
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) {
|
||||
// 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}`
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
...EdgeSmoothProps
|
||||
},
|
||||
setup(props) {
|
||||
const centered = reactify(({ sourceX, sourceY, targetX, targetY, targetPosition, sourcePosition }: GetCenterParams) => {
|
||||
return getCenter({
|
||||
sourceX: sourceX,
|
||||
sourceY: sourceY,
|
||||
targetX: targetX,
|
||||
targetY: targetY,
|
||||
sourcePosition: sourcePosition,
|
||||
targetPosition: targetPosition
|
||||
})
|
||||
})
|
||||
const path = reactify(({ sourceX, sourceY, targetX, targetY, targetPosition, sourcePosition }: GetSmoothStepPathParams) => {
|
||||
return getBezierPath({
|
||||
sourceX: sourceX,
|
||||
sourceY: sourceY,
|
||||
targetX: targetX,
|
||||
targetY: targetY,
|
||||
targetPosition: targetPosition,
|
||||
sourcePosition: sourcePosition
|
||||
})
|
||||
})
|
||||
|
||||
const markerEnd = reactify((arrowHeadType?: ArrowHeadType, markerEndId?: string) => getMarkerEnd(arrowHeadType, markerEndId))
|
||||
|
||||
return () => (
|
||||
<>
|
||||
<path
|
||||
class="revue-flow__edge-path"
|
||||
style={props.style}
|
||||
d={path({ ...props }).value}
|
||||
marker-end={markerEnd(props.arrowHeadType, props.markerEndId).value}
|
||||
/>
|
||||
{props.label ? (
|
||||
<EdgeText
|
||||
x={centered({ ...props }).value[0]}
|
||||
y={centered({ ...props }).value[1]}
|
||||
label={props.label}
|
||||
labelStyle={props.labelStyle}
|
||||
labelShowBg={props.labelShowBg}
|
||||
labelBgStyle={props.labelBgStyle}
|
||||
labelBgPadding={props.labelBgPadding}
|
||||
labelBgBorderRadius={props.labelBgBorderRadius}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}) as unknown as EdgeType
|
||||
@@ -0,0 +1,70 @@
|
||||
<script lang="ts" setup>
|
||||
import { VNode } from 'vue'
|
||||
import EdgeText from './EdgeText.vue'
|
||||
import { ArrowHeadType, ElementId, Position } from '~/types'
|
||||
import { getCenter, getMarkerEnd, getSmoothStepPath } from '~/components/Edges/utils'
|
||||
|
||||
export interface EdgeSmoothStepProps<T = any> {
|
||||
id: ElementId
|
||||
source: ElementId
|
||||
target: ElementId
|
||||
sourceX: number
|
||||
sourceY: number
|
||||
targetX: number
|
||||
targetY: number
|
||||
selected?: boolean
|
||||
animated?: boolean
|
||||
sourcePosition?: Position
|
||||
targetPosition?: Position
|
||||
label?: string | VNode
|
||||
labelStyle?: any
|
||||
labelShowBg?: boolean
|
||||
labelBgStyle?: any
|
||||
labelBgPadding?: [number, number]
|
||||
labelBgBorderRadius?: number
|
||||
style?: any
|
||||
arrowHeadType?: ArrowHeadType
|
||||
markerEndId?: string
|
||||
data?: T
|
||||
borderRadius?: number
|
||||
sourceHandleId?: ElementId | null
|
||||
targetHandleId?: ElementId | null
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<EdgeSmoothStepProps>(), {
|
||||
selected: false,
|
||||
sourcePosition: Position.Bottom,
|
||||
targetPosition: Position.Top,
|
||||
label: () => '',
|
||||
labelStyle: () => ({}),
|
||||
labelShowBg: true,
|
||||
labelBgStyle: () => ({}),
|
||||
})
|
||||
|
||||
const centered = computed(() =>
|
||||
getCenter({
|
||||
...props,
|
||||
}),
|
||||
)
|
||||
const path = computed(() =>
|
||||
getSmoothStepPath({
|
||||
...props,
|
||||
}),
|
||||
)
|
||||
|
||||
const markerEnd = computed(() => getMarkerEnd(props.arrowHeadType, props.markerEndId))
|
||||
</script>
|
||||
<template>
|
||||
<path class="revue-flow__edge-path" :style="props.style" :d="path" :marker-end="markerEnd" />
|
||||
<EdgeText
|
||||
v-if="props.label"
|
||||
:x="centered[0]"
|
||||
: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"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,15 +0,0 @@
|
||||
import { defineComponent } from 'vue'
|
||||
import SmoothStepEdge from './SmoothStepEdge'
|
||||
import { EdgeSmoothProps } from './utils'
|
||||
import { EdgeType } from '../../types'
|
||||
|
||||
export default defineComponent({
|
||||
components: { SmoothStepEdge },
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
...EdgeSmoothProps
|
||||
},
|
||||
setup(props) {
|
||||
return () => <SmoothStepEdge {...props} borderRadius={0} />
|
||||
}
|
||||
}) as EdgeType
|
||||
@@ -0,0 +1,44 @@
|
||||
<script lang="ts" setup>
|
||||
import { VNode } from 'vue'
|
||||
import SmoothStepEdge from './SmoothStepEdge.vue'
|
||||
import { ArrowHeadType, ElementId, Position } from '~/types'
|
||||
|
||||
export interface EdgeStepProps<T = any> {
|
||||
id: ElementId
|
||||
source: ElementId
|
||||
target: ElementId
|
||||
sourceX: number
|
||||
sourceY: number
|
||||
targetX: number
|
||||
targetY: number
|
||||
selected?: boolean
|
||||
animated?: boolean
|
||||
sourcePosition?: Position
|
||||
targetPosition?: Position
|
||||
label?: string | VNode
|
||||
labelStyle?: any
|
||||
labelShowBg?: boolean
|
||||
labelBgStyle?: any
|
||||
labelBgPadding?: [number, number]
|
||||
labelBgBorderRadius?: number
|
||||
style?: any
|
||||
arrowHeadType?: ArrowHeadType
|
||||
markerEndId?: string
|
||||
data?: T
|
||||
sourceHandleId?: ElementId | null
|
||||
targetHandleId?: ElementId | null
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<EdgeStepProps>(), {
|
||||
selected: false,
|
||||
sourcePosition: Position.Bottom,
|
||||
targetPosition: Position.Top,
|
||||
label: () => '',
|
||||
labelStyle: () => ({}),
|
||||
labelShowBg: true,
|
||||
labelBgStyle: () => ({}),
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<SmoothStepEdge v-bind="props" :border-radius="0" />
|
||||
</template>
|
||||
@@ -1,47 +0,0 @@
|
||||
import { defineComponent } from 'vue'
|
||||
import EdgeText from './EdgeText'
|
||||
import { getMarkerEnd, DefaultEdgeProps } from './utils'
|
||||
import { reactify } from '@vueuse/core'
|
||||
import { ArrowHeadType, EdgeType } from '../../types'
|
||||
|
||||
export default defineComponent({
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
...DefaultEdgeProps
|
||||
},
|
||||
setup(props) {
|
||||
const centerY = reactify((targetY: number, sourceY: number) => {
|
||||
const yOffset = Math.abs(targetY - sourceY) / 2
|
||||
return targetY < sourceY ? targetY + yOffset : targetY - yOffset
|
||||
})
|
||||
const centerX = reactify((targetX: number, sourceX: number) => {
|
||||
const xOffset = Math.abs(targetX - sourceX) / 2
|
||||
return targetX < sourceX ? targetX + xOffset : targetX - xOffset
|
||||
})
|
||||
|
||||
const markerEnd = reactify((arrowHeadType?: ArrowHeadType, markerEndId?: string) => getMarkerEnd(arrowHeadType, markerEndId))
|
||||
|
||||
return () => (
|
||||
<>
|
||||
<path
|
||||
style={props.style}
|
||||
class="revue-flow__edge-path"
|
||||
d={`M ${props.sourceX},${props.sourceY}L ${props.targetX},${props.targetY}`}
|
||||
marker-end={markerEnd(props.arrowHeadType, props.markerEndId).value}
|
||||
/>
|
||||
{props.label ? (
|
||||
<EdgeText
|
||||
x={centerX(props.targetX, props.sourceX).value}
|
||||
y={centerY(props.targetY, props.sourceY).value}
|
||||
label={props.label}
|
||||
labelStyle={props.labelStyle}
|
||||
labelShowBg={props.labelShowBg}
|
||||
labelBgStyle={props.labelBgStyle}
|
||||
labelBgPadding={props.labelBgPadding}
|
||||
labelBgBorderRadius={props.labelBgBorderRadius}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}) as unknown as EdgeType
|
||||
@@ -0,0 +1,77 @@
|
||||
<script lang="ts" setup>
|
||||
import { VNode } from 'vue'
|
||||
import EdgeText from './EdgeText.vue'
|
||||
import { ArrowHeadType, ElementId, Position } from '~/types'
|
||||
import { getMarkerEnd, getBezierPath } from '~/components/Edges/utils'
|
||||
|
||||
interface StraightEdgeProps<T = any> {
|
||||
id: ElementId
|
||||
source: ElementId
|
||||
target: ElementId
|
||||
sourceX: number
|
||||
sourceY: number
|
||||
targetX: number
|
||||
targetY: number
|
||||
selected?: boolean
|
||||
animated?: boolean
|
||||
sourcePosition?: Position
|
||||
targetPosition?: Position
|
||||
label?: string | VNode
|
||||
labelStyle?: any
|
||||
labelShowBg?: boolean
|
||||
labelBgStyle?: any
|
||||
labelBgPadding?: [number, number]
|
||||
labelBgBorderRadius?: number
|
||||
style?: any
|
||||
arrowHeadType?: ArrowHeadType
|
||||
markerEndId?: string
|
||||
data?: T
|
||||
sourceHandleId?: ElementId | null
|
||||
targetHandleId?: ElementId | null
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<StraightEdgeProps>(), {
|
||||
selected: false,
|
||||
sourcePosition: Position.Bottom,
|
||||
targetPosition: Position.Top,
|
||||
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(() =>
|
||||
getBezierPath({
|
||||
...props,
|
||||
}),
|
||||
)
|
||||
|
||||
const markerEnd = computed(() => getMarkerEnd(props.arrowHeadType, props.markerEndId))
|
||||
</script>
|
||||
<template>
|
||||
<path
|
||||
:style="props.style"
|
||||
class="revue-flow__edge-path"
|
||||
:d="`M ${props.sourceX},${props.sourceY}L ${props.targetX},${props.targetY}`"
|
||||
:marker-end="markerEnd"
|
||||
/>
|
||||
<EdgeText
|
||||
v-if="props.label"
|
||||
:x="centerX"
|
||||
: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"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,4 +1,4 @@
|
||||
export { default as BezierEdge } from './BezierEdge'
|
||||
export { default as StepEdge } from './StepEdge'
|
||||
export { default as SmoothStepEdge } from './SmoothStepEdge'
|
||||
export { default as StraightEdge } from './StraightEdge'
|
||||
export { default as BezierEdge } from './BezierEdge.vue'
|
||||
export { default as StepEdge } from './StepEdge.vue'
|
||||
export { default as SmoothStepEdge } from './SmoothStepEdge.vue'
|
||||
export { default as StraightEdge } from './StraightEdge.vue'
|
||||
|
||||
+124
-120
@@ -1,5 +1,4 @@
|
||||
import { ArrowHeadType, Position, EdgeProps, EdgeSmoothStepProps } from '../../types'
|
||||
import { PropType } from 'vue'
|
||||
import { ArrowHeadType, Position } from '~/types'
|
||||
|
||||
export const getMarkerEnd = (arrowHeadType?: ArrowHeadType, markerEndId?: string): string => {
|
||||
if (typeof markerEndId !== 'undefined' && markerEndId) {
|
||||
@@ -26,7 +25,7 @@ export const getCenter = ({
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition = Position.Bottom,
|
||||
targetPosition = Position.Top
|
||||
targetPosition = Position.Top,
|
||||
}: GetCenterParams): [number, number, number, number] => {
|
||||
const sourceIsLeftOrRight = LeftOrRight.includes(sourcePosition)
|
||||
const targetIsLeftOrRight = LeftOrRight.includes(targetPosition)
|
||||
@@ -54,124 +53,129 @@ export const getCenter = ({
|
||||
return [centerX, centerY, xOffset, yOffset]
|
||||
}
|
||||
|
||||
export const DefaultEdgeProps = {
|
||||
id: {
|
||||
type: String as PropType<EdgeProps['id']>,
|
||||
required: true,
|
||||
default: 0
|
||||
},
|
||||
sourceX: {
|
||||
type: Number as PropType<EdgeProps['sourceX']>,
|
||||
required: true,
|
||||
default: 0
|
||||
},
|
||||
sourceY: {
|
||||
type: Number as PropType<EdgeProps['sourceY']>,
|
||||
required: true,
|
||||
default: 0
|
||||
},
|
||||
source: {
|
||||
type: String as PropType<EdgeProps['source']>,
|
||||
required: true,
|
||||
default: 0
|
||||
},
|
||||
target: {
|
||||
type: String as PropType<EdgeProps['target']>,
|
||||
required: true,
|
||||
default: 0
|
||||
},
|
||||
selected: {
|
||||
type: Boolean as PropType<EdgeProps['selected']>,
|
||||
required: true,
|
||||
default: false
|
||||
},
|
||||
targetX: {
|
||||
type: Number as PropType<EdgeProps['targetX']>,
|
||||
required: true,
|
||||
default: 0
|
||||
},
|
||||
targetY: {
|
||||
type: Number as PropType<EdgeProps['targetY']>,
|
||||
required: true,
|
||||
default: 0
|
||||
},
|
||||
sourcePosition: {
|
||||
type: String as PropType<EdgeProps['sourcePosition']>,
|
||||
required: false,
|
||||
default: Position.Bottom
|
||||
},
|
||||
targetPosition: {
|
||||
type: String as PropType<EdgeProps['targetPosition']>,
|
||||
required: false,
|
||||
default: Position.Top
|
||||
},
|
||||
label: {
|
||||
type: [String, Object] as PropType<EdgeProps['label']>,
|
||||
required: false,
|
||||
default: () => {}
|
||||
},
|
||||
labelStyle: {
|
||||
type: Object as PropType<EdgeProps['labelStyle']>,
|
||||
required: false,
|
||||
default: undefined
|
||||
},
|
||||
labelShowBg: {
|
||||
type: Boolean as PropType<EdgeProps['labelShowBg']>,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
labelBgStyle: {
|
||||
type: [String, Object] as PropType<EdgeProps['labelBgStyle']>,
|
||||
required: false,
|
||||
default: undefined
|
||||
},
|
||||
labelBgPadding: {
|
||||
type: Array as unknown as PropType<[number, number]>,
|
||||
required: false,
|
||||
default: undefined
|
||||
},
|
||||
labelBgBorderRadius: {
|
||||
type: Number as PropType<EdgeProps['labelBgBorderRadius']>,
|
||||
required: false,
|
||||
default: undefined
|
||||
},
|
||||
arrowHeadType: {
|
||||
type: String as PropType<EdgeProps['arrowHeadType']>,
|
||||
required: false,
|
||||
default: undefined
|
||||
},
|
||||
markerEndId: {
|
||||
type: String as PropType<EdgeProps['markerEndId']>,
|
||||
required: false,
|
||||
default: undefined
|
||||
},
|
||||
style: {
|
||||
type: Object as PropType<EdgeProps['style']>,
|
||||
required: false,
|
||||
default: undefined
|
||||
},
|
||||
data: {
|
||||
type: Object as PropType<EdgeProps['data']>,
|
||||
required: false,
|
||||
default: () => ({} as any)
|
||||
},
|
||||
sourceHandleId: {
|
||||
type: String as PropType<EdgeProps['sourceHandleId']>,
|
||||
required: false,
|
||||
default: undefined
|
||||
},
|
||||
targetHandleId: {
|
||||
type: String as PropType<EdgeProps['targetHandleId']>,
|
||||
required: false,
|
||||
default: undefined
|
||||
}
|
||||
interface GetBezierPathParams {
|
||||
sourceX: number
|
||||
sourceY: number
|
||||
sourcePosition?: Position
|
||||
targetX: number
|
||||
targetY: number
|
||||
targetPosition?: Position
|
||||
centerX?: number
|
||||
centerY?: number
|
||||
}
|
||||
|
||||
export const EdgeSmoothProps = {
|
||||
...DefaultEdgeProps,
|
||||
borderRadius: {
|
||||
type: Number as PropType<EdgeSmoothStepProps['borderRadius']>,
|
||||
required: false,
|
||||
default: 5
|
||||
export function getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition = Position.Bottom,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition = Position.Top,
|
||||
centerX,
|
||||
centerY,
|
||||
}: GetBezierPathParams): string {
|
||||
const [_centerX, _centerY] = getCenter({ sourceX, sourceY, targetX, targetY })
|
||||
const leftAndRight = [Position.Left, Position.Right]
|
||||
|
||||
const cX = typeof centerX !== 'undefined' ? centerX : _centerX
|
||||
const cY = typeof centerY !== 'undefined' ? centerY : _centerY
|
||||
|
||||
let path = `M${sourceX},${sourceY} C${sourceX},${cY} ${targetX},${cY} ${targetX},${targetY}`
|
||||
|
||||
if (leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
|
||||
path = `M${sourceX},${sourceY} C${cX},${sourceY} ${cX},${targetY} ${targetX},${targetY}`
|
||||
} else if (leftAndRight.includes(targetPosition)) {
|
||||
path = `M${sourceX},${sourceY} C${sourceX},${targetY} ${sourceX},${targetY} ${targetX},${targetY}`
|
||||
} else if (leftAndRight.includes(sourcePosition)) {
|
||||
path = `M${sourceX},${sourceY} C${targetX},${sourceY} ${targetX},${sourceY} ${targetX},${targetY}`
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
// 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.
|
||||
export const bottomLeftCorner = (x: number, y: number, size: number): string => `L ${x},${y - size}Q ${x},${y} ${x + size},${y}`
|
||||
export const leftBottomCorner = (x: number, y: number, size: number): string => `L ${x + size},${y}Q ${x},${y} ${x},${y - size}`
|
||||
export const bottomRightCorner = (x: number, y: number, size: number): string => `L ${x},${y - size}Q ${x},${y} ${x - size},${y}`
|
||||
export const rightBottomCorner = (x: number, y: number, size: number): string => `L ${x - size},${y}Q ${x},${y} ${x},${y - size}`
|
||||
export const leftTopCorner = (x: number, y: number, size: number): string => `L ${x + size},${y}Q ${x},${y} ${x},${y + size}`
|
||||
export const topLeftCorner = (x: number, y: number, size: number): string => `L ${x},${y + size}Q ${x},${y} ${x + size},${y}`
|
||||
export const topRightCorner = (x: number, y: number, size: number): string => `L ${x},${y + size}Q ${x},${y} ${x - size},${y}`
|
||||
export const rightTopCorner = (x: number, y: number, size: number): string => `L ${x - size},${y}Q ${x},${y} ${x},${y + size}`
|
||||
|
||||
export interface GetSmoothStepPathParams {
|
||||
sourceX: number
|
||||
sourceY: number
|
||||
sourcePosition?: Position
|
||||
targetX: number
|
||||
targetY: number
|
||||
targetPosition?: Position
|
||||
borderRadius?: number
|
||||
centerX?: number
|
||||
centerY?: number
|
||||
}
|
||||
|
||||
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) {
|
||||
// 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}`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<script lang="ts" setup>
|
||||
import { ConnectionMode, ElementId, Position, RevueFlowStore } from '~/types'
|
||||
import { onMouseDown, ValidConnectionFunc } from '~/components/Handle/handler'
|
||||
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
|
||||
|
||||
interface HandleProps {
|
||||
id?: string
|
||||
type?: string
|
||||
position?: Position
|
||||
isValidConnection?: ValidConnectionFunc
|
||||
connectable?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<HandleProps>(), {
|
||||
id: '',
|
||||
type: 'source',
|
||||
position: Position.Top,
|
||||
isValidConnection: () => true,
|
||||
connectable: true,
|
||||
})
|
||||
|
||||
const store = inject<RevueFlowStore>('store')!
|
||||
const hooks = inject<RevueFlowHooks>('hooks')!
|
||||
const nodeId = inject<ElementId>('NodeIdContext')!
|
||||
|
||||
const onMouseDownHandler = (event: MouseEvent) =>
|
||||
onMouseDown(event, store, hooks, props.id, nodeId, props.type === 'target', props.isValidConnection)
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
:data-handleid="props.id"
|
||||
:data-nodeid="nodeId"
|
||||
:data-handlepos="props.position"
|
||||
:class="[
|
||||
'revue-flow__handle',
|
||||
`revue-flow__handle-${props.position}`,
|
||||
'nodrag',
|
||||
{
|
||||
source: props.type !== 'target',
|
||||
target: props.type === 'target',
|
||||
connectable: props.connectable,
|
||||
},
|
||||
]"
|
||||
@mousedown="onMouseDownHandler"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getHostForElement } from '../../utils'
|
||||
import { ElementId, ConnectionMode, Connection, HandleType, RevueFlowStore } from '../../types'
|
||||
import { RevueFlowHooks } from '../../hooks/RevueFlowHooks'
|
||||
import { getHostForElement } from '~/utils'
|
||||
import { ElementId, ConnectionMode, Connection, HandleType, XYPosition, RevueFlowStore } from '~/types'
|
||||
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
|
||||
|
||||
export type ValidConnectionFunc = (connection: Connection) => boolean
|
||||
|
||||
@@ -19,7 +19,7 @@ function checkElementBelowIsValid(
|
||||
nodeId: ElementId,
|
||||
handleId: ElementId | null,
|
||||
isValidConnection: ValidConnectionFunc,
|
||||
doc: Document | ShadowRoot
|
||||
doc: Document,
|
||||
) {
|
||||
const elementBelow = doc.elementFromPoint(event.clientX, event.clientY)
|
||||
const elementBelowIsTarget = elementBelow?.classList.contains('target') || false
|
||||
@@ -29,7 +29,7 @@ function checkElementBelowIsValid(
|
||||
elementBelow,
|
||||
isValid: false,
|
||||
connection: { source: null, target: null, sourceHandle: null, targetHandle: null },
|
||||
isHoveringHandle: false
|
||||
isHoveringHandle: false,
|
||||
}
|
||||
|
||||
if (elementBelow && (elementBelowIsTarget || elementBelowIsSource)) {
|
||||
@@ -47,13 +47,13 @@ function checkElementBelowIsValid(
|
||||
source: elementBelowNodeId,
|
||||
sourceHandle: elementBelowHandleId,
|
||||
target: nodeId,
|
||||
targetHandle: handleId
|
||||
targetHandle: handleId,
|
||||
}
|
||||
: {
|
||||
source: nodeId,
|
||||
sourceHandle: handleId,
|
||||
target: elementBelowNodeId,
|
||||
targetHandle: elementBelowHandleId
|
||||
targetHandle: elementBelowHandleId,
|
||||
}
|
||||
|
||||
result.connection = connection
|
||||
@@ -79,11 +79,11 @@ export function onMouseDown(
|
||||
connect: RevueFlowHooks['connect']
|
||||
edgeUpdateEnd: RevueFlowHooks['edgeUpdateEnd']
|
||||
},
|
||||
handleId: ElementId | null,
|
||||
handleId: ElementId,
|
||||
nodeId: ElementId,
|
||||
isTarget: boolean,
|
||||
isValidConnection: ValidConnectionFunc,
|
||||
elementEdgeUpdaterType?: HandleType
|
||||
isValidConnection: ValidConnectionFunc = () => true,
|
||||
elementEdgeUpdaterType?: HandleType,
|
||||
): void {
|
||||
const revueFlowNode = (event.target as Element).closest('.revue-flow')
|
||||
// when revue-flow is used inside a shadow root we can't use document
|
||||
@@ -101,23 +101,29 @@ export function onMouseDown(
|
||||
return
|
||||
}
|
||||
|
||||
const handleType = elementEdgeUpdaterType ? elementEdgeUpdaterType : elementBelowIsTarget ? 'target' : 'source'
|
||||
const handleType = elementEdgeUpdaterType || (elementBelowIsTarget ? 'target' : 'source')
|
||||
const containerBounds = revueFlowNode.getBoundingClientRect()
|
||||
let recentHoveredHandle: Element
|
||||
|
||||
const connectionPosition = ref<XYPosition>({
|
||||
x: event.clientX - containerBounds.left,
|
||||
y: event.clientY - containerBounds.top,
|
||||
})
|
||||
|
||||
if (!store.connectionPosition) store.connectionPosition = { x: 0, y: 0 }
|
||||
store.connectionPosition.x = event.clientX - containerBounds.left
|
||||
store.connectionPosition.y = event.clientY - containerBounds.top
|
||||
|
||||
store.setConnectionNodeId({
|
||||
connectionNodeId: nodeId,
|
||||
connectionHandleId: handleId,
|
||||
connectionHandleType: handleType
|
||||
connectionHandleType: handleType,
|
||||
})
|
||||
hooks.connectStart.trigger({ event, params: { nodeId, handleId, handleType } })
|
||||
|
||||
function onMouseMove(event: MouseEvent) {
|
||||
store.connectionPosition.x = event.clientX - containerBounds.left
|
||||
store.connectionPosition.y = event.clientY - containerBounds.top
|
||||
connectionPosition.value.x = event.clientX - containerBounds.left
|
||||
connectionPosition.value.y = event.clientY - containerBounds.top
|
||||
|
||||
const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid(
|
||||
event,
|
||||
@@ -126,7 +132,7 @@ export function onMouseDown(
|
||||
nodeId,
|
||||
handleId,
|
||||
isValidConnection,
|
||||
doc
|
||||
doc,
|
||||
)
|
||||
|
||||
if (!isHoveringHandle) {
|
||||
@@ -150,7 +156,7 @@ export function onMouseDown(
|
||||
nodeId,
|
||||
handleId,
|
||||
isValidConnection,
|
||||
doc
|
||||
doc,
|
||||
)
|
||||
|
||||
hooks.connectStop.trigger(event)
|
||||
@@ -166,7 +172,7 @@ export function onMouseDown(
|
||||
}
|
||||
|
||||
resetRecentHandle(recentHoveredHandle)
|
||||
store.setConnectionNodeId({ connectionNodeId: null, connectionHandleId: null, connectionHandleType: null })
|
||||
store.setConnectionNodeId({ connectionNodeId: undefined, connectionHandleId: undefined, connectionHandleType: undefined })
|
||||
|
||||
doc.removeEventListener('mousemove', onMouseMove as EventListenerOrEventListenerObject)
|
||||
doc.removeEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="ts" setup>
|
||||
import Handle from '~/components/Handle/Handle.vue'
|
||||
import { NodeProps, Position } from '~/types'
|
||||
|
||||
interface DefaultNodeProps {
|
||||
data?: NodeProps['data']
|
||||
connectable?: NodeProps['connectable']
|
||||
targetPosition?: NodeProps['targetPosition']
|
||||
sourcePosition?: NodeProps['sourcePosition']
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<DefaultNodeProps>(), {
|
||||
data: () => {},
|
||||
connectable: false,
|
||||
targetPosition: Position.Top,
|
||||
sourcePosition: Position.Bottom,
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div class="revue-flow__node-default">
|
||||
<Handle type="target" :position="props.targetPosition" :is-connectable="props.connectable" />
|
||||
{{ props.data?.label }}
|
||||
<Handle type="source" :position="props.sourcePosition" :is-connectable="props.connectable" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts" setup>
|
||||
import Handle from '~/components/Handle/Handle.vue'
|
||||
import { NodeProps, Position } from '~/types'
|
||||
|
||||
interface InputNodeProps {
|
||||
data?: NodeProps['data']
|
||||
connectable?: NodeProps['connectable']
|
||||
sourcePosition?: NodeProps['sourcePosition']
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<InputNodeProps>(), {
|
||||
data: () => {},
|
||||
connectable: false,
|
||||
sourcePosition: Position.Bottom,
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div class="revue-flow__node-input">
|
||||
{{ props.data?.label }}
|
||||
<Handle type="source" :position="props.sourcePosition" :connectable="props.connectable" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,37 +1,31 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, onMounted, provide } from 'vue'
|
||||
import { DraggableEventListener } from '@braks/revue-draggable'
|
||||
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
|
||||
import { Node, RevueFlowStore, NodeDimensionUpdate } from '~/types'
|
||||
import { Node, NodeDimensionUpdate, NodeType, RevueFlowStore } from '~/types'
|
||||
|
||||
interface NodeProps {
|
||||
node: Node
|
||||
snapToGrid?: boolean
|
||||
snapGrid?: [number, number]
|
||||
scale?: number
|
||||
selectNodesOnDrag: boolean
|
||||
type: NodeType
|
||||
selected?: boolean
|
||||
draggable?: boolean
|
||||
selectable?: boolean
|
||||
connectable?: boolean
|
||||
selectNodesOnDrag?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<NodeProps>(), {
|
||||
selected: false,
|
||||
draggable: true,
|
||||
selectable: true,
|
||||
connectable: true,
|
||||
selectNodesOnDrag: true,
|
||||
scale: 1,
|
||||
})
|
||||
const emit = defineEmits(['updateNodeDimensions'])
|
||||
|
||||
const store = inject<RevueFlowStore>('store')!
|
||||
const hooks = inject<RevueFlowHooks>('hooks')!
|
||||
provide('NodeIdContext', props.node.id)
|
||||
|
||||
const nodeElement = templateRef<HTMLDivElement>('nodeElement', null)
|
||||
|
||||
const selected = computed(() => store.selectedElements?.some(({ id }) => id === props.node.id) || false)
|
||||
const isDraggable = computed(() => props.node.draggable || (store.nodesDraggable && typeof props.node.draggable === 'undefined'))
|
||||
const isSelectable = computed(
|
||||
() => props.node.selectable || (store.elementsSelectable && typeof props.node.selectable === 'undefined'),
|
||||
)
|
||||
const isConnectable = computed(
|
||||
() => props.node.connectable || (store.nodesConnectable && typeof props.node.connectable === 'undefined'),
|
||||
)
|
||||
const nodeElement = templateRef<HTMLDivElement>('node-element', null)
|
||||
|
||||
const onMouseEnterHandler = () => {
|
||||
if (props.node.__rf.isDragging) {
|
||||
@@ -62,12 +56,12 @@ const onContextMenuHandler = () => {
|
||||
}
|
||||
|
||||
const onSelectNodeHandler = (event: MouseEvent) => {
|
||||
if (!isDraggable.value) {
|
||||
if (!props.draggable) {
|
||||
const n = props.node
|
||||
if (isSelectable.value) {
|
||||
if (props.selectable) {
|
||||
store.unsetNodesSelection()
|
||||
|
||||
if (!selected.value) {
|
||||
if (!props.selected) {
|
||||
store.addSelectedElements([n])
|
||||
}
|
||||
}
|
||||
@@ -80,13 +74,13 @@ const onDragStart: DraggableEventListener = ({ event }) => {
|
||||
const n = props.node
|
||||
hooks.nodeDragStart.trigger({ event, node: n })
|
||||
|
||||
if (props.selectNodesOnDrag && isSelectable.value) {
|
||||
if (props.selectNodesOnDrag && props.selectable) {
|
||||
store.unsetNodesSelection()
|
||||
|
||||
if (!selected.value) {
|
||||
if (!props.selected) {
|
||||
store.addSelectedElements([n])
|
||||
}
|
||||
} else if (!props.selectNodesOnDrag && !selected.value && isSelectable.value) {
|
||||
} else if (!props.selectNodesOnDrag && !props.selected && props.selectable) {
|
||||
store.unsetNodesSelection()
|
||||
store.addSelectedElements([])
|
||||
}
|
||||
@@ -98,8 +92,8 @@ const onDrag: DraggableEventListener = ({ event, data }) => {
|
||||
n.position.y += data.deltaY
|
||||
hooks.nodeDrag.trigger({ event, node: n })
|
||||
|
||||
store?.updateNodePosDiff({
|
||||
id: props.node.id as string,
|
||||
store.updateNodePosDiff({
|
||||
id: props.node.id,
|
||||
diff: {
|
||||
x: data.deltaX,
|
||||
y: data.deltaY,
|
||||
@@ -113,7 +107,7 @@ const onDragStop: DraggableEventListener = ({ event }) => {
|
||||
// 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 (!props.node.__rf.isDragging) {
|
||||
if (isSelectable.value && !props.selectNodesOnDrag && !selected.value) {
|
||||
if (props.selectable && !props.selectNodesOnDrag && !props.selected) {
|
||||
store.addSelectedElements([n])
|
||||
}
|
||||
|
||||
@@ -135,12 +129,11 @@ useResizeObserver(nodeElement, (entries) => {
|
||||
id: entry.target.getAttribute('data-id') as string,
|
||||
nodeElement: entry.target as HTMLDivElement,
|
||||
}))
|
||||
|
||||
emit('updateNodeDimensions', updates)
|
||||
store.updateNodeDimensions(updates)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
emit('updateNodeDimensions', [
|
||||
store.updateNodeDimensions([
|
||||
{
|
||||
id: props.node.id,
|
||||
nodeElement: nodeElement.value,
|
||||
@@ -154,28 +147,28 @@ onMounted(() => {
|
||||
<DraggableCore
|
||||
v-if="!props.node.isHidden"
|
||||
cancel=".nodrag"
|
||||
:scale="props.scale"
|
||||
:disabled="!isDraggable"
|
||||
:grid="props.snapGrid"
|
||||
:disabled="!props.draggable"
|
||||
:scale="store.transform[2]"
|
||||
:grid="store.snapToGrid ? store.snapGrid : undefined"
|
||||
:enable-user-select-hack="false"
|
||||
@start="onDragStart"
|
||||
@move="onDrag"
|
||||
@stop="onDragStop"
|
||||
>
|
||||
<div
|
||||
ref="nodeElement"
|
||||
ref="node-element"
|
||||
:class="[
|
||||
'revue-flow__node',
|
||||
`revue-flow__node-${props.node.type}`,
|
||||
{
|
||||
selected: selected,
|
||||
selectable: isSelectable,
|
||||
selected: props.selected,
|
||||
selectable: props.selectable,
|
||||
},
|
||||
]"
|
||||
:style="{
|
||||
zIndex: selected ? 10 : 3,
|
||||
zIndex: props.selected ? 10 : 3,
|
||||
transform: `translate(${props.node.__rf.position.x}px,${props.node.__rf.position.y}px)`,
|
||||
pointerEvents: isSelectable || isDraggable ? 'all' : 'none',
|
||||
pointerEvents: props.selectable || props.draggable ? 'all' : 'none',
|
||||
opacity: props.node.__rf.width !== null && props.node.__rf.height !== null ? 1 : 0,
|
||||
...props.node.style,
|
||||
}"
|
||||
@@ -186,7 +179,20 @@ onMounted(() => {
|
||||
@contextmenu="onContextMenuHandler"
|
||||
@click="onSelectNodeHandler"
|
||||
>
|
||||
<slot :selected="selected" :isConnectable="isConnectable"></slot>
|
||||
<component
|
||||
:is="props.type"
|
||||
v-bind="{
|
||||
data: props.node.data,
|
||||
type: props.node.type,
|
||||
xPos: props.node.__rf.position.x,
|
||||
yPos: props.node.__rf.position.y,
|
||||
selected: props.selected,
|
||||
connectable: props.connectable,
|
||||
sourcePosition: props.node.sourcePosition,
|
||||
targetPosition: props.node.targetPosition,
|
||||
dragging: props.node.__rf.isDragging,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</DraggableCore>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts" setup>
|
||||
import Handle from '~/components/Handle/Handle.vue'
|
||||
import { NodeProps, Position } from '~/types'
|
||||
|
||||
interface OutputNodeProps {
|
||||
data?: NodeProps['data']
|
||||
connectable?: NodeProps['connectable']
|
||||
targetPosition?: NodeProps['targetPosition']
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<OutputNodeProps>(), {
|
||||
data: () => {},
|
||||
connectable: false,
|
||||
targetPosition: Position.Top,
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div class="revue-flow__node-output">
|
||||
{{ props.data?.label }}
|
||||
<Handle type="source" :position="props.targetPosition" :is-connectable="props.connectable" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -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,88 @@
|
||||
<script lang="ts" setup>
|
||||
import { DraggableEventListener } from '@braks/revue-draggable'
|
||||
import { Node, RevueFlowStore } from '~/types'
|
||||
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
|
||||
import { isNode } from '~/utils/graph'
|
||||
|
||||
interface NodesSelectionProps {
|
||||
onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void
|
||||
onSelectionDrag?: (event: MouseEvent, nodes: Node[]) => void
|
||||
onSelectionDragStop?: (event: MouseEvent, nodes: Node[]) => void
|
||||
onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void
|
||||
}
|
||||
|
||||
const props = defineProps<NodesSelectionProps>()
|
||||
|
||||
const store = inject<RevueFlowStore>('store')!
|
||||
const hooks = inject<RevueFlowHooks>('hooks')!
|
||||
|
||||
const selectedNodes = computed(() =>
|
||||
store.selectedElements
|
||||
? store.selectedElements.filter(isNode).map((selectedNode) => {
|
||||
const matchingNode = store.nodes.find((node) => node.id === selectedNode.id)
|
||||
|
||||
return {
|
||||
...matchingNode,
|
||||
position: matchingNode?.__rf.position,
|
||||
} as Node
|
||||
})
|
||||
: [],
|
||||
)
|
||||
|
||||
const style = computed(() => ({
|
||||
transform: `translate(${store.transform[0]}px,${store.transform[1]}px) scale(${store.transform[2]})`,
|
||||
}))
|
||||
|
||||
const innerStyle = computed(() => ({
|
||||
width: `${store.selectedNodesBbox.width}px`,
|
||||
height: `${store.selectedNodesBbox.height}px`,
|
||||
top: `${store.selectedNodesBbox.y}px`,
|
||||
left: `${store.selectedNodesBbox.x}px`,
|
||||
}))
|
||||
|
||||
const onStart: DraggableEventListener = ({ event }) => {
|
||||
hooks.selectionDragStart.trigger({ event, nodes: selectedNodes.value })
|
||||
}
|
||||
|
||||
const onDrag: DraggableEventListener = ({ event, data }) => {
|
||||
hooks.selectionDrag.trigger({ event, nodes: selectedNodes.value })
|
||||
|
||||
store.updateNodePosDiff({
|
||||
diff: {
|
||||
x: data.deltaX,
|
||||
y: data.deltaY,
|
||||
},
|
||||
isDragging: true,
|
||||
})
|
||||
}
|
||||
|
||||
const onStop: DraggableEventListener = ({ event }) => {
|
||||
store.updateNodePosDiff({
|
||||
isDragging: false,
|
||||
})
|
||||
|
||||
hooks.selectionDragStop.trigger({ event, nodes: selectedNodes.value })
|
||||
}
|
||||
|
||||
const onContextMenu = (event: MouseEvent) => {
|
||||
const selectedNodes = store.selectedElements
|
||||
? store.selectedElements.filter(isNode).map((selectedNode) => store.nodes.find((node) => node.id === selectedNode.id))
|
||||
: []
|
||||
|
||||
if (selectedNodes) hooks.selectionContextMenu.trigger({ event, nodes: selectedNodes as Node[] })
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="revue-flow__nodesselection" :style="style">
|
||||
<Draggable
|
||||
:scale="store.transform[2]"
|
||||
:grid="store.snapToGrid ? store.snapGrid : undefined"
|
||||
:enable-user-select-hack="false"
|
||||
@start="onStart"
|
||||
@move="onDrag"
|
||||
@stop="onStop"
|
||||
>
|
||||
<div class="revue-flow__nodesselection-rect" :style="innerStyle" @contextmenu="onContextMenu" />
|
||||
</Draggable>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts" setup>
|
||||
interface SelectionRectProps {
|
||||
width: number
|
||||
height: number
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
const props = defineProps<SelectionRectProps>()
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="revue-flow__selection"
|
||||
:style="{
|
||||
width: `${props.width}px`,
|
||||
height: `${props.height}px`,
|
||||
transform: `translate(${props.x}px, ${props.y}px)`,
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,57 @@
|
||||
<script lang="ts" setup>
|
||||
import SelectionRect from './SelectionRect.vue'
|
||||
import { RevueFlowStore } from '~/types'
|
||||
import { getMousePosition } from '~/components/UserSelection/utils'
|
||||
|
||||
const store = inject<RevueFlowStore>('store')!
|
||||
const el = templateRef('user-selection', null)
|
||||
|
||||
const onMouseDown = (event: MouseEvent) => {
|
||||
const mousePos = getMousePosition(event)
|
||||
if (!mousePos) {
|
||||
return
|
||||
}
|
||||
|
||||
store.setUserSelection(mousePos)
|
||||
}
|
||||
|
||||
const onMouseMove = (event: MouseEvent) => {
|
||||
if (!store.selectionActive) {
|
||||
return
|
||||
}
|
||||
const mousePos = getMousePosition(event)
|
||||
if (!mousePos) {
|
||||
return
|
||||
}
|
||||
|
||||
store.updateUserSelection(mousePos)
|
||||
}
|
||||
|
||||
const onMouseUp = () => {
|
||||
store.unsetUserSelection()
|
||||
}
|
||||
|
||||
const onMouseLeave = () => {
|
||||
store.unsetUserSelection()
|
||||
store.unsetNodesSelection()
|
||||
}
|
||||
|
||||
useEventListener(el, 'mousedown', onMouseDown)
|
||||
useEventListener(el, 'mousemove', onMouseMove)
|
||||
useEventListener(el, 'click', onMouseUp)
|
||||
useEventListener(el, 'mouseup', onMouseUp)
|
||||
useEventListener(el, 'mouseleave', onMouseLeave)
|
||||
|
||||
const userSelectionRect = computed(() => store.userSelectionRect)
|
||||
</script>
|
||||
<template>
|
||||
<div ref="user-selection" class="revue-flow__selectionpane">
|
||||
<SelectionRect
|
||||
v-if="userSelectionRect.draw"
|
||||
:width="userSelectionRect.width"
|
||||
:height="userSelectionRect.height"
|
||||
:x="userSelectionRect.x"
|
||||
:y="userSelectionRect.y"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,15 @@
|
||||
import { XYPosition } from '~/types'
|
||||
|
||||
export function getMousePosition(event: MouseEvent): XYPosition | void {
|
||||
const revueFlowNode = (event.target as Element).closest('.revue-flow')
|
||||
if (!revueFlowNode) {
|
||||
return
|
||||
}
|
||||
|
||||
const containerBounds = revueFlowNode.getBoundingClientRect()
|
||||
|
||||
return {
|
||||
x: event.clientX - containerBounds.left,
|
||||
y: event.clientY - containerBounds.top,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user