update(script-setup): Refactor remaining files to script setup style
This commit is contained in:
@@ -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>
|
||||||
+17
-13
@@ -3,51 +3,55 @@ import { createRouter, createWebHashHistory, RouterOptions } from 'vue-router'
|
|||||||
export const routes: RouterOptions['routes'] = [
|
export const routes: RouterOptions['routes'] = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
redirect: '/basic'
|
redirect: '/basic',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/basic',
|
path: '/basic',
|
||||||
component: () => import('./Basic/Basic.vue')
|
component: () => import('./Basic/Basic.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/custom-connectionline',
|
path: '/custom-connectionline',
|
||||||
component: () => import('./CustomConnectionLine/CustomConnectionLine.vue')
|
component: () => import('./CustomConnectionLine/CustomConnectionLine.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/custom-node',
|
path: '/custom-node',
|
||||||
component: () => import('./CustomNode/CustomNode.vue')
|
component: () => import('./CustomNode/CustomNode.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/drag-n-drop',
|
path: '/drag-n-drop',
|
||||||
component: () => import('./DragNDrop/DnD.vue')
|
component: () => import('./DragNDrop/DnD.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/edges',
|
path: '/edges',
|
||||||
component: () => import('./Edges')
|
component: () => import('./Edges'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/button-edge',
|
path: '/button-edge',
|
||||||
component: () => import('./EdgeWithButton/EdgeWithButton.vue')
|
component: () => import('./EdgeWithButton/EdgeWithButton.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/edge-types',
|
path: '/edge-types',
|
||||||
component: () => import('./EdgeTypes')
|
component: () => import('./EdgeTypes'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/empty',
|
path: '/empty',
|
||||||
component: () => import('./Empty')
|
component: () => import('./Empty'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/hidden',
|
path: '/hidden',
|
||||||
component: () => import('./Hidden')
|
component: () => import('./Hidden'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/interaction',
|
path: '/interaction',
|
||||||
component: () => import('./Interaction')
|
component: () => import('./Interaction'),
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
path: '/super-flow',
|
||||||
|
component: () => import('./Superflow/Superflow.vue'),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
history: createWebHashHistory(),
|
history: createWebHashHistory(),
|
||||||
routes
|
routes,
|
||||||
})
|
})
|
||||||
|
|||||||
+1
-1
@@ -35,7 +35,7 @@
|
|||||||
"lint": "yarn lint:js"
|
"lint": "yarn lint:js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braks/revue-draggable": "^0.2.9",
|
"@braks/revue-draggable": "0.2.5",
|
||||||
"@types/d3": "^7.0.0",
|
"@types/d3": "^7.0.0",
|
||||||
"@vueuse/core": "^6.5.3",
|
"@vueuse/core": "^6.5.3",
|
||||||
"d3": "^7.1.1",
|
"d3": "^7.1.1",
|
||||||
|
|||||||
Generated
+4
-5
@@ -3,7 +3,7 @@ lockfileVersion: 5.3
|
|||||||
specifiers:
|
specifiers:
|
||||||
'@antfu/eslint-config': ^0.9.0
|
'@antfu/eslint-config': ^0.9.0
|
||||||
'@babel/core': ^7.15.8
|
'@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-babel': ^5.3.0
|
||||||
'@rollup/plugin-commonjs': ^19.0.2
|
'@rollup/plugin-commonjs': ^19.0.2
|
||||||
'@rollup/plugin-node-resolve': ^13.0.5
|
'@rollup/plugin-node-resolve': ^13.0.5
|
||||||
@@ -49,7 +49,7 @@ specifiers:
|
|||||||
vue-tsc: ^0.2.3
|
vue-tsc: ^0.2.3
|
||||||
|
|
||||||
dependencies:
|
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
|
'@types/d3': 7.0.0
|
||||||
'@vueuse/core': 6.5.3_vue@3.2.20
|
'@vueuse/core': 6.5.3_vue@3.2.20
|
||||||
d3: 7.1.1
|
d3: 7.1.1
|
||||||
@@ -500,8 +500,8 @@ packages:
|
|||||||
to-fast-properties: 2.0.0
|
to-fast-properties: 2.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@braks/revue-draggable/0.2.9_vue@3.2.20:
|
/@braks/revue-draggable/0.2.5_vue@3.2.20:
|
||||||
resolution: {integrity: sha512-gTDRdv30VD6YmEqrc2mocHmS5vEEOhsDifRNYBJKstopN0Vp3rrJEnZu8bQaE3kEzFJbyfnaxfY1CYNkyCUyIQ==}
|
resolution: {integrity: sha512-iKGLFXaurfEqoyPbkANv8ZuO1229krVvUQDDYN9le5WlVssFWcQtA3hNvBD2TPuwil226qXv9SqXGp5kgDhAsA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@vue/composition-api': ^1.0.0-rc.1
|
'@vue/composition-api': ^1.0.0-rc.1
|
||||||
vue: ^2.0.0 || >=3.0.5
|
vue: ^2.0.0 || >=3.0.5
|
||||||
@@ -510,7 +510,6 @@ packages:
|
|||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vueuse/core': 5.3.0_vue@3.2.20
|
'@vueuse/core': 5.3.0_vue@3.2.20
|
||||||
fast-deep-equal: 3.1.3
|
|
||||||
vue: 3.2.20
|
vue: 3.2.20
|
||||||
vue-demi: 0.11.4_vue@3.2.20
|
vue-demi: 0.11.4_vue@3.2.20
|
||||||
dev: false
|
dev: false
|
||||||
|
|||||||
@@ -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 { ref, defineComponent, CSSProperties, PropType, computed, inject, h } from 'vue'
|
||||||
|
|
||||||
import { getBezierPath } from '../Edges/BezierEdge'
|
import { getBezierPath } from '../Edges/BezierEdgeDepr'
|
||||||
import { getSmoothStepPath } from '../Edges/SmoothStepEdge'
|
import { getSmoothStepPath } from '../Edges/SmoothStepEdgeDepr'
|
||||||
import { Node, HandleElement, Position, ConnectionLineType, ConnectionLineComponent, RevueFlowStore } from '../../types'
|
import { Node, HandleElement, Position, ConnectionLineType, ConnectionLineComponent, RevueFlowStore } from '../../types'
|
||||||
|
|
||||||
interface ConnectionLineProps {
|
interface ConnectionLineProps {
|
||||||
@@ -15,18 +15,18 @@ export default defineComponent({
|
|||||||
connectionLineStyle: {
|
connectionLineStyle: {
|
||||||
type: Object as PropType<ConnectionLineProps['connectionLineStyle']>,
|
type: Object as PropType<ConnectionLineProps['connectionLineStyle']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => {}
|
default: () => {},
|
||||||
},
|
},
|
||||||
connectionLineType: {
|
connectionLineType: {
|
||||||
type: String as PropType<ConnectionLineProps['connectionLineType']>,
|
type: String as PropType<ConnectionLineProps['connectionLineType']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: ConnectionLineType.Bezier
|
default: ConnectionLineType.Bezier,
|
||||||
},
|
},
|
||||||
customConnectionLine: {
|
customConnectionLine: {
|
||||||
type: Object as PropType<ConnectionLineProps['customConnectionLine']>,
|
type: Object as PropType<ConnectionLineProps['customConnectionLine']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: undefined
|
default: undefined,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const store = inject<RevueFlowStore>('store')!
|
const store = inject<RevueFlowStore>('store')!
|
||||||
@@ -36,15 +36,15 @@ export default defineComponent({
|
|||||||
const sourceHandle = computed(() =>
|
const sourceHandle = computed(() =>
|
||||||
store.connectionHandleId && store.connectionHandleType
|
store.connectionHandleId && store.connectionHandleType
|
||||||
? sourceNode.value?.__rf.handleBounds[store.connectionHandleType].find(
|
? 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(() =>
|
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(() =>
|
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 sourceX = computed(() => sourceNode.value?.__rf.position.x + sourceHandleX.value)
|
||||||
const sourceY = computed(() => sourceNode.value?.__rf.position.y + sourceHandleY.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 targetY = computed(() => (store.connectionPosition.y - store.transform[1]) / store.transform[2])
|
||||||
|
|
||||||
const isRightOrLeft = computed(
|
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))
|
const targetPosition = computed(() => (isRightOrLeft.value ? Position.Left : Position.Top))
|
||||||
|
|
||||||
@@ -67,8 +67,8 @@ export default defineComponent({
|
|||||||
sourcePosition: sourceHandle.value?.position,
|
sourcePosition: sourceHandle.value?.position,
|
||||||
targetX: targetX.value,
|
targetX: targetX.value,
|
||||||
targetY: targetY.value,
|
targetY: targetY.value,
|
||||||
targetPosition: targetPosition.value
|
targetPosition: targetPosition.value,
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
} else if (props.connectionLineType === ConnectionLineType.Step) {
|
} else if (props.connectionLineType === ConnectionLineType.Step) {
|
||||||
dAttr = computed(() =>
|
dAttr = computed(() =>
|
||||||
@@ -79,8 +79,8 @@ export default defineComponent({
|
|||||||
targetX: targetX.value,
|
targetX: targetX.value,
|
||||||
targetY: targetY.value,
|
targetY: targetY.value,
|
||||||
targetPosition: targetPosition.value,
|
targetPosition: targetPosition.value,
|
||||||
borderRadius: 0
|
borderRadius: 0,
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
} else if (props.connectionLineType === ConnectionLineType.SmoothStep) {
|
} else if (props.connectionLineType === ConnectionLineType.SmoothStep) {
|
||||||
dAttr = computed(() =>
|
dAttr = computed(() =>
|
||||||
@@ -90,14 +90,14 @@ export default defineComponent({
|
|||||||
sourcePosition: sourceHandle.value?.position,
|
sourcePosition: sourceHandle.value?.position,
|
||||||
targetX: targetX.value,
|
targetX: targetX.value,
|
||||||
targetY: targetY.value,
|
targetY: targetY.value,
|
||||||
targetPosition: targetPosition.value
|
targetPosition: targetPosition.value,
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.customConnectionLine) {
|
if (props.customConnectionLine) {
|
||||||
return () => (
|
return () => (
|
||||||
<g class="revue-flow__connection">
|
<g className="revue-flow__connection">
|
||||||
{props.customConnectionLine &&
|
{props.customConnectionLine &&
|
||||||
h(props.customConnectionLine, {
|
h(props.customConnectionLine, {
|
||||||
sourceX: sourceX.value,
|
sourceX: sourceX.value,
|
||||||
@@ -107,7 +107,7 @@ export default defineComponent({
|
|||||||
targetY: targetY.value,
|
targetY: targetY.value,
|
||||||
targetPosition: targetPosition.value,
|
targetPosition: targetPosition.value,
|
||||||
connectionLineType: props.connectionLineType,
|
connectionLineType: props.connectionLineType,
|
||||||
connectionLineStyle: props.connectionLineStyle
|
connectionLineStyle: props.connectionLineStyle,
|
||||||
})}
|
})}
|
||||||
</g>
|
</g>
|
||||||
)
|
)
|
||||||
@@ -115,11 +115,11 @@ export default defineComponent({
|
|||||||
|
|
||||||
return () =>
|
return () =>
|
||||||
nodesConnectable.value && sourceNode.value ? (
|
nodesConnectable.value && sourceNode.value ? (
|
||||||
<g class="revue-flow__connection">
|
<g className="revue-flow__connection">
|
||||||
<path d={dAttr.value} class="revue-flow__connection-path" style={props.connectionLineStyle} />
|
<path d={dAttr.value} className="revue-flow__connection-path" style={props.connectionLineStyle} />
|
||||||
</g>
|
</g>
|
||||||
) : (
|
) : (
|
||||||
''
|
''
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
import { defineComponent } from 'vue'
|
|
||||||
import EdgeText from './EdgeText'
|
|
||||||
import { getMarkerEnd, getCenter, GetCenterParams, DefaultEdgeProps } from './utils'
|
|
||||||
import { ArrowHeadType, EdgeType, Position } from '../../types'
|
|
||||||
import { reactify } from '@vueuse/core'
|
|
||||||
|
|
||||||
interface GetBezierPathParams {
|
|
||||||
sourceX: number
|
|
||||||
sourceY: number
|
|
||||||
sourcePosition?: Position
|
|
||||||
targetX: number
|
|
||||||
targetY: number
|
|
||||||
targetPosition?: Position
|
|
||||||
centerX?: number
|
|
||||||
centerY?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getBezierPath({
|
|
||||||
sourceX,
|
|
||||||
sourceY,
|
|
||||||
sourcePosition = Position.Bottom,
|
|
||||||
targetX,
|
|
||||||
targetY,
|
|
||||||
targetPosition = Position.Top,
|
|
||||||
centerX,
|
|
||||||
centerY
|
|
||||||
}: GetBezierPathParams): string {
|
|
||||||
const [_centerX, _centerY] = getCenter({ sourceX, sourceY, targetX, targetY })
|
|
||||||
const leftAndRight = [Position.Left, Position.Right]
|
|
||||||
|
|
||||||
const cX = typeof centerX !== 'undefined' ? centerX : _centerX
|
|
||||||
const cY = typeof centerY !== 'undefined' ? centerY : _centerY
|
|
||||||
|
|
||||||
let path = `M${sourceX},${sourceY} C${sourceX},${cY} ${targetX},${cY} ${targetX},${targetY}`
|
|
||||||
|
|
||||||
if (leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
|
|
||||||
path = `M${sourceX},${sourceY} C${cX},${sourceY} ${cX},${targetY} ${targetX},${targetY}`
|
|
||||||
} else if (leftAndRight.includes(targetPosition)) {
|
|
||||||
path = `M${sourceX},${sourceY} C${sourceX},${targetY} ${sourceX},${targetY} ${targetX},${targetY}`
|
|
||||||
} else if (leftAndRight.includes(sourcePosition)) {
|
|
||||||
path = `M${sourceX},${sourceY} C${targetX},${sourceY} ${targetX},${sourceY} ${targetX},${targetY}`
|
|
||||||
}
|
|
||||||
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
inheritAttrs: false,
|
|
||||||
props: {
|
|
||||||
...DefaultEdgeProps
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const centered = reactify(({ sourceX, sourceY, targetX, targetY, targetPosition, sourcePosition }: GetCenterParams) => {
|
|
||||||
return getCenter({
|
|
||||||
sourceX: sourceX,
|
|
||||||
sourceY: sourceY,
|
|
||||||
targetX: targetX,
|
|
||||||
targetY: targetY,
|
|
||||||
sourcePosition: sourcePosition,
|
|
||||||
targetPosition: targetPosition
|
|
||||||
})
|
|
||||||
})
|
|
||||||
const path = reactify(({ sourceX, sourceY, targetX, targetY, targetPosition, sourcePosition }: GetBezierPathParams) => {
|
|
||||||
return getBezierPath({
|
|
||||||
sourceX: sourceX,
|
|
||||||
sourceY: sourceY,
|
|
||||||
targetX: targetX,
|
|
||||||
targetY: targetY,
|
|
||||||
targetPosition: targetPosition,
|
|
||||||
sourcePosition: sourcePosition
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const markerEnd = reactify((arrowHeadType?: ArrowHeadType, markerEndId?: string) => getMarkerEnd(arrowHeadType, markerEndId))
|
|
||||||
|
|
||||||
return () => (
|
|
||||||
<>
|
|
||||||
<path
|
|
||||||
class="revue-flow__edge-path"
|
|
||||||
style={props.style}
|
|
||||||
d={path({ ...props }).value}
|
|
||||||
marker-end={markerEnd(props.arrowHeadType, props.markerEndId).value}
|
|
||||||
/>
|
|
||||||
{props.label ? (
|
|
||||||
<EdgeText
|
|
||||||
x={centered({ ...props }).value[0]}
|
|
||||||
y={centered({ ...props }).value[1]}
|
|
||||||
label={props.label}
|
|
||||||
labelStyle={props.labelStyle}
|
|
||||||
labelShowBg={props.labelShowBg}
|
|
||||||
labelBgStyle={props.labelBgStyle}
|
|
||||||
labelBgPadding={props.labelBgPadding}
|
|
||||||
labelBgBorderRadius={props.labelBgBorderRadius}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}) as EdgeType
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { CSSProperties, VNode } from 'vue'
|
||||||
|
import EdgeText from './EdgeText.vue'
|
||||||
|
import { ArrowHeadType, ElementId, Position } from '~/types'
|
||||||
|
import { getCenter, getMarkerEnd, getBezierPath } from '~/components/Edges/utils'
|
||||||
|
|
||||||
|
interface BezierEdgeProps<T = any> {
|
||||||
|
id: ElementId
|
||||||
|
source: ElementId
|
||||||
|
target: ElementId
|
||||||
|
sourceX: number
|
||||||
|
sourceY: number
|
||||||
|
targetX: number
|
||||||
|
targetY: number
|
||||||
|
selected?: boolean
|
||||||
|
animated?: boolean
|
||||||
|
sourcePosition?: Position
|
||||||
|
targetPosition?: Position
|
||||||
|
label?: string | VNode
|
||||||
|
labelStyle?: any
|
||||||
|
labelShowBg?: boolean
|
||||||
|
labelBgStyle?: any
|
||||||
|
labelBgPadding?: [number, number]
|
||||||
|
labelBgBorderRadius?: number
|
||||||
|
style?: CSSProperties
|
||||||
|
arrowHeadType?: ArrowHeadType
|
||||||
|
markerEndId?: string
|
||||||
|
data?: T
|
||||||
|
sourceHandleId?: ElementId
|
||||||
|
targetHandleId?: ElementId
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<BezierEdgeProps>(), {
|
||||||
|
selected: false,
|
||||||
|
sourcePosition: Position.Bottom,
|
||||||
|
targetPosition: Position.Top,
|
||||||
|
labelStyle: () => ({}),
|
||||||
|
labelShowBg: true,
|
||||||
|
labelBgStyle: () => ({}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const centered = computed(() =>
|
||||||
|
getCenter({
|
||||||
|
...props,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const path = computed(() =>
|
||||||
|
getBezierPath({
|
||||||
|
...props,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const markerEnd = computed(() => getMarkerEnd(props.arrowHeadType, props.markerEndId))
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<path class="revue-flow__edge-path" :style="props.style" :d="path" :marker-end="markerEnd" />
|
||||||
|
<EdgeText
|
||||||
|
v-if="props.label"
|
||||||
|
:x="centered[0]"
|
||||||
|
:y="centered[1]"
|
||||||
|
:label="props.label"
|
||||||
|
:label-style="props.labelStyle"
|
||||||
|
:label-show-bg="props.labelShowBg"
|
||||||
|
:label-bg-style="props.labelBgStyle"
|
||||||
|
:label-bg-padding="props.labelBgPadding"
|
||||||
|
:label-bg-border-radius="props.labelBgBorderRadius"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -1,248 +0,0 @@
|
|||||||
import { computed, defineComponent, h, inject, PropType, ref, watchPostEffect } from 'vue'
|
|
||||||
import EdgeAnchor from './EdgeAnchor'
|
|
||||||
import { ConnectionMode, Edge, Elements, Node, Position, RevueFlowStore, Transform } from '../../types'
|
|
||||||
import { RevueFlowHooks } from '../../hooks/RevueFlowHooks'
|
|
||||||
import { getEdgePositions, getHandle, getSourceTargetNodes, isEdgeVisible } from '../../container/EdgeRenderer/utils'
|
|
||||||
import { isEdge } from '../../utils/graph'
|
|
||||||
import { onMouseDown } from '../Handle/handler'
|
|
||||||
import { controlledRef } from '@vueuse/core'
|
|
||||||
|
|
||||||
interface EdgeProps {
|
|
||||||
edge: Edge
|
|
||||||
nodes: Node[]
|
|
||||||
selectedElements: Elements | null
|
|
||||||
elementsSelectable: boolean
|
|
||||||
transform: Transform
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
onlyRenderVisibleElements: boolean
|
|
||||||
connectionMode?: ConnectionMode
|
|
||||||
markerEndId?: string
|
|
||||||
edgeUpdaterRadius?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
components: { EdgeAnchor },
|
|
||||||
props: {
|
|
||||||
edge: {
|
|
||||||
type: Object as PropType<EdgeProps['edge']>,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
markerEndId: {
|
|
||||||
type: Object as PropType<EdgeProps['markerEndId']>,
|
|
||||||
required: false,
|
|
||||||
default: undefined
|
|
||||||
},
|
|
||||||
edgeUpdaterRadius: {
|
|
||||||
type: Number as PropType<EdgeProps['edgeUpdaterRadius']>,
|
|
||||||
required: false,
|
|
||||||
default: undefined
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
onlyRenderVisibleElements: {
|
|
||||||
type: Boolean as PropType<EdgeProps['onlyRenderVisibleElements']>,
|
|
||||||
required: false,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
connectionMode: {
|
|
||||||
type: String as PropType<EdgeProps['connectionMode']>,
|
|
||||||
required: false,
|
|
||||||
default: undefined
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const store = inject<RevueFlowStore>('store')!
|
|
||||||
const hooks = inject<RevueFlowHooks>('hooks')!
|
|
||||||
const nodes = controlledRef(getSourceTargetNodes(props.edge, store.nodes), {
|
|
||||||
onBeforeChange: (value, oldValue) => {
|
|
||||||
if (JSON.stringify(value) === JSON.stringify(oldValue)) return false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
watchPostEffect(() => {
|
|
||||||
nodes.value = getSourceTargetNodes(props.edge, store.nodes)
|
|
||||||
})
|
|
||||||
|
|
||||||
hooks.connect.on((connection) => {
|
|
||||||
hooks.edgeUpdate.trigger({ edge: props.edge, connection })
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!nodes.value.sourceNode) {
|
|
||||||
console.warn(`couldn't create edge for source id: ${props.edge.source}; edge id: ${props.edge.id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!nodes.value.targetNode) {
|
|
||||||
console.warn(`couldn't create edge for target id: ${props.edge.target}; edge id: ${props.edge.id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// when connection type is loose we can define all handles as sources
|
|
||||||
const targetNodeHandles =
|
|
||||||
props.connectionMode === ConnectionMode.Strict
|
|
||||||
? nodes.value.targetNode?.__rf.handleBounds.target
|
|
||||||
: nodes.value.targetNode?.__rf.handleBounds.target || nodes.value.targetNode?.__rf.handleBounds.source
|
|
||||||
|
|
||||||
const sourceHandle =
|
|
||||||
nodes.value.sourceNode && getHandle(nodes.value.sourceNode.__rf.handleBounds.source, props.edge.sourceHandle || null)
|
|
||||||
const targetHandle = getHandle(targetNodeHandles, props.edge.targetHandle || null)
|
|
||||||
const sourcePosition = sourceHandle ? sourceHandle.position : Position.Bottom
|
|
||||||
const targetPosition = targetHandle ? targetHandle.position : Position.Top
|
|
||||||
|
|
||||||
const isVisible = ({ sourceX, sourceY, targetX, targetY }: Record<string, number>) => {
|
|
||||||
return props.onlyRenderVisibleElements
|
|
||||||
? isEdgeVisible({
|
|
||||||
sourcePos: { x: sourceX, y: sourceY },
|
|
||||||
targetPos: { x: targetX, y: targetY },
|
|
||||||
width: store.width || 0,
|
|
||||||
height: store.height || 0,
|
|
||||||
transform: store.transform
|
|
||||||
})
|
|
||||||
: true
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSelected = computed(() => store.selectedElements?.some((elm) => isEdge(elm) && elm.id === props.edge.id) || false)
|
|
||||||
|
|
||||||
const edgeElement = () => {
|
|
||||||
const el: Edge = {
|
|
||||||
id: props.edge.id || '',
|
|
||||||
source: props.edge.source,
|
|
||||||
target: props.edge.target,
|
|
||||||
type: props.edge.type
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.edge.sourceHandle) {
|
|
||||||
el.sourceHandle = props.edge.sourceHandle
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.edge.targetHandle) {
|
|
||||||
el.targetHandle = props.edge.targetHandle
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof props.edge.data !== 'undefined') {
|
|
||||||
el.data = props.edge.data
|
|
||||||
}
|
|
||||||
|
|
||||||
return el
|
|
||||||
}
|
|
||||||
|
|
||||||
const onEdgeClick = (event: MouseEvent) => {
|
|
||||||
if (store.elementsSelectable) {
|
|
||||||
store.unsetNodesSelection()
|
|
||||||
store.addSelectedElements(edgeElement() as any)
|
|
||||||
}
|
|
||||||
|
|
||||||
hooks.edgeClick.trigger({ event, edge: edgeElement() })
|
|
||||||
}
|
|
||||||
|
|
||||||
const onEdgeContextMenu = (event: MouseEvent) =>
|
|
||||||
hooks.edgeContextMenu.trigger({
|
|
||||||
event,
|
|
||||||
edge: edgeElement()
|
|
||||||
})
|
|
||||||
|
|
||||||
const onEdgeMouseEnter = (event: MouseEvent) => hooks.edgeMouseEnter.trigger({ event, edge: edgeElement() })
|
|
||||||
|
|
||||||
const onEdgeMouseMove = (event: MouseEvent) => hooks.edgeMouseMove.trigger({ event, edge: edgeElement() })
|
|
||||||
|
|
||||||
const onEdgeMouseLeave = (event: MouseEvent) => hooks.edgeMouseLeave.trigger({ event, edge: edgeElement() })
|
|
||||||
|
|
||||||
const handleEdgeUpdater = (event: MouseEvent, isSourceHandle: boolean) => {
|
|
||||||
const nodeId = isSourceHandle ? props.edge.target : props.edge.source
|
|
||||||
const handleId = isSourceHandle ? props.edge.targetHandle : props.edge.sourceHandle
|
|
||||||
const isValidConnection = () => true
|
|
||||||
const isTarget = isSourceHandle
|
|
||||||
|
|
||||||
hooks.edgeUpdateStart.trigger({ event, edge: edgeElement() })
|
|
||||||
onMouseDown(
|
|
||||||
event,
|
|
||||||
store,
|
|
||||||
hooks,
|
|
||||||
handleId as string,
|
|
||||||
nodeId,
|
|
||||||
isTarget,
|
|
||||||
isValidConnection,
|
|
||||||
isSourceHandle ? 'target' : 'source'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onEdgeUpdaterSourceMouseDown = (event: MouseEvent) => {
|
|
||||||
handleEdgeUpdater(event, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onEdgeUpdaterTargetMouseDown = (event: MouseEvent) => {
|
|
||||||
handleEdgeUpdater(event, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updating = ref<boolean>(false)
|
|
||||||
const onEdgeUpdaterMouseEnter = () => (updating.value = true)
|
|
||||||
const onEdgeUpdaterMouseOut = () => (updating.value = false)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
const { targetX, targetY, sourceX, sourceY } = getEdgePositions(
|
|
||||||
nodes.value.sourceNode as Node,
|
|
||||||
sourceHandle,
|
|
||||||
sourcePosition,
|
|
||||||
nodes.value.targetNode as Node,
|
|
||||||
targetHandle,
|
|
||||||
targetPosition
|
|
||||||
)
|
|
||||||
return !props.edge.isHidden && isVisible({ targetX, targetY, sourceX, sourceY }) ? (
|
|
||||||
<g
|
|
||||||
class={[
|
|
||||||
'revue-flow__edge',
|
|
||||||
`revue-flow__edge-${props.type}`,
|
|
||||||
{
|
|
||||||
selected: isSelected.value,
|
|
||||||
animated: props.edge.animated,
|
|
||||||
inactive: !store.elementsSelectable,
|
|
||||||
updating: updating.value
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
onClick={onEdgeClick}
|
|
||||||
onContextmenu={onEdgeContextMenu}
|
|
||||||
onMouseenter={onEdgeMouseEnter}
|
|
||||||
onMousemove={onEdgeMouseMove}
|
|
||||||
onMouseleave={onEdgeMouseLeave}
|
|
||||||
>
|
|
||||||
{h(
|
|
||||||
props.type,
|
|
||||||
{
|
|
||||||
id: props.edge.id,
|
|
||||||
source: props.edge.source,
|
|
||||||
target: props.edge.target,
|
|
||||||
selected: isSelected.value,
|
|
||||||
animated: props.edge.animated,
|
|
||||||
label: props.edge.label,
|
|
||||||
labelStyle: props.edge.labelStyle,
|
|
||||||
labelShowBg: props.edge.labelShowBg,
|
|
||||||
labelBgStyle: props.edge.labelBgStyle,
|
|
||||||
labelBgPadding: props.edge.labelBgPadding,
|
|
||||||
labelBgBorderRadius: props.edge.labelBgBorderRadius,
|
|
||||||
data: props.edge.data,
|
|
||||||
style: props.edge.style,
|
|
||||||
arrowHeadType: props.edge.arrowHeadType,
|
|
||||||
sourceX: sourceX,
|
|
||||||
sourceY: sourceY,
|
|
||||||
targetX: targetX,
|
|
||||||
targetY: targetY,
|
|
||||||
markerEndId: props.markerEndId,
|
|
||||||
sourceHandleId: props.edge.sourceHandle,
|
|
||||||
targetHandleId: props.edge.targetHandle
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
)}
|
|
||||||
<g onMousedown={onEdgeUpdaterSourceMouseDown} onMouseenter={onEdgeUpdaterMouseEnter} onMouseout={onEdgeUpdaterMouseOut}>
|
|
||||||
<EdgeAnchor position={sourcePosition} centerX={sourceX} centerY={sourceY} radius={props.edgeUpdaterRadius} />
|
|
||||||
</g>
|
|
||||||
<g onMousedown={onEdgeUpdaterTargetMouseDown} onMouseenter={onEdgeUpdaterMouseEnter} onMouseout={onEdgeUpdaterMouseOut}>
|
|
||||||
<EdgeAnchor position={sourcePosition} centerX={sourceX} centerY={sourceY} radius={props.edgeUpdaterRadius} />
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import EdgeAnchor from './EdgeAnchor.vue'
|
||||||
|
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
|
||||||
|
import { getEdgePositions, getHandle, getSourceTargetNodes, isEdgeVisible } from '~/container/EdgeRenderer/utils'
|
||||||
|
import { isEdge } from '~/utils/graph'
|
||||||
|
import { ConnectionMode, Dimensions, Edge, EdgeType, Elements, Position, RevueFlowStore, Transform } from '~/types'
|
||||||
|
import { onMouseDown } from '~/components/Handle/handler'
|
||||||
|
|
||||||
|
interface EdgeProps {
|
||||||
|
type: EdgeType
|
||||||
|
edge: Edge
|
||||||
|
nodes: ReturnType<typeof getSourceTargetNodes>
|
||||||
|
dimensions: Dimensions
|
||||||
|
transform: Transform
|
||||||
|
selectedElements?: Elements | null
|
||||||
|
elementsSelectable?: boolean
|
||||||
|
onlyRenderVisibleElements?: boolean
|
||||||
|
connectionMode?: ConnectionMode
|
||||||
|
markerEndId?: string
|
||||||
|
edgeUpdaterRadius?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<EdgeProps>(), {
|
||||||
|
elementsSelectable: true,
|
||||||
|
onlyRenderVisibleElements: false,
|
||||||
|
selectedElements: () => null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const store = inject<RevueFlowStore>('store')!
|
||||||
|
const hooks = inject<RevueFlowHooks>('hooks')!
|
||||||
|
|
||||||
|
hooks.connect.on((connection) => {
|
||||||
|
hooks.edgeUpdate.trigger({ edge: props.edge, connection })
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!props.nodes.value.sourceNode) {
|
||||||
|
console.warn(`couldn't create edge for source id: ${props.edge.source}; edge id: ${props.edge.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.nodes.value.targetNode) {
|
||||||
|
console.warn(`couldn't create edge for target id: ${props.edge.target}; edge id: ${props.edge.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// when connection type is loose we can define all handles as sources
|
||||||
|
const targetNodeHandles =
|
||||||
|
props.connectionMode === ConnectionMode.Strict
|
||||||
|
? props.nodes.value.targetNode?.__rf.handleBounds.target
|
||||||
|
: props.nodes.value.targetNode?.__rf.handleBounds.target || props.nodes.value.targetNode?.__rf.handleBounds.source
|
||||||
|
|
||||||
|
const sourceHandle =
|
||||||
|
props.nodes.value.sourceNode &&
|
||||||
|
getHandle(props.nodes.value.sourceNode.__rf.handleBounds.source, props.edge.sourceHandle || null)
|
||||||
|
const targetHandle = getHandle(targetNodeHandles, props.edge.targetHandle || null)
|
||||||
|
const sourcePosition = sourceHandle ? sourceHandle.position : Position.Bottom
|
||||||
|
const targetPosition = targetHandle ? targetHandle.position : Position.Top
|
||||||
|
|
||||||
|
const isSelected = computed(() => props.selectedElements?.some((elm) => isEdge(elm) && elm.id === props.edge.id) || false)
|
||||||
|
|
||||||
|
const edgeElement = computed(() => {
|
||||||
|
const el: Edge = {
|
||||||
|
id: props.edge.id || '',
|
||||||
|
source: props.edge.source,
|
||||||
|
target: props.edge.target,
|
||||||
|
type: props.edge.type,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.edge.sourceHandle) {
|
||||||
|
el.sourceHandle = props.edge.sourceHandle
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.edge.targetHandle) {
|
||||||
|
el.targetHandle = props.edge.targetHandle
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof props.edge.data !== 'undefined') {
|
||||||
|
el.data = props.edge.data
|
||||||
|
}
|
||||||
|
|
||||||
|
return el
|
||||||
|
})
|
||||||
|
|
||||||
|
const onEdgeClick = (event: MouseEvent) => {
|
||||||
|
if (props.elementsSelectable) {
|
||||||
|
store.unsetNodesSelection()
|
||||||
|
store.addSelectedElements([edgeElement.value])
|
||||||
|
}
|
||||||
|
|
||||||
|
hooks.edgeClick.trigger({ event, edge: edgeElement.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEdgeContextMenu = (event: MouseEvent) =>
|
||||||
|
hooks.edgeContextMenu.trigger({
|
||||||
|
event,
|
||||||
|
edge: edgeElement.value,
|
||||||
|
})
|
||||||
|
|
||||||
|
const onEdgeMouseEnter = (event: MouseEvent) => hooks.edgeMouseEnter.trigger({ event, edge: edgeElement.value })
|
||||||
|
|
||||||
|
const onEdgeMouseMove = (event: MouseEvent) => hooks.edgeMouseMove.trigger({ event, edge: edgeElement.value })
|
||||||
|
|
||||||
|
const onEdgeMouseLeave = (event: MouseEvent) => hooks.edgeMouseLeave.trigger({ event, edge: edgeElement.value })
|
||||||
|
|
||||||
|
const handleEdgeUpdater = (event: MouseEvent, isSourceHandle: boolean) => {
|
||||||
|
const nodeId = isSourceHandle ? props.edge.target : props.edge.source
|
||||||
|
const handleId = isSourceHandle ? props.edge.targetHandle : props.edge.sourceHandle
|
||||||
|
const isValidConnection = () => true
|
||||||
|
const isTarget = isSourceHandle
|
||||||
|
|
||||||
|
hooks.edgeUpdateStart.trigger({ event, edge: edgeElement.value })
|
||||||
|
handleId &&
|
||||||
|
onMouseDown(event, store, hooks, handleId, nodeId, isTarget, isValidConnection, isSourceHandle ? 'target' : 'source')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEdgeUpdaterSourceMouseDown = (event: MouseEvent) => {
|
||||||
|
handleEdgeUpdater(event, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEdgeUpdaterTargetMouseDown = (event: MouseEvent) => {
|
||||||
|
handleEdgeUpdater(event, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updating = ref<boolean>(false)
|
||||||
|
const onEdgeUpdaterMouseEnter = () => (updating.value = true)
|
||||||
|
const onEdgeUpdaterMouseOut = () => (updating.value = false)
|
||||||
|
const edgePos = computed(() =>
|
||||||
|
getEdgePositions(
|
||||||
|
props.nodes.value.sourceNode,
|
||||||
|
sourceHandle,
|
||||||
|
sourcePosition,
|
||||||
|
props.nodes.value.targetNode,
|
||||||
|
targetHandle,
|
||||||
|
targetPosition,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
const isVisible = ({ sourceX, sourceY, targetX, targetY }: ReturnType<typeof getEdgePositions>) => {
|
||||||
|
return props.onlyRenderVisibleElements
|
||||||
|
? isEdgeVisible({
|
||||||
|
sourcePos: { x: sourceX, y: sourceY },
|
||||||
|
targetPos: { x: targetX, y: targetY },
|
||||||
|
width: props.dimensions.width,
|
||||||
|
height: props.dimensions.height,
|
||||||
|
transform: props.transform,
|
||||||
|
})
|
||||||
|
: true
|
||||||
|
}
|
||||||
|
const visible = computed(() => !props.edge.isHidden && isVisible(edgePos.value))
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<g
|
||||||
|
v-if="visible"
|
||||||
|
:class="[
|
||||||
|
'revue-flow__edge',
|
||||||
|
`revue-flow__edge-${props.type.name || 'default'}`,
|
||||||
|
{
|
||||||
|
selected: isSelected.value,
|
||||||
|
animated: props.edge.animated,
|
||||||
|
inactive: !store.elementsSelectable,
|
||||||
|
updating: updating.value,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
@click="onEdgeClick"
|
||||||
|
@contextmenu="onEdgeContextMenu"
|
||||||
|
@mouseenter="onEdgeMouseEnter"
|
||||||
|
@mousemove="onEdgeMouseMove"
|
||||||
|
@mouseleave="onEdgeMouseLeave"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="props.type"
|
||||||
|
v-bind="{
|
||||||
|
id: props.edge.id,
|
||||||
|
source: props.edge.source,
|
||||||
|
target: props.edge.target,
|
||||||
|
selected: isSelected.value,
|
||||||
|
animated: props.edge.animated,
|
||||||
|
label: props.edge.label,
|
||||||
|
labelStyle: props.edge.labelStyle,
|
||||||
|
labelShowBg: props.edge.labelShowBg,
|
||||||
|
labelBgStyle: props.edge.labelBgStyle,
|
||||||
|
labelBgPadding: props.edge.labelBgPadding,
|
||||||
|
labelBgBorderRadius: props.edge.labelBgBorderRadius,
|
||||||
|
data: props.edge.data,
|
||||||
|
style: props.edge.style,
|
||||||
|
arrowHeadType: props.edge.arrowHeadType,
|
||||||
|
sourceX: edgePos.sourceX,
|
||||||
|
sourceY: edgePos.sourceY,
|
||||||
|
targetX: edgePos.targetX,
|
||||||
|
targetY: edgePos.targetY,
|
||||||
|
markerEndId: props.markerEndId,
|
||||||
|
sourceHandleId: props.edge.sourceHandle,
|
||||||
|
targetHandleId: props.edge.targetHandle,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<g @mousedown="onEdgeUpdaterSourceMouseDown" @mouseenter="onEdgeUpdaterMouseEnter" @mouseout="onEdgeUpdaterMouseOut">
|
||||||
|
<EdgeAnchor
|
||||||
|
:position="sourcePosition"
|
||||||
|
:center-x="edgePos.sourceX"
|
||||||
|
:center-y="edgePos.sourceY"
|
||||||
|
:radius="props.edgeUpdaterRadius"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<g @mousedown="onEdgeUpdaterTargetMouseDown" @mouseenter="onEdgeUpdaterMouseEnter" @mouseout="onEdgeUpdaterMouseOut">
|
||||||
|
<EdgeAnchor
|
||||||
|
:position="sourcePosition"
|
||||||
|
:center-x="edgePos.sourceX"
|
||||||
|
:center-y="edgePos.sourceY"
|
||||||
|
:radius="props.edgeUpdaterRadius"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</template>
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import { Position } from '../../types'
|
|
||||||
import { defineComponent, HTMLAttributes, PropType } from 'vue'
|
|
||||||
|
|
||||||
const shiftX = (x: number, shift: number, position: Position): number => {
|
|
||||||
if (position === Position.Left) return x - shift
|
|
||||||
if (position === Position.Right) return x + shift
|
|
||||||
return x
|
|
||||||
}
|
|
||||||
|
|
||||||
const shiftY = (y: number, shift: number, position: Position): number => {
|
|
||||||
if (position === Position.Top) return y - shift
|
|
||||||
if (position === Position.Bottom) return y + shift
|
|
||||||
return y
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EdgeAnchorProps extends HTMLAttributes {
|
|
||||||
position: Position
|
|
||||||
centerX: number
|
|
||||||
centerY: number
|
|
||||||
radius?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
position: {
|
|
||||||
type: String as PropType<EdgeAnchorProps['position']>,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
centerX: {
|
|
||||||
type: Number as PropType<EdgeAnchorProps['centerX']>,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
centerY: {
|
|
||||||
type: Number as PropType<EdgeAnchorProps['centerY']>,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
radius: {
|
|
||||||
type: Number as PropType<EdgeAnchorProps['radius']>,
|
|
||||||
required: false,
|
|
||||||
default: 10
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
return () => (
|
|
||||||
<circle
|
|
||||||
class="revue-flow__edgeupdater"
|
|
||||||
cx={shiftX(props.centerX, props.radius || 10, props.position)}
|
|
||||||
cy={shiftY(props.centerY, props.radius || 10, props.position)}
|
|
||||||
r={props.radius || 10}
|
|
||||||
stroke="transparent"
|
|
||||||
fill="transparent"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { HTMLAttributes } from 'vue'
|
||||||
|
import { Position } from '~/types'
|
||||||
|
|
||||||
|
const shiftX = (x: number, shift: number, position: Position): number => {
|
||||||
|
if (position === Position.Left) return x - shift
|
||||||
|
if (position === Position.Right) return x + shift
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
|
const shiftY = (y: number, shift: number, position: Position): number => {
|
||||||
|
if (position === Position.Top) return y - shift
|
||||||
|
if (position === Position.Bottom) return y + shift
|
||||||
|
return y
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EdgeAnchorProps extends HTMLAttributes {
|
||||||
|
position: Position
|
||||||
|
centerX: number
|
||||||
|
centerY: number
|
||||||
|
radius?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<EdgeAnchorProps>(), {
|
||||||
|
radius: 10,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<circle
|
||||||
|
class="revue-flow__edgeupdater"
|
||||||
|
:cx="shiftX(props.centerX, props.radius, props.position)"
|
||||||
|
:cy="shiftY(props.centerY, props.radius, props.position)"
|
||||||
|
:r="props.radius"
|
||||||
|
stroke="transparent"
|
||||||
|
fill="transparent"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import { EdgeTextProps, Rect } from '../../types'
|
|
||||||
import { defineComponent, PropType, ref, watchEffect } from 'vue'
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
x: {
|
|
||||||
type: Number as PropType<EdgeTextProps['x']>,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
type: Number as PropType<EdgeTextProps['y']>,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
type: [String, Object] as PropType<EdgeTextProps['label']>,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
labelStyle: {
|
|
||||||
type: Object as PropType<EdgeTextProps['labelStyle']>,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
labelShowBg: {
|
|
||||||
type: Boolean as PropType<EdgeTextProps['labelShowBg']>,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
labelBgStyle: {
|
|
||||||
type: Object as PropType<EdgeTextProps['labelBgStyle']>,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
labelBgPadding: {
|
|
||||||
type: Array as unknown as PropType<[number, number]>,
|
|
||||||
default: () => [2, 4]
|
|
||||||
},
|
|
||||||
labelBgBorderRadius: {
|
|
||||||
type: Number as PropType<EdgeTextProps['labelBgBorderRadius']>,
|
|
||||||
default: 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setup(props, { slots }) {
|
|
||||||
const edgeRef = ref<SVGTextElement | null>(null)
|
|
||||||
const edgeTextBox = ref<Rect>({ x: 0, y: 0, width: 0, height: 0 })
|
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
const textBbox = edgeRef.value?.getBBox()
|
|
||||||
|
|
||||||
if (textBbox) {
|
|
||||||
edgeTextBox.value = {
|
|
||||||
x: textBbox.x,
|
|
||||||
y: textBbox.y,
|
|
||||||
width: textBbox.width,
|
|
||||||
height: textBbox.height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => (
|
|
||||||
<g
|
|
||||||
transform={`translate(${props.x - edgeTextBox.value.width / 2} ${props.y - edgeTextBox.value.height / 2})`}
|
|
||||||
class="revue-flow__edge-textwrapper"
|
|
||||||
>
|
|
||||||
{props.labelShowBg && (
|
|
||||||
<rect
|
|
||||||
width={edgeTextBox.value.width + 2 * props.labelBgPadding[0] + 'px'}
|
|
||||||
height={edgeTextBox.value.height + 2 * props.labelBgPadding[1] + 'px'}
|
|
||||||
x={-props.labelBgPadding[0]}
|
|
||||||
y={-props.labelBgPadding[1]}
|
|
||||||
class="revue-flow__edge-textbg"
|
|
||||||
style={props.labelBgStyle}
|
|
||||||
rx={props.labelBgBorderRadius}
|
|
||||||
ry={props.labelBgBorderRadius}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<text ref={edgeRef} class="revue-flow__edge-text" y={edgeTextBox.value.height / 2} dy="0.3em" style={props.labelStyle}>
|
|
||||||
{props.label}
|
|
||||||
</text>
|
|
||||||
{slots.default ? slots.default() : ''}
|
|
||||||
</g>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { HTMLAttributes, ref, VNode, watchEffect } from 'vue'
|
||||||
|
import { Rect } from '~/types'
|
||||||
|
|
||||||
|
interface EdgeTextProps extends HTMLAttributes {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
label?: string | VNode
|
||||||
|
labelStyle?: any
|
||||||
|
labelShowBg?: boolean
|
||||||
|
labelBgStyle?: any
|
||||||
|
labelBgPadding?: [number, number]
|
||||||
|
labelBgBorderRadius?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<EdgeTextProps>(), {
|
||||||
|
labelStyle: () => ({}),
|
||||||
|
labelShowBg: true,
|
||||||
|
labelBgStyle: () => ({}),
|
||||||
|
labelBgPadding: () => [2, 4],
|
||||||
|
labelBgBorderRadius: 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
const edgeRef = templateRef<SVGTextElement>('edge-text', null)
|
||||||
|
const edgeTextBox = ref<Rect>({ x: 0, y: 0, width: 0, height: 0 })
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
const textBbox = edgeRef.value?.getBBox()
|
||||||
|
|
||||||
|
if (textBbox) {
|
||||||
|
edgeTextBox.value = {
|
||||||
|
x: textBbox.x,
|
||||||
|
y: textBbox.y,
|
||||||
|
width: textBbox.width,
|
||||||
|
height: textBbox.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<g
|
||||||
|
:transform="`translate(${props.x - edgeTextBox.width / 2} ${props.y - edgeTextBox.height / 2})`"
|
||||||
|
class="revue-flow__edge-textwrapper"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
v-if="props.labelShowBg"
|
||||||
|
:width="edgeTextBox.width + 2 * props.labelBgPadding[0] + 'px'"
|
||||||
|
:height="edgeTextBox.height + 2 * props.labelBgPadding[1] + 'px'"
|
||||||
|
:x="-props.labelBgPadding[0]"
|
||||||
|
:y="-props.labelBgPadding[1]"
|
||||||
|
class="revue-flow__edge-textbg"
|
||||||
|
:style="props.labelBgStyle"
|
||||||
|
:rx="props.labelBgBorderRadius"
|
||||||
|
:ry="props.labelBgBorderRadius"
|
||||||
|
/>
|
||||||
|
<text ref="edge-text" class="revue-flow__edge-text" :y="edgeTextBox.height / 2" dy="0.3em" :style="props.labelStyle">
|
||||||
|
{{ props.label }}
|
||||||
|
</text>
|
||||||
|
<slot></slot>
|
||||||
|
</g>
|
||||||
|
</template>
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
import { defineComponent } from 'vue'
|
|
||||||
import EdgeText from './EdgeText'
|
|
||||||
import { getMarkerEnd, getCenter, EdgeSmoothProps, GetCenterParams } from './utils'
|
|
||||||
import { ArrowHeadType, EdgeType, Position } from '../../types'
|
|
||||||
import { reactify } from '@vueuse/core'
|
|
||||||
import { getBezierPath } from './BezierEdge'
|
|
||||||
|
|
||||||
// These are some helper methods for drawing the round corners
|
|
||||||
// The name indicates the direction of the path. "bottomLeftCorner" goes
|
|
||||||
// from bottom to the left and "leftBottomCorner" goes from left to the bottom.
|
|
||||||
// We have to consider the direction of the paths because of the animated lines.
|
|
||||||
const bottomLeftCorner = (x: number, y: number, size: number): string => `L ${x},${y - size}Q ${x},${y} ${x + size},${y}`
|
|
||||||
const leftBottomCorner = (x: number, y: number, size: number): string => `L ${x + size},${y}Q ${x},${y} ${x},${y - size}`
|
|
||||||
const bottomRightCorner = (x: number, y: number, size: number): string => `L ${x},${y - size}Q ${x},${y} ${x - size},${y}`
|
|
||||||
const rightBottomCorner = (x: number, y: number, size: number): string => `L ${x - size},${y}Q ${x},${y} ${x},${y - size}`
|
|
||||||
const leftTopCorner = (x: number, y: number, size: number): string => `L ${x + size},${y}Q ${x},${y} ${x},${y + size}`
|
|
||||||
const topLeftCorner = (x: number, y: number, size: number): string => `L ${x},${y + size}Q ${x},${y} ${x + size},${y}`
|
|
||||||
const topRightCorner = (x: number, y: number, size: number): string => `L ${x},${y + size}Q ${x},${y} ${x - size},${y}`
|
|
||||||
const rightTopCorner = (x: number, y: number, size: number): string => `L ${x - size},${y}Q ${x},${y} ${x},${y + size}`
|
|
||||||
|
|
||||||
interface GetSmoothStepPathParams {
|
|
||||||
sourceX: number
|
|
||||||
sourceY: number
|
|
||||||
sourcePosition?: Position
|
|
||||||
targetX: number
|
|
||||||
targetY: number
|
|
||||||
targetPosition?: Position
|
|
||||||
borderRadius?: number
|
|
||||||
centerX?: number
|
|
||||||
centerY?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSmoothStepPath({
|
|
||||||
sourceX,
|
|
||||||
sourceY,
|
|
||||||
sourcePosition = Position.Bottom,
|
|
||||||
targetX,
|
|
||||||
targetY,
|
|
||||||
targetPosition = Position.Top,
|
|
||||||
borderRadius = 5,
|
|
||||||
centerX,
|
|
||||||
centerY
|
|
||||||
}: GetSmoothStepPathParams): string {
|
|
||||||
const [_centerX, _centerY, offsetX, offsetY] = getCenter({ sourceX, sourceY, targetX, targetY })
|
|
||||||
const cornerWidth = Math.min(borderRadius, Math.abs(targetX - sourceX))
|
|
||||||
const cornerHeight = Math.min(borderRadius, Math.abs(targetY - sourceY))
|
|
||||||
const cornerSize = Math.min(cornerWidth, cornerHeight, offsetX, offsetY)
|
|
||||||
const leftAndRight = [Position.Left, Position.Right]
|
|
||||||
const cX = typeof centerX !== 'undefined' ? centerX : _centerX
|
|
||||||
const cY = typeof centerY !== 'undefined' ? centerY : _centerY
|
|
||||||
|
|
||||||
let firstCornerPath
|
|
||||||
let secondCornerPath
|
|
||||||
|
|
||||||
if (sourceX <= targetX) {
|
|
||||||
firstCornerPath = sourceY <= targetY ? bottomLeftCorner(sourceX, cY, cornerSize) : topLeftCorner(sourceX, cY, cornerSize)
|
|
||||||
secondCornerPath = sourceY <= targetY ? rightTopCorner(targetX, cY, cornerSize) : rightBottomCorner(targetX, cY, cornerSize)
|
|
||||||
} else {
|
|
||||||
firstCornerPath = sourceY < targetY ? bottomRightCorner(sourceX, cY, cornerSize) : topRightCorner(sourceX, cY, cornerSize)
|
|
||||||
secondCornerPath = sourceY < targetY ? leftTopCorner(targetX, cY, cornerSize) : leftBottomCorner(targetX, cY, cornerSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
|
|
||||||
if (sourceX <= targetX) {
|
|
||||||
firstCornerPath = sourceY <= targetY ? rightTopCorner(cX, sourceY, cornerSize) : rightBottomCorner(cX, sourceY, cornerSize)
|
|
||||||
secondCornerPath = sourceY <= targetY ? bottomLeftCorner(cX, targetY, cornerSize) : topLeftCorner(cX, targetY, cornerSize)
|
|
||||||
} else if (sourcePosition === Position.Right && targetPosition === Position.Left) {
|
|
||||||
// and sourceX > targetX
|
|
||||||
firstCornerPath = sourceY <= targetY ? leftTopCorner(cX, sourceY, cornerSize) : leftBottomCorner(cX, sourceY, cornerSize)
|
|
||||||
secondCornerPath = sourceY <= targetY ? bottomRightCorner(cX, targetY, cornerSize) : topRightCorner(cX, targetY, cornerSize)
|
|
||||||
}
|
|
||||||
} else if (leftAndRight.includes(sourcePosition) && !leftAndRight.includes(targetPosition)) {
|
|
||||||
if (sourceX <= targetX) {
|
|
||||||
firstCornerPath =
|
|
||||||
sourceY <= targetY ? rightTopCorner(targetX, sourceY, cornerSize) : rightBottomCorner(targetX, sourceY, cornerSize)
|
|
||||||
} else {
|
|
||||||
firstCornerPath =
|
|
||||||
sourceY <= targetY ? leftTopCorner(targetX, sourceY, cornerSize) : leftBottomCorner(targetX, sourceY, cornerSize)
|
|
||||||
}
|
|
||||||
secondCornerPath = ''
|
|
||||||
} else if (!leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
|
|
||||||
if (sourceX <= targetX) {
|
|
||||||
firstCornerPath =
|
|
||||||
sourceY <= targetY ? bottomLeftCorner(sourceX, targetY, cornerSize) : topLeftCorner(sourceX, targetY, cornerSize)
|
|
||||||
} else {
|
|
||||||
firstCornerPath =
|
|
||||||
sourceY <= targetY ? bottomRightCorner(sourceX, targetY, cornerSize) : topRightCorner(sourceX, targetY, cornerSize)
|
|
||||||
}
|
|
||||||
secondCornerPath = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
return `M ${sourceX},${sourceY}${firstCornerPath}${secondCornerPath}L ${targetX},${targetY}`
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
inheritAttrs: false,
|
|
||||||
props: {
|
|
||||||
...EdgeSmoothProps
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const centered = reactify(({ sourceX, sourceY, targetX, targetY, targetPosition, sourcePosition }: GetCenterParams) => {
|
|
||||||
return getCenter({
|
|
||||||
sourceX: sourceX,
|
|
||||||
sourceY: sourceY,
|
|
||||||
targetX: targetX,
|
|
||||||
targetY: targetY,
|
|
||||||
sourcePosition: sourcePosition,
|
|
||||||
targetPosition: targetPosition
|
|
||||||
})
|
|
||||||
})
|
|
||||||
const path = reactify(({ sourceX, sourceY, targetX, targetY, targetPosition, sourcePosition }: GetSmoothStepPathParams) => {
|
|
||||||
return getBezierPath({
|
|
||||||
sourceX: sourceX,
|
|
||||||
sourceY: sourceY,
|
|
||||||
targetX: targetX,
|
|
||||||
targetY: targetY,
|
|
||||||
targetPosition: targetPosition,
|
|
||||||
sourcePosition: sourcePosition
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const markerEnd = reactify((arrowHeadType?: ArrowHeadType, markerEndId?: string) => getMarkerEnd(arrowHeadType, markerEndId))
|
|
||||||
|
|
||||||
return () => (
|
|
||||||
<>
|
|
||||||
<path
|
|
||||||
class="revue-flow__edge-path"
|
|
||||||
style={props.style}
|
|
||||||
d={path({ ...props }).value}
|
|
||||||
marker-end={markerEnd(props.arrowHeadType, props.markerEndId).value}
|
|
||||||
/>
|
|
||||||
{props.label ? (
|
|
||||||
<EdgeText
|
|
||||||
x={centered({ ...props }).value[0]}
|
|
||||||
y={centered({ ...props }).value[1]}
|
|
||||||
label={props.label}
|
|
||||||
labelStyle={props.labelStyle}
|
|
||||||
labelShowBg={props.labelShowBg}
|
|
||||||
labelBgStyle={props.labelBgStyle}
|
|
||||||
labelBgPadding={props.labelBgPadding}
|
|
||||||
labelBgBorderRadius={props.labelBgBorderRadius}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}) as unknown as EdgeType
|
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { VNode } from 'vue'
|
||||||
|
import EdgeText from './EdgeText.vue'
|
||||||
|
import { ArrowHeadType, ElementId, Position } from '~/types'
|
||||||
|
import { getCenter, getMarkerEnd, getSmoothStepPath } from '~/components/Edges/utils'
|
||||||
|
|
||||||
|
export interface EdgeSmoothStepProps<T = any> {
|
||||||
|
id: ElementId
|
||||||
|
source: ElementId
|
||||||
|
target: ElementId
|
||||||
|
sourceX: number
|
||||||
|
sourceY: number
|
||||||
|
targetX: number
|
||||||
|
targetY: number
|
||||||
|
selected?: boolean
|
||||||
|
animated?: boolean
|
||||||
|
sourcePosition?: Position
|
||||||
|
targetPosition?: Position
|
||||||
|
label?: string | VNode
|
||||||
|
labelStyle?: any
|
||||||
|
labelShowBg?: boolean
|
||||||
|
labelBgStyle?: any
|
||||||
|
labelBgPadding?: [number, number]
|
||||||
|
labelBgBorderRadius?: number
|
||||||
|
style?: any
|
||||||
|
arrowHeadType?: ArrowHeadType
|
||||||
|
markerEndId?: string
|
||||||
|
data?: T
|
||||||
|
borderRadius?: number
|
||||||
|
sourceHandleId?: ElementId | null
|
||||||
|
targetHandleId?: ElementId | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<EdgeSmoothStepProps>(), {
|
||||||
|
selected: false,
|
||||||
|
sourcePosition: Position.Bottom,
|
||||||
|
targetPosition: Position.Top,
|
||||||
|
label: () => '',
|
||||||
|
labelStyle: () => ({}),
|
||||||
|
labelShowBg: true,
|
||||||
|
labelBgStyle: () => ({}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const centered = computed(() =>
|
||||||
|
getCenter({
|
||||||
|
...props,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const path = computed(() =>
|
||||||
|
getSmoothStepPath({
|
||||||
|
...props,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const markerEnd = computed(() => getMarkerEnd(props.arrowHeadType, props.markerEndId))
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<path class="revue-flow__edge-path" :style="props.style" :d="path" :marker-end="markerEnd" />
|
||||||
|
<EdgeText
|
||||||
|
v-if="props.label"
|
||||||
|
:x="centered[0]"
|
||||||
|
:y="centered[1]"
|
||||||
|
:label="props.label"
|
||||||
|
:label-style="props.labelStyle"
|
||||||
|
:label-show-bg="props.labelShowBg"
|
||||||
|
:label-bg-style="props.labelBgStyle"
|
||||||
|
:label-bg-padding="props.labelBgPadding"
|
||||||
|
:label-bg-border-radius="props.labelBgBorderRadius"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { defineComponent } from 'vue'
|
|
||||||
import SmoothStepEdge from './SmoothStepEdge'
|
|
||||||
import { EdgeSmoothProps } from './utils'
|
|
||||||
import { EdgeType } from '../../types'
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
components: { SmoothStepEdge },
|
|
||||||
inheritAttrs: false,
|
|
||||||
props: {
|
|
||||||
...EdgeSmoothProps
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
return () => <SmoothStepEdge {...props} borderRadius={0} />
|
|
||||||
}
|
|
||||||
}) as EdgeType
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { VNode } from 'vue'
|
||||||
|
import SmoothStepEdge from './SmoothStepEdge.vue'
|
||||||
|
import { ArrowHeadType, ElementId, Position } from '~/types'
|
||||||
|
|
||||||
|
export interface EdgeStepProps<T = any> {
|
||||||
|
id: ElementId
|
||||||
|
source: ElementId
|
||||||
|
target: ElementId
|
||||||
|
sourceX: number
|
||||||
|
sourceY: number
|
||||||
|
targetX: number
|
||||||
|
targetY: number
|
||||||
|
selected?: boolean
|
||||||
|
animated?: boolean
|
||||||
|
sourcePosition?: Position
|
||||||
|
targetPosition?: Position
|
||||||
|
label?: string | VNode
|
||||||
|
labelStyle?: any
|
||||||
|
labelShowBg?: boolean
|
||||||
|
labelBgStyle?: any
|
||||||
|
labelBgPadding?: [number, number]
|
||||||
|
labelBgBorderRadius?: number
|
||||||
|
style?: any
|
||||||
|
arrowHeadType?: ArrowHeadType
|
||||||
|
markerEndId?: string
|
||||||
|
data?: T
|
||||||
|
sourceHandleId?: ElementId | null
|
||||||
|
targetHandleId?: ElementId | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<EdgeStepProps>(), {
|
||||||
|
selected: false,
|
||||||
|
sourcePosition: Position.Bottom,
|
||||||
|
targetPosition: Position.Top,
|
||||||
|
label: () => '',
|
||||||
|
labelStyle: () => ({}),
|
||||||
|
labelShowBg: true,
|
||||||
|
labelBgStyle: () => ({}),
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<SmoothStepEdge v-bind="props" :border-radius="0" />
|
||||||
|
</template>
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import { defineComponent } from 'vue'
|
|
||||||
import EdgeText from './EdgeText'
|
|
||||||
import { getMarkerEnd, DefaultEdgeProps } from './utils'
|
|
||||||
import { reactify } from '@vueuse/core'
|
|
||||||
import { ArrowHeadType, EdgeType } from '../../types'
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
inheritAttrs: false,
|
|
||||||
props: {
|
|
||||||
...DefaultEdgeProps
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const centerY = reactify((targetY: number, sourceY: number) => {
|
|
||||||
const yOffset = Math.abs(targetY - sourceY) / 2
|
|
||||||
return targetY < sourceY ? targetY + yOffset : targetY - yOffset
|
|
||||||
})
|
|
||||||
const centerX = reactify((targetX: number, sourceX: number) => {
|
|
||||||
const xOffset = Math.abs(targetX - sourceX) / 2
|
|
||||||
return targetX < sourceX ? targetX + xOffset : targetX - xOffset
|
|
||||||
})
|
|
||||||
|
|
||||||
const markerEnd = reactify((arrowHeadType?: ArrowHeadType, markerEndId?: string) => getMarkerEnd(arrowHeadType, markerEndId))
|
|
||||||
|
|
||||||
return () => (
|
|
||||||
<>
|
|
||||||
<path
|
|
||||||
style={props.style}
|
|
||||||
class="revue-flow__edge-path"
|
|
||||||
d={`M ${props.sourceX},${props.sourceY}L ${props.targetX},${props.targetY}`}
|
|
||||||
marker-end={markerEnd(props.arrowHeadType, props.markerEndId).value}
|
|
||||||
/>
|
|
||||||
{props.label ? (
|
|
||||||
<EdgeText
|
|
||||||
x={centerX(props.targetX, props.sourceX).value}
|
|
||||||
y={centerY(props.targetY, props.sourceY).value}
|
|
||||||
label={props.label}
|
|
||||||
labelStyle={props.labelStyle}
|
|
||||||
labelShowBg={props.labelShowBg}
|
|
||||||
labelBgStyle={props.labelBgStyle}
|
|
||||||
labelBgPadding={props.labelBgPadding}
|
|
||||||
labelBgBorderRadius={props.labelBgBorderRadius}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}) as unknown as EdgeType
|
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { VNode } from 'vue'
|
||||||
|
import EdgeText from './EdgeText.vue'
|
||||||
|
import { ArrowHeadType, ElementId, Position } from '~/types'
|
||||||
|
import { getMarkerEnd, getBezierPath } from '~/components/Edges/utils'
|
||||||
|
|
||||||
|
interface StraightEdgeProps<T = any> {
|
||||||
|
id: ElementId
|
||||||
|
source: ElementId
|
||||||
|
target: ElementId
|
||||||
|
sourceX: number
|
||||||
|
sourceY: number
|
||||||
|
targetX: number
|
||||||
|
targetY: number
|
||||||
|
selected?: boolean
|
||||||
|
animated?: boolean
|
||||||
|
sourcePosition?: Position
|
||||||
|
targetPosition?: Position
|
||||||
|
label?: string | VNode
|
||||||
|
labelStyle?: any
|
||||||
|
labelShowBg?: boolean
|
||||||
|
labelBgStyle?: any
|
||||||
|
labelBgPadding?: [number, number]
|
||||||
|
labelBgBorderRadius?: number
|
||||||
|
style?: any
|
||||||
|
arrowHeadType?: ArrowHeadType
|
||||||
|
markerEndId?: string
|
||||||
|
data?: T
|
||||||
|
sourceHandleId?: ElementId | null
|
||||||
|
targetHandleId?: ElementId | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<StraightEdgeProps>(), {
|
||||||
|
selected: false,
|
||||||
|
sourcePosition: Position.Bottom,
|
||||||
|
targetPosition: Position.Top,
|
||||||
|
label: () => '',
|
||||||
|
labelStyle: () => ({}),
|
||||||
|
labelShowBg: true,
|
||||||
|
labelBgStyle: () => ({}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const centerY = computed(() => {
|
||||||
|
const yOffset = Math.abs(props.targetY - props.sourceY) / 2
|
||||||
|
return props.targetY < props.sourceY ? props.targetY + yOffset : props.targetY - yOffset
|
||||||
|
})
|
||||||
|
const centerX = computed(() => {
|
||||||
|
const xOffset = Math.abs(props.targetX - props.sourceX) / 2
|
||||||
|
return props.targetX < props.sourceX ? props.targetX + xOffset : props.targetX - xOffset
|
||||||
|
})
|
||||||
|
const path = computed(() =>
|
||||||
|
getBezierPath({
|
||||||
|
...props,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const markerEnd = computed(() => getMarkerEnd(props.arrowHeadType, props.markerEndId))
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<path
|
||||||
|
:style="props.style"
|
||||||
|
class="revue-flow__edge-path"
|
||||||
|
:d="`M ${props.sourceX},${props.sourceY}L ${props.targetX},${props.targetY}`"
|
||||||
|
:marker-end="markerEnd"
|
||||||
|
/>
|
||||||
|
<EdgeText
|
||||||
|
v-if="props.label"
|
||||||
|
:x="centerX"
|
||||||
|
:y="centerY"
|
||||||
|
:label="props.label"
|
||||||
|
:label-style="props.labelStyle"
|
||||||
|
:label-show-bg="props.labelShowBg"
|
||||||
|
:label-bg-style="props.labelBgStyle"
|
||||||
|
:label-bg-padding="props.labelBgPadding"
|
||||||
|
:label-bg-border-radius="props.labelBgBorderRadius"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export { default as BezierEdge } from './BezierEdge'
|
export { default as BezierEdge } from './BezierEdge.vue'
|
||||||
export { default as StepEdge } from './StepEdge'
|
export { default as StepEdge } from './StepEdge.vue'
|
||||||
export { default as SmoothStepEdge } from './SmoothStepEdge'
|
export { default as SmoothStepEdge } from './SmoothStepEdge.vue'
|
||||||
export { default as StraightEdge } from './StraightEdge'
|
export { default as StraightEdge } from './StraightEdge.vue'
|
||||||
|
|||||||
+124
-120
@@ -1,5 +1,4 @@
|
|||||||
import { ArrowHeadType, Position, EdgeProps, EdgeSmoothStepProps } from '../../types'
|
import { ArrowHeadType, Position } from '~/types'
|
||||||
import { PropType } from 'vue'
|
|
||||||
|
|
||||||
export const getMarkerEnd = (arrowHeadType?: ArrowHeadType, markerEndId?: string): string => {
|
export const getMarkerEnd = (arrowHeadType?: ArrowHeadType, markerEndId?: string): string => {
|
||||||
if (typeof markerEndId !== 'undefined' && markerEndId) {
|
if (typeof markerEndId !== 'undefined' && markerEndId) {
|
||||||
@@ -26,7 +25,7 @@ export const getCenter = ({
|
|||||||
targetX,
|
targetX,
|
||||||
targetY,
|
targetY,
|
||||||
sourcePosition = Position.Bottom,
|
sourcePosition = Position.Bottom,
|
||||||
targetPosition = Position.Top
|
targetPosition = Position.Top,
|
||||||
}: GetCenterParams): [number, number, number, number] => {
|
}: GetCenterParams): [number, number, number, number] => {
|
||||||
const sourceIsLeftOrRight = LeftOrRight.includes(sourcePosition)
|
const sourceIsLeftOrRight = LeftOrRight.includes(sourcePosition)
|
||||||
const targetIsLeftOrRight = LeftOrRight.includes(targetPosition)
|
const targetIsLeftOrRight = LeftOrRight.includes(targetPosition)
|
||||||
@@ -54,124 +53,129 @@ export const getCenter = ({
|
|||||||
return [centerX, centerY, xOffset, yOffset]
|
return [centerX, centerY, xOffset, yOffset]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DefaultEdgeProps = {
|
interface GetBezierPathParams {
|
||||||
id: {
|
sourceX: number
|
||||||
type: String as PropType<EdgeProps['id']>,
|
sourceY: number
|
||||||
required: true,
|
sourcePosition?: Position
|
||||||
default: 0
|
targetX: number
|
||||||
},
|
targetY: number
|
||||||
sourceX: {
|
targetPosition?: Position
|
||||||
type: Number as PropType<EdgeProps['sourceX']>,
|
centerX?: number
|
||||||
required: true,
|
centerY?: number
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EdgeSmoothProps = {
|
export function getBezierPath({
|
||||||
...DefaultEdgeProps,
|
sourceX,
|
||||||
borderRadius: {
|
sourceY,
|
||||||
type: Number as PropType<EdgeSmoothStepProps['borderRadius']>,
|
sourcePosition = Position.Bottom,
|
||||||
required: false,
|
targetX,
|
||||||
default: 5
|
targetY,
|
||||||
|
targetPosition = Position.Top,
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
}: GetBezierPathParams): string {
|
||||||
|
const [_centerX, _centerY] = getCenter({ sourceX, sourceY, targetX, targetY })
|
||||||
|
const leftAndRight = [Position.Left, Position.Right]
|
||||||
|
|
||||||
|
const cX = typeof centerX !== 'undefined' ? centerX : _centerX
|
||||||
|
const cY = typeof centerY !== 'undefined' ? centerY : _centerY
|
||||||
|
|
||||||
|
let path = `M${sourceX},${sourceY} C${sourceX},${cY} ${targetX},${cY} ${targetX},${targetY}`
|
||||||
|
|
||||||
|
if (leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
|
||||||
|
path = `M${sourceX},${sourceY} C${cX},${sourceY} ${cX},${targetY} ${targetX},${targetY}`
|
||||||
|
} else if (leftAndRight.includes(targetPosition)) {
|
||||||
|
path = `M${sourceX},${sourceY} C${sourceX},${targetY} ${sourceX},${targetY} ${targetX},${targetY}`
|
||||||
|
} else if (leftAndRight.includes(sourcePosition)) {
|
||||||
|
path = `M${sourceX},${sourceY} C${targetX},${sourceY} ${targetX},${sourceY} ${targetX},${targetY}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
// These are some helper methods for drawing the round corners
|
||||||
|
// The name indicates the direction of the path. "bottomLeftCorner" goes
|
||||||
|
// from bottom to the left and "leftBottomCorner" goes from left to the bottom.
|
||||||
|
// We have to consider the direction of the paths because of the animated lines.
|
||||||
|
export const bottomLeftCorner = (x: number, y: number, size: number): string => `L ${x},${y - size}Q ${x},${y} ${x + size},${y}`
|
||||||
|
export const leftBottomCorner = (x: number, y: number, size: number): string => `L ${x + size},${y}Q ${x},${y} ${x},${y - size}`
|
||||||
|
export const bottomRightCorner = (x: number, y: number, size: number): string => `L ${x},${y - size}Q ${x},${y} ${x - size},${y}`
|
||||||
|
export const rightBottomCorner = (x: number, y: number, size: number): string => `L ${x - size},${y}Q ${x},${y} ${x},${y - size}`
|
||||||
|
export const leftTopCorner = (x: number, y: number, size: number): string => `L ${x + size},${y}Q ${x},${y} ${x},${y + size}`
|
||||||
|
export const topLeftCorner = (x: number, y: number, size: number): string => `L ${x},${y + size}Q ${x},${y} ${x + size},${y}`
|
||||||
|
export const topRightCorner = (x: number, y: number, size: number): string => `L ${x},${y + size}Q ${x},${y} ${x - size},${y}`
|
||||||
|
export const rightTopCorner = (x: number, y: number, size: number): string => `L ${x - size},${y}Q ${x},${y} ${x},${y + size}`
|
||||||
|
|
||||||
|
export interface GetSmoothStepPathParams {
|
||||||
|
sourceX: number
|
||||||
|
sourceY: number
|
||||||
|
sourcePosition?: Position
|
||||||
|
targetX: number
|
||||||
|
targetY: number
|
||||||
|
targetPosition?: Position
|
||||||
|
borderRadius?: number
|
||||||
|
centerX?: number
|
||||||
|
centerY?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSmoothStepPath({
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
sourcePosition = Position.Bottom,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
targetPosition = Position.Top,
|
||||||
|
borderRadius = 5,
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
}: GetSmoothStepPathParams): string {
|
||||||
|
const [_centerX, _centerY, offsetX, offsetY] = getCenter({ sourceX, sourceY, targetX, targetY })
|
||||||
|
const cornerWidth = Math.min(borderRadius, Math.abs(targetX - sourceX))
|
||||||
|
const cornerHeight = Math.min(borderRadius, Math.abs(targetY - sourceY))
|
||||||
|
const cornerSize = Math.min(cornerWidth, cornerHeight, offsetX, offsetY)
|
||||||
|
const leftAndRight = [Position.Left, Position.Right]
|
||||||
|
const cX = typeof centerX !== 'undefined' ? centerX : _centerX
|
||||||
|
const cY = typeof centerY !== 'undefined' ? centerY : _centerY
|
||||||
|
|
||||||
|
let firstCornerPath
|
||||||
|
let secondCornerPath
|
||||||
|
|
||||||
|
if (sourceX <= targetX) {
|
||||||
|
firstCornerPath = sourceY <= targetY ? bottomLeftCorner(sourceX, cY, cornerSize) : topLeftCorner(sourceX, cY, cornerSize)
|
||||||
|
secondCornerPath = sourceY <= targetY ? rightTopCorner(targetX, cY, cornerSize) : rightBottomCorner(targetX, cY, cornerSize)
|
||||||
|
} else {
|
||||||
|
firstCornerPath = sourceY < targetY ? bottomRightCorner(sourceX, cY, cornerSize) : topRightCorner(sourceX, cY, cornerSize)
|
||||||
|
secondCornerPath = sourceY < targetY ? leftTopCorner(targetX, cY, cornerSize) : leftBottomCorner(targetX, cY, cornerSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
|
||||||
|
if (sourceX <= targetX) {
|
||||||
|
firstCornerPath = sourceY <= targetY ? rightTopCorner(cX, sourceY, cornerSize) : rightBottomCorner(cX, sourceY, cornerSize)
|
||||||
|
secondCornerPath = sourceY <= targetY ? bottomLeftCorner(cX, targetY, cornerSize) : topLeftCorner(cX, targetY, cornerSize)
|
||||||
|
} else if (sourcePosition === Position.Right && targetPosition === Position.Left) {
|
||||||
|
// and sourceX > targetX
|
||||||
|
firstCornerPath = sourceY <= targetY ? leftTopCorner(cX, sourceY, cornerSize) : leftBottomCorner(cX, sourceY, cornerSize)
|
||||||
|
secondCornerPath = sourceY <= targetY ? bottomRightCorner(cX, targetY, cornerSize) : topRightCorner(cX, targetY, cornerSize)
|
||||||
|
}
|
||||||
|
} else if (leftAndRight.includes(sourcePosition) && !leftAndRight.includes(targetPosition)) {
|
||||||
|
if (sourceX <= targetX) {
|
||||||
|
firstCornerPath =
|
||||||
|
sourceY <= targetY ? rightTopCorner(targetX, sourceY, cornerSize) : rightBottomCorner(targetX, sourceY, cornerSize)
|
||||||
|
} else {
|
||||||
|
firstCornerPath =
|
||||||
|
sourceY <= targetY ? leftTopCorner(targetX, sourceY, cornerSize) : leftBottomCorner(targetX, sourceY, cornerSize)
|
||||||
|
}
|
||||||
|
secondCornerPath = ''
|
||||||
|
} else if (!leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
|
||||||
|
if (sourceX <= targetX) {
|
||||||
|
firstCornerPath =
|
||||||
|
sourceY <= targetY ? bottomLeftCorner(sourceX, targetY, cornerSize) : topLeftCorner(sourceX, targetY, cornerSize)
|
||||||
|
} else {
|
||||||
|
firstCornerPath =
|
||||||
|
sourceY <= targetY ? bottomRightCorner(sourceX, targetY, cornerSize) : topRightCorner(sourceX, targetY, cornerSize)
|
||||||
|
}
|
||||||
|
secondCornerPath = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return `M ${sourceX},${sourceY}${firstCornerPath}${secondCornerPath}L ${targetX},${targetY}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ConnectionMode, ElementId, Position, RevueFlowStore } from '~/types'
|
||||||
|
import { onMouseDown, ValidConnectionFunc } from '~/components/Handle/handler'
|
||||||
|
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
|
||||||
|
|
||||||
|
interface HandleProps {
|
||||||
|
id?: string
|
||||||
|
type?: string
|
||||||
|
position?: Position
|
||||||
|
isValidConnection?: ValidConnectionFunc
|
||||||
|
connectable?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<HandleProps>(), {
|
||||||
|
id: '',
|
||||||
|
type: 'source',
|
||||||
|
position: Position.Top,
|
||||||
|
isValidConnection: () => true,
|
||||||
|
connectable: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const store = inject<RevueFlowStore>('store')!
|
||||||
|
const hooks = inject<RevueFlowHooks>('hooks')!
|
||||||
|
const nodeId = inject<ElementId>('NodeIdContext')!
|
||||||
|
|
||||||
|
const onMouseDownHandler = (event: MouseEvent) =>
|
||||||
|
onMouseDown(event, store, hooks, props.id, nodeId, props.type === 'target', props.isValidConnection)
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:data-handleid="props.id"
|
||||||
|
:data-nodeid="nodeId"
|
||||||
|
:data-handlepos="props.position"
|
||||||
|
:class="[
|
||||||
|
'revue-flow__handle',
|
||||||
|
`revue-flow__handle-${props.position}`,
|
||||||
|
'nodrag',
|
||||||
|
{
|
||||||
|
source: props.type !== 'target',
|
||||||
|
target: props.type === 'target',
|
||||||
|
connectable: props.connectable,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
@mousedown="onMouseDownHandler"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { getHostForElement } from '../../utils'
|
import { getHostForElement } from '~/utils'
|
||||||
import { ElementId, ConnectionMode, Connection, HandleType, RevueFlowStore } from '../../types'
|
import { ElementId, ConnectionMode, Connection, HandleType, XYPosition, RevueFlowStore } from '~/types'
|
||||||
import { RevueFlowHooks } from '../../hooks/RevueFlowHooks'
|
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
|
||||||
|
|
||||||
export type ValidConnectionFunc = (connection: Connection) => boolean
|
export type ValidConnectionFunc = (connection: Connection) => boolean
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ function checkElementBelowIsValid(
|
|||||||
nodeId: ElementId,
|
nodeId: ElementId,
|
||||||
handleId: ElementId | null,
|
handleId: ElementId | null,
|
||||||
isValidConnection: ValidConnectionFunc,
|
isValidConnection: ValidConnectionFunc,
|
||||||
doc: Document | ShadowRoot
|
doc: Document,
|
||||||
) {
|
) {
|
||||||
const elementBelow = doc.elementFromPoint(event.clientX, event.clientY)
|
const elementBelow = doc.elementFromPoint(event.clientX, event.clientY)
|
||||||
const elementBelowIsTarget = elementBelow?.classList.contains('target') || false
|
const elementBelowIsTarget = elementBelow?.classList.contains('target') || false
|
||||||
@@ -29,7 +29,7 @@ function checkElementBelowIsValid(
|
|||||||
elementBelow,
|
elementBelow,
|
||||||
isValid: false,
|
isValid: false,
|
||||||
connection: { source: null, target: null, sourceHandle: null, targetHandle: null },
|
connection: { source: null, target: null, sourceHandle: null, targetHandle: null },
|
||||||
isHoveringHandle: false
|
isHoveringHandle: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (elementBelow && (elementBelowIsTarget || elementBelowIsSource)) {
|
if (elementBelow && (elementBelowIsTarget || elementBelowIsSource)) {
|
||||||
@@ -47,13 +47,13 @@ function checkElementBelowIsValid(
|
|||||||
source: elementBelowNodeId,
|
source: elementBelowNodeId,
|
||||||
sourceHandle: elementBelowHandleId,
|
sourceHandle: elementBelowHandleId,
|
||||||
target: nodeId,
|
target: nodeId,
|
||||||
targetHandle: handleId
|
targetHandle: handleId,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
source: nodeId,
|
source: nodeId,
|
||||||
sourceHandle: handleId,
|
sourceHandle: handleId,
|
||||||
target: elementBelowNodeId,
|
target: elementBelowNodeId,
|
||||||
targetHandle: elementBelowHandleId
|
targetHandle: elementBelowHandleId,
|
||||||
}
|
}
|
||||||
|
|
||||||
result.connection = connection
|
result.connection = connection
|
||||||
@@ -79,11 +79,11 @@ export function onMouseDown(
|
|||||||
connect: RevueFlowHooks['connect']
|
connect: RevueFlowHooks['connect']
|
||||||
edgeUpdateEnd: RevueFlowHooks['edgeUpdateEnd']
|
edgeUpdateEnd: RevueFlowHooks['edgeUpdateEnd']
|
||||||
},
|
},
|
||||||
handleId: ElementId | null,
|
handleId: ElementId,
|
||||||
nodeId: ElementId,
|
nodeId: ElementId,
|
||||||
isTarget: boolean,
|
isTarget: boolean,
|
||||||
isValidConnection: ValidConnectionFunc,
|
isValidConnection: ValidConnectionFunc = () => true,
|
||||||
elementEdgeUpdaterType?: HandleType
|
elementEdgeUpdaterType?: HandleType,
|
||||||
): void {
|
): void {
|
||||||
const revueFlowNode = (event.target as Element).closest('.revue-flow')
|
const revueFlowNode = (event.target as Element).closest('.revue-flow')
|
||||||
// when revue-flow is used inside a shadow root we can't use document
|
// when revue-flow is used inside a shadow root we can't use document
|
||||||
@@ -101,23 +101,29 @@ export function onMouseDown(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleType = elementEdgeUpdaterType ? elementEdgeUpdaterType : elementBelowIsTarget ? 'target' : 'source'
|
const handleType = elementEdgeUpdaterType || (elementBelowIsTarget ? 'target' : 'source')
|
||||||
const containerBounds = revueFlowNode.getBoundingClientRect()
|
const containerBounds = revueFlowNode.getBoundingClientRect()
|
||||||
let recentHoveredHandle: Element
|
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.x = event.clientX - containerBounds.left
|
||||||
store.connectionPosition.y = event.clientY - containerBounds.top
|
store.connectionPosition.y = event.clientY - containerBounds.top
|
||||||
|
|
||||||
store.setConnectionNodeId({
|
store.setConnectionNodeId({
|
||||||
connectionNodeId: nodeId,
|
connectionNodeId: nodeId,
|
||||||
connectionHandleId: handleId,
|
connectionHandleId: handleId,
|
||||||
connectionHandleType: handleType
|
connectionHandleType: handleType,
|
||||||
})
|
})
|
||||||
hooks.connectStart.trigger({ event, params: { nodeId, handleId, handleType } })
|
hooks.connectStart.trigger({ event, params: { nodeId, handleId, handleType } })
|
||||||
|
|
||||||
function onMouseMove(event: MouseEvent) {
|
function onMouseMove(event: MouseEvent) {
|
||||||
store.connectionPosition.x = event.clientX - containerBounds.left
|
connectionPosition.value.x = event.clientX - containerBounds.left
|
||||||
store.connectionPosition.y = event.clientY - containerBounds.top
|
connectionPosition.value.y = event.clientY - containerBounds.top
|
||||||
|
|
||||||
const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid(
|
const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid(
|
||||||
event,
|
event,
|
||||||
@@ -126,7 +132,7 @@ export function onMouseDown(
|
|||||||
nodeId,
|
nodeId,
|
||||||
handleId,
|
handleId,
|
||||||
isValidConnection,
|
isValidConnection,
|
||||||
doc
|
doc,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!isHoveringHandle) {
|
if (!isHoveringHandle) {
|
||||||
@@ -150,7 +156,7 @@ export function onMouseDown(
|
|||||||
nodeId,
|
nodeId,
|
||||||
handleId,
|
handleId,
|
||||||
isValidConnection,
|
isValidConnection,
|
||||||
doc
|
doc,
|
||||||
)
|
)
|
||||||
|
|
||||||
hooks.connectStop.trigger(event)
|
hooks.connectStop.trigger(event)
|
||||||
@@ -166,7 +172,7 @@ export function onMouseDown(
|
|||||||
}
|
}
|
||||||
|
|
||||||
resetRecentHandle(recentHoveredHandle)
|
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('mousemove', onMouseMove as EventListenerOrEventListenerObject)
|
||||||
doc.removeEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject)
|
doc.removeEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject)
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import Handle from '~/components/Handle/Handle.vue'
|
||||||
|
import { NodeProps, Position } from '~/types'
|
||||||
|
|
||||||
|
interface DefaultNodeProps {
|
||||||
|
data?: NodeProps['data']
|
||||||
|
connectable?: NodeProps['connectable']
|
||||||
|
targetPosition?: NodeProps['targetPosition']
|
||||||
|
sourcePosition?: NodeProps['sourcePosition']
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<DefaultNodeProps>(), {
|
||||||
|
data: () => {},
|
||||||
|
connectable: false,
|
||||||
|
targetPosition: Position.Top,
|
||||||
|
sourcePosition: Position.Bottom,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="revue-flow__node-default">
|
||||||
|
<Handle type="target" :position="props.targetPosition" :is-connectable="props.connectable" />
|
||||||
|
{{ props.data?.label }}
|
||||||
|
<Handle type="source" :position="props.sourcePosition" :is-connectable="props.connectable" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import Handle from '~/components/Handle/Handle.vue'
|
||||||
|
import { NodeProps, Position } from '~/types'
|
||||||
|
|
||||||
|
interface InputNodeProps {
|
||||||
|
data?: NodeProps['data']
|
||||||
|
connectable?: NodeProps['connectable']
|
||||||
|
sourcePosition?: NodeProps['sourcePosition']
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<InputNodeProps>(), {
|
||||||
|
data: () => {},
|
||||||
|
connectable: false,
|
||||||
|
sourcePosition: Position.Bottom,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="revue-flow__node-input">
|
||||||
|
{{ props.data?.label }}
|
||||||
|
<Handle type="source" :position="props.sourcePosition" :connectable="props.connectable" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,37 +1,31 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, onMounted, provide } from 'vue'
|
|
||||||
import { DraggableEventListener } from '@braks/revue-draggable'
|
import { DraggableEventListener } from '@braks/revue-draggable'
|
||||||
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
|
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
|
||||||
import { Node, RevueFlowStore, NodeDimensionUpdate } from '~/types'
|
import { Node, NodeDimensionUpdate, NodeType, RevueFlowStore } from '~/types'
|
||||||
|
|
||||||
interface NodeProps {
|
interface NodeProps {
|
||||||
node: Node
|
node: Node
|
||||||
snapToGrid?: boolean
|
type: NodeType
|
||||||
snapGrid?: [number, number]
|
selected?: boolean
|
||||||
scale?: number
|
draggable?: boolean
|
||||||
selectNodesOnDrag: boolean
|
selectable?: boolean
|
||||||
|
connectable?: boolean
|
||||||
|
selectNodesOnDrag?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<NodeProps>(), {
|
const props = withDefaults(defineProps<NodeProps>(), {
|
||||||
|
selected: false,
|
||||||
|
draggable: true,
|
||||||
|
selectable: true,
|
||||||
|
connectable: true,
|
||||||
selectNodesOnDrag: true,
|
selectNodesOnDrag: true,
|
||||||
scale: 1,
|
|
||||||
})
|
})
|
||||||
const emit = defineEmits(['updateNodeDimensions'])
|
|
||||||
|
|
||||||
const store = inject<RevueFlowStore>('store')!
|
const store = inject<RevueFlowStore>('store')!
|
||||||
const hooks = inject<RevueFlowHooks>('hooks')!
|
const hooks = inject<RevueFlowHooks>('hooks')!
|
||||||
provide('NodeIdContext', props.node.id)
|
provide('NodeIdContext', props.node.id)
|
||||||
|
|
||||||
const nodeElement = templateRef<HTMLDivElement>('nodeElement', null)
|
const nodeElement = templateRef<HTMLDivElement>('node-element', 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 onMouseEnterHandler = () => {
|
const onMouseEnterHandler = () => {
|
||||||
if (props.node.__rf.isDragging) {
|
if (props.node.__rf.isDragging) {
|
||||||
@@ -62,12 +56,12 @@ const onContextMenuHandler = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onSelectNodeHandler = (event: MouseEvent) => {
|
const onSelectNodeHandler = (event: MouseEvent) => {
|
||||||
if (!isDraggable.value) {
|
if (!props.draggable) {
|
||||||
const n = props.node
|
const n = props.node
|
||||||
if (isSelectable.value) {
|
if (props.selectable) {
|
||||||
store.unsetNodesSelection()
|
store.unsetNodesSelection()
|
||||||
|
|
||||||
if (!selected.value) {
|
if (!props.selected) {
|
||||||
store.addSelectedElements([n])
|
store.addSelectedElements([n])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,13 +74,13 @@ const onDragStart: DraggableEventListener = ({ event }) => {
|
|||||||
const n = props.node
|
const n = props.node
|
||||||
hooks.nodeDragStart.trigger({ event, node: n })
|
hooks.nodeDragStart.trigger({ event, node: n })
|
||||||
|
|
||||||
if (props.selectNodesOnDrag && isSelectable.value) {
|
if (props.selectNodesOnDrag && props.selectable) {
|
||||||
store.unsetNodesSelection()
|
store.unsetNodesSelection()
|
||||||
|
|
||||||
if (!selected.value) {
|
if (!props.selected) {
|
||||||
store.addSelectedElements([n])
|
store.addSelectedElements([n])
|
||||||
}
|
}
|
||||||
} else if (!props.selectNodesOnDrag && !selected.value && isSelectable.value) {
|
} else if (!props.selectNodesOnDrag && !props.selected && props.selectable) {
|
||||||
store.unsetNodesSelection()
|
store.unsetNodesSelection()
|
||||||
store.addSelectedElements([])
|
store.addSelectedElements([])
|
||||||
}
|
}
|
||||||
@@ -98,8 +92,8 @@ const onDrag: DraggableEventListener = ({ event, data }) => {
|
|||||||
n.position.y += data.deltaY
|
n.position.y += data.deltaY
|
||||||
hooks.nodeDrag.trigger({ event, node: n })
|
hooks.nodeDrag.trigger({ event, node: n })
|
||||||
|
|
||||||
store?.updateNodePosDiff({
|
store.updateNodePosDiff({
|
||||||
id: props.node.id as string,
|
id: props.node.id,
|
||||||
diff: {
|
diff: {
|
||||||
x: data.deltaX,
|
x: data.deltaX,
|
||||||
y: data.deltaY,
|
y: data.deltaY,
|
||||||
@@ -113,7 +107,7 @@ const onDragStop: DraggableEventListener = ({ event }) => {
|
|||||||
// onDragStop also gets called when user just clicks on a node.
|
// 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
|
// Because of that we set dragging to true inside the onDrag handler and handle the click here
|
||||||
if (!props.node.__rf.isDragging) {
|
if (!props.node.__rf.isDragging) {
|
||||||
if (isSelectable.value && !props.selectNodesOnDrag && !selected.value) {
|
if (props.selectable && !props.selectNodesOnDrag && !props.selected) {
|
||||||
store.addSelectedElements([n])
|
store.addSelectedElements([n])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,12 +129,11 @@ useResizeObserver(nodeElement, (entries) => {
|
|||||||
id: entry.target.getAttribute('data-id') as string,
|
id: entry.target.getAttribute('data-id') as string,
|
||||||
nodeElement: entry.target as HTMLDivElement,
|
nodeElement: entry.target as HTMLDivElement,
|
||||||
}))
|
}))
|
||||||
|
store.updateNodeDimensions(updates)
|
||||||
emit('updateNodeDimensions', updates)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
emit('updateNodeDimensions', [
|
store.updateNodeDimensions([
|
||||||
{
|
{
|
||||||
id: props.node.id,
|
id: props.node.id,
|
||||||
nodeElement: nodeElement.value,
|
nodeElement: nodeElement.value,
|
||||||
@@ -154,28 +147,28 @@ onMounted(() => {
|
|||||||
<DraggableCore
|
<DraggableCore
|
||||||
v-if="!props.node.isHidden"
|
v-if="!props.node.isHidden"
|
||||||
cancel=".nodrag"
|
cancel=".nodrag"
|
||||||
:scale="props.scale"
|
:disabled="!props.draggable"
|
||||||
:disabled="!isDraggable"
|
:scale="store.transform[2]"
|
||||||
:grid="props.snapGrid"
|
:grid="store.snapToGrid ? store.snapGrid : undefined"
|
||||||
:enable-user-select-hack="false"
|
:enable-user-select-hack="false"
|
||||||
@start="onDragStart"
|
@start="onDragStart"
|
||||||
@move="onDrag"
|
@move="onDrag"
|
||||||
@stop="onDragStop"
|
@stop="onDragStop"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref="nodeElement"
|
ref="node-element"
|
||||||
:class="[
|
:class="[
|
||||||
'revue-flow__node',
|
'revue-flow__node',
|
||||||
`revue-flow__node-${props.node.type}`,
|
`revue-flow__node-${props.node.type}`,
|
||||||
{
|
{
|
||||||
selected: selected,
|
selected: props.selected,
|
||||||
selectable: isSelectable,
|
selectable: props.selectable,
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
:style="{
|
:style="{
|
||||||
zIndex: selected ? 10 : 3,
|
zIndex: props.selected ? 10 : 3,
|
||||||
transform: `translate(${props.node.__rf.position.x}px,${props.node.__rf.position.y}px)`,
|
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,
|
opacity: props.node.__rf.width !== null && props.node.__rf.height !== null ? 1 : 0,
|
||||||
...props.node.style,
|
...props.node.style,
|
||||||
}"
|
}"
|
||||||
@@ -186,7 +179,20 @@ onMounted(() => {
|
|||||||
@contextmenu="onContextMenuHandler"
|
@contextmenu="onContextMenuHandler"
|
||||||
@click="onSelectNodeHandler"
|
@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>
|
</div>
|
||||||
</DraggableCore>
|
</DraggableCore>
|
||||||
</template>
|
</template>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import Handle from '~/components/Handle/Handle.vue'
|
||||||
|
import { NodeProps, Position } from '~/types'
|
||||||
|
|
||||||
|
interface OutputNodeProps {
|
||||||
|
data?: NodeProps['data']
|
||||||
|
connectable?: NodeProps['connectable']
|
||||||
|
targetPosition?: NodeProps['targetPosition']
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<OutputNodeProps>(), {
|
||||||
|
data: () => {},
|
||||||
|
connectable: false,
|
||||||
|
targetPosition: Position.Top,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="revue-flow__node-output">
|
||||||
|
{{ props.data?.label }}
|
||||||
|
<Handle type="source" :position="props.targetPosition" :is-connectable="props.connectable" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { default as DefaultNode } from './DefaultNode.vue'
|
||||||
|
export { default as InputNode } from './InputNode.vue'
|
||||||
|
export { default as OutputNode } from './OutputNode.vue'
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { DraggableEventListener } from '@braks/revue-draggable'
|
||||||
|
import { Node, RevueFlowStore } from '~/types'
|
||||||
|
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
|
||||||
|
import { isNode } from '~/utils/graph'
|
||||||
|
|
||||||
|
interface NodesSelectionProps {
|
||||||
|
onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void
|
||||||
|
onSelectionDrag?: (event: MouseEvent, nodes: Node[]) => void
|
||||||
|
onSelectionDragStop?: (event: MouseEvent, nodes: Node[]) => void
|
||||||
|
onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<NodesSelectionProps>()
|
||||||
|
|
||||||
|
const store = inject<RevueFlowStore>('store')!
|
||||||
|
const hooks = inject<RevueFlowHooks>('hooks')!
|
||||||
|
|
||||||
|
const selectedNodes = computed(() =>
|
||||||
|
store.selectedElements
|
||||||
|
? store.selectedElements.filter(isNode).map((selectedNode) => {
|
||||||
|
const matchingNode = store.nodes.find((node) => node.id === selectedNode.id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...matchingNode,
|
||||||
|
position: matchingNode?.__rf.position,
|
||||||
|
} as Node
|
||||||
|
})
|
||||||
|
: [],
|
||||||
|
)
|
||||||
|
|
||||||
|
const style = computed(() => ({
|
||||||
|
transform: `translate(${store.transform[0]}px,${store.transform[1]}px) scale(${store.transform[2]})`,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const innerStyle = computed(() => ({
|
||||||
|
width: `${store.selectedNodesBbox.width}px`,
|
||||||
|
height: `${store.selectedNodesBbox.height}px`,
|
||||||
|
top: `${store.selectedNodesBbox.y}px`,
|
||||||
|
left: `${store.selectedNodesBbox.x}px`,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const onStart: DraggableEventListener = ({ event }) => {
|
||||||
|
hooks.selectionDragStart.trigger({ event, nodes: selectedNodes.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDrag: DraggableEventListener = ({ event, data }) => {
|
||||||
|
hooks.selectionDrag.trigger({ event, nodes: selectedNodes.value })
|
||||||
|
|
||||||
|
store.updateNodePosDiff({
|
||||||
|
diff: {
|
||||||
|
x: data.deltaX,
|
||||||
|
y: data.deltaY,
|
||||||
|
},
|
||||||
|
isDragging: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onStop: DraggableEventListener = ({ event }) => {
|
||||||
|
store.updateNodePosDiff({
|
||||||
|
isDragging: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
hooks.selectionDragStop.trigger({ event, nodes: selectedNodes.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
const onContextMenu = (event: MouseEvent) => {
|
||||||
|
const selectedNodes = store.selectedElements
|
||||||
|
? store.selectedElements.filter(isNode).map((selectedNode) => store.nodes.find((node) => node.id === selectedNode.id))
|
||||||
|
: []
|
||||||
|
|
||||||
|
if (selectedNodes) hooks.selectionContextMenu.trigger({ event, nodes: selectedNodes as Node[] })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="revue-flow__nodesselection" :style="style">
|
||||||
|
<Draggable
|
||||||
|
:scale="store.transform[2]"
|
||||||
|
:grid="store.snapToGrid ? store.snapGrid : undefined"
|
||||||
|
:enable-user-select-hack="false"
|
||||||
|
@start="onStart"
|
||||||
|
@move="onDrag"
|
||||||
|
@stop="onStop"
|
||||||
|
>
|
||||||
|
<div class="revue-flow__nodesselection-rect" :style="innerStyle" @contextmenu="onContextMenu" />
|
||||||
|
</Draggable>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
interface SelectionRectProps {
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<SelectionRectProps>()
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="revue-flow__selection"
|
||||||
|
:style="{
|
||||||
|
width: `${props.width}px`,
|
||||||
|
height: `${props.height}px`,
|
||||||
|
transform: `translate(${props.x}px, ${props.y}px)`,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import SelectionRect from './SelectionRect.vue'
|
||||||
|
import { RevueFlowStore } from '~/types'
|
||||||
|
import { getMousePosition } from '~/components/UserSelection/utils'
|
||||||
|
|
||||||
|
const store = inject<RevueFlowStore>('store')!
|
||||||
|
const el = templateRef('user-selection', null)
|
||||||
|
|
||||||
|
const onMouseDown = (event: MouseEvent) => {
|
||||||
|
const mousePos = getMousePosition(event)
|
||||||
|
if (!mousePos) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
store.setUserSelection(mousePos)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseMove = (event: MouseEvent) => {
|
||||||
|
if (!store.selectionActive) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const mousePos = getMousePosition(event)
|
||||||
|
if (!mousePos) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
store.updateUserSelection(mousePos)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseUp = () => {
|
||||||
|
store.unsetUserSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseLeave = () => {
|
||||||
|
store.unsetUserSelection()
|
||||||
|
store.unsetNodesSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
useEventListener(el, 'mousedown', onMouseDown)
|
||||||
|
useEventListener(el, 'mousemove', onMouseMove)
|
||||||
|
useEventListener(el, 'click', onMouseUp)
|
||||||
|
useEventListener(el, 'mouseup', onMouseUp)
|
||||||
|
useEventListener(el, 'mouseleave', onMouseLeave)
|
||||||
|
|
||||||
|
const userSelectionRect = computed(() => store.userSelectionRect)
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div ref="user-selection" class="revue-flow__selectionpane">
|
||||||
|
<SelectionRect
|
||||||
|
v-if="userSelectionRect.draw"
|
||||||
|
:width="userSelectionRect.width"
|
||||||
|
:height="userSelectionRect.height"
|
||||||
|
:x="userSelectionRect.x"
|
||||||
|
:y="userSelectionRect.y"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { XYPosition } from '~/types'
|
||||||
|
|
||||||
|
export function getMousePosition(event: MouseEvent): XYPosition | void {
|
||||||
|
const revueFlowNode = (event.target as Element).closest('.revue-flow')
|
||||||
|
if (!revueFlowNode) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerBounds = revueFlowNode.getBoundingClientRect()
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: event.clientX - containerBounds.left,
|
||||||
|
y: event.clientY - containerBounds.top,
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
-37
@@ -1,11 +1,20 @@
|
|||||||
import { D3ZoomEvent, zoom, zoomIdentity, ZoomTransform } from 'd3-zoom'
|
import { D3ZoomEvent, zoom, zoomIdentity, ZoomTransform } from 'd3-zoom'
|
||||||
import { pointer, select } from 'd3-selection'
|
import { pointer, select } from 'd3-selection'
|
||||||
import { Ref } from 'vue'
|
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 { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
|
||||||
import { clamp } from '~/utils'
|
import { clamp } from '~/utils'
|
||||||
import useKeyPress from '~/hooks/useKeyPress'
|
import useKeyPress from '~/hooks/useKeyPress'
|
||||||
import useZoomPanHelper from '~/hooks/useZoomPanHelper'
|
|
||||||
// import { onLoadGetElements, onLoadProject, onLoadToObject } from '~/utils/graph'
|
// import { onLoadGetElements, onLoadProject, onLoadToObject } from '~/utils/graph'
|
||||||
|
|
||||||
const viewChanged = (prevTransform: FlowTransform, eventTransform: ZoomTransform): boolean =>
|
const viewChanged = (prevTransform: FlowTransform, eventTransform: ZoomTransform): boolean =>
|
||||||
@@ -17,9 +26,34 @@ const eventToFlowTransform = (eventTransform: ZoomTransform): FlowTransform => (
|
|||||||
zoom: eventTransform.k,
|
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 {
|
const {
|
||||||
selectionKeyCode = 'Shift',
|
selectionKeyCode = 'Shift',
|
||||||
zoomActivationKeyCode = 'Meta',
|
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 clampedX = clamp(defaultPosition[0], translateExtent[0][0], translateExtent[1][0])
|
||||||
const clampedY = clamp(defaultPosition[1], translateExtent[0][1], translateExtent[1][1])
|
const clampedY = clamp(defaultPosition[1], translateExtent[0][1], translateExtent[1][1])
|
||||||
const clampedZoom = clamp(defaultZoom, minZoom, maxZoom)
|
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()
|
.toBeTruthy()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const d3Zoom = zoom().scaleExtent([minZoom, maxZoom]).translateExtent(translateExtent)
|
const d3z = get(d3Zoom)!
|
||||||
const d3Selection = select(unrefElement(zoomPane) as Element).call(d3Zoom)
|
d3Selection.value = select(el.value).call(d3z)
|
||||||
const d3ZoomHandler = d3Selection.on('wheel.zoom')
|
const d3s = get(d3Selection)!
|
||||||
|
const d3ZoomHandler = d3s.on('wheel.zoom')
|
||||||
|
|
||||||
const updatedTransform = zoomIdentity.translate(clampedX, clampedY).scale(clampedZoom)
|
const updatedTransform = zoomIdentity.translate(clampedX, clampedY).scale(clampedZoom)
|
||||||
|
d3z.transform(d3s, updatedTransform)
|
||||||
const { fitView, zoomIn, zoomOut, zoomTo } = useZoomPanHelper({ ...options, d3Zoom, d3Selection } as any)
|
init({ d3Zoom: d3z, d3ZoomHandler, d3Selection: d3s })
|
||||||
hooks.load.trigger({
|
|
||||||
fitView: (params = { padding: 0.1 }) => fitView(params),
|
|
||||||
zoomIn,
|
|
||||||
zoomOut,
|
|
||||||
zoomTo,
|
|
||||||
setTransform: transform,
|
|
||||||
} as any)
|
|
||||||
d3Zoom.transform(d3Selection, updatedTransform)
|
|
||||||
|
|
||||||
const applyZoomHandlers = () => {
|
const applyZoomHandlers = () => {
|
||||||
d3Zoom.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
d3z.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||||
if (viewChanged(prevTransform.value, event.transform)) {
|
if (viewChanged(prevTransform.value, event.transform)) {
|
||||||
const flowTransform = eventToFlowTransform(event.transform)
|
const flowTransform = eventToFlowTransform(event.transform)
|
||||||
prevTransform.value = flowTransform
|
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)) {
|
if (viewChanged(prevTransform.value, event.transform)) {
|
||||||
const flowTransform = eventToFlowTransform(event.transform)
|
const flowTransform = eventToFlowTransform(event.transform)
|
||||||
prevTransform.value = flowTransform
|
prevTransform.value = flowTransform
|
||||||
@@ -87,9 +116,9 @@ const useZoom = (zoomPane: Ref<HTMLDivElement>, options: UseZoomOptions) => {
|
|||||||
|
|
||||||
useKeyPress(selectionKeyCode, (keyPress) => {
|
useKeyPress(selectionKeyCode, (keyPress) => {
|
||||||
if (keyPress) {
|
if (keyPress) {
|
||||||
d3Zoom.on('zoom', null)
|
d3z.on('zoom', null)
|
||||||
} else {
|
} 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]
|
transform.value = [event.transform.x, event.transform.y, event.transform.k]
|
||||||
hooks.move.trigger(eventToFlowTransform(event.transform))
|
hooks.move.trigger(eventToFlowTransform(event.transform))
|
||||||
})
|
})
|
||||||
@@ -98,19 +127,19 @@ const useZoom = (zoomPane: Ref<HTMLDivElement>, options: UseZoomOptions) => {
|
|||||||
|
|
||||||
useKeyPress(zoomActivationKeyCode, (keyPress) => {
|
useKeyPress(zoomActivationKeyCode, (keyPress) => {
|
||||||
if (panOnScroll && keyPress) {
|
if (panOnScroll && keyPress) {
|
||||||
d3Selection
|
d3s
|
||||||
?.on('wheel', (event: WheelEvent) => {
|
?.on('wheel', (event: WheelEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopImmediatePropagation()
|
event.stopImmediatePropagation()
|
||||||
|
|
||||||
const currentZoom = d3Selection?.property('__zoom').k || 1
|
const currentZoom = d3s?.property('__zoom').k || 1
|
||||||
|
|
||||||
if (event.ctrlKey && zoomOnPinch) {
|
if (event.ctrlKey && zoomOnPinch) {
|
||||||
const point = pointer(event)
|
const point = pointer(event)
|
||||||
// taken from https://github.com/d3/d3-zoom/blob/master/src/zoom.js
|
// 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 pinchDelta = -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002) * 10
|
||||||
const zoom = currentZoom * Math.pow(2, pinchDelta)
|
const zoom = currentZoom * Math.pow(2, pinchDelta)
|
||||||
if (d3Selection) d3Zoom?.scaleTo(d3Selection, zoom, point)
|
if (d3s) d3z.scaleTo(d3s, zoom, point)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -121,23 +150,19 @@ const useZoom = (zoomPane: Ref<HTMLDivElement>, options: UseZoomOptions) => {
|
|||||||
const deltaX = panOnScrollMode === PanOnScrollMode.Vertical ? 0 : event.deltaX * deltaNormalize
|
const deltaX = panOnScrollMode === PanOnScrollMode.Vertical ? 0 : event.deltaX * deltaNormalize
|
||||||
const deltaY = panOnScrollMode === PanOnScrollMode.Horizontal ? 0 : event.deltaY * deltaNormalize
|
const deltaY = panOnScrollMode === PanOnScrollMode.Horizontal ? 0 : event.deltaY * deltaNormalize
|
||||||
|
|
||||||
if (d3Selection && panOnScrollSpeed)
|
if (d3s && panOnScrollSpeed)
|
||||||
d3Zoom?.translateBy(
|
d3z?.translateBy(d3s, -(deltaX / currentZoom) * panOnScrollSpeed, -(deltaY / currentZoom) * panOnScrollSpeed)
|
||||||
d3Selection,
|
|
||||||
-(deltaX / currentZoom) * panOnScrollSpeed,
|
|
||||||
-(deltaY / currentZoom) * panOnScrollSpeed,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.on('wheel.zoom', null)
|
.on('wheel.zoom', null)
|
||||||
} else if (typeof d3ZoomHandler !== 'undefined') {
|
} else if (typeof d3ZoomHandler !== 'undefined') {
|
||||||
d3Selection?.on('wheel', null).on('wheel.zoom', d3ZoomHandler)
|
d3s?.on('wheel', null).on('wheel.zoom', d3ZoomHandler)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const applyZoomFilter = () => {
|
const applyZoomFilter = () => {
|
||||||
const keyPress = useKeyPress(selectionKeyCode)
|
const keyPress = useKeyPress(selectionKeyCode)
|
||||||
d3Zoom.filter((event: MouseEvent) => {
|
d3z.filter((event: MouseEvent) => {
|
||||||
const zoomScroll = zoomOnScroll
|
const zoomScroll = zoomOnScroll
|
||||||
const pinchZoom = zoomOnPinch && event.ctrlKey
|
const pinchZoom = zoomOnPinch && event.ctrlKey
|
||||||
|
|
||||||
@@ -179,7 +204,5 @@ const useZoom = (zoomPane: Ref<HTMLDivElement>, options: UseZoomOptions) => {
|
|||||||
applyZoomFilter()
|
applyZoomFilter()
|
||||||
})
|
})
|
||||||
|
|
||||||
return transform
|
return { transform, d3Zoom, d3Selection }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useZoom
|
|
||||||
|
|||||||
@@ -1,49 +1,72 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { CSSProperties } from 'vue'
|
import { CSSProperties } from 'vue'
|
||||||
import Edge from '~/components/Edges/Edge'
|
import { getSourceTargetNodes } from '~/container/EdgeRenderer/utils'
|
||||||
import { ConnectionLineComponent, ConnectionLineType, ConnectionMode, Dimensions, Edge as TEdge, Transform } from '~/types'
|
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'
|
import MarkerDefinitions from '~/container/MarkerDefinitions.vue'
|
||||||
|
|
||||||
interface EdgeRendererProps {
|
interface EdgeRendererProps {
|
||||||
edges: TEdge[]
|
edgeTypes: Record<string, EdgeType>
|
||||||
edgeTypes: any
|
|
||||||
transform: Transform
|
|
||||||
connectionLineType: ConnectionLineType
|
|
||||||
connectionLineStyle?: CSSProperties
|
|
||||||
connectionLineComponent?: ConnectionLineComponent
|
|
||||||
connectionMode?: ConnectionMode
|
|
||||||
arrowHeadColor: string
|
|
||||||
markerEndId?: string
|
|
||||||
onlyRenderVisibleElements: boolean
|
|
||||||
edgeUpdaterRadius?: number
|
|
||||||
dimensions: Dimensions
|
dimensions: Dimensions
|
||||||
|
transform: Transform
|
||||||
|
connectionLineType?: ConnectionLineType
|
||||||
|
connectionLineStyle?: CSSProperties
|
||||||
|
customConnectionLine?: CustomConnectionLine
|
||||||
|
connectionMode?: ConnectionMode
|
||||||
|
arrowHeadColor?: string
|
||||||
|
markerEndId?: string
|
||||||
|
onlyRenderVisibleElements?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<EdgeRendererProps>(), {
|
const props = withDefaults(defineProps<EdgeRendererProps>(), {
|
||||||
transform: () => [0, 0, 1],
|
transform: () => [0, 0, 1],
|
||||||
edges: () => [],
|
arrowHeadColor: '#b1b1b7',
|
||||||
arrowHeadColor: '#fff',
|
|
||||||
dimensions: () => ({ width: 0, height: 0 }),
|
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>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<svg :width="props.dimensions.width" :height="props.dimensions.height" class="revue-flow__edges">
|
<svg :width="props.dimensions.width" :height="props.dimensions.height" class="revue-flow__edges">
|
||||||
<MarkerDefinitions :color="props.arrowHeadColor" />
|
<MarkerDefinitions :color="props.arrowHeadColor" />
|
||||||
<g
|
<g
|
||||||
:transform="
|
:transform="props.transform.length && `translate(${props.transform[0]},${props.transform[1]}) scale(${props.transform[2]})`"
|
||||||
props.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="edge"
|
:edge="edge"
|
||||||
|
:nodes="getSourceTargetNodes(edge, store.nodes)"
|
||||||
:type="props.edgeTypes[edge.type || 'default']"
|
:type="props.edgeTypes[edge.type || 'default']"
|
||||||
|
:dimensions="props.dimensions"
|
||||||
|
:transform="props.transform"
|
||||||
:only-render-visible-elements="props.onlyRenderVisibleElements"
|
:only-render-visible-elements="props.onlyRenderVisibleElements"
|
||||||
:marker-end-id="props.markerEndId"
|
:marker-end-id="props.markerEndId"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
<ConnectionLine
|
||||||
|
v-if="connectionLineVisible"
|
||||||
|
:source-node="sourceNode"
|
||||||
|
:connection-line-style="props.connectionLineStyle"
|
||||||
|
:connection-line-type="props.connectionLineType"
|
||||||
|
:custom-connection-line="props.customConnectionLine"
|
||||||
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { CSSProperties, defineComponent, inject, PropType } from 'vue'
|
import { CSSProperties, defineComponent, inject, PropType } from 'vue'
|
||||||
import { ConnectionLineType, ConnectionLineComponent, ConnectionMode, RevueFlowStore } from '../../types'
|
import { ConnectionLineType, ConnectionLineComponent, ConnectionMode, RevueFlowStore } from '../../types'
|
||||||
import ConnectionLine from '../../components/ConnectionLine'
|
import ConnectionLine from '../../components/ConnectionLine'
|
||||||
|
import Edge from '../../components/Edges/EdgeDepr'
|
||||||
import MarkerDefinitions from './MarkerDefinitions'
|
import MarkerDefinitions from './MarkerDefinitions'
|
||||||
import Edge from '../../components/Edges/Edge'
|
|
||||||
|
|
||||||
interface EdgeRendererProps {
|
interface EdgeRendererProps {
|
||||||
edgeTypes: any
|
edgeTypes: any
|
||||||
@@ -21,59 +21,59 @@ export default defineComponent({
|
|||||||
components: {
|
components: {
|
||||||
Edge,
|
Edge,
|
||||||
ConnectionLine,
|
ConnectionLine,
|
||||||
MarkerDefinitions
|
MarkerDefinitions,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
edgeTypes: {
|
edgeTypes: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
connectionLineType: {
|
connectionLineType: {
|
||||||
type: String as PropType<EdgeRendererProps['connectionLineType']>,
|
type: String as PropType<EdgeRendererProps['connectionLineType']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: undefined
|
default: undefined,
|
||||||
},
|
},
|
||||||
connectionLineStyle: {
|
connectionLineStyle: {
|
||||||
type: Object as PropType<EdgeRendererProps['connectionLineStyle']>,
|
type: Object as PropType<EdgeRendererProps['connectionLineStyle']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: undefined
|
default: undefined,
|
||||||
},
|
},
|
||||||
connectionLineComponent: {
|
connectionLineComponent: {
|
||||||
type: Object as PropType<EdgeRendererProps['connectionLineComponent']>,
|
type: Object as PropType<EdgeRendererProps['connectionLineComponent']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: undefined
|
default: undefined,
|
||||||
},
|
},
|
||||||
connectionMode: {
|
connectionMode: {
|
||||||
type: String as PropType<EdgeRendererProps['connectionMode']>,
|
type: String as PropType<EdgeRendererProps['connectionMode']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: undefined
|
default: undefined,
|
||||||
},
|
},
|
||||||
arrowHeadColor: {
|
arrowHeadColor: {
|
||||||
type: String as PropType<EdgeRendererProps['arrowHeadColor']>,
|
type: String as PropType<EdgeRendererProps['arrowHeadColor']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: undefined
|
default: undefined,
|
||||||
},
|
},
|
||||||
markerEndId: {
|
markerEndId: {
|
||||||
type: String as PropType<EdgeRendererProps['markerEndId']>,
|
type: String as PropType<EdgeRendererProps['markerEndId']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: undefined
|
default: undefined,
|
||||||
},
|
},
|
||||||
onlyRenderVisibleElements: {
|
onlyRenderVisibleElements: {
|
||||||
type: Boolean as PropType<EdgeRendererProps['onlyRenderVisibleElements']>,
|
type: Boolean as PropType<EdgeRendererProps['onlyRenderVisibleElements']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: undefined
|
default: undefined,
|
||||||
},
|
},
|
||||||
edgeUpdaterRadius: {
|
edgeUpdaterRadius: {
|
||||||
type: Number as PropType<EdgeRendererProps['edgeUpdaterRadius']>,
|
type: Number as PropType<EdgeRendererProps['edgeUpdaterRadius']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: undefined
|
default: undefined,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const store = inject<RevueFlowStore>('store')!
|
const store = inject<RevueFlowStore>('store')!
|
||||||
|
|
||||||
return () => (
|
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} />
|
<MarkerDefinitions color={props.arrowHeadColor} />
|
||||||
<g transform={`translate(${store.transform[0]},${store.transform[1]}) scale(${store.transform[2]})`}>
|
<g transform={`translate(${store.transform[0]},${store.transform[1]}) scale(${store.transform[2]})`}>
|
||||||
{store.edges.map((edge, i) => (
|
{store.edges.map((edge, i) => (
|
||||||
@@ -95,5 +95,5 @@ export default defineComponent({
|
|||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Ref } from 'vue'
|
||||||
import { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '~/components/Edges'
|
import { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '~/components/Edges'
|
||||||
import { rectToBox } from '~/utils/graph'
|
import { rectToBox } from '~/utils/graph'
|
||||||
import { Position, Node, XYPosition, ElementId, HandleElement, Transform, Edge, EdgeType } from '~/types'
|
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 = {
|
type SourceTargetNode = {
|
||||||
sourceNode: Node | null
|
sourceNode: Node
|
||||||
targetNode: Node | null
|
targetNode: Node
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getSourceTargetNodes = (edge: Edge, nodes: Node[]): SourceTargetNode => {
|
export const getSourceTargetNodes = (edge: Edge, nodes: Node[]): Ref<SourceTargetNode> => {
|
||||||
return nodes.reduce(
|
const sourceTargetNode: SourceTargetNode = nodes.reduce(
|
||||||
(res, node) => {
|
(res, node) => {
|
||||||
if (node.id === edge.source) {
|
if (node.id === edge.source) {
|
||||||
res.sourceNode = node
|
res.sourceNode = node
|
||||||
@@ -152,6 +153,8 @@ export const getSourceTargetNodes = (edge: Edge, nodes: Node[]): SourceTargetNod
|
|||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
},
|
},
|
||||||
{ sourceNode: null, targetNode: null } as SourceTargetNode,
|
{ sourceNode: null, targetNode: null } as any,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return ref(sourceTargetNode)
|
||||||
}
|
}
|
||||||
|
|||||||
+50
-96
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import '~/style.css'
|
import '~/style.css'
|
||||||
import '~/theme-default.css'
|
import '~/theme-default.css'
|
||||||
import { CSSProperties } from 'vue'
|
import { CSSProperties, onBeforeUnmount } from 'vue'
|
||||||
import ZoomPane from './ZoomPane.vue'
|
import ZoomPane from './ZoomPane.vue'
|
||||||
import SelectionPane from './SelectionPane.vue'
|
import SelectionPane from './SelectionPane.vue'
|
||||||
import NodeRenderer from '~/container/NodeRenderer.vue'
|
import NodeRenderer from '~/container/NodeRenderer.vue'
|
||||||
@@ -12,11 +12,9 @@ import {
|
|||||||
Elements,
|
Elements,
|
||||||
PanOnScrollMode,
|
PanOnScrollMode,
|
||||||
RevueFlowStore,
|
RevueFlowStore,
|
||||||
Node,
|
|
||||||
Edge,
|
|
||||||
NodeType,
|
NodeType,
|
||||||
EdgeType,
|
EdgeType,
|
||||||
ConnectionLineComponent,
|
CustomConnectionLine,
|
||||||
KeyCode,
|
KeyCode,
|
||||||
TranslateExtent,
|
TranslateExtent,
|
||||||
NodeExtent,
|
NodeExtent,
|
||||||
@@ -25,21 +23,18 @@ import { RevueFlowHooks, useRevueFlow } from '~/hooks/RevueFlowHooks'
|
|||||||
import configureStore from '~/store/configure-store'
|
import configureStore from '~/store/configure-store'
|
||||||
import { initialState } from '~/store'
|
import { initialState } from '~/store'
|
||||||
import { createNodeTypes } from '~/container/NodeRenderer/utils'
|
import { createNodeTypes } from '~/container/NodeRenderer/utils'
|
||||||
import InputNode from '~/components/Nodes/InputNode'
|
import { DefaultNode, InputNode, OutputNode } from '~/components/Nodes'
|
||||||
import DefaultNode from '~/components/Nodes/DefaultNode'
|
|
||||||
import OutputNode from '~/components/Nodes/OutputNode'
|
|
||||||
import { BezierEdge, SmoothStepEdge, StepEdge, StraightEdge } from '~/components/Edges'
|
import { BezierEdge, SmoothStepEdge, StepEdge, StraightEdge } from '~/components/Edges'
|
||||||
import { createEdgeTypes } from '~/container/EdgeRenderer/utils'
|
import { createEdgeTypes } from '~/container/EdgeRenderer/utils'
|
||||||
import { isEdge, isNode, parseEdge, parseNode } from '~/utils/graph'
|
|
||||||
|
|
||||||
interface RevueFlowProps {
|
interface RevueFlowProps {
|
||||||
modelValue: Elements
|
elements: Elements
|
||||||
nodeTypes?: Record<string, NodeType>
|
nodeTypes?: Record<string, NodeType>
|
||||||
edgeTypes?: Record<string, EdgeType>
|
edgeTypes?: Record<string, EdgeType>
|
||||||
connectionMode?: ConnectionMode
|
connectionMode?: ConnectionMode
|
||||||
connectionLineType?: ConnectionLineType
|
connectionLineType?: ConnectionLineType
|
||||||
connectionLineStyle?: CSSProperties
|
connectionLineStyle?: CSSProperties
|
||||||
connectionLineComponent?: ConnectionLineComponent
|
connectionLineComponent?: CustomConnectionLine
|
||||||
deleteKeyCode?: KeyCode
|
deleteKeyCode?: KeyCode
|
||||||
selectionKeyCode?: KeyCode
|
selectionKeyCode?: KeyCode
|
||||||
multiSelectionKeyCode?: KeyCode
|
multiSelectionKeyCode?: KeyCode
|
||||||
@@ -69,11 +64,6 @@ interface RevueFlowProps {
|
|||||||
edgeUpdaterRadius?: number
|
edgeUpdaterRadius?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type NextElements = {
|
|
||||||
nextNodes: Node[]
|
|
||||||
nextEdges: Edge[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<RevueFlowProps>(), {
|
const props = withDefaults(defineProps<RevueFlowProps>(), {
|
||||||
modelValue: () => [] as Elements,
|
modelValue: () => [] as Elements,
|
||||||
connectionMode: ConnectionMode.Strict,
|
connectionMode: ConnectionMode.Strict,
|
||||||
@@ -86,6 +76,7 @@ const props = withDefaults(defineProps<RevueFlowProps>(), {
|
|||||||
snapGrid: () => [15, 15],
|
snapGrid: () => [15, 15],
|
||||||
onlyRenderVisibleElements: false,
|
onlyRenderVisibleElements: false,
|
||||||
nodesConnectable: true,
|
nodesConnectable: true,
|
||||||
|
nodesDraggable: true,
|
||||||
elementsSelectable: true,
|
elementsSelectable: true,
|
||||||
selectNodesOnDrag: true,
|
selectNodesOnDrag: true,
|
||||||
minZoom: 0.5,
|
minZoom: 0.5,
|
||||||
@@ -112,102 +103,65 @@ const props = withDefaults(defineProps<RevueFlowProps>(), {
|
|||||||
})
|
})
|
||||||
const emit = defineEmits(Object.keys(useRevueFlow()))
|
const emit = defineEmits(Object.keys(useRevueFlow()))
|
||||||
|
|
||||||
const store = configureStore(initialState)()
|
const defaultNodeTypes: Record<string, NodeType> = {
|
||||||
provide<RevueFlowStore>('store', store)
|
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)
|
const hooks = useRevueFlow().bind(emit)
|
||||||
provide<RevueFlowHooks>('hooks', hooks)
|
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 nodeTypes = createNodeTypes({ ...defaultNodeTypes, ...props.nodeTypes })
|
||||||
const edgeTypes = createEdgeTypes({ ...defaultEdgeTypes, ...props.edgeTypes })
|
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>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="revue-flow">
|
<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 }">
|
<template #default="{ transform, dimensions }">
|
||||||
<SelectionPane
|
<SelectionPane
|
||||||
:delete-key-code="props.deleteKeyCode"
|
:delete-key-code="props.deleteKeyCode"
|
||||||
:multi-selection-key-code="props.multiSelectionKeyCode"
|
:multi-selection-key-code="props.multiSelectionKeyCode"
|
||||||
:selection-key-code="props.selectionKeyCode"
|
:selection-key-code="props.selectionKeyCode"
|
||||||
>
|
>
|
||||||
<NodeRenderer v-bind="props" :node-types="nodeTypes" :nodes="nodes" :transform="transform" :dimensions="dimensions" />
|
<NodeRenderer :node-types="nodeTypes" :transform="transform" :dimensions="dimensions" />
|
||||||
<EdgeRenderer
|
<EdgeRenderer :edge-types="edgeTypes" :transform="transform" :dimensions="dimensions" />
|
||||||
v-if="false"
|
|
||||||
v-bind="props"
|
|
||||||
:edge-types="edgeTypes"
|
|
||||||
:edges="edges"
|
|
||||||
:transform="transform"
|
|
||||||
:dimensions="dimensions"
|
|
||||||
/>
|
|
||||||
</SelectionPane>
|
</SelectionPane>
|
||||||
</template>
|
</template>
|
||||||
</ZoomPane>
|
</ZoomPane>
|
||||||
|
|||||||
@@ -1,37 +1,26 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import Node from '~/components/Node.vue'
|
import Node from '~/components/Nodes/Node.vue'
|
||||||
import { NodeType, Node as TNode, Transform, Dimensions, NodeDimensionUpdate } from '~/types'
|
import { NodeType, Node as TNode, Transform, Dimensions, RevueFlowStore } from '~/types'
|
||||||
import { getNodesInside } from '~/utils/graph'
|
import { getNodesInside } from '~/utils/graph'
|
||||||
import { getDimensions } from '~/utils'
|
|
||||||
import { getHandleBounds } from '~/components/Nodes/utils'
|
|
||||||
|
|
||||||
interface NodeRendererProps {
|
interface NodeRendererProps {
|
||||||
nodes: TNode[]
|
|
||||||
transform: Transform
|
|
||||||
nodeTypes: Record<string, NodeType>
|
nodeTypes: Record<string, NodeType>
|
||||||
selectNodesOnDrag: boolean
|
|
||||||
snapToGrid: boolean
|
|
||||||
snapGrid: [number, number]
|
|
||||||
onlyRenderVisibleElements: boolean
|
|
||||||
dimensions: Dimensions
|
dimensions: Dimensions
|
||||||
|
transform: Transform
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<NodeRendererProps>(), {
|
const props = withDefaults(defineProps<NodeRendererProps>(), {
|
||||||
selectNodesOnDrag: true,
|
|
||||||
snapGrid: () => [15, 15],
|
|
||||||
snapToGrid: false,
|
|
||||||
onlyRenderVisibleElements: false,
|
|
||||||
transform: () => [0, 0, 1],
|
transform: () => [0, 0, 1],
|
||||||
nodes: () => [],
|
|
||||||
dimensions: () => ({ width: 0, height: 0 }),
|
dimensions: () => ({ width: 0, height: 0 }),
|
||||||
})
|
})
|
||||||
|
|
||||||
const nodes = ref(props.nodes)
|
const store = inject<RevueFlowStore>('store')!
|
||||||
const getNodes = () =>
|
|
||||||
props.onlyRenderVisibleElements
|
const getNodes = computed(() =>
|
||||||
? nodes.value &&
|
store.onlyRenderVisibleElements
|
||||||
|
? store.nodes &&
|
||||||
getNodesInside(
|
getNodesInside(
|
||||||
nodes.value,
|
store.nodes,
|
||||||
{
|
{
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
@@ -41,9 +30,8 @@ const getNodes = () =>
|
|||||||
props.transform,
|
props.transform,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
: nodes.value
|
: store.nodes,
|
||||||
|
)
|
||||||
const cNodes = computed(() => getNodes())
|
|
||||||
|
|
||||||
const type = (node: TNode) => {
|
const type = (node: TNode) => {
|
||||||
const nodeType = node.type || 'default'
|
const nodeType = node.type || 'default'
|
||||||
@@ -54,65 +42,23 @@ const type = (node: TNode) => {
|
|||||||
return type
|
return type
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateNodeDimensions = (updates: NodeDimensionUpdate[]) => {
|
const selected = (nodeId: string) => store.selectedElements?.some(({ id }) => id === nodeId)
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="revue-flow__nodes"
|
class="revue-flow__nodes"
|
||||||
:style="{ transform: `translate(${props.transform[0]}px,${props.transform[1]}px) scale(${props.transform[2]})` }"
|
: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="node"
|
:node="node"
|
||||||
:snap-grid="props.snapGrid"
|
:type="type(node)"
|
||||||
:snap-to-grid="props.snapToGrid"
|
|
||||||
:select-nodes-on-drag="props.selectNodesOnDrag"
|
|
||||||
:scale="props.transform[2]"
|
:scale="props.transform[2]"
|
||||||
@updateNodeDimensions="updateNodeDimensions"
|
:selected="selected(node.id)"
|
||||||
>
|
:selectable="node.selectable || store.elementsSelectable"
|
||||||
<template #default="{ selected, isConnectable }">
|
:connectable="node.connectable || store.nodesConnectable"
|
||||||
<component
|
:draggable="node.draggable || store.nodesDraggable"
|
||||||
: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>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { defineComponent, inject, PropType } from 'vue'
|
import { defineComponent, inject, PropType } from 'vue'
|
||||||
import Node from '../../components/Nodes/Node'
|
import Node from '../../components/Nodes/NodeDepr'
|
||||||
import { getNodesInside } from '../../utils/graph'
|
import { getNodesInside } from '../../utils/graph'
|
||||||
import { NodeType, RevueFlowStore } from '../../types'
|
import { NodeType, RevueFlowStore } from '../../types'
|
||||||
|
|
||||||
@@ -18,28 +18,28 @@ export default defineComponent({
|
|||||||
nodeTypes: {
|
nodeTypes: {
|
||||||
type: Object as PropType<NodeRendererProps['nodeTypes']>,
|
type: Object as PropType<NodeRendererProps['nodeTypes']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: undefined
|
default: undefined,
|
||||||
},
|
},
|
||||||
selectNodesOnDrag: {
|
selectNodesOnDrag: {
|
||||||
type: Boolean as PropType<NodeRendererProps['selectNodesOnDrag']>,
|
type: Boolean as PropType<NodeRendererProps['selectNodesOnDrag']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: undefined
|
default: undefined,
|
||||||
},
|
},
|
||||||
snapToGrid: {
|
snapToGrid: {
|
||||||
type: Boolean as PropType<NodeRendererProps['snapToGrid']>,
|
type: Boolean as PropType<NodeRendererProps['snapToGrid']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: undefined
|
default: undefined,
|
||||||
},
|
},
|
||||||
snapGrid: {
|
snapGrid: {
|
||||||
type: Array as unknown as PropType<NodeRendererProps['snapGrid']>,
|
type: Array as unknown as PropType<NodeRendererProps['snapGrid']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: undefined
|
default: undefined,
|
||||||
},
|
},
|
||||||
onlyRenderVisibleElements: {
|
onlyRenderVisibleElements: {
|
||||||
type: Boolean as PropType<NodeRendererProps['onlyRenderVisibleElements']>,
|
type: Boolean as PropType<NodeRendererProps['onlyRenderVisibleElements']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: undefined
|
default: undefined,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const store = inject<RevueFlowStore>('store')!
|
const store = inject<RevueFlowStore>('store')!
|
||||||
@@ -53,19 +53,19 @@ export default defineComponent({
|
|||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
width: store?.width,
|
width: store?.width,
|
||||||
height: store?.height
|
height: store?.height,
|
||||||
},
|
},
|
||||||
store.transform,
|
store.transform,
|
||||||
true
|
true,
|
||||||
)
|
)
|
||||||
: store.nodes
|
: store.nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => (
|
return () => (
|
||||||
<div
|
<div
|
||||||
class="revue-flow__nodes"
|
className="revue-flow__nodes"
|
||||||
style={{
|
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) => {
|
{getNodes().map((node, i) => {
|
||||||
@@ -90,5 +90,5 @@ export default defineComponent({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import DefaultNode from '~/components/Nodes/DefaultNode'
|
import { DefaultNode, InputNode, OutputNode } from '~/components/Nodes'
|
||||||
import InputNode from '~/components/Nodes/InputNode'
|
|
||||||
import OutputNode from '~/components/Nodes/OutputNode'
|
|
||||||
import { NodeType } from '~/types'
|
import { NodeType } from '~/types'
|
||||||
|
|
||||||
export function createNodeTypes(nodeTypes: Record<string, NodeType>): Record<string, NodeType> {
|
export function createNodeTypes(nodeTypes: Record<string, NodeType>): Record<string, NodeType> {
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { PropType } from 'vue'
|
|||||||
import '../../style.css'
|
import '../../style.css'
|
||||||
import '../../theme-default.css'
|
import '../../theme-default.css'
|
||||||
import GraphView from '../GraphView'
|
import GraphView from '../GraphView'
|
||||||
import DefaultNode from '../../components/Nodes/DefaultNode'
|
import DefaultNode from '../../components/Nodes/DefaultNodeDepr'
|
||||||
import InputNode from '../../components/Nodes/InputNode'
|
import InputNode from '../../components/Nodes/InputNodeDepr'
|
||||||
import OutputNode from '../../components/Nodes/OutputNode'
|
import OutputNode from '../../components/Nodes/OutputNodeDepr'
|
||||||
import { createNodeTypes } from '../NodeRenderer/utils'
|
import { createNodeTypes } from '../NodeRenderer/utils'
|
||||||
import { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '../../components/Edges'
|
import { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '../../components/Edges'
|
||||||
import { createEdgeTypes } from '../EdgeRenderer/utils'
|
import { createEdgeTypes } from '../EdgeRenderer/utils'
|
||||||
@@ -16,14 +16,14 @@ import { RevueFlowHooks, useRevueFlow } from '../../hooks/RevueFlowHooks'
|
|||||||
const defaultNodeTypes = {
|
const defaultNodeTypes = {
|
||||||
input: InputNode,
|
input: InputNode,
|
||||||
default: DefaultNode,
|
default: DefaultNode,
|
||||||
output: OutputNode
|
output: OutputNode,
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultEdgeTypes = {
|
const defaultEdgeTypes = {
|
||||||
default: BezierEdge,
|
default: BezierEdge,
|
||||||
straight: StraightEdge,
|
straight: StraightEdge,
|
||||||
step: StepEdge,
|
step: StepEdge,
|
||||||
smoothstep: SmoothStepEdge
|
smoothstep: SmoothStepEdge,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@@ -33,189 +33,189 @@ export default defineComponent({
|
|||||||
modelValue: {
|
modelValue: {
|
||||||
type: Array as PropType<RevueFlowProps['modelValue']>,
|
type: Array as PropType<RevueFlowProps['modelValue']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => [],
|
||||||
},
|
},
|
||||||
nodeTypesId: {
|
nodeTypesId: {
|
||||||
type: String as PropType<RevueFlowProps['nodeTypesId']>,
|
type: String as PropType<RevueFlowProps['nodeTypesId']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: '1'
|
default: '1',
|
||||||
},
|
},
|
||||||
edgeTypesId: {
|
edgeTypesId: {
|
||||||
type: String as PropType<RevueFlowProps['edgeTypesId']>,
|
type: String as PropType<RevueFlowProps['edgeTypesId']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: '1'
|
default: '1',
|
||||||
},
|
},
|
||||||
nodeTypes: {
|
nodeTypes: {
|
||||||
type: Object as PropType<RevueFlowProps['nodeTypes']>,
|
type: Object as PropType<RevueFlowProps['nodeTypes']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => defaultNodeTypes
|
default: () => defaultNodeTypes,
|
||||||
},
|
},
|
||||||
edgeTypes: {
|
edgeTypes: {
|
||||||
type: Object as PropType<RevueFlowProps['edgeTypes']>,
|
type: Object as PropType<RevueFlowProps['edgeTypes']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => defaultEdgeTypes
|
default: () => defaultEdgeTypes,
|
||||||
},
|
},
|
||||||
connectionMode: {
|
connectionMode: {
|
||||||
type: String as PropType<RevueFlowProps['connectionMode']>,
|
type: String as PropType<RevueFlowProps['connectionMode']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: ConnectionMode.Strict
|
default: ConnectionMode.Strict,
|
||||||
},
|
},
|
||||||
connectionLineType: {
|
connectionLineType: {
|
||||||
type: String as PropType<RevueFlowProps['connectionLineType']>,
|
type: String as PropType<RevueFlowProps['connectionLineType']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: ConnectionLineType.Bezier
|
default: ConnectionLineType.Bezier,
|
||||||
},
|
},
|
||||||
connectionLineStyle: {
|
connectionLineStyle: {
|
||||||
type: Object as PropType<RevueFlowProps['connectionLineStyle']>,
|
type: Object as PropType<RevueFlowProps['connectionLineStyle']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: undefined
|
default: undefined,
|
||||||
},
|
},
|
||||||
connectionLineComponent: {
|
connectionLineComponent: {
|
||||||
type: Object as PropType<RevueFlowProps['connectionLineComponent']>,
|
type: Object as PropType<RevueFlowProps['connectionLineComponent']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: undefined
|
default: undefined,
|
||||||
},
|
},
|
||||||
selectionKeyCode: {
|
selectionKeyCode: {
|
||||||
type: [Number, String] as PropType<RevueFlowProps['selectionKeyCode']>,
|
type: [Number, String] as PropType<RevueFlowProps['selectionKeyCode']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'Shift'
|
default: 'Shift',
|
||||||
},
|
},
|
||||||
multiSelectionKeyCode: {
|
multiSelectionKeyCode: {
|
||||||
type: [Number, String] as PropType<RevueFlowProps['multiSelectionKeyCode']>,
|
type: [Number, String] as PropType<RevueFlowProps['multiSelectionKeyCode']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'Meta'
|
default: 'Meta',
|
||||||
},
|
},
|
||||||
zoomActivationKeyCode: {
|
zoomActivationKeyCode: {
|
||||||
type: [Number, String] as PropType<RevueFlowProps['zoomActivationKeyCode']>,
|
type: [Number, String] as PropType<RevueFlowProps['zoomActivationKeyCode']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'Meta'
|
default: 'Meta',
|
||||||
},
|
},
|
||||||
deleteKeyCode: {
|
deleteKeyCode: {
|
||||||
type: [Number, String] as PropType<RevueFlowProps['deleteKeyCode']>,
|
type: [Number, String] as PropType<RevueFlowProps['deleteKeyCode']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'Backspace'
|
default: 'Backspace',
|
||||||
},
|
},
|
||||||
snapToGrid: {
|
snapToGrid: {
|
||||||
type: Boolean as PropType<RevueFlowProps['snapToGrid']>,
|
type: Boolean as PropType<RevueFlowProps['snapToGrid']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false,
|
||||||
},
|
},
|
||||||
snapGrid: {
|
snapGrid: {
|
||||||
type: Array as unknown as PropType<RevueFlowProps['snapGrid']>,
|
type: Array as unknown as PropType<RevueFlowProps['snapGrid']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => [15, 15]
|
default: () => [15, 15],
|
||||||
},
|
},
|
||||||
onlyRenderVisibleElements: {
|
onlyRenderVisibleElements: {
|
||||||
type: Boolean as PropType<RevueFlowProps['onlyRenderVisibleElements']>,
|
type: Boolean as PropType<RevueFlowProps['onlyRenderVisibleElements']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false,
|
||||||
},
|
},
|
||||||
nodesDraggable: {
|
nodesDraggable: {
|
||||||
type: Boolean as PropType<RevueFlowProps['nodesDraggable']>,
|
type: Boolean as PropType<RevueFlowProps['nodesDraggable']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: undefined
|
default: undefined,
|
||||||
},
|
},
|
||||||
nodesConnectable: {
|
nodesConnectable: {
|
||||||
type: Boolean as PropType<RevueFlowProps['nodesConnectable']>,
|
type: Boolean as PropType<RevueFlowProps['nodesConnectable']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true,
|
||||||
},
|
},
|
||||||
elementsSelectable: {
|
elementsSelectable: {
|
||||||
type: Boolean as PropType<RevueFlowProps['elementsSelectable']>,
|
type: Boolean as PropType<RevueFlowProps['elementsSelectable']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true,
|
||||||
},
|
},
|
||||||
selectNodesOnDrag: {
|
selectNodesOnDrag: {
|
||||||
type: Boolean as PropType<RevueFlowProps['selectNodesOnDrag']>,
|
type: Boolean as PropType<RevueFlowProps['selectNodesOnDrag']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true,
|
||||||
},
|
},
|
||||||
minZoom: {
|
minZoom: {
|
||||||
type: Number as PropType<RevueFlowProps['minZoom']>,
|
type: Number as PropType<RevueFlowProps['minZoom']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: 0.5
|
default: 0.5,
|
||||||
},
|
},
|
||||||
maxZoom: {
|
maxZoom: {
|
||||||
type: Number as PropType<RevueFlowProps['maxZoom']>,
|
type: Number as PropType<RevueFlowProps['maxZoom']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: 2
|
default: 2,
|
||||||
},
|
},
|
||||||
defaultZoom: {
|
defaultZoom: {
|
||||||
type: Number as PropType<RevueFlowProps['defaultZoom']>,
|
type: Number as PropType<RevueFlowProps['defaultZoom']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: 1
|
default: 1,
|
||||||
},
|
},
|
||||||
defaultPosition: {
|
defaultPosition: {
|
||||||
type: Array as unknown as PropType<RevueFlowProps['defaultPosition']>,
|
type: Array as unknown as PropType<RevueFlowProps['defaultPosition']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => [0, 0]
|
default: () => [0, 0],
|
||||||
},
|
},
|
||||||
translateExtent: {
|
translateExtent: {
|
||||||
type: Array as unknown as PropType<RevueFlowProps['translateExtent']>,
|
type: Array as unknown as PropType<RevueFlowProps['translateExtent']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => [
|
default: () => [
|
||||||
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
||||||
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY]
|
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
nodeExtent: {
|
nodeExtent: {
|
||||||
type: Array as unknown as PropType<RevueFlowProps['nodeExtent']>,
|
type: Array as unknown as PropType<RevueFlowProps['nodeExtent']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => [
|
default: () => [
|
||||||
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
||||||
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY]
|
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
arrowHeadColor: {
|
arrowHeadColor: {
|
||||||
type: String as PropType<RevueFlowProps['arrowHeadColor']>,
|
type: String as PropType<RevueFlowProps['arrowHeadColor']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: '#b1b1b7'
|
default: '#b1b1b7',
|
||||||
},
|
},
|
||||||
markerEndId: {
|
markerEndId: {
|
||||||
type: String as PropType<RevueFlowProps['markerEndId']>,
|
type: String as PropType<RevueFlowProps['markerEndId']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: undefined
|
default: undefined,
|
||||||
},
|
},
|
||||||
zoomOnScroll: {
|
zoomOnScroll: {
|
||||||
type: Boolean as PropType<RevueFlowProps['zoomOnScroll']>,
|
type: Boolean as PropType<RevueFlowProps['zoomOnScroll']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true,
|
||||||
},
|
},
|
||||||
zoomOnPinch: {
|
zoomOnPinch: {
|
||||||
type: Boolean as PropType<RevueFlowProps['zoomOnPinch']>,
|
type: Boolean as PropType<RevueFlowProps['zoomOnPinch']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true,
|
||||||
},
|
},
|
||||||
panOnScroll: {
|
panOnScroll: {
|
||||||
type: Boolean as PropType<RevueFlowProps['panOnScroll']>,
|
type: Boolean as PropType<RevueFlowProps['panOnScroll']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false,
|
||||||
},
|
},
|
||||||
panOnScrollSpeed: {
|
panOnScrollSpeed: {
|
||||||
type: Number as PropType<RevueFlowProps['panOnScrollSpeed']>,
|
type: Number as PropType<RevueFlowProps['panOnScrollSpeed']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: 0.5
|
default: 0.5,
|
||||||
},
|
},
|
||||||
panOnScrollMode: {
|
panOnScrollMode: {
|
||||||
type: String as PropType<RevueFlowProps['panOnScrollMode']>,
|
type: String as PropType<RevueFlowProps['panOnScrollMode']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: PanOnScrollMode.Free
|
default: PanOnScrollMode.Free,
|
||||||
},
|
},
|
||||||
zoomOnDoubleClick: {
|
zoomOnDoubleClick: {
|
||||||
type: Boolean as PropType<RevueFlowProps['zoomOnDoubleClick']>,
|
type: Boolean as PropType<RevueFlowProps['zoomOnDoubleClick']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true,
|
||||||
},
|
},
|
||||||
paneMoveable: {
|
paneMoveable: {
|
||||||
type: Boolean as PropType<RevueFlowProps['paneMoveable']>,
|
type: Boolean as PropType<RevueFlowProps['paneMoveable']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true,
|
||||||
},
|
},
|
||||||
edgeUpdaterRadius: {
|
edgeUpdaterRadius: {
|
||||||
type: Number as PropType<RevueFlowProps['edgeUpdaterRadius']>,
|
type: Number as PropType<RevueFlowProps['edgeUpdaterRadius']>,
|
||||||
required: false,
|
required: false,
|
||||||
default: 10
|
default: 10,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
emits: Object.keys(useRevueFlow()),
|
emits: Object.keys(useRevueFlow()),
|
||||||
setup(props, { emit, slots }) {
|
setup(props, { emit, slots }) {
|
||||||
@@ -240,7 +240,7 @@ export default defineComponent({
|
|||||||
})
|
})
|
||||||
|
|
||||||
return () => (
|
return () => (
|
||||||
<div class="revue-flow">
|
<div className="revue-flow">
|
||||||
<GraphView
|
<GraphView
|
||||||
nodeTypes={createNodeTypes({ ...defaultNodeTypes, ...props.nodeTypes })}
|
nodeTypes={createNodeTypes({ ...defaultNodeTypes, ...props.nodeTypes })}
|
||||||
edgeTypes={createEdgeTypes({ ...defaultEdgeTypes, ...props.edgeTypes })}
|
edgeTypes={createEdgeTypes({ ...defaultEdgeTypes, ...props.edgeTypes })}
|
||||||
@@ -279,5 +279,5 @@ export default defineComponent({
|
|||||||
{slots.default ? slots.default() : ''}
|
{slots.default ? slots.default() : ''}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,31 +1,29 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { inject } from 'vue'
|
|
||||||
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
|
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
|
||||||
import useKeyPress from '~/hooks/useKeyPress'
|
import useKeyPress from '~/hooks/useKeyPress'
|
||||||
import { KeyCode } from '~/types'
|
import { KeyCode, RevueFlowStore } from '~/types'
|
||||||
import useGlobalKeyHandler from '~/hooks/useGlobalKeyHandler'
|
import useGlobalKeyHandler from '~/hooks/useGlobalKeyHandler'
|
||||||
|
import NodesSelection from '~/components/NodesSelection/NodesSelection.vue'
|
||||||
|
import UserSelection from '~/components/UserSelection/UserSelection.vue'
|
||||||
|
|
||||||
const props = withDefaults(
|
interface SelectionPaneProps {
|
||||||
defineProps<{
|
selectionKeyCode?: KeyCode
|
||||||
selectionKeyCode: KeyCode
|
deleteKeyCode?: KeyCode
|
||||||
deleteKeyCode: KeyCode
|
multiSelectionKeyCode?: KeyCode
|
||||||
multiSelectionKeyCode: KeyCode
|
}
|
||||||
}>(),
|
const props = withDefaults(defineProps<SelectionPaneProps>(), {
|
||||||
{
|
selectionKeyCode: 'Shift',
|
||||||
selectionKeyCode: 'Shift',
|
deleteKeyCode: 'Backspace',
|
||||||
deleteKeyCode: 'Backspace',
|
multiSelectionKeyCode: 'Meta',
|
||||||
multiSelectionKeyCode: 'Meta',
|
})
|
||||||
},
|
const store = inject<RevueFlowStore>('store')!
|
||||||
)
|
|
||||||
const hooks = inject<RevueFlowHooks>('hooks')!
|
const hooks = inject<RevueFlowHooks>('hooks')!
|
||||||
const keyPressed = useKeyPress(props.selectionKeyCode)
|
const keyPressed = useKeyPress(props.selectionKeyCode)
|
||||||
const nodeSelectionActive = ref(false)
|
|
||||||
const selectedElements = ref([])
|
|
||||||
|
|
||||||
const onClick = (event: MouseEvent) => {
|
const onClick = (event: MouseEvent) => {
|
||||||
hooks.paneClick.trigger(event)
|
hooks.paneClick.trigger(event)
|
||||||
nodeSelectionActive.value = false
|
store.unsetNodesSelection()
|
||||||
selectedElements.value = []
|
store.resetSelectedElements()
|
||||||
}
|
}
|
||||||
|
|
||||||
const onContextMenu = (event: MouseEvent) => hooks.paneContextMenu.trigger(event)
|
const onContextMenu = (event: MouseEvent) => hooks.paneContextMenu.trigger(event)
|
||||||
@@ -37,10 +35,13 @@ useGlobalKeyHandler({
|
|||||||
deleteKeyCode: props.deleteKeyCode,
|
deleteKeyCode: props.deleteKeyCode,
|
||||||
multiSelectionKeyCode: props.multiSelectionKeyCode,
|
multiSelectionKeyCode: props.multiSelectionKeyCode,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const userSelectionVisible = computed(() => keyPressed.value && (store.selectionActive || store.elementsSelectable))
|
||||||
|
const nodesSelectionVisible = computed(() => store.nodesSelectionActive)
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<div v-if="keyPressed" id="user-selection">User Selection</div>
|
<UserSelection v-if="userSelectionVisible" id="user-selection" />
|
||||||
<div v-if="nodeSelectionActive" id="nodes-selection">Nodes Selection</div>
|
<NodesSelection v-if="nodesSelectionVisible" id="nodes-selection" />
|
||||||
<div class="revue-flow__pane" @click="onClick" @contextmenu="onContextMenu" @wheel="onWheel" />
|
<div class="revue-flow__pane" @click="onClick" @contextmenu="onContextMenu" @wheel="onWheel" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+35
-30
@@ -1,39 +1,28 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { RevueFlowOptions, ConnectionLineType, ConnectionMode, Elements, PanOnScrollMode } from '~/types'
|
import { KeyCode, PanOnScrollMode, RevueFlowStore } from '~/types'
|
||||||
import useZoom from '~/composables/useZoom'
|
import useZoom from '~/composables/useZoom'
|
||||||
import useResizeHandler from '~/hooks/useResizeHandler'
|
import useResizeHandler from '~/hooks/useResizeHandler'
|
||||||
|
import useZoomPanHelper from '~/hooks/useZoomPanHelper'
|
||||||
|
|
||||||
interface RevueFlowProps extends RevueFlowOptions {
|
interface ZoomPaneProps {
|
||||||
modelValue: Elements
|
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>(), {
|
const props = withDefaults(defineProps<ZoomPaneProps>(), {
|
||||||
modelValue: () => [] as Elements,
|
|
||||||
connectionMode: ConnectionMode.Strict,
|
|
||||||
connectionLineType: ConnectionLineType.Bezier,
|
|
||||||
selectionKeyCode: 'Shift',
|
selectionKeyCode: 'Shift',
|
||||||
multiSelectionKeyCode: 'Meta',
|
|
||||||
zoomActivationKeyCode: '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,
|
defaultZoom: 1,
|
||||||
defaultPosition: () => [0, 0],
|
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,
|
zoomOnScroll: true,
|
||||||
zoomOnPinch: true,
|
zoomOnPinch: true,
|
||||||
zoomOnDoubleClick: true,
|
zoomOnDoubleClick: true,
|
||||||
@@ -41,11 +30,27 @@ const props = withDefaults(defineProps<RevueFlowProps>(), {
|
|||||||
panOnScrollSpeed: 0.5,
|
panOnScrollSpeed: 0.5,
|
||||||
panOnScrollMode: PanOnScrollMode.Free,
|
panOnScrollMode: PanOnScrollMode.Free,
|
||||||
paneMoveable: true,
|
paneMoveable: true,
|
||||||
edgeUpdaterRadius: 10,
|
|
||||||
})
|
})
|
||||||
const zoomPane = templateRef<HTMLDivElement>('zoom-pane', null)
|
const store = inject<RevueFlowStore>('store')!
|
||||||
const transform = useZoom(zoomPane, props)
|
const zoomPaneEl = templateRef<HTMLDivElement>('zoom-pane', null)
|
||||||
const dimensions = useResizeHandler(zoomPane)
|
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>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div ref="zoom-pane" class="revue-flow__renderer revue-flow__zoompane">
|
<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 useKeyPress from './useKeyPress'
|
||||||
import { isNode, getConnectedEdges } from '~/utils/graph'
|
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 {
|
interface HookParams {
|
||||||
deleteKeyCode: KeyCode
|
deleteKeyCode: KeyCode
|
||||||
@@ -9,19 +8,14 @@ interface HookParams {
|
|||||||
onElementsRemove?: (elements: Elements) => void
|
onElementsRemove?: (elements: Elements) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ({ deleteKeyCode, multiSelectionKeyCode, onElementsRemove }: HookParams): void => {
|
export default ({ deleteKeyCode, multiSelectionKeyCode, onElementsRemove = () => {} }: HookParams): void => {
|
||||||
const selectedElements = ref([])
|
const store = inject<RevueFlowStore>('store')!
|
||||||
const deleteKeyPressed = useKeyPress(deleteKeyCode)
|
|
||||||
const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode)
|
|
||||||
/**
|
|
||||||
* const selectedElements = computed(() => store.selectedElements || [])
|
|
||||||
const edges = computed(() => store.edges)
|
|
||||||
|
|
||||||
watch(selectedElements, () => {
|
useKeyPress(deleteKeyCode, (keyPressed) => {
|
||||||
if (onElementsRemove && deleteKeyPressed.value && selectedElements.value.length > 0) {
|
if (keyPressed && store.selectedElements) {
|
||||||
const selectedNodes = selectedElements.value.filter(isNode)
|
const selectedNodes = store.selectedElements.filter(isNode)
|
||||||
const connectedEdges = getConnectedEdges(selectedNodes, edges.value as Edge[])
|
const connectedEdges = getConnectedEdges(selectedNodes, store.edges)
|
||||||
const elementsToRemove = [...selectedElements.value, ...connectedEdges].reduce(
|
const elementsToRemove = [...store.selectedElements, ...connectedEdges].reduce(
|
||||||
(res, item) => res.set(item.id, item),
|
(res, item) => res.set(item.id, item),
|
||||||
new Map<ElementId, FlowElement>(),
|
new Map<ElementId, FlowElement>(),
|
||||||
)
|
)
|
||||||
@@ -32,8 +26,5 @@ export default ({ deleteKeyCode, multiSelectionKeyCode, onElementsRemove }: Hook
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(multiSelectionKeyPressed, () => {
|
useKeyPress(multiSelectionKeyCode, (keyPressed) => (store.multiSelectionActive = keyPressed))
|
||||||
store.multiSelectionActive = multiSelectionKeyPressed.value
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Ref } from 'vue'
|
|||||||
import { getDimensions } from '~/utils'
|
import { getDimensions } from '~/utils'
|
||||||
import { Dimensions } from '~/types'
|
import { Dimensions } from '~/types'
|
||||||
|
|
||||||
export default (el: Ref<HTMLDivElement>) => {
|
export default function (el: Ref<HTMLDivElement>) {
|
||||||
const dimensions = ref<Dimensions>({ width: 0, height: 0 })
|
const dimensions = ref<Dimensions>({ width: 0, height: 0 })
|
||||||
const updateDimensions = () => {
|
const updateDimensions = () => {
|
||||||
const unrefEl = unrefElement(el)
|
const unrefEl = unrefElement(el)
|
||||||
@@ -11,10 +11,13 @@ export default (el: Ref<HTMLDivElement>) => {
|
|||||||
const size = getDimensions(unrefEl as HTMLDivElement)
|
const size = getDimensions(unrefEl as HTMLDivElement)
|
||||||
if (size.height === 0 || size.width === 0)
|
if (size.height === 0 || size.width === 0)
|
||||||
console.log('The revue Flow parent container needs a width and a height to render the graph.')
|
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)
|
useEventListener(window, 'resize', updateDimensions)
|
||||||
useResizeObserver(el, () => updateDimensions())
|
useResizeObserver(el, () => updateDimensions())
|
||||||
|
|
||||||
|
until(el).toBeTruthy().then(updateDimensions)
|
||||||
|
|
||||||
return dimensions
|
return dimensions
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { ElementId, RevueFlowStore, UpdateNodeInternals } from '../types'
|
import { ElementId, RevueFlowStore, UpdateNodeInternals } from '../types'
|
||||||
import { inject } from 'vue'
|
|
||||||
|
|
||||||
function useUpdateNodeInternals(): UpdateNodeInternals {
|
function useUpdateNodeInternals(): UpdateNodeInternals {
|
||||||
const store = inject<RevueFlowStore>('store')!
|
const store = inject<RevueFlowStore>('store')!
|
||||||
|
|||||||
@@ -1,76 +1,90 @@
|
|||||||
import { zoomIdentity } from 'd3-zoom'
|
import { zoomIdentity } from 'd3-zoom'
|
||||||
import { Selection, ZoomBehavior } from 'd3'
|
|
||||||
import { getRectOfNodes, pointToRendererPoint, getTransformForBounds } from '~/utils/graph'
|
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 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,
|
d3Zoom,
|
||||||
d3Selection,
|
d3Selection,
|
||||||
nodes = [],
|
nodes = [],
|
||||||
width = 0,
|
transform = [0, 0, 0],
|
||||||
height = 0,
|
dimensions = {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
},
|
||||||
minZoom = 0.5,
|
minZoom = 0.5,
|
||||||
maxZoom = 2,
|
maxZoom = 2,
|
||||||
transform = [0, 0, 0],
|
|
||||||
snapToGrid = false,
|
snapToGrid = false,
|
||||||
snapGrid = [15, 15],
|
snapGrid = [15, 15],
|
||||||
}: {
|
): ZoomPanHelperFunctions {
|
||||||
d3Zoom: ZoomBehavior<Element, unknown>
|
return {
|
||||||
d3Selection: Selection<Element, any, any, any>
|
zoomIn: () => d3Zoom?.scaleBy(d3Selection, 1.2),
|
||||||
nodes: Node[]
|
zoomOut: () => d3Zoom?.scaleBy(d3Selection, 1 / 1.2),
|
||||||
width: number
|
zoomTo: (zoomLevel: number) => d3Zoom.scaleTo(d3Selection, zoomLevel),
|
||||||
height: number
|
transform: (transform: FlowTransform) => {
|
||||||
minZoom: number
|
const nextTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.zoom)
|
||||||
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)
|
|
||||||
|
|
||||||
d3Zoom.transform(d3Selection, nextTransform)
|
d3Zoom.transform(d3Selection, nextTransform)
|
||||||
},
|
},
|
||||||
fitView: (options: FitViewParams = { padding: DEFAULT_PADDING, includeHiddenNodes: false }) => {
|
fitView: (options: FitViewParams = { padding: DEFAULT_PADDING, includeHiddenNodes: false }) => {
|
||||||
if (!nodes.length) {
|
if (!nodes.length) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const bounds = getRectOfNodes(options.includeHiddenNodes ? nodes : nodes.filter((node) => !node.isHidden))
|
const bounds = getRectOfNodes(options.includeHiddenNodes ? nodes : nodes.filter((node) => !node.isHidden))
|
||||||
const [x, y, zoom] = getTransformForBounds(
|
const [x, y, zoom] = getTransformForBounds(
|
||||||
bounds,
|
bounds,
|
||||||
width,
|
dimensions.width,
|
||||||
height,
|
dimensions.height,
|
||||||
options.minZoom || minZoom,
|
options.minZoom || minZoom,
|
||||||
options.maxZoom || maxZoom,
|
options.maxZoom || maxZoom,
|
||||||
options.padding || DEFAULT_PADDING,
|
options.padding || DEFAULT_PADDING,
|
||||||
)
|
)
|
||||||
const transform = zoomIdentity.translate(x, y).scale(zoom)
|
const transform = zoomIdentity.translate(x, y).scale(zoom)
|
||||||
|
|
||||||
d3Zoom?.transform(d3Selection, transform)
|
d3Zoom?.transform(d3Selection, transform)
|
||||||
},
|
},
|
||||||
setCenter: (x: number, y: number, zoom?: number) => {
|
setCenter: (x: number, y: number, zoom?: number) => {
|
||||||
const nextZoom = typeof zoom !== 'undefined' ? zoom : maxZoom
|
const nextZoom = typeof zoom !== 'undefined' ? zoom : maxZoom
|
||||||
const centerX = width / 2 - x * nextZoom
|
const centerX = dimensions.width / 2 - x * nextZoom
|
||||||
const centerY = height / 2 - y * nextZoom
|
const centerY = dimensions.height / 2 - y * nextZoom
|
||||||
const transform = zoomIdentity.translate(centerX, centerY).scale(nextZoom)
|
const transform = zoomIdentity.translate(centerX, centerY).scale(nextZoom)
|
||||||
|
|
||||||
d3Zoom.transform(d3Selection, transform)
|
d3Zoom.transform(d3Selection, transform)
|
||||||
},
|
},
|
||||||
fitBounds: (bounds: Rect, padding = DEFAULT_PADDING) => {
|
fitBounds: (bounds: Rect, padding = DEFAULT_PADDING) => {
|
||||||
const [x, y, zoom] = getTransformForBounds(bounds, width, height, minZoom, maxZoom, padding)
|
const [x, y, zoom] = getTransformForBounds(bounds, dimensions.width, dimensions.height, minZoom, maxZoom, padding)
|
||||||
const transform = zoomIdentity.translate(x, y).scale(zoom)
|
const transform = zoomIdentity.translate(x, y).scale(zoom)
|
||||||
|
|
||||||
d3Zoom.transform(d3Selection, transform)
|
d3Zoom.transform(d3Selection, transform)
|
||||||
},
|
},
|
||||||
project: (position: XYPosition) => {
|
project: (position: XYPosition) => {
|
||||||
return pointToRendererPoint(position, transform, snapToGrid, snapGrid)
|
return pointToRendererPoint(position, transform, snapToGrid, snapGrid)
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
|
} as UseZoomPanHelper)
|
||||||
export default useZoomPanHelper
|
|
||||||
|
|||||||
+5
-7
@@ -1,9 +1,7 @@
|
|||||||
export { default as default } from './container/RevueFlow'
|
export { default } from './container/Flow.vue'
|
||||||
export { default as Handle } from './components/Handle'
|
export { default as Handle } from './components/Handle/Handle.vue'
|
||||||
export { default as EdgeText } from './components/Edges/EdgeText'
|
export { default as EdgeText } from './components/Edges/EdgeText.vue'
|
||||||
export { getBezierPath } from './components/Edges/BezierEdge'
|
export { getBezierPath, getSmoothStepPath, getMarkerEnd, getCenter as getEdgeCenter } from './components/Edges/utils'
|
||||||
export { getSmoothStepPath } from './components/Edges/SmoothStepEdge'
|
|
||||||
export { getMarkerEnd, getCenter as getEdgeCenter } from './components/Edges/utils'
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
isNode,
|
isNode,
|
||||||
@@ -15,7 +13,7 @@ export {
|
|||||||
getConnectedEdges,
|
getConnectedEdges,
|
||||||
updateEdge,
|
updateEdge,
|
||||||
getTransformForBounds,
|
getTransformForBounds,
|
||||||
getRectOfNodes
|
getRectOfNodes,
|
||||||
} from './utils/graph'
|
} from './utils/graph'
|
||||||
export { default as useZoomPanHelper } from './hooks/useZoomPanHelper'
|
export { default as useZoomPanHelper } from './hooks/useZoomPanHelper'
|
||||||
export { default as useUpdateNodeInternals } from './hooks/useUpdateNodeInternals'
|
export { default as useUpdateNodeInternals } from './hooks/useUpdateNodeInternals'
|
||||||
|
|||||||
@@ -24,12 +24,11 @@ export default function configureStore(
|
|||||||
getters: {},
|
getters: {},
|
||||||
actions: {
|
actions: {
|
||||||
setElements(elements) {
|
setElements(elements) {
|
||||||
const propElements = elements
|
|
||||||
const nextElements: NextElements = {
|
const nextElements: NextElements = {
|
||||||
nextNodes: [],
|
nextNodes: [],
|
||||||
nextEdges: [],
|
nextEdges: [],
|
||||||
}
|
}
|
||||||
const { nextNodes, nextEdges } = propElements.reduce((res, propElement): NextElements => {
|
const { nextNodes, nextEdges } = elements.reduce((res, propElement): NextElements => {
|
||||||
if (isNode(propElement)) {
|
if (isNode(propElement)) {
|
||||||
const storeNode = this.nodes.find((node) => node.id === propElement.id)
|
const storeNode = this.nodes.find((node) => node.id === propElement.id)
|
||||||
|
|
||||||
@@ -189,7 +188,7 @@ export default function configureStore(
|
|||||||
this.userSelectionRect.draw = false
|
this.userSelectionRect.draw = false
|
||||||
|
|
||||||
if (!selectedNodes || selectedNodes.length === 0) {
|
if (!selectedNodes || selectedNodes.length === 0) {
|
||||||
this.selectedElements = null
|
this.selectedElements = undefined
|
||||||
this.nodesSelectionActive = false
|
this.nodesSelectionActive = false
|
||||||
} else {
|
} else {
|
||||||
this.selectedNodesBbox = getRectOfNodes(selectedNodes)
|
this.selectedNodesBbox = getRectOfNodes(selectedNodes)
|
||||||
@@ -202,12 +201,11 @@ export default function configureStore(
|
|||||||
this.selectedElements = selectedElementsUpdated ? selectedElementsArr : this.selectedElements
|
this.selectedElements = selectedElementsUpdated ? selectedElementsArr : this.selectedElements
|
||||||
},
|
},
|
||||||
initD3Zoom(payload) {
|
initD3Zoom(payload) {
|
||||||
const { d3Zoom, d3Selection, d3ZoomHandler, transform } = payload
|
const { d3Zoom, d3Selection, d3ZoomHandler } = payload
|
||||||
|
|
||||||
this.d3Zoom = d3Zoom
|
this.d3Zoom = d3Zoom
|
||||||
this.d3Selection = d3Selection
|
this.d3Selection = d3Selection
|
||||||
this.d3ZoomHandler = d3ZoomHandler
|
this.d3ZoomHandler = d3ZoomHandler
|
||||||
this.transform = transform
|
|
||||||
},
|
},
|
||||||
setMinZoom(minZoom) {
|
setMinZoom(minZoom) {
|
||||||
this.d3Zoom?.scaleExtent([minZoom, this.maxZoom])
|
this.d3Zoom?.scaleExtent([minZoom, this.maxZoom])
|
||||||
@@ -237,14 +235,13 @@ export default function configureStore(
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
resetSelectedElements() {
|
resetSelectedElements() {
|
||||||
this.selectedElements = null
|
this.selectedElements = undefined
|
||||||
},
|
},
|
||||||
unsetNodesSelection() {
|
unsetNodesSelection() {
|
||||||
this.nodesSelectionActive = false
|
this.nodesSelectionActive = false
|
||||||
},
|
},
|
||||||
updateSize(size) {
|
updateSize(size) {
|
||||||
this.height = size.height
|
this.dimensions = size
|
||||||
this.width = size.width
|
|
||||||
},
|
},
|
||||||
setConnectionNodeId(payload) {
|
setConnectionNodeId(payload) {
|
||||||
this.connectionNodeId = payload.connectionNodeId
|
this.connectionNodeId = payload.connectionNodeId
|
||||||
|
|||||||
+9
-7
@@ -1,16 +1,18 @@
|
|||||||
import { RevueFlowState, ConnectionMode } from '../types'
|
import { RevueFlowState, ConnectionMode } from '../types'
|
||||||
|
|
||||||
export const initialState: RevueFlowState = {
|
export const initialState: RevueFlowState = {
|
||||||
width: 0,
|
dimensions: {
|
||||||
height: 0,
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
},
|
||||||
transform: [0, 0, 1],
|
transform: [0, 0, 1],
|
||||||
nodes: [],
|
nodes: [],
|
||||||
edges: [],
|
edges: [],
|
||||||
selectedElements: null,
|
selectedElements: undefined,
|
||||||
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
|
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
|
||||||
|
|
||||||
d3Zoom: null,
|
d3Zoom: undefined,
|
||||||
d3Selection: null,
|
d3Selection: undefined,
|
||||||
d3ZoomHandler: undefined,
|
d3ZoomHandler: undefined,
|
||||||
minZoom: 0.5,
|
minZoom: 0.5,
|
||||||
maxZoom: 2,
|
maxZoom: 2,
|
||||||
@@ -36,8 +38,8 @@ export const initialState: RevueFlowState = {
|
|||||||
height: 0,
|
height: 0,
|
||||||
draw: false,
|
draw: false,
|
||||||
},
|
},
|
||||||
connectionNodeId: null,
|
connectionNodeId: undefined,
|
||||||
connectionHandleId: null,
|
connectionHandleId: undefined,
|
||||||
connectionHandleType: 'source',
|
connectionHandleType: 'source',
|
||||||
connectionPosition: { x: 0, y: 0 },
|
connectionPosition: { x: 0, y: 0 },
|
||||||
connectionMode: ConnectionMode.Strict,
|
connectionMode: ConnectionMode.Strict,
|
||||||
|
|||||||
+10
-9
@@ -1,5 +1,6 @@
|
|||||||
import { ComponentPublicInstance } from 'vue'
|
import { DefineComponent } from 'vue'
|
||||||
import { ElementId, HandleType, Position } from './types'
|
import { ElementId, Position } from './types'
|
||||||
|
import { HandleType } from '~/types/handle'
|
||||||
|
|
||||||
export enum ConnectionLineType {
|
export enum ConnectionLineType {
|
||||||
Bezier = 'default',
|
Bezier = 'default',
|
||||||
@@ -26,13 +27,13 @@ export type ConnectionLineComponentProps = {
|
|||||||
connectionLineType: ConnectionLineType
|
connectionLineType: ConnectionLineType
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConnectionLineComponent = ComponentPublicInstance<ConnectionLineComponentProps>
|
export type CustomConnectionLine = DefineComponent<ConnectionLineComponentProps>
|
||||||
|
|
||||||
export type OnConnectFunc = (connection: Connection) => void
|
export type OnConnectFunc = (connection: Connection) => void
|
||||||
export type OnConnectStartParams = {
|
export type OnConnectStartParams = {
|
||||||
nodeId: ElementId | null
|
nodeId: ElementId | undefined
|
||||||
handleId: ElementId | null
|
handleId: ElementId | undefined
|
||||||
handleType: HandleType | null
|
handleType: HandleType | undefined
|
||||||
}
|
}
|
||||||
export type OnConnectStartFunc = (event: MouseEvent, params: OnConnectStartParams) => void
|
export type OnConnectStartFunc = (event: MouseEvent, params: OnConnectStartParams) => void
|
||||||
export type OnConnectStopFunc = (event: MouseEvent) => void
|
export type OnConnectStopFunc = (event: MouseEvent) => void
|
||||||
@@ -44,7 +45,7 @@ export enum ConnectionMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type SetConnectionId = {
|
export type SetConnectionId = {
|
||||||
connectionNodeId: ElementId | null
|
connectionNodeId: ElementId | undefined
|
||||||
connectionHandleId: ElementId | null
|
connectionHandleId: ElementId | undefined
|
||||||
connectionHandleType: HandleType | null
|
connectionHandleType: HandleType | undefined
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-12
@@ -1,4 +1,4 @@
|
|||||||
import { DefineComponent, HTMLAttributes, VNode } from 'vue'
|
import { DefineComponent, VNode } from 'vue'
|
||||||
import { ArrowHeadType, ElementId, Position } from './types'
|
import { ArrowHeadType, ElementId, Position } from './types'
|
||||||
import { Connection } from './connection'
|
import { Connection } from './connection'
|
||||||
|
|
||||||
@@ -53,17 +53,6 @@ export interface EdgeSmoothStepProps<T = any> extends EdgeProps<T> {
|
|||||||
borderRadius?: number
|
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 EdgeType = DefineComponent<EdgeSmoothStepProps>
|
||||||
|
|
||||||
export type OnEdgeUpdateFunc<T = any> = (oldEdge: Edge<T>, newConnection: Connection) => void
|
export type OnEdgeUpdateFunc<T = any> = (oldEdge: Edge<T>, newConnection: Connection) => void
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Dimensions, ElementId, Position, XYPosition } from './types'
|
import { Dimensions, ElementId, Position, XYPosition } from './types'
|
||||||
import { Connection, OnConnectFunc } from './connection'
|
|
||||||
|
|
||||||
export type HandleType = 'source' | 'target'
|
export type HandleType = 'source' | 'target'
|
||||||
|
|
||||||
@@ -7,12 +6,3 @@ export interface HandleElement extends XYPosition, Dimensions {
|
|||||||
id?: ElementId | null
|
id?: ElementId | null
|
||||||
position: Position
|
position: Position
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HandleProps {
|
|
||||||
type: HandleType
|
|
||||||
position: Position
|
|
||||||
isConnectable?: boolean
|
|
||||||
onConnect?: OnConnectFunc
|
|
||||||
isValidConnection?: (connection: Connection) => boolean
|
|
||||||
id?: ElementId
|
|
||||||
}
|
|
||||||
|
|||||||
+4
-4
@@ -47,14 +47,14 @@ export type NodeDimensionUpdate = {
|
|||||||
export interface NodeProps<T = any> {
|
export interface NodeProps<T = any> {
|
||||||
id: ElementId
|
id: ElementId
|
||||||
type: string
|
type: string
|
||||||
data: T
|
data?: T
|
||||||
selected: boolean
|
selected?: boolean
|
||||||
isConnectable: boolean
|
connectable?: boolean
|
||||||
xPos?: number
|
xPos?: number
|
||||||
yPos?: number
|
yPos?: number
|
||||||
targetPosition?: Position
|
targetPosition?: Position
|
||||||
sourcePosition?: Position
|
sourcePosition?: Position
|
||||||
isDragging?: boolean
|
dragging?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NodeType = DefineComponent<NodeProps>
|
export type NodeType = DefineComponent<NodeProps>
|
||||||
|
|||||||
+9
-6
@@ -1,5 +1,9 @@
|
|||||||
import { Selection as D3Selection, ZoomBehavior } from 'd3'
|
import { Selection, ZoomBehavior } from 'd3'
|
||||||
import { FitViewFunc, FlowTransform, Rect, Transform, XYPosition } from './types'
|
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
|
export type KeyCode = number | string
|
||||||
|
|
||||||
@@ -21,8 +25,7 @@ export interface ZoomPanHelperFunctions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type InitD3ZoomPayload = {
|
export type InitD3ZoomPayload = {
|
||||||
d3Zoom: ZoomBehavior<Element, unknown>
|
d3Zoom: D3Zoom
|
||||||
d3Selection: D3Selection<Element, unknown, null, undefined>
|
d3Selection: D3Selection
|
||||||
d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined
|
d3ZoomHandler: D3ZoomHandler
|
||||||
transform: Transform
|
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-12
@@ -1,24 +1,23 @@
|
|||||||
import { ZoomBehavior, Selection } from 'd3'
|
|
||||||
import { Store } from 'pinia'
|
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 { HandleType } from './handle'
|
||||||
import { ConnectionMode, OnConnectEndFunc, OnConnectFunc, OnConnectStartFunc, OnConnectStopFunc } from './connection'
|
import { ConnectionMode, OnConnectEndFunc, OnConnectFunc, OnConnectStartFunc, OnConnectStopFunc } from './connection'
|
||||||
import { Edge } from './edge'
|
import { Edge } from './edge'
|
||||||
import { Node, NodeExtent, TranslateExtent } from './node'
|
import { Node, NodeExtent, TranslateExtent } from './node'
|
||||||
import { RevueFlowActions } from './actions'
|
import { RevueFlowActions } from './actions'
|
||||||
|
import { D3Selection, D3Zoom, D3ZoomHandler } from '~/types/panel'
|
||||||
|
|
||||||
export interface RevueFlowState {
|
export interface RevueFlowState {
|
||||||
width: number
|
dimensions: Dimensions
|
||||||
height: number
|
|
||||||
transform: Transform
|
transform: Transform
|
||||||
nodes: Node[]
|
nodes: Node[]
|
||||||
edges: Edge[]
|
edges: Edge[]
|
||||||
selectedElements: Elements | null
|
selectedElements?: Elements
|
||||||
selectedNodesBbox: Rect
|
selectedNodesBbox: Rect
|
||||||
|
|
||||||
d3Zoom: ZoomBehavior<Element, unknown> | null
|
d3Zoom?: D3Zoom
|
||||||
d3Selection: Selection<Element, unknown, null, undefined> | null
|
d3Selection?: D3Selection
|
||||||
d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined
|
d3ZoomHandler?: D3ZoomHandler
|
||||||
minZoom: number
|
minZoom: number
|
||||||
maxZoom: number
|
maxZoom: number
|
||||||
translateExtent: TranslateExtent
|
translateExtent: TranslateExtent
|
||||||
@@ -29,10 +28,10 @@ export interface RevueFlowState {
|
|||||||
|
|
||||||
userSelectionRect: SelectionRect
|
userSelectionRect: SelectionRect
|
||||||
|
|
||||||
connectionNodeId: ElementId | null
|
connectionNodeId?: ElementId
|
||||||
connectionHandleId: ElementId | null
|
connectionHandleId?: ElementId
|
||||||
connectionHandleType: HandleType | null
|
connectionHandleType?: HandleType
|
||||||
connectionPosition: XYPosition
|
connectionPosition?: XYPosition
|
||||||
connectionMode: ConnectionMode
|
connectionMode: ConnectionMode
|
||||||
|
|
||||||
snapToGrid: boolean
|
snapToGrid: boolean
|
||||||
|
|||||||
+5
-2
@@ -17,5 +17,8 @@ export const clampPosition = (position: XYPosition, extent: NodeExtent): XYPosit
|
|||||||
y: clamp(position.y, extent[0][1], extent[1][1]),
|
y: clamp(position.y, extent[0][1], extent[1][1]),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getHostForElement = (element: HTMLElement): Document | ShadowRoot =>
|
export const getHostForElement = (element: HTMLElement): Document => {
|
||||||
(element.getRootNode() as Document | ShadowRoot) || window.document
|
const doc = element.getRootNode() as Document
|
||||||
|
if ('getElementFromPoint' in doc) return doc
|
||||||
|
else return window.document
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user