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