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

This commit is contained in:
Braks
2021-10-18 22:39:50 +02:00
parent 9b2f4b7406
commit 560bdc203b
62 changed files with 1683 additions and 1425 deletions

View File

@@ -0,0 +1,20 @@
<script lang="ts" setup>
import { ref } from 'vue'
import Flowy from '~/container/Flow.vue'
import { Elements } from '~/types'
const elements = ref<Elements>([
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
] as Elements)
</script>
<template>
<Flowy :elements="elements">
<div>Foobar!</div>
</Flowy>
</template>

View File

@@ -3,51 +3,55 @@ import { createRouter, createWebHashHistory, RouterOptions } from 'vue-router'
export const routes: RouterOptions['routes'] = [
{
path: '/',
redirect: '/basic'
redirect: '/basic',
},
{
path: '/basic',
component: () => import('./Basic/Basic.vue')
component: () => import('./Basic/Basic.vue'),
},
{
path: '/custom-connectionline',
component: () => import('./CustomConnectionLine/CustomConnectionLine.vue')
component: () => import('./CustomConnectionLine/CustomConnectionLine.vue'),
},
{
path: '/custom-node',
component: () => import('./CustomNode/CustomNode.vue')
component: () => import('./CustomNode/CustomNode.vue'),
},
{
path: '/drag-n-drop',
component: () => import('./DragNDrop/DnD.vue')
component: () => import('./DragNDrop/DnD.vue'),
},
{
path: '/edges',
component: () => import('./Edges')
component: () => import('./Edges'),
},
{
path: '/button-edge',
component: () => import('./EdgeWithButton/EdgeWithButton.vue')
component: () => import('./EdgeWithButton/EdgeWithButton.vue'),
},
{
path: '/edge-types',
component: () => import('./EdgeTypes')
component: () => import('./EdgeTypes'),
},
{
path: '/empty',
component: () => import('./Empty')
component: () => import('./Empty'),
},
{
path: '/hidden',
component: () => import('./Hidden')
component: () => import('./Hidden'),
},
{
path: '/interaction',
component: () => import('./Interaction')
}
component: () => import('./Interaction'),
},
{
path: '/super-flow',
component: () => import('./Superflow/Superflow.vue'),
},
]
export const router = createRouter({
history: createWebHashHistory(),
routes
routes,
})

View File

