update(script-setup): Refactor remaining files to script setup style

This commit is contained in:
Braks
2021-10-20 22:39:54 +02:00
parent 9b2f4b7406
commit 560bdc203b
62 changed files with 1683 additions and 1425 deletions
@@ -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>
+22 -22
View File
@@ -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>
) : (
''
)
}
},
})
-99
View File
@@ -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
+67
View File
@@ -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>
-248
View File
@@ -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>
) : (
''
)
}
}
})
+210
View File
@@ -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>
-55
View File
@@ -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"
/>
)
}
})
+37
View File
@@ -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>
-80
View File
@@ -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>
)
}
})
+61
View File
@@ -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>
-147
View File
@@ -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
+70
View File
@@ -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>
-15
View File
@@ -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
+44
View File
@@ -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>
-47
View File
@@ -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
+77
View File
@@ -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>
+4 -4
View File
@@ -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
View File
@@ -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}`
}
+48
View File
@@ -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>
+23 -17
View File
@@ -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)
+25
View File
@@ -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>
+22
View File
@@ -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>
+22
View File
@@ -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>
+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,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>
+15
View File
@@ -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,
}
}