@@ -35,7 +35,7 @@
"lint": "yarn lint:js"
},
"dependencies": {
"@braks/revue-draggable": "^0.2.9",
"@braks/revue-draggable": "0.2.5",
"@types/d3": "^7.0.0",
"@vueuse/core": "^6.5.3",
"d3": "^7.1.1",

9
pnpm-lock.yaml generated
View File

@@ -3,7 +3,7 @@ lockfileVersion: 5.3
specifiers:
'@antfu/eslint-config': ^0.9.0
'@babel/core': ^7.15.8
'@braks/revue-draggable': ^0.2.9
'@braks/revue-draggable': 0.2.5
'@rollup/plugin-babel': ^5.3.0
'@rollup/plugin-commonjs': ^19.0.2
'@rollup/plugin-node-resolve': ^13.0.5
@@ -49,7 +49,7 @@ specifiers:
vue-tsc: ^0.2.3
dependencies:
'@braks/revue-draggable': 0.2.9_vue@3.2.20
'@braks/revue-draggable': 0.2.5_vue@3.2.20
'@types/d3': 7.0.0
'@vueuse/core': 6.5.3_vue@3.2.20
d3: 7.1.1
@@ -500,8 +500,8 @@ packages:
to-fast-properties: 2.0.0
dev: true
/@braks/revue-draggable/0.2.9_vue@3.2.20:
resolution: {integrity: sha512-gTDRdv30VD6YmEqrc2mocHmS5vEEOhsDifRNYBJKstopN0Vp3rrJEnZu8bQaE3kEzFJbyfnaxfY1CYNkyCUyIQ==}
/@braks/revue-draggable/0.2.5_vue@3.2.20:
resolution: {integrity: sha512-iKGLFXaurfEqoyPbkANv8ZuO1229krVvUQDDYN9le5WlVssFWcQtA3hNvBD2TPuwil226qXv9SqXGp5kgDhAsA==}
peerDependencies:
'@vue/composition-api': ^1.0.0-rc.1
vue: ^2.0.0 || >=3.0.5
@@ -510,7 +510,6 @@ packages:
optional: true
dependencies:
'@vueuse/core': 5.3.0_vue@3.2.20
fast-deep-equal: 3.1.3
vue: 3.2.20
vue-demi: 0.11.4_vue@3.2.20
dev: false

View File

@@ -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>

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>
) : (
''
)
}
},
})

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

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>

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>
) : (
''
)
}
}
})

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>

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"
/>
)
}
})

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>

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>
)
}
})

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>

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

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>

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

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>

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

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>

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'

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}`
}

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>

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)

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>

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>

View File

@@ -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>

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>

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'

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

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,
}
}

View File

@@ -1,11 +1,20 @@
import { D3ZoomEvent, zoom, zoomIdentity, ZoomTransform } from 'd3-zoom'
import { pointer, select } from 'd3-selection'
import { Ref } from 'vue'
import { FlowTransform, PanOnScrollMode, RevueFlowOptions } from '~/types'
import { get } from '@vueuse/core'
import {
D3Selection,
D3Zoom,
FlowTransform,
InitD3ZoomPayload,
KeyCode,
PanOnScrollMode,
Transform,
TranslateExtent,
} from '~/types'
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
import { clamp } from '~/utils'
import useKeyPress from '~/hooks/useKeyPress'
import useZoomPanHelper from '~/hooks/useZoomPanHelper'
// import { onLoadGetElements, onLoadProject, onLoadToObject } from '~/utils/graph'
const viewChanged = (prevTransform: FlowTransform, eventTransform: ZoomTransform): boolean =>
@@ -17,9 +26,34 @@ const eventToFlowTransform = (eventTransform: ZoomTransform): FlowTransform => (
zoom: eventTransform.k,
})
type UseZoomOptions = RevueFlowOptions
interface UseZoomOptions {
selectionKeyCode?: KeyCode
zoomActivationKeyCode?: KeyCode
paneMoveable?: boolean
minZoom?: number
maxZoom?: number
defaultZoom?: number
defaultPosition?: [number, number]
translateExtent?: TranslateExtent
zoomOnScroll?: boolean
zoomOnPinch?: boolean
panOnScroll?: boolean
panOnScrollSpeed?: number
panOnScrollMode?: PanOnScrollMode
zoomOnDoubleClick?: boolean
}
const useZoom = (zoomPane: Ref<HTMLDivElement>, options: UseZoomOptions) => {
interface UseZoom {
transform: Ref<Transform>
d3Zoom: Ref<D3Zoom>
d3Selection: Ref<D3Selection>
}
export default function (
el: Ref<HTMLDivElement>,
options: UseZoomOptions,
init: (initD3ZoomPayload: InitD3ZoomPayload) => void = () => {},
): UseZoom {
const {
selectionKeyCode = 'Shift',
zoomActivationKeyCode = 'Meta',
@@ -45,29 +79,24 @@ const useZoom = (zoomPane: Ref<HTMLDivElement>, options: UseZoomOptions) => {
const clampedX = clamp(defaultPosition[0], translateExtent[0][0], translateExtent[1][0])
const clampedY = clamp(defaultPosition[1], translateExtent[0][1], translateExtent[1][1])
const clampedZoom = clamp(defaultZoom, minZoom, maxZoom)
const transform = ref([clampedX, clampedY, clampedZoom])
const transform = ref<Transform>([clampedX, clampedY, clampedZoom])
const d3Zoom = ref(zoom<HTMLDivElement, any>().scaleExtent([minZoom, maxZoom]).translateExtent(translateExtent))
const d3Selection = ref()
until(zoomPane)
until(el)
.toBeTruthy()
.then(() => {
const d3Zoom = zoom().scaleExtent([minZoom, maxZoom]).translateExtent(translateExtent)
const d3Selection = select(unrefElement(zoomPane) as Element).call(d3Zoom)
const d3ZoomHandler = d3Selection.on('wheel.zoom')
const d3z = get(d3Zoom)!
d3Selection.value = select(el.value).call(d3z)
const d3s = get(d3Selection)!
const d3ZoomHandler = d3s.on('wheel.zoom')
const updatedTransform = zoomIdentity.translate(clampedX, clampedY).scale(clampedZoom)
const { fitView, zoomIn, zoomOut, zoomTo } = useZoomPanHelper({ ...options, d3Zoom, d3Selection } as any)
hooks.load.trigger({
fitView: (params = { padding: 0.1 }) => fitView(params),
zoomIn,
zoomOut,
zoomTo,
setTransform: transform,
} as any)
d3Zoom.transform(d3Selection, updatedTransform)
d3z.transform(d3s, updatedTransform)
init({ d3Zoom: d3z, d3ZoomHandler, d3Selection: d3s })
const applyZoomHandlers = () => {
d3Zoom.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => {
d3z.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => {
if (viewChanged(prevTransform.value, event.transform)) {
const flowTransform = eventToFlowTransform(event.transform)
prevTransform.value = flowTransform
@@ -76,7 +105,7 @@ const useZoom = (zoomPane: Ref<HTMLDivElement>, options: UseZoomOptions) => {
}
})
d3Zoom.on('end', (event: D3ZoomEvent<HTMLDivElement, any>) => {
d3z.on('end', (event: D3ZoomEvent<HTMLDivElement, any>) => {
if (viewChanged(prevTransform.value, event.transform)) {
const flowTransform = eventToFlowTransform(event.transform)
prevTransform.value = flowTransform
@@ -87,9 +116,9 @@ const useZoom = (zoomPane: Ref<HTMLDivElement>, options: UseZoomOptions) => {
useKeyPress(selectionKeyCode, (keyPress) => {
if (keyPress) {
d3Zoom.on('zoom', null)
d3z.on('zoom', null)
} else {
d3Zoom.on('zoom', (event: D3ZoomEvent<HTMLDivElement, any>) => {
d3z.on('zoom', (event: D3ZoomEvent<HTMLDivElement, any>) => {
transform.value = [event.transform.x, event.transform.y, event.transform.k]
hooks.move.trigger(eventToFlowTransform(event.transform))
})
@@ -98,19 +127,19 @@ const useZoom = (zoomPane: Ref<HTMLDivElement>, options: UseZoomOptions) => {
useKeyPress(zoomActivationKeyCode, (keyPress) => {
if (panOnScroll && keyPress) {
d3Selection
d3s
?.on('wheel', (event: WheelEvent) => {
event.preventDefault()
event.stopImmediatePropagation()
const currentZoom = d3Selection?.property('__zoom').k || 1
const currentZoom = d3s?.property('__zoom').k || 1
if (event.ctrlKey && zoomOnPinch) {
const point = pointer(event)
// taken from https://github.com/d3/d3-zoom/blob/master/src/zoom.js
const pinchDelta = -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002) * 10
const zoom = currentZoom * Math.pow(2, pinchDelta)
if (d3Selection) d3Zoom?.scaleTo(d3Selection, zoom, point)
if (d3s) d3z.scaleTo(d3s, zoom, point)
return
}
@@ -121,23 +150,19 @@ const useZoom = (zoomPane: Ref<HTMLDivElement>, options: UseZoomOptions) => {
const deltaX = panOnScrollMode === PanOnScrollMode.Vertical ? 0 : event.deltaX * deltaNormalize
const deltaY = panOnScrollMode === PanOnScrollMode.Horizontal ? 0 : event.deltaY * deltaNormalize
if (d3Selection && panOnScrollSpeed)
d3Zoom?.translateBy(
d3Selection,
-(deltaX / currentZoom) * panOnScrollSpeed,
-(deltaY / currentZoom) * panOnScrollSpeed,
)
if (d3s && panOnScrollSpeed)
d3z?.translateBy(d3s, -(deltaX / currentZoom) * panOnScrollSpeed, -(deltaY / currentZoom) * panOnScrollSpeed)
})
.on('wheel.zoom', null)
} else if (typeof d3ZoomHandler !== 'undefined') {
d3Selection?.on('wheel', null).on('wheel.zoom', d3ZoomHandler)
d3s?.on('wheel', null).on('wheel.zoom', d3ZoomHandler)
}
})
}
const applyZoomFilter = () => {
const keyPress = useKeyPress(selectionKeyCode)
d3Zoom.filter((event: MouseEvent) => {
d3z.filter((event: MouseEvent) => {
const zoomScroll = zoomOnScroll
const pinchZoom = zoomOnPinch && event.ctrlKey
@@ -179,7 +204,5 @@ const useZoom = (zoomPane: Ref<HTMLDivElement>, options: UseZoomOptions) => {
applyZoomFilter()
})
return transform
return { transform, d3Zoom, d3Selection }
}
export default useZoom

View File

@@ -1,49 +1,72 @@
<script lang="ts" setup>
import { CSSProperties } from 'vue'
import Edge from '~/components/Edges/Edge'
import { ConnectionLineComponent, ConnectionLineType, ConnectionMode, Dimensions, Edge as TEdge, Transform } from '~/types'
import { getSourceTargetNodes } from '~/container/EdgeRenderer/utils'
import Edge from '~/components/Edges/Edge.vue'
import ConnectionLine from '~/components/ConnectionLine/ConnectionLine.vue'
import {
ConnectionLineType,
ConnectionMode,
CustomConnectionLine,
Dimensions,
EdgeType,
RevueFlowStore,
Transform,
} from '~/types'
import MarkerDefinitions from '~/container/MarkerDefinitions.vue'
interface EdgeRendererProps {
edges: TEdge[]
edgeTypes: any
transform: Transform
connectionLineType: ConnectionLineType
connectionLineStyle?: CSSProperties
connectionLineComponent?: ConnectionLineComponent
connectionMode?: ConnectionMode
arrowHeadColor: string
markerEndId?: string
onlyRenderVisibleElements: boolean
edgeUpdaterRadius?: number
edgeTypes: Record<string, EdgeType>
dimensions: Dimensions
transform: Transform
connectionLineType?: ConnectionLineType
connectionLineStyle?: CSSProperties
customConnectionLine?: CustomConnectionLine
connectionMode?: ConnectionMode
arrowHeadColor?: string
markerEndId?: string
onlyRenderVisibleElements?: boolean
}
const props = withDefaults(defineProps<EdgeRendererProps>(), {
transform: () => [0, 0, 1],
edges: () => [],
arrowHeadColor: '#fff',
arrowHeadColor: '#b1b1b7',
dimensions: () => ({ width: 0, height: 0 }),
onlyRenderVisibleElements: false,
connectionMode: ConnectionMode.Strict,
connectionLineType: ConnectionLineType.Bezier,
})
const store = inject<RevueFlowStore>('store')!
const sourceNode = computed(() => store.nodes.find((n) => n.id === store.connectionNodeId))
const connectionLineVisible = computed(
() => store.nodesConnectable && sourceNode.value && store.connectionNodeId && store.connectionHandleType,
)
</script>
<template>
<svg :width="props.dimensions.width" :height="props.dimensions.height" class="revue-flow__edges">
<MarkerDefinitions :color="props.arrowHeadColor" />
<g
:transform="
props.transform &&
props.transform.length &&
`translate(${props.transform[0]},${props.transform[1]}) scale(${props.transform[2]})`
"
:transform="props.transform.length && `translate(${props.transform[0]},${props.transform[1]}) scale(${props.transform[2]})`"
>
<template v-for="(edge, i) of props.edges" :key="`edge-${i}`">
<template v-for="(edge, i) of store.edges" :key="`edge-${i}`">
<Edge
:edge="edge"
:nodes="getSourceTargetNodes(edge, store.nodes)"
:type="props.edgeTypes[edge.type || 'default']"
:dimensions="props.dimensions"
:transform="props.transform"
:only-render-visible-elements="props.onlyRenderVisibleElements"
:marker-end-id="props.markerEndId"
/>
</template>
<ConnectionLine
v-if="connectionLineVisible"
:source-node="sourceNode"
:connection-line-style="props.connectionLineStyle"
:connection-line-type="props.connectionLineType"
:custom-connection-line="props.customConnectionLine"
/>
</g>
</svg>
</template>

View File

@@ -1,8 +1,8 @@
import { CSSProperties, defineComponent, inject, PropType } from 'vue'
import { ConnectionLineType, ConnectionLineComponent, ConnectionMode, RevueFlowStore } from '../../types'
import ConnectionLine from '../../components/ConnectionLine'
import Edge from '../../components/Edges/EdgeDepr'
import MarkerDefinitions from './MarkerDefinitions'
import Edge from '../../components/Edges/Edge'
interface EdgeRendererProps {
edgeTypes: any
@@ -21,59 +21,59 @@ export default defineComponent({
components: {
Edge,
ConnectionLine,
MarkerDefinitions
MarkerDefinitions,
},
props: {
edgeTypes: {
type: Object,
required: true
required: true,
},
connectionLineType: {
type: String as PropType<EdgeRendererProps['connectionLineType']>,
required: false,
default: undefined
default: undefined,
},
connectionLineStyle: {
type: Object as PropType<EdgeRendererProps['connectionLineStyle']>,
required: false,
default: undefined
default: undefined,
},
connectionLineComponent: {
type: Object as PropType<EdgeRendererProps['connectionLineComponent']>,
required: false,
default: undefined
default: undefined,
},
connectionMode: {
type: String as PropType<EdgeRendererProps['connectionMode']>,
required: false,
default: undefined
default: undefined,
},
arrowHeadColor: {
type: String as PropType<EdgeRendererProps['arrowHeadColor']>,
required: false,
default: undefined
default: undefined,
},
markerEndId: {
type: String as PropType<EdgeRendererProps['markerEndId']>,
required: false,
default: undefined
default: undefined,
},
onlyRenderVisibleElements: {
type: Boolean as PropType<EdgeRendererProps['onlyRenderVisibleElements']>,
required: false,
default: undefined
default: undefined,
},
edgeUpdaterRadius: {
type: Number as PropType<EdgeRendererProps['edgeUpdaterRadius']>,
required: false,
default: undefined
}
default: undefined,
},
},
setup(props) {
const store = inject<RevueFlowStore>('store')!
return () => (
<svg width={store.width} height={store.height} class="revue-flow__edges">
<svg width={store.width} height={store.height} className="revue-flow__edges">
<MarkerDefinitions color={props.arrowHeadColor} />
<g transform={`translate(${store.transform[0]},${store.transform[1]}) scale(${store.transform[2]})`}>
{store.edges.map((edge, i) => (
@@ -95,5 +95,5 @@ export default defineComponent({
</g>
</svg>
)
}
},
})

View File

@@ -1,3 +1,4 @@
import { Ref } from 'vue'
import { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '~/components/Edges'
import { rectToBox } from '~/utils/graph'
import { Position, Node, XYPosition, ElementId, HandleElement, Transform, Edge, EdgeType } from '~/types'
@@ -137,12 +138,12 @@ export function isEdgeVisible({ sourcePos, targetPos, width, height, transform }
}
type SourceTargetNode = {
sourceNode: Node | null
targetNode: Node | null
sourceNode: Node
targetNode: Node
}
export const getSourceTargetNodes = (edge: Edge, nodes: Node[]): SourceTargetNode => {
return nodes.reduce(
export const getSourceTargetNodes = (edge: Edge, nodes: Node[]): Ref<SourceTargetNode> => {
const sourceTargetNode: SourceTargetNode = nodes.reduce(
(res, node) => {
if (node.id === edge.source) {
res.sourceNode = node
@@ -152,6 +153,8 @@ export const getSourceTargetNodes = (edge: Edge, nodes: Node[]): SourceTargetNod
}
return res
},
{ sourceNode: null, targetNode: null } as SourceTargetNode,
{ sourceNode: null, targetNode: null } as any,
)
return ref(sourceTargetNode)
}

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import '~/style.css'
import '~/theme-default.css'
import { CSSProperties } from 'vue'
import { CSSProperties, onBeforeUnmount } from 'vue'
import ZoomPane from './ZoomPane.vue'
import SelectionPane from './SelectionPane.vue'
import NodeRenderer from '~/container/NodeRenderer.vue'
@@ -12,11 +12,9 @@ import {
Elements,
PanOnScrollMode,
RevueFlowStore,
Node,
Edge,
NodeType,
EdgeType,
ConnectionLineComponent,
CustomConnectionLine,
KeyCode,
TranslateExtent,
NodeExtent,
@@ -25,21 +23,18 @@ import { RevueFlowHooks, useRevueFlow } from '~/hooks/RevueFlowHooks'
import configureStore from '~/store/configure-store'
import { initialState } from '~/store'
import { createNodeTypes } from '~/container/NodeRenderer/utils'
import InputNode from '~/components/Nodes/InputNode'
import DefaultNode from '~/components/Nodes/DefaultNode'
import OutputNode from '~/components/Nodes/OutputNode'
import { DefaultNode, InputNode, OutputNode } from '~/components/Nodes'
import { BezierEdge, SmoothStepEdge, StepEdge, StraightEdge } from '~/components/Edges'
import { createEdgeTypes } from '~/container/EdgeRenderer/utils'
import { isEdge, isNode, parseEdge, parseNode } from '~/utils/graph'
interface RevueFlowProps {
modelValue: Elements
elements: Elements
nodeTypes?: Record<string, NodeType>
edgeTypes?: Record<string, EdgeType>
connectionMode?: ConnectionMode
connectionLineType?: ConnectionLineType
connectionLineStyle?: CSSProperties
connectionLineComponent?: ConnectionLineComponent
connectionLineComponent?: CustomConnectionLine
deleteKeyCode?: KeyCode
selectionKeyCode?: KeyCode
multiSelectionKeyCode?: KeyCode
@@ -69,11 +64,6 @@ interface RevueFlowProps {
edgeUpdaterRadius?: number
}
type NextElements = {
nextNodes: Node[]
nextEdges: Edge[]
}
const props = withDefaults(defineProps<RevueFlowProps>(), {
modelValue: () => [] as Elements,
connectionMode: ConnectionMode.Strict,
@@ -86,6 +76,7 @@ const props = withDefaults(defineProps<RevueFlowProps>(), {
snapGrid: () => [15, 15],
onlyRenderVisibleElements: false,
nodesConnectable: true,
nodesDraggable: true,
elementsSelectable: true,
selectNodesOnDrag: true,
minZoom: 0.5,
@@ -112,102 +103,65 @@ const props = withDefaults(defineProps<RevueFlowProps>(), {
})
const emit = defineEmits(Object.keys(useRevueFlow()))
const store = configureStore(initialState)()
provide<RevueFlowStore>('store', store)
const defaultNodeTypes: Record<string, NodeType> = {
input: InputNode as NodeType,
default: DefaultNode as NodeType,
output: OutputNode as NodeType,
}
const defaultEdgeTypes: Record<string, EdgeType> = {
default: BezierEdge as EdgeType,
straight: StraightEdge as EdgeType,
step: StepEdge as EdgeType,
smoothstep: SmoothStepEdge as EdgeType,
}
let store = inject<RevueFlowStore>('store')
if (!store) {
store = configureStore({
...initialState,
...props,
})()
provide<RevueFlowStore>('store', store)
}
onBeforeUnmount(() => store?.$dispose())
store.setElements(props.elements)
store.setMinZoom(props.minZoom)
store.setMaxZoom(props.maxZoom)
store.setTranslateExtent(props.translateExtent)
store.setNodeExtent(props.nodeExtent)
const hooks = useRevueFlow().bind(emit)
provide<RevueFlowHooks>('hooks', hooks)
const elements = useVModel(props, 'modelValue', emit)
const defaultNodeTypes = {
input: InputNode,
default: DefaultNode,
output: OutputNode,
}
const defaultEdgeTypes = {
default: BezierEdge,
straight: StraightEdge,
step: StepEdge,
smoothstep: SmoothStepEdge,
}
const nodeTypes = createNodeTypes({ ...defaultNodeTypes, ...props.nodeTypes })
const edgeTypes = createEdgeTypes({ ...defaultEdgeTypes, ...props.edgeTypes })
const nodes = ref<Node[]>([])
const edges = ref<Edge[]>([])
const setElements = (elements: Elements) => {
const propElements = elements
const nextElements: NextElements = {
nextNodes: [],
nextEdges: [],
}
const { nextNodes, nextEdges } = propElements.reduce((res, propElement): NextElements => {
if (isNode(propElement)) {
const storeNode = nodes.value.find((node) => node.id === propElement.id)
if (storeNode) {
const updatedNode: Node = {
...storeNode,
...propElement,
}
if (storeNode.position.x !== propElement.position.x || storeNode.position.y !== propElement.position.y) {
updatedNode.__rf.position = propElement.position
}
if (typeof propElement.type !== 'undefined' && propElement.type !== storeNode.type) {
// we reset the elements dimensions here in order to force a re-calculation of the bounds.
// When the type of a node changes it is possible that the number or positions of handles changes too.
updatedNode.__rf.width = null
}
res.nextNodes.push(updatedNode)
} else {
res.nextNodes.push(parseNode(propElement, props.nodeExtent))
}
} else if (isEdge(propElement)) {
const storeEdge = edges.value.find((se) => se.id === propElement.id)
if (storeEdge) {
res.nextEdges.push({
...storeEdge,
...propElement,
})
} else {
res.nextEdges.push(parseEdge(propElement))
}
}
return res
}, nextElements)
nodes.value = nextNodes
edges.value = nextEdges
}
setElements(elements.value)
</script>
<template>
<div class="revue-flow">
<ZoomPane>
<ZoomPane
:selection-key-code="props.selectionKeyCode"
:zoom-activation-key-code="props.zoomActivationKeyCode"
:default-zoom="props.defaultZoom"
:default-position="props.defaultPosition"
:zoom-on-scroll="props.zoomOnScroll"
:zoom-on-pinch="props.zoomOnPinch"
:zoom-on-double-click="props.zoomOnDoubleClick"
:pan-on-scroll="props.panOnScroll"
:pan-on-scroll-speed="props.panOnScrollSpeed"
:pan-on-scroll-mode="props.panOnScrollMode"
:pane-moveable="props.paneMoveable"
>
<template #default="{ transform, dimensions }">
<SelectionPane
:delete-key-code="props.deleteKeyCode"
:multi-selection-key-code="props.multiSelectionKeyCode"
:selection-key-code="props.selectionKeyCode"
>
<NodeRenderer v-bind="props" :node-types="nodeTypes" :nodes="nodes" :transform="transform" :dimensions="dimensions" />
<EdgeRenderer
v-if="false"
v-bind="props"
:edge-types="edgeTypes"
:edges="edges"
:transform="transform"
:dimensions="dimensions"
/>
<NodeRenderer :node-types="nodeTypes" :transform="transform" :dimensions="dimensions" />
<EdgeRenderer :edge-types="edgeTypes" :transform="transform" :dimensions="dimensions" />
</SelectionPane>
</template>
</ZoomPane>

View File

@@ -1,37 +1,26 @@
<script lang="ts" setup>
import Node from '~/components/Node.vue'
import { NodeType, Node as TNode, Transform, Dimensions, NodeDimensionUpdate } from '~/types'
import Node from '~/components/Nodes/Node.vue'
import { NodeType, Node as TNode, Transform, Dimensions, RevueFlowStore } from '~/types'
import { getNodesInside } from '~/utils/graph'
import { getDimensions } from '~/utils'
import { getHandleBounds } from '~/components/Nodes/utils'
interface NodeRendererProps {
nodes: TNode[]
transform: Transform
nodeTypes: Record<string, NodeType>
selectNodesOnDrag: boolean
snapToGrid: boolean
snapGrid: [number, number]
onlyRenderVisibleElements: boolean
dimensions: Dimensions
transform: Transform
}
const props = withDefaults(defineProps<NodeRendererProps>(), {
selectNodesOnDrag: true,
snapGrid: () => [15, 15],
snapToGrid: false,
onlyRenderVisibleElements: false,
transform: () => [0, 0, 1],
nodes: () => [],
dimensions: () => ({ width: 0, height: 0 }),
})
const nodes = ref(props.nodes)
const getNodes = () =>
props.onlyRenderVisibleElements
? nodes.value &&
const store = inject<RevueFlowStore>('store')!
const getNodes = computed(() =>
store.onlyRenderVisibleElements
? store.nodes &&
getNodesInside(
nodes.value,
store.nodes,
{
x: 0,
y: 0,
@@ -41,9 +30,8 @@ const getNodes = () =>
props.transform,
true,
)
: nodes.value
const cNodes = computed(() => getNodes())
: store.nodes,
)
const type = (node: TNode) => {
const nodeType = node.type || 'default'
@@ -54,65 +42,23 @@ const type = (node: TNode) => {
return type
}
const updateNodeDimensions = (updates: NodeDimensionUpdate[]) => {
nodes.value = nodes.value.map((node) => {
const update = updates.find((u) => u.id === node.id)
if (update) {
const dimensions = getDimensions(update.nodeElement)
const doUpdate =
dimensions.width &&
dimensions.height &&
(node.__rf.width !== dimensions.width || node.__rf.height !== dimensions.height || update.forceUpdate)
if (doUpdate) {
const handleBounds = getHandleBounds(update.nodeElement, props.transform[2])
return {
...node,
__rf: {
...node.__rf,
...dimensions,
handleBounds,
},
}
}
}
return node
})
}
const selected = (nodeId: string) => store.selectedElements?.some(({ id }) => id === nodeId)
</script>
<template>
<div
class="revue-flow__nodes"
:style="{ transform: `translate(${props.transform[0]}px,${props.transform[1]}px) scale(${props.transform[2]})` }"
>
<template v-for="(node, i) of cNodes" :key="`node-${i}`">
<template v-for="(node, i) of getNodes" :key="`node-${i}`">
<Node
:node="node"
:snap-grid="props.snapGrid"
:snap-to-grid="props.snapToGrid"
:select-nodes-on-drag="props.selectNodesOnDrag"
:type="type(node)"
:scale="props.transform[2]"
@updateNodeDimensions="updateNodeDimensions"
>
<template #default="{ selected, isConnectable }">
<component
:is="type(node)"
v-bind="{
data: node.data,
type: node.type,
xPos: node.__rf.position.x,
yPos: node.__rf.position.y,
selected: selected,
isConnectable: isConnectable,
sourcePosition: node.sourcePosition,
targetPosition: node.targetPosition,
isDragging: node.__rf.isDragging,
}"
/>
</template>
</Node>
:selected="selected(node.id)"
:selectable="node.selectable || store.elementsSelectable"
:connectable="node.connectable || store.nodesConnectable"
:draggable="node.draggable || store.nodesDraggable"
/>
</template>
</div>
</template>

View File

@@ -1,5 +1,5 @@
import { defineComponent, inject, PropType } from 'vue'
import Node from '../../components/Nodes/Node'
import Node from '../../components/Nodes/NodeDepr'
import { getNodesInside } from '../../utils/graph'
import { NodeType, RevueFlowStore } from '../../types'
@@ -18,28 +18,28 @@ export default defineComponent({
nodeTypes: {
type: Object as PropType<NodeRendererProps['nodeTypes']>,
required: false,
default: undefined
default: undefined,
},
selectNodesOnDrag: {
type: Boolean as PropType<NodeRendererProps['selectNodesOnDrag']>,
required: false,
default: undefined
default: undefined,
},
snapToGrid: {
type: Boolean as PropType<NodeRendererProps['snapToGrid']>,
required: false,
default: undefined
default: undefined,
},
snapGrid: {
type: Array as unknown as PropType<NodeRendererProps['snapGrid']>,
required: false,
default: undefined
default: undefined,
},
onlyRenderVisibleElements: {
type: Boolean as PropType<NodeRendererProps['onlyRenderVisibleElements']>,
required: false,
default: undefined
}
default: undefined,
},
},
setup(props) {
const store = inject<RevueFlowStore>('store')!
@@ -53,19 +53,19 @@ export default defineComponent({
x: 0,
y: 0,
width: store?.width,
height: store?.height
height: store?.height,
},
store.transform,
true
true,
)
: store.nodes
}
return () => (
<div
class="revue-flow__nodes"
className="revue-flow__nodes"
style={{
transform: `translate(${store.transform[0]}px,${store.transform[1]}px) scale(${store.transform[2]})`
transform: `translate(${store.transform[0]}px,${store.transform[1]}px) scale(${store.transform[2]})`,
}}
>
{getNodes().map((node, i) => {
@@ -90,5 +90,5 @@ export default defineComponent({
})}
</div>
)
}
},
})

View File

@@ -1,6 +1,4 @@
import DefaultNode from '~/components/Nodes/DefaultNode'
import InputNode from '~/components/Nodes/InputNode'
import OutputNode from '~/components/Nodes/OutputNode'
import { DefaultNode, InputNode, OutputNode } from '~/components/Nodes'
import { NodeType } from '~/types'
export function createNodeTypes(nodeTypes: Record<string, NodeType>): Record<string, NodeType> {

View File

@@ -2,9 +2,9 @@ import { PropType } from 'vue'
import '../../style.css'
import '../../theme-default.css'
import GraphView from '../GraphView'
import DefaultNode from '../../components/Nodes/DefaultNode'
import InputNode from '../../components/Nodes/InputNode'
import OutputNode from '../../components/Nodes/OutputNode'
import DefaultNode from '../../components/Nodes/DefaultNodeDepr'
import InputNode from '../../components/Nodes/InputNodeDepr'
import OutputNode from '../../components/Nodes/OutputNodeDepr'
import { createNodeTypes } from '../NodeRenderer/utils'
import { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '../../components/Edges'
import { createEdgeTypes } from '../EdgeRenderer/utils'
@@ -16,14 +16,14 @@ import { RevueFlowHooks, useRevueFlow } from '../../hooks/RevueFlowHooks'
const defaultNodeTypes = {
input: InputNode,
default: DefaultNode,
output: OutputNode
output: OutputNode,
}
const defaultEdgeTypes = {
default: BezierEdge,
straight: StraightEdge,
step: StepEdge,
smoothstep: SmoothStepEdge
smoothstep: SmoothStepEdge,
}
export default defineComponent({
@@ -33,189 +33,189 @@ export default defineComponent({
modelValue: {
type: Array as PropType<RevueFlowProps['modelValue']>,
required: false,
default: () => []
default: () => [],
},
nodeTypesId: {
type: String as PropType<RevueFlowProps['nodeTypesId']>,
required: false,
default: '1'
default: '1',
},
edgeTypesId: {
type: String as PropType<RevueFlowProps['edgeTypesId']>,
required: false,
default: '1'
default: '1',
},
nodeTypes: {
type: Object as PropType<RevueFlowProps['nodeTypes']>,
required: false,
default: () => defaultNodeTypes
default: () => defaultNodeTypes,
},
edgeTypes: {
type: Object as PropType<RevueFlowProps['edgeTypes']>,
required: false,
default: () => defaultEdgeTypes
default: () => defaultEdgeTypes,
},
connectionMode: {
type: String as PropType<RevueFlowProps['connectionMode']>,
required: false,
default: ConnectionMode.Strict
default: ConnectionMode.Strict,
},
connectionLineType: {
type: String as PropType<RevueFlowProps['connectionLineType']>,
required: false,
default: ConnectionLineType.Bezier
default: ConnectionLineType.Bezier,
},
connectionLineStyle: {
type: Object as PropType<RevueFlowProps['connectionLineStyle']>,
required: false,
default: undefined
default: undefined,
},
connectionLineComponent: {
type: Object as PropType<RevueFlowProps['connectionLineComponent']>,
required: false,
default: undefined
default: undefined,
},
selectionKeyCode: {
type: [Number, String] as PropType<RevueFlowProps['selectionKeyCode']>,
required: false,
default: 'Shift'
default: 'Shift',
},
multiSelectionKeyCode: {
type: [Number, String] as PropType<RevueFlowProps['multiSelectionKeyCode']>,
required: false,
default: 'Meta'
default: 'Meta',
},
zoomActivationKeyCode: {
type: [Number, String] as PropType<RevueFlowProps['zoomActivationKeyCode']>,
required: false,
default: 'Meta'
default: 'Meta',
},
deleteKeyCode: {
type: [Number, String] as PropType<RevueFlowProps['deleteKeyCode']>,
required: false,
default: 'Backspace'
default: 'Backspace',
},
snapToGrid: {
type: Boolean as PropType<RevueFlowProps['snapToGrid']>,
required: false,
default: false
default: false,
},
snapGrid: {
type: Array as unknown as PropType<RevueFlowProps['snapGrid']>,
required: false,
default: () => [15, 15]
default: () => [15, 15],
},
onlyRenderVisibleElements: {
type: Boolean as PropType<RevueFlowProps['onlyRenderVisibleElements']>,
required: false,
default: false
default: false,
},
nodesDraggable: {
type: Boolean as PropType<RevueFlowProps['nodesDraggable']>,
required: false,
default: undefined
default: undefined,
},
nodesConnectable: {
type: Boolean as PropType<RevueFlowProps['nodesConnectable']>,
required: false,
default: true
default: true,
},
elementsSelectable: {
type: Boolean as PropType<RevueFlowProps['elementsSelectable']>,
required: false,
default: true
default: true,
},
selectNodesOnDrag: {
type: Boolean as PropType<RevueFlowProps['selectNodesOnDrag']>,
required: false,
default: true
default: true,
},
minZoom: {
type: Number as PropType<RevueFlowProps['minZoom']>,
required: false,
default: 0.5
default: 0.5,
},
maxZoom: {
type: Number as PropType<RevueFlowProps['maxZoom']>,
required: false,
default: 2
default: 2,
},
defaultZoom: {
type: Number as PropType<RevueFlowProps['defaultZoom']>,
required: false,
default: 1
default: 1,
},
defaultPosition: {
type: Array as unknown as PropType<RevueFlowProps['defaultPosition']>,
required: false,
default: () => [0, 0]
default: () => [0, 0],
},
translateExtent: {
type: Array as unknown as PropType<RevueFlowProps['translateExtent']>,
required: false,
default: () => [
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY]
]
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
],
},
nodeExtent: {
type: Array as unknown as PropType<RevueFlowProps['nodeExtent']>,
required: false,
default: () => [
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY]
]
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
],
},
arrowHeadColor: {
type: String as PropType<RevueFlowProps['arrowHeadColor']>,
required: false,
default: '#b1b1b7'
default: '#b1b1b7',
},
markerEndId: {
type: String as PropType<RevueFlowProps['markerEndId']>,
required: false,
default: undefined
default: undefined,
},
zoomOnScroll: {
type: Boolean as PropType<RevueFlowProps['zoomOnScroll']>,
required: false,
default: true
default: true,
},
zoomOnPinch: {
type: Boolean as PropType<RevueFlowProps['zoomOnPinch']>,
required: false,
default: true
default: true,
},
panOnScroll: {
type: Boolean as PropType<RevueFlowProps['panOnScroll']>,
required: false,
default: false
default: false,
},
panOnScrollSpeed: {
type: Number as PropType<RevueFlowProps['panOnScrollSpeed']>,
required: false,
default: 0.5
default: 0.5,
},
panOnScrollMode: {
type: String as PropType<RevueFlowProps['panOnScrollMode']>,
required: false,
default: PanOnScrollMode.Free
default: PanOnScrollMode.Free,
},
zoomOnDoubleClick: {
type: Boolean as PropType<RevueFlowProps['zoomOnDoubleClick']>,
required: false,
default: true
default: true,
},
paneMoveable: {
type: Boolean as PropType<RevueFlowProps['paneMoveable']>,
required: false,
default: true
default: true,
},
edgeUpdaterRadius: {
type: Number as PropType<RevueFlowProps['edgeUpdaterRadius']>,
required: false,
default: 10
}
default: 10,
},
},
emits: Object.keys(useRevueFlow()),
setup(props, { emit, slots }) {
@@ -240,7 +240,7 @@ export default defineComponent({
})
return () => (
<div class="revue-flow">
<div className="revue-flow">
<GraphView
nodeTypes={createNodeTypes({ ...defaultNodeTypes, ...props.nodeTypes })}
edgeTypes={createEdgeTypes({ ...defaultEdgeTypes, ...props.edgeTypes })}
@@ -279,5 +279,5 @@ export default defineComponent({
{slots.default ? slots.default() : ''}
</div>
)
}
},
})

View File

@@ -1,31 +1,29 @@
<script lang="ts" setup>
import { inject } from 'vue'
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
import useKeyPress from '~/hooks/useKeyPress'
import { KeyCode } from '~/types'
import { KeyCode, RevueFlowStore } from '~/types'
import useGlobalKeyHandler from '~/hooks/useGlobalKeyHandler'
import NodesSelection from '~/components/NodesSelection/NodesSelection.vue'
import UserSelection from '~/components/UserSelection/UserSelection.vue'
const props = withDefaults(
defineProps<{
selectionKeyCode: KeyCode
deleteKeyCode: KeyCode
multiSelectionKeyCode: KeyCode
}>(),
{
selectionKeyCode: 'Shift',
deleteKeyCode: 'Backspace',
multiSelectionKeyCode: 'Meta',
},
)
interface SelectionPaneProps {
selectionKeyCode?: KeyCode
deleteKeyCode?: KeyCode
multiSelectionKeyCode?: KeyCode
}
const props = withDefaults(defineProps<SelectionPaneProps>(), {
selectionKeyCode: 'Shift',
deleteKeyCode: 'Backspace',
multiSelectionKeyCode: 'Meta',
})
const store = inject<RevueFlowStore>('store')!
const hooks = inject<RevueFlowHooks>('hooks')!
const keyPressed = useKeyPress(props.selectionKeyCode)
const nodeSelectionActive = ref(false)
const selectedElements = ref([])
const onClick = (event: MouseEvent) => {
hooks.paneClick.trigger(event)
nodeSelectionActive.value = false
selectedElements.value = []
store.unsetNodesSelection()
store.resetSelectedElements()
}
const onContextMenu = (event: MouseEvent) => hooks.paneContextMenu.trigger(event)
@@ -37,10 +35,13 @@ useGlobalKeyHandler({
deleteKeyCode: props.deleteKeyCode,
multiSelectionKeyCode: props.multiSelectionKeyCode,
})
const userSelectionVisible = computed(() => keyPressed.value && (store.selectionActive || store.elementsSelectable))
const nodesSelectionVisible = computed(() => store.nodesSelectionActive)
</script>
<template>
<slot></slot>
<div v-if="keyPressed" id="user-selection">User Selection</div>
<div v-if="nodeSelectionActive" id="nodes-selection">Nodes Selection</div>
<UserSelection v-if="userSelectionVisible" id="user-selection" />
<NodesSelection v-if="nodesSelectionVisible" id="nodes-selection" />
<div class="revue-flow__pane" @click="onClick" @contextmenu="onContextMenu" @wheel="onWheel" />
</template>

View File

@@ -1,39 +1,28 @@
<script lang="ts" setup>
import { RevueFlowOptions, ConnectionLineType, ConnectionMode, Elements, PanOnScrollMode } from '~/types'
import { KeyCode, PanOnScrollMode, RevueFlowStore } from '~/types'
import useZoom from '~/composables/useZoom'
import useResizeHandler from '~/hooks/useResizeHandler'
import useZoomPanHelper from '~/hooks/useZoomPanHelper'
interface RevueFlowProps extends RevueFlowOptions {
modelValue: Elements
interface ZoomPaneProps {
selectionKeyCode?: KeyCode
zoomActivationKeyCode?: KeyCode
paneMoveable?: boolean
defaultZoom?: number
defaultPosition?: [number, number]
zoomOnScroll?: boolean
zoomOnPinch?: boolean
panOnScroll?: boolean
panOnScrollSpeed?: number
panOnScrollMode?: PanOnScrollMode
zoomOnDoubleClick?: boolean
}
const props = withDefaults(defineProps<RevueFlowProps>(), {
modelValue: () => [] as Elements,
connectionMode: ConnectionMode.Strict,
connectionLineType: ConnectionLineType.Bezier,
const props = withDefaults(defineProps<ZoomPaneProps>(), {
selectionKeyCode: 'Shift',
multiSelectionKeyCode: 'Meta',
zoomActivationKeyCode: 'Meta',
deleteKeyCode: 'Backspace',
snapToGrid: false,
snapGrid: () => [15, 15],
onlyRenderVisibleElements: false,
nodesConnectable: true,
elementsSelectable: true,
selectNodesOnDrag: true,
minZoom: 0.5,
maxZoom: 2,
defaultZoom: 1,
defaultPosition: () => [0, 0],
translateExtent: () => [
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
],
nodeExtent: () => [
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
],
arrowHeadColor: '#b1b1b7',
zoomOnScroll: true,
zoomOnPinch: true,
zoomOnDoubleClick: true,
@@ -41,11 +30,27 @@ const props = withDefaults(defineProps<RevueFlowProps>(), {
panOnScrollSpeed: 0.5,
panOnScrollMode: PanOnScrollMode.Free,
paneMoveable: true,
edgeUpdaterRadius: 10,
})
const zoomPane = templateRef<HTMLDivElement>('zoom-pane', null)
const transform = useZoom(zoomPane, props)
const dimensions = useResizeHandler(zoomPane)
const store = inject<RevueFlowStore>('store')!
const zoomPaneEl = templateRef<HTMLDivElement>('zoom-pane', null)
const { transform, d3Selection, d3Zoom } = useZoom(zoomPaneEl, props, (initD3ZoomPayload) => store.initD3Zoom(initD3ZoomPayload))
watch(transform, (val) => (store.transform = val), { flush: 'post' })
const dimensions = useResizeHandler(zoomPaneEl)
watch(dimensions, (val) => (store.dimensions = val), { flush: 'post' })
const getZoomPanHelper = reactify(useZoomPanHelper)
const zoomPanHelper = getZoomPanHelper(
d3Zoom,
d3Selection,
store.nodes,
transform,
dimensions,
store.minZoom,
store.maxZoom,
store.snapToGrid,
store.snapGrid,
)
</script>
<template>
<div ref="zoom-pane" class="revue-flow__renderer revue-flow__zoompane">

View File

@@ -1,7 +1,6 @@
import { computed, watch } from 'vue'
import useKeyPress from './useKeyPress'
import { isNode, getConnectedEdges } from '~/utils/graph'
import { Elements, KeyCode, ElementId, FlowElement, RevueFlowStore, Edge } from '~/types'
import { Elements, KeyCode, ElementId, FlowElement, RevueFlowStore } from '~/types'
interface HookParams {
deleteKeyCode: KeyCode
@@ -9,19 +8,14 @@ interface HookParams {
onElementsRemove?: (elements: Elements) => void
}
export default ({ deleteKeyCode, multiSelectionKeyCode, onElementsRemove }: HookParams): void => {
const selectedElements = ref([])
const deleteKeyPressed = useKeyPress(deleteKeyCode)
const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode)
/**
* const selectedElements = computed(() => store.selectedElements || [])
const edges = computed(() => store.edges)
export default ({ deleteKeyCode, multiSelectionKeyCode, onElementsRemove = () => {} }: HookParams): void => {
const store = inject<RevueFlowStore>('store')!
watch(selectedElements, () => {
if (onElementsRemove && deleteKeyPressed.value && selectedElements.value.length > 0) {
const selectedNodes = selectedElements.value.filter(isNode)
const connectedEdges = getConnectedEdges(selectedNodes, edges.value as Edge[])
const elementsToRemove = [...selectedElements.value, ...connectedEdges].reduce(
useKeyPress(deleteKeyCode, (keyPressed) => {
if (keyPressed && store.selectedElements) {
const selectedNodes = store.selectedElements.filter(isNode)
const connectedEdges = getConnectedEdges(selectedNodes, store.edges)
const elementsToRemove = [...store.selectedElements, ...connectedEdges].reduce(
(res, item) => res.set(item.id, item),
new Map<ElementId, FlowElement>(),
)
@@ -32,8 +26,5 @@ export default ({ deleteKeyCode, multiSelectionKeyCode, onElementsRemove }: Hook
}
})
watch(multiSelectionKeyPressed, () => {
store.multiSelectionActive = multiSelectionKeyPressed.value
})
*/
useKeyPress(multiSelectionKeyCode, (keyPressed) => (store.multiSelectionActive = keyPressed))
}

View File

@@ -2,7 +2,7 @@ import { Ref } from 'vue'
import { getDimensions } from '~/utils'
import { Dimensions } from '~/types'
export default (el: Ref<HTMLDivElement>) => {
export default function (el: Ref<HTMLDivElement>) {
const dimensions = ref<Dimensions>({ width: 0, height: 0 })
const updateDimensions = () => {
const unrefEl = unrefElement(el)
@@ -11,10 +11,13 @@ export default (el: Ref<HTMLDivElement>) => {
const size = getDimensions(unrefEl as HTMLDivElement)
if (size.height === 0 || size.width === 0)
console.log('The revue Flow parent container needs a width and a height to render the graph.')
else dimensions.value = size
}
useEventListener(window, 'resize', updateDimensions)
useResizeObserver(el, () => updateDimensions())
until(el).toBeTruthy().then(updateDimensions)
return dimensions
}

View File

@@ -1,5 +1,4 @@
import { ElementId, RevueFlowStore, UpdateNodeInternals } from '../types'
import { inject } from 'vue'
function useUpdateNodeInternals(): UpdateNodeInternals {
const store = inject<RevueFlowStore>('store')!

View File

@@ -1,76 +1,90 @@
import { zoomIdentity } from 'd3-zoom'
import { Selection, ZoomBehavior } from 'd3'
import { getRectOfNodes, pointToRendererPoint, getTransformForBounds } from '~/utils/graph'
import { FitViewParams, FlowTransform, ZoomPanHelperFunctions, Rect, XYPosition, Node, Transform, SnapGrid } from '~/types'
import {
FitViewParams,
FlowTransform,
ZoomPanHelperFunctions,
Rect,
XYPosition,
Node,
Transform,
SnapGrid,
Dimensions,
D3Zoom,
D3Selection,
} from '~/types'
const DEFAULT_PADDING = 0.1
const useZoomPanHelper = ({
type UseZoomPanHelper = (
d3Zoom: D3Zoom,
d3Selection: D3Selection,
nodes: Node[],
transform: Transform,
dimensions: Dimensions,
minZoom: number,
maxZoom: number,
snapToGrid: boolean,
snapGrid: SnapGrid,
) => ZoomPanHelperFunctions
export default (function (
d3Zoom,
d3Selection,
nodes = [],
width = 0,
height = 0,
transform = [0, 0, 0],
dimensions = {
width: 0,
height: 0,
},
minZoom = 0.5,
maxZoom = 2,
transform = [0, 0, 0],
snapToGrid = false,
snapGrid = [15, 15],
}: {
d3Zoom: ZoomBehavior<Element, unknown>
d3Selection: Selection<Element, any, any, any>
nodes: Node[]
width: number
height: number
minZoom: number
maxZoom: number
transform: Transform
snapToGrid: boolean
snapGrid: SnapGrid
}): ZoomPanHelperFunctions => ({
zoomIn: () => d3Zoom?.scaleBy(d3Selection, 1.2),
zoomOut: () => d3Zoom?.scaleBy(d3Selection, 1 / 1.2),
zoomTo: (zoomLevel: number) => d3Zoom.scaleTo(d3Selection, zoomLevel),
transform: (transform: FlowTransform) => {
const nextTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.zoom)
): ZoomPanHelperFunctions {
return {
zoomIn: () => d3Zoom?.scaleBy(d3Selection, 1.2),
zoomOut: () => d3Zoom?.scaleBy(d3Selection, 1 / 1.2),
zoomTo: (zoomLevel: number) => d3Zoom.scaleTo(d3Selection, zoomLevel),
transform: (transform: FlowTransform) => {
const nextTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.zoom)
d3Zoom.transform(d3Selection, nextTransform)
},
fitView: (options: FitViewParams = { padding: DEFAULT_PADDING, includeHiddenNodes: false }) => {
if (!nodes.length) {
return
}
d3Zoom.transform(d3Selection, nextTransform)
},
fitView: (options: FitViewParams = { padding: DEFAULT_PADDING, includeHiddenNodes: false }) => {
if (!nodes.length) {
return
}
const bounds = getRectOfNodes(options.includeHiddenNodes ? nodes : nodes.filter((node) => !node.isHidden))
const [x, y, zoom] = getTransformForBounds(
bounds,
width,
height,
options.minZoom || minZoom,
options.maxZoom || maxZoom,
options.padding || DEFAULT_PADDING,
)
const transform = zoomIdentity.translate(x, y).scale(zoom)
const bounds = getRectOfNodes(options.includeHiddenNodes ? nodes : nodes.filter((node) => !node.isHidden))
const [x, y, zoom] = getTransformForBounds(
bounds,
dimensions.width,
dimensions.height,
options.minZoom || minZoom,
options.maxZoom || maxZoom,
options.padding || DEFAULT_PADDING,
)
const transform = zoomIdentity.translate(x, y).scale(zoom)
d3Zoom?.transform(d3Selection, transform)
},
setCenter: (x: number, y: number, zoom?: number) => {
const nextZoom = typeof zoom !== 'undefined' ? zoom : maxZoom
const centerX = width / 2 - x * nextZoom
const centerY = height / 2 - y * nextZoom
const transform = zoomIdentity.translate(centerX, centerY).scale(nextZoom)
d3Zoom?.transform(d3Selection, transform)
},
setCenter: (x: number, y: number, zoom?: number) => {
const nextZoom = typeof zoom !== 'undefined' ? zoom : maxZoom
const centerX = dimensions.width / 2 - x * nextZoom
const centerY = dimensions.height / 2 - y * nextZoom
const transform = zoomIdentity.translate(centerX, centerY).scale(nextZoom)
d3Zoom.transform(d3Selection, transform)
},
fitBounds: (bounds: Rect, padding = DEFAULT_PADDING) => {
const [x, y, zoom] = getTransformForBounds(bounds, width, height, minZoom, maxZoom, padding)
const transform = zoomIdentity.translate(x, y).scale(zoom)
d3Zoom.transform(d3Selection, transform)
},
fitBounds: (bounds: Rect, padding = DEFAULT_PADDING) => {
const [x, y, zoom] = getTransformForBounds(bounds, dimensions.width, dimensions.height, minZoom, maxZoom, padding)
const transform = zoomIdentity.translate(x, y).scale(zoom)
d3Zoom.transform(d3Selection, transform)
},
project: (position: XYPosition) => {
return pointToRendererPoint(position, transform, snapToGrid, snapGrid)
},
})
export default useZoomPanHelper
d3Zoom.transform(d3Selection, transform)
},
project: (position: XYPosition) => {
return pointToRendererPoint(position, transform, snapToGrid, snapGrid)
},
}
} as UseZoomPanHelper)

View File

@@ -1,9 +1,7 @@
export { default as default } from './container/RevueFlow'
export { default as Handle } from './components/Handle'
export { default as EdgeText } from './components/Edges/EdgeText'
export { getBezierPath } from './components/Edges/BezierEdge'
export { getSmoothStepPath } from './components/Edges/SmoothStepEdge'
export { getMarkerEnd, getCenter as getEdgeCenter } from './components/Edges/utils'
export { default } from './container/Flow.vue'
export { default as Handle } from './components/Handle/Handle.vue'
export { default as EdgeText } from './components/Edges/EdgeText.vue'
export { getBezierPath, getSmoothStepPath, getMarkerEnd, getCenter as getEdgeCenter } from './components/Edges/utils'
export {
isNode,
@@ -15,7 +13,7 @@ export {
getConnectedEdges,
updateEdge,
getTransformForBounds,
getRectOfNodes
getRectOfNodes,
} from './utils/graph'
export { default as useZoomPanHelper } from './hooks/useZoomPanHelper'
export { default as useUpdateNodeInternals } from './hooks/useUpdateNodeInternals'

View File

@@ -24,12 +24,11 @@ export default function configureStore(
getters: {},
actions: {
setElements(elements) {
const propElements = elements
const nextElements: NextElements = {
nextNodes: [],
nextEdges: [],
}
const { nextNodes, nextEdges } = propElements.reduce((res, propElement): NextElements => {
const { nextNodes, nextEdges } = elements.reduce((res, propElement): NextElements => {
if (isNode(propElement)) {
const storeNode = this.nodes.find((node) => node.id === propElement.id)
@@ -189,7 +188,7 @@ export default function configureStore(
this.userSelectionRect.draw = false
if (!selectedNodes || selectedNodes.length === 0) {
this.selectedElements = null
this.selectedElements = undefined
this.nodesSelectionActive = false
} else {
this.selectedNodesBbox = getRectOfNodes(selectedNodes)
@@ -202,12 +201,11 @@ export default function configureStore(
this.selectedElements = selectedElementsUpdated ? selectedElementsArr : this.selectedElements
},
initD3Zoom(payload) {
const { d3Zoom, d3Selection, d3ZoomHandler, transform } = payload
const { d3Zoom, d3Selection, d3ZoomHandler } = payload
this.d3Zoom = d3Zoom
this.d3Selection = d3Selection
this.d3ZoomHandler = d3ZoomHandler
this.transform = transform
},
setMinZoom(minZoom) {
this.d3Zoom?.scaleExtent([minZoom, this.maxZoom])
@@ -237,14 +235,13 @@ export default function configureStore(
})
},
resetSelectedElements() {
this.selectedElements = null
this.selectedElements = undefined
},
unsetNodesSelection() {
this.nodesSelectionActive = false
},
updateSize(size) {
this.height = size.height
this.width = size.width
this.dimensions = size
},
setConnectionNodeId(payload) {
this.connectionNodeId = payload.connectionNodeId

View File

@@ -1,16 +1,18 @@
import { RevueFlowState, ConnectionMode } from '../types'
export const initialState: RevueFlowState = {
width: 0,
height: 0,
dimensions: {
width: 0,
height: 0,
},
transform: [0, 0, 1],
nodes: [],
edges: [],
selectedElements: null,
selectedElements: undefined,
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
d3Zoom: null,
d3Selection: null,
d3Zoom: undefined,
d3Selection: undefined,
d3ZoomHandler: undefined,
minZoom: 0.5,
maxZoom: 2,
@@ -36,8 +38,8 @@ export const initialState: RevueFlowState = {
height: 0,
draw: false,
},
connectionNodeId: null,
connectionHandleId: null,
connectionNodeId: undefined,
connectionHandleId: undefined,
connectionHandleType: 'source',
connectionPosition: { x: 0, y: 0 },
connectionMode: ConnectionMode.Strict,

View File

@@ -1,5 +1,6 @@
import { ComponentPublicInstance } from 'vue'
import { ElementId, HandleType, Position } from './types'
import { DefineComponent } from 'vue'
import { ElementId, Position } from './types'
import { HandleType } from '~/types/handle'
export enum ConnectionLineType {
Bezier = 'default',
@@ -26,13 +27,13 @@ export type ConnectionLineComponentProps = {
connectionLineType: ConnectionLineType
}
export type ConnectionLineComponent = ComponentPublicInstance<ConnectionLineComponentProps>
export type CustomConnectionLine = DefineComponent<ConnectionLineComponentProps>
export type OnConnectFunc = (connection: Connection) => void
export type OnConnectStartParams = {
nodeId: ElementId | null
handleId: ElementId | null
handleType: HandleType | null
nodeId: ElementId | undefined
handleId: ElementId | undefined
handleType: HandleType | undefined
}
export type OnConnectStartFunc = (event: MouseEvent, params: OnConnectStartParams) => void
export type OnConnectStopFunc = (event: MouseEvent) => void
@@ -44,7 +45,7 @@ export enum ConnectionMode {
}
export type SetConnectionId = {
connectionNodeId: ElementId | null
connectionHandleId: ElementId | null
connectionHandleType: HandleType | null
connectionNodeId: ElementId | undefined
connectionHandleId: ElementId | undefined
connectionHandleType: HandleType | undefined
}

View File

@@ -1,4 +1,4 @@
import { DefineComponent, HTMLAttributes, VNode } from 'vue'
import { DefineComponent, VNode } from 'vue'
import { ArrowHeadType, ElementId, Position } from './types'
import { Connection } from './connection'
@@ -53,17 +53,6 @@ export interface EdgeSmoothStepProps<T = any> extends EdgeProps<T> {
borderRadius?: number
}
export interface EdgeTextProps extends HTMLAttributes {
x: number
y: number
label?: string | VNode
labelStyle?: any
labelShowBg?: boolean
labelBgStyle?: any
labelBgPadding?: [number, number]
labelBgBorderRadius?: number
}
export type EdgeType = DefineComponent<EdgeSmoothStepProps>
export type OnEdgeUpdateFunc<T = any> = (oldEdge: Edge<T>, newConnection: Connection) => void

View File

@@ -1,5 +1,4 @@
import { Dimensions, ElementId, Position, XYPosition } from './types'
import { Connection, OnConnectFunc } from './connection'
export type HandleType = 'source' | 'target'
@@ -7,12 +6,3 @@ export interface HandleElement extends XYPosition, Dimensions {
id?: ElementId | null
position: Position
}
export interface HandleProps {
type: HandleType
position: Position
isConnectable?: boolean
onConnect?: OnConnectFunc
isValidConnection?: (connection: Connection) => boolean
id?: ElementId
}

View File

@@ -47,14 +47,14 @@ export type NodeDimensionUpdate = {
export interface NodeProps<T = any> {
id: ElementId
type: string
data: T
selected: boolean
isConnectable: boolean
data?: T
selected?: boolean
connectable?: boolean
xPos?: number
yPos?: number
targetPosition?: Position
sourcePosition?: Position
isDragging?: boolean
dragging?: boolean
}
export type NodeType = DefineComponent<NodeProps>

View File

@@ -1,5 +1,9 @@
import { Selection as D3Selection, ZoomBehavior } from 'd3'
import { FitViewFunc, FlowTransform, Rect, Transform, XYPosition } from './types'
import { Selection, ZoomBehavior } from 'd3'
import { FitViewFunc, FlowTransform, Rect, XYPosition } from './types'
export type D3Zoom = ZoomBehavior<HTMLDivElement, unknown>
export type D3Selection = Selection<HTMLDivElement, unknown, any, any>
export type D3ZoomHandler = (this: HTMLDivElement, event: any, d: unknown) => void
export type KeyCode = number | string
@@ -21,8 +25,7 @@ export interface ZoomPanHelperFunctions {
}
export type InitD3ZoomPayload = {
d3Zoom: ZoomBehavior<Element, unknown>
d3Selection: D3Selection<Element, unknown, null, undefined>
d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined
transform: Transform
d3Zoom: D3Zoom
d3Selection: D3Selection
d3ZoomHandler: D3ZoomHandler
}

View File

@@ -1,24 +1,23 @@
import { ZoomBehavior, Selection } from 'd3'
import { Store } from 'pinia'
import { ElementId, Elements, Rect, SelectionRect, SnapGrid, Transform, XYPosition } from './types'
import { Dimensions, ElementId, Elements, Rect, SelectionRect, SnapGrid, Transform, XYPosition } from './types'
import { HandleType } from './handle'
import { ConnectionMode, OnConnectEndFunc, OnConnectFunc, OnConnectStartFunc, OnConnectStopFunc } from './connection'
import { Edge } from './edge'
import { Node, NodeExtent, TranslateExtent } from './node'
import { RevueFlowActions } from './actions'
import { D3Selection, D3Zoom, D3ZoomHandler } from '~/types/panel'
export interface RevueFlowState {
width: number
height: number
dimensions: Dimensions
transform: Transform
nodes: Node[]
edges: Edge[]
selectedElements: Elements | null
selectedElements?: Elements
selectedNodesBbox: Rect
d3Zoom: ZoomBehavior<Element, unknown> | null
d3Selection: Selection<Element, unknown, null, undefined> | null
d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined
d3Zoom?: D3Zoom
d3Selection?: D3Selection
d3ZoomHandler?: D3ZoomHandler
minZoom: number
maxZoom: number
translateExtent: TranslateExtent
@@ -29,10 +28,10 @@ export interface RevueFlowState {
userSelectionRect: SelectionRect
connectionNodeId: ElementId | null
connectionHandleId: ElementId | null
connectionHandleType: HandleType | null
connectionPosition: XYPosition
connectionNodeId?: ElementId
connectionHandleId?: ElementId
connectionHandleType?: HandleType
connectionPosition?: XYPosition
connectionMode: ConnectionMode
snapToGrid: boolean

View File

@@ -17,5 +17,8 @@ export const clampPosition = (position: XYPosition, extent: NodeExtent): XYPosit
y: clamp(position.y, extent[0][1], extent[1][1]),
})
export const getHostForElement = (element: HTMLElement): Document | ShadowRoot =>
(element.getRootNode() as Document | ShadowRoot) || window.document
export const getHostForElement = (element: HTMLElement): Document => {
const doc = element.getRootNode() as Document
if ('getElementFromPoint' in doc) return doc
else return window.document
}