feat: implement workspaces
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
<script lang="ts" setup>
|
||||
import { BackgroundVariant } from '../../types'
|
||||
import { useVueFlow } from '../../composables'
|
||||
import type { BackgroundProps } from '../../types/components'
|
||||
|
||||
const props = withDefaults(defineProps<BackgroundProps>(), {
|
||||
variant: 'dots' as BackgroundVariant,
|
||||
gap: 10,
|
||||
size: 0.4,
|
||||
height: 100,
|
||||
width: 100,
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
|
||||
const defaultColors: Record<BackgroundVariant, string> = {
|
||||
[BackgroundVariant.Dots]: '#81818a',
|
||||
[BackgroundVariant.Lines]: '#eee',
|
||||
}
|
||||
|
||||
const { store } = useVueFlow()
|
||||
|
||||
const background = computed(() => {
|
||||
const scaledGap = props.gap && props.gap * store.transform[2]
|
||||
const xOffset = scaledGap && store.transform[0] % scaledGap
|
||||
const yOffset = scaledGap && store.transform[1] % scaledGap
|
||||
const size = props.size || 0.4 * store.transform[2]
|
||||
|
||||
return {
|
||||
scaledGap,
|
||||
xOffset,
|
||||
yOffset,
|
||||
size,
|
||||
}
|
||||
})
|
||||
|
||||
// when there are multiple flows on a page we need to make sure that every background gets its own pattern.
|
||||
const patternId = `pattern-${Math.floor(Math.random() * 100000)}`
|
||||
const patternColor = computed(() =>
|
||||
props.patternColor ? props.patternColor : defaultColors[props.variant || BackgroundVariant.Dots],
|
||||
)
|
||||
const d = computed(
|
||||
() =>
|
||||
`M${background.value.scaledGap / 2} 0 V${background.value.scaledGap} M0 ${background.value.scaledGap / 2} H${
|
||||
background.value.scaledGap
|
||||
}`,
|
||||
)
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'Background',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<svg
|
||||
class="vue-flow__background"
|
||||
:style="{
|
||||
height: `${props.height > 100 ? 100 : props.height}%`,
|
||||
width: `${props.width > 100 ? 100 : props.width}%`,
|
||||
}"
|
||||
>
|
||||
<pattern
|
||||
:id="patternId"
|
||||
:x="background.xOffset"
|
||||
:y="background.yOffset"
|
||||
:width="background.scaledGap"
|
||||
:height="background.scaledGap"
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<template v-if="props.variant === BackgroundVariant.Lines">
|
||||
<path :stroke="patternColor" :stroke-width="props.size" :d="d" />
|
||||
</template>
|
||||
<template v-else-if="props.variant === BackgroundVariant.Dots">
|
||||
<circle :cx="background.size" :cy="background.size" :r="background.size" :fill="patternColor" />
|
||||
</template>
|
||||
<svg v-if="props.bgColor" height="100" width="100">
|
||||
<rect width="100%" height="100%" :fill="props.bgColor" />
|
||||
</svg>
|
||||
</pattern>
|
||||
<rect :x="props.x" :y="props.y" width="100%" height="100%" :fill="`url(#${patternId})`" />
|
||||
<slot></slot>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ControlButton',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<button class="vue-flow__controls-button">
|
||||
<slot></slot>
|
||||
</button>
|
||||
</template>
|
||||
@@ -0,0 +1,85 @@
|
||||
<script lang="ts" setup>
|
||||
import { useZoomPanHelper, useVueFlow } from '../../composables'
|
||||
import type { ControlProps, ControlEvents } from '../../types/components'
|
||||
import ControlButton from './ControlButton.vue'
|
||||
import PlusIcon from '~/assets/icons/plus.svg'
|
||||
import MinusIcon from '~/assets/icons/minus.svg'
|
||||
import Fitview from '~/assets/icons/fitview.svg'
|
||||
import Lock from '~/assets/icons/lock.svg'
|
||||
import Unlock from '~/assets/icons/unlock.svg'
|
||||
|
||||
const props = withDefaults(defineProps<ControlProps>(), {
|
||||
showZoom: true,
|
||||
showFitView: true,
|
||||
showInteractive: true,
|
||||
})
|
||||
const emit = defineEmits<ControlEvents>()
|
||||
|
||||
const { store } = useVueFlow()
|
||||
const { zoomIn, zoomOut, fitView } = useZoomPanHelper()
|
||||
|
||||
const isInteractive = computed(() => store.nodesDraggable && store.nodesConnectable && store.elementsSelectable)
|
||||
|
||||
const onZoomInHandler = () => {
|
||||
zoomIn()
|
||||
emit('zoom-in')
|
||||
}
|
||||
|
||||
const onZoomOutHandler = () => {
|
||||
zoomOut()
|
||||
emit('zoom-out')
|
||||
}
|
||||
|
||||
const onFitViewHandler = () => {
|
||||
fitView(props.fitViewParams)
|
||||
emit('fit-view')
|
||||
}
|
||||
|
||||
const onInteractiveChangeHandler = () => {
|
||||
store.setInteractive(!isInteractive.value)
|
||||
emit('interaction-change', !isInteractive.value)
|
||||
}
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'Controls',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="vue-flow__controls">
|
||||
<template v-if="props.showZoom">
|
||||
<slot name="control-zoom-in">
|
||||
<ControlButton class="vue-flow__controls-zoomin" @click="onZoomInHandler">
|
||||
<slot name="icon-zoom-in">
|
||||
<PlusIcon />
|
||||
</slot>
|
||||
</ControlButton>
|
||||
</slot>
|
||||
<slot name="control-zoom-out">
|
||||
<ControlButton class="vue-flow__controls-zoomout" @click="onZoomOutHandler">
|
||||
<slot name="icon-zoom-out">
|
||||
<MinusIcon />
|
||||
</slot>
|
||||
</ControlButton>
|
||||
</slot>
|
||||
</template>
|
||||
<slot name="control-fitview">
|
||||
<ControlButton v-if="props.showFitView" class="vue-flow__controls-fitview" @click="onFitViewHandler">
|
||||
<slot name="icon-fitview">
|
||||
<Fitview />
|
||||
</slot>
|
||||
</ControlButton>
|
||||
</slot>
|
||||
<slot name="control-interactive">
|
||||
<ControlButton v-if="props.showInteractive" class="vue-flow__controls-interactive" @click="onInteractiveChangeHandler">
|
||||
<slot name="icon-unlock">
|
||||
<Unlock v-if="isInteractive" />
|
||||
</slot>
|
||||
<slot name="icon-lock">
|
||||
<Lock v-if="!isInteractive" />
|
||||
</slot>
|
||||
</ControlButton>
|
||||
</slot>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,113 @@
|
||||
<script lang="ts" setup>
|
||||
import { ShapeRendering, StringFunc } from '../../types'
|
||||
import { useVueFlow, useWindow } from '../../composables'
|
||||
import { getBoundsofRects, getRectOfNodes } from '../../utils'
|
||||
import type { MiniMapProps } from '../../types/components'
|
||||
import MiniMapNode from './MiniMapNode.vue'
|
||||
|
||||
const props = withDefaults(defineProps<MiniMapProps>(), {
|
||||
nodeStrokeColor: '#555',
|
||||
nodeColor: '#fff',
|
||||
nodeClassName: '',
|
||||
nodeBorderRadius: 5,
|
||||
nodeStrokeWidth: 2,
|
||||
maskColor: 'rgb(240, 242, 243, 0.7)',
|
||||
})
|
||||
|
||||
const attrs: Record<string, any> = useAttrs()
|
||||
const window = useWindow()
|
||||
|
||||
const defaultWidth = 200
|
||||
const defaultHeight = 150
|
||||
|
||||
const { store } = useVueFlow()
|
||||
|
||||
const elementWidth = attrs.style?.width ?? defaultWidth
|
||||
const elementHeight = attrs.style?.height ?? defaultHeight
|
||||
const nodeColorFunc: StringFunc = props.nodeColor instanceof Function ? props.nodeColor : () => props.nodeColor as string
|
||||
const nodeStrokeColorFunc: StringFunc =
|
||||
props.nodeStrokeColor instanceof Function ? props.nodeStrokeColor : () => props.nodeStrokeColor as string
|
||||
|
||||
const nodeClassNameFunc = props.nodeClassName instanceof Function ? props.nodeClassName : () => props.nodeClassName as StringFunc
|
||||
|
||||
const shapeRendering: ShapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision'
|
||||
|
||||
const bb = computed(() => getRectOfNodes(store.getNodes))
|
||||
const viewBB = computed(() => ({
|
||||
x: -store.transform[0] / store.transform[2],
|
||||
y: -store.transform[1] / store.transform[2],
|
||||
width: store.dimensions.width / store.transform[2],
|
||||
height: store.dimensions.height / store.transform[2],
|
||||
}))
|
||||
const viewBox = computed(() => {
|
||||
const boundingRect = store.getNodes && store.getNodes.length ? getBoundsofRects(bb.value, viewBB.value) : viewBB.value
|
||||
const scaledWidth = boundingRect.width / elementWidth
|
||||
const scaledHeight = boundingRect.height / elementHeight
|
||||
const viewScale = Math.max(scaledWidth, scaledHeight)
|
||||
const viewWidth = viewScale * elementWidth
|
||||
const viewHeight = viewScale * elementHeight
|
||||
const offset = 5 * viewScale
|
||||
return {
|
||||
offset,
|
||||
x: boundingRect.x - (viewWidth - boundingRect.width) / 2 - offset,
|
||||
y: boundingRect.y - (viewHeight - boundingRect.height) / 2 - offset,
|
||||
width: viewWidth + offset * 2,
|
||||
height: viewHeight + offset * 2,
|
||||
}
|
||||
})
|
||||
|
||||
const d = computed(() => {
|
||||
if (viewBox.value.x && viewBox.value.y)
|
||||
return `
|
||||
M${viewBox.value.x - viewBox.value.offset},${viewBox.value.y - viewBox.value.offset}
|
||||
h${viewBox.value.width + viewBox.value.offset * 2}
|
||||
v${viewBox.value.height + viewBox.value.offset * 2}
|
||||
h${-viewBox.value.width - viewBox.value.offset * 2}z
|
||||
M${viewBB.value.x},${viewBB.value.y}
|
||||
h${viewBB.value.width}
|
||||
v${viewBB.value.height}
|
||||
h${-viewBB.value.width}z`
|
||||
else return ''
|
||||
})
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'MiniMap',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<svg
|
||||
:width="elementWidth"
|
||||
:height="elementHeight"
|
||||
:viewBox="`${viewBox.x || 0} ${viewBox.y || 0} ${viewBox.width || 0} ${viewBox.height || 0}`"
|
||||
class="vue-flow__minimap"
|
||||
>
|
||||
<template v-for="node of store.getNodes" :key="`mini-map-node-${node.id}`">
|
||||
<slot
|
||||
:name="`node-${node.type}`"
|
||||
:position="node.computedPosition"
|
||||
:dimensions="node.dimensions"
|
||||
:style="node.style"
|
||||
:class="nodeClassNameFunc(node)"
|
||||
:color="nodeColorFunc(node)"
|
||||
:border-radius="props.nodeBorderRadius"
|
||||
:stroke-color="nodeStrokeColorFunc(node)"
|
||||
:stroke-width="props.nodeStrokeWidth"
|
||||
:shape-rendering="shapeRendering"
|
||||
>
|
||||
<MiniMapNode
|
||||
:position="node.computedPosition"
|
||||
:dimensions="node.dimensions"
|
||||
:style="node.style"
|
||||
:class="nodeClassNameFunc(node)"
|
||||
:color="nodeColorFunc(node)"
|
||||
:border-radius="props.nodeBorderRadius"
|
||||
:stroke-color="nodeStrokeColorFunc(node)"
|
||||
:stroke-width="props.nodeStrokeWidth"
|
||||
:shape-rendering="shapeRendering"
|
||||
/>
|
||||
</slot>
|
||||
</template>
|
||||
<path class="vue-flow__minimap-mask" :d="d" :fill="props.maskColor" fill-rule="evenodd" />
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script lang="ts" setup>
|
||||
import { CSSProperties } from 'vue'
|
||||
import type { MiniMapNodeProps } from '../../types/components'
|
||||
|
||||
const props = withDefaults(defineProps<MiniMapNodeProps>(), {
|
||||
shapeRendering: 'geometricPrecision',
|
||||
})
|
||||
const attrs = useAttrs()
|
||||
|
||||
const styles = (attrs.style ?? {}) as CSSProperties
|
||||
const fill = computed(() => props.color || (styles.background as string) || styles.backgroundColor)
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'MiniMapNode',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<rect
|
||||
class="vue-flow__minimap-node"
|
||||
:x="props.position.x"
|
||||
:y="props.position.y"
|
||||
:rx="props.borderRadius"
|
||||
:ry="props.borderRadius"
|
||||
:width="props.dimensions.width"
|
||||
:height="props.dimensions.height"
|
||||
:fill="fill"
|
||||
:stroke="props.strokeColor"
|
||||
:stroke-width="props.strokeWidth"
|
||||
:shape-rendering="props.shapeRendering"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,6 @@
|
||||
// These components are not used by vue flow directly
|
||||
// but the user can add them as children of a vue flow component
|
||||
|
||||
export { default as MiniMap } from './MiniMap/MiniMap.vue'
|
||||
export { default as Controls } from './Controls/Controls.vue'
|
||||
export { default as Background } from './Background/Background.vue'
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 30">
|
||||
<path d="M3.692 4.63c0-.53.4-.938.939-.938h5.215V0H4.708C2.13 0 0 2.054 0 4.63v5.216h3.692V4.631zM27.354 0h-5.2v3.692h5.17c.53 0 .984.4.984.939v5.215H32V4.631A4.624 4.624 0 0027.354 0zm.954 24.83c0 .532-.4.94-.939.94h-5.215v3.768h5.215c2.577 0 4.631-2.13 4.631-4.707v-5.139h-3.692v5.139zm-23.677.94c-.531 0-.939-.4-.939-.94v-5.138H0v5.139c0 2.577 2.13 4.707 4.708 4.707h5.138V25.77H4.631z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 463 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 32">
|
||||
<path d="M21.333 10.667H19.81V7.619C19.81 3.429 16.38 0 12.19 0 8 0 4.571 3.429 4.571 7.619v3.048H3.048A3.056 3.056 0 000 13.714v15.238A3.056 3.056 0 003.048 32h18.285a3.056 3.056 0 003.048-3.048V13.714a3.056 3.056 0 00-3.048-3.047zM12.19 24.533a3.056 3.056 0 01-3.047-3.047 3.056 3.056 0 013.047-3.048 3.056 3.056 0 013.048 3.048 3.056 3.056 0 01-3.048 3.047zm4.724-13.866H7.467V7.619c0-2.59 2.133-4.724 4.723-4.724 2.591 0 4.724 2.133 4.724 4.724v3.048z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 530 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 5">
|
||||
<path d="M0 0h32v4.2H0z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 96 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<path d="M32 18.133H18.133V32h-4.266V18.133H0v-4.266h13.867V0h4.266v13.867H32z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 152 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 32">
|
||||
<path d="M21.333 10.667H19.81V7.619C19.81 3.429 16.38 0 12.19 0c-4.114 1.828-1.37 2.133.305 2.438 1.676.305 4.42 2.59 4.42 5.181v3.048H3.047A3.056 3.056 0 000 13.714v15.238A3.056 3.056 0 003.048 32h18.285a3.056 3.056 0 003.048-3.048V13.714a3.056 3.056 0 00-3.048-3.047zM12.19 24.533a3.056 3.056 0 01-3.047-3.047 3.056 3.056 0 013.047-3.048 3.056 3.056 0 013.048 3.048 3.056 3.056 0 01-3.048 3.047z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 472 B |
Binary file not shown.
|
After Width: | Height: | Size: 4.6 MiB |
Vendored
+212
@@ -0,0 +1,212 @@
|
||||
// Generated by 'unplugin-auto-import'
|
||||
// We suggest you to commit this file into source control
|
||||
declare global {
|
||||
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
||||
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
||||
const biSyncRef: typeof import('@vueuse/core')['biSyncRef']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const computedInject: typeof import('@vueuse/core')['computedInject']
|
||||
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
|
||||
const controlledRef: typeof import('@vueuse/core')['controlledRef']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const createEventHook: typeof import('@vueuse/core')['createEventHook']
|
||||
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
|
||||
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
|
||||
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
|
||||
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
|
||||
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const extendRef: typeof import('@vueuse/core')['extendRef']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const h: typeof import('vue')['h']
|
||||
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isDefined: typeof import('@vueuse/core')['isDefined']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
|
||||
const onLongPress: typeof import('@vueuse/core')['onLongPress']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const reactify: typeof import('@vueuse/core')['reactify']
|
||||
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
|
||||
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
|
||||
const reactivePick: typeof import('@vueuse/core')['reactivePick']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const refDefault: typeof import('@vueuse/core')['refDefault']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const syncRef: typeof import('@vueuse/core')['syncRef']
|
||||
const templateRef: typeof import('@vueuse/core')['templateRef']
|
||||
const throttledRef: typeof import('@vueuse/core')['throttledRef']
|
||||
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toReactive: typeof import('@vueuse/core')['toReactive']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
|
||||
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
|
||||
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
|
||||
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const unrefElement: typeof import('@vueuse/core')['unrefElement']
|
||||
const until: typeof import('@vueuse/core')['until']
|
||||
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
|
||||
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
|
||||
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useBase64: typeof import('@vueuse/core')['useBase64']
|
||||
const useBattery: typeof import('@vueuse/core')['useBattery']
|
||||
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
|
||||
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
|
||||
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
|
||||
const useCached: typeof import('@vueuse/core')['useCached']
|
||||
const useClamp: typeof import('@vueuse/core')['useClamp']
|
||||
const useClipboard: typeof import('@vueuse/core')['useClipboard']
|
||||
const useColorMode: typeof import('@vueuse/core')['useColorMode']
|
||||
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
|
||||
const useCounter: typeof import('@vueuse/core')['useCounter']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVar: typeof import('@vueuse/core')['useCssVar']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useCycleList: typeof import('@vueuse/core')['useCycleList']
|
||||
const useDark: typeof import('@vueuse/core')['useDark']
|
||||
const useDebounce: typeof import('@vueuse/core')['useDebounce']
|
||||
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
|
||||
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
|
||||
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
|
||||
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
|
||||
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
|
||||
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
|
||||
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
|
||||
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
|
||||
const useDraggable: typeof import('@vueuse/core')['useDraggable']
|
||||
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
|
||||
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
|
||||
const useElementHover: typeof import('@vueuse/core')['useElementHover']
|
||||
const useElementSize: typeof import('@vueuse/core')['useElementSize']
|
||||
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
|
||||
const useEventBus: typeof import('@vueuse/core')['useEventBus']
|
||||
const useEventListener: typeof import('@vueuse/core')['useEventListener']
|
||||
const useEventSource: typeof import('@vueuse/core')['useEventSource']
|
||||
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
|
||||
const useFavicon: typeof import('@vueuse/core')['useFavicon']
|
||||
const useFetch: typeof import('@vueuse/core')['useFetch']
|
||||
const useFocus: typeof import('@vueuse/core')['useFocus']
|
||||
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
|
||||
const useFps: typeof import('@vueuse/core')['useFps']
|
||||
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
|
||||
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
|
||||
const useIdle: typeof import('@vueuse/core')['useIdle']
|
||||
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
|
||||
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
|
||||
const useInterval: typeof import('@vueuse/core')['useInterval']
|
||||
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
|
||||
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
|
||||
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
|
||||
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
|
||||
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
|
||||
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
|
||||
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
|
||||
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
|
||||
const useMemoize: typeof import('@vueuse/core')['useMemoize']
|
||||
const useMemory: typeof import('@vueuse/core')['useMemory']
|
||||
const useMounted: typeof import('@vueuse/core')['useMounted']
|
||||
const useMouse: typeof import('@vueuse/core')['useMouse']
|
||||
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
|
||||
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
|
||||
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
|
||||
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
|
||||
const useNetwork: typeof import('@vueuse/core')['useNetwork']
|
||||
const useNow: typeof import('@vueuse/core')['useNow']
|
||||
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
|
||||
const useOnline: typeof import('@vueuse/core')['useOnline']
|
||||
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
|
||||
const useParallax: typeof import('@vueuse/core')['useParallax']
|
||||
const usePermission: typeof import('@vueuse/core')['usePermission']
|
||||
const usePointer: typeof import('@vueuse/core')['usePointer']
|
||||
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
|
||||
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
|
||||
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
|
||||
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
|
||||
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
||||
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
||||
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
|
||||
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
|
||||
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
|
||||
const useScroll: typeof import('@vueuse/core')['useScroll']
|
||||
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
|
||||
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
|
||||
const useShare: typeof import('@vueuse/core')['useShare']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
|
||||
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
|
||||
const useStorage: typeof import('@vueuse/core')['useStorage']
|
||||
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
|
||||
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
|
||||
const useSwipe: typeof import('@vueuse/core')['useSwipe']
|
||||
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
|
||||
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
|
||||
const useThrottle: typeof import('@vueuse/core')['useThrottle']
|
||||
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
|
||||
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
|
||||
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
|
||||
const useTimeout: typeof import('@vueuse/core')['useTimeout']
|
||||
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
|
||||
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
|
||||
const useTitle: typeof import('@vueuse/core')['useTitle']
|
||||
const useToggle: typeof import('@vueuse/core')['useToggle']
|
||||
const useTransition: typeof import('@vueuse/core')['useTransition']
|
||||
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
|
||||
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
|
||||
const useVibrate: typeof import('@vueuse/core')['useVibrate']
|
||||
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
|
||||
const useVModel: typeof import('@vueuse/core')['useVModel']
|
||||
const useVModels: typeof import('@vueuse/core')['useVModels']
|
||||
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
|
||||
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
|
||||
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
|
||||
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
|
||||
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
|
||||
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
|
||||
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
|
||||
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchOnce: typeof import('@vueuse/core')['watchOnce']
|
||||
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
|
||||
const whenever: typeof import('@vueuse/core')['whenever']
|
||||
}
|
||||
export {}
|
||||
@@ -0,0 +1,89 @@
|
||||
<script lang="ts" setup>
|
||||
import { getBezierPath, getSmoothStepPath } from '../Edges/utils'
|
||||
import { ConnectionLineType, GraphNode, HandleElement, Position } from '../../types'
|
||||
import { useVueFlow } from '../../composables'
|
||||
|
||||
interface ConnectionLineProps {
|
||||
sourceNode: GraphNode
|
||||
}
|
||||
const props = defineProps<ConnectionLineProps>()
|
||||
const { store } = useVueFlow()
|
||||
const sourceHandle =
|
||||
store.connectionHandleId && store.connectionHandleType
|
||||
? props.sourceNode.handleBounds[store.connectionHandleType]?.find((d: HandleElement) => d.id === store.connectionHandleId)
|
||||
: store.connectionHandleType && props.sourceNode.handleBounds[store.connectionHandleType ?? 'source']?.[0]
|
||||
const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : props.sourceNode.dimensions.width / 2
|
||||
const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : props.sourceNode.dimensions.height
|
||||
const sourceX = props.sourceNode.computedPosition.x + sourceHandleX
|
||||
const sourceY = props.sourceNode.computedPosition.y + sourceHandleY
|
||||
|
||||
const isRightOrLeft = sourceHandle?.position === Position.Left || sourceHandle?.position === Position.Right
|
||||
const targetPosition = isRightOrLeft ? Position.Left : Position.Top
|
||||
|
||||
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 dAttr = computed(() => {
|
||||
let path = `M${sourceX},${sourceY} ${targetX.value},${targetY.value}`
|
||||
switch (store.connectionLineType) {
|
||||
case ConnectionLineType.Bezier:
|
||||
path = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition: sourceHandle?.position,
|
||||
targetX: targetX.value,
|
||||
targetY: targetY.value,
|
||||
targetPosition,
|
||||
})
|
||||
break
|
||||
case ConnectionLineType.Step:
|
||||
path = getSmoothStepPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition: sourceHandle?.position,
|
||||
targetX: targetX.value,
|
||||
targetY: targetY.value,
|
||||
targetPosition,
|
||||
borderRadius: 0,
|
||||
})
|
||||
break
|
||||
case ConnectionLineType.SmoothStep:
|
||||
path = getSmoothStepPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition: sourceHandle?.position,
|
||||
targetX: targetX.value,
|
||||
targetY: targetY.value,
|
||||
targetPosition,
|
||||
})
|
||||
break
|
||||
}
|
||||
return path
|
||||
})
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ConnectionLine',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<g class="vue-flow__connection">
|
||||
<slot
|
||||
v-bind="{
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition: sourceHandle?.position,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
connectionLineType: store.connectionLineType,
|
||||
connectionLineStyle: store.connectionLineStyle,
|
||||
nodes: store.getNodes,
|
||||
sourceNode: props.sourceNode,
|
||||
sourceHandle,
|
||||
}"
|
||||
>
|
||||
<path :d="dAttr" class="vue-flow__connection-path" :style="store.connectionLineStyle || {}" />
|
||||
</slot>
|
||||
</g>
|
||||
</template>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script lang="ts" setup>
|
||||
import { CSSProperties } from 'vue'
|
||||
import EdgeText from './EdgeText.vue'
|
||||
|
||||
interface Props {
|
||||
centerX: number
|
||||
centerY: number
|
||||
path: string
|
||||
label?:
|
||||
| string
|
||||
| {
|
||||
props?: any
|
||||
component: any
|
||||
}
|
||||
style?: CSSProperties
|
||||
labelStyle?: any
|
||||
labelShowBg?: boolean
|
||||
labelBgStyle?: any
|
||||
labelBgPadding?: [number, number]
|
||||
labelBgBorderRadius?: number
|
||||
markerStart?: string
|
||||
markerEnd?: string
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
</script>
|
||||
<template>
|
||||
<path
|
||||
:style="props.style"
|
||||
:d="props.path"
|
||||
class="vue-flow__edge-path"
|
||||
:marker-end="props.markerEnd"
|
||||
:marker-start="props.markerStart"
|
||||
/>
|
||||
<EdgeText
|
||||
v-if="props.label"
|
||||
:x="props.centerX"
|
||||
:y="props.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>
|
||||
@@ -0,0 +1,62 @@
|
||||
<script lang="ts" setup>
|
||||
import { Position } from '../../types/flow'
|
||||
import type { EdgeProps } from '../../types/edge'
|
||||
import { getBezierPath, getBezierCenter } from './utils'
|
||||
import BaseEdge from './BaseEdge.vue'
|
||||
|
||||
const props = withDefaults(defineProps<EdgeProps>(), {
|
||||
selected: false,
|
||||
sourcePosition: 'bottom' as Position,
|
||||
targetPosition: 'top' as Position,
|
||||
labelStyle: () => ({}),
|
||||
label: () => '',
|
||||
labelShowBg: true,
|
||||
labelBgStyle: () => ({}),
|
||||
})
|
||||
|
||||
const centered = computed(() =>
|
||||
getBezierCenter({
|
||||
sourceX: props.sourceX,
|
||||
sourceY: props.sourceY,
|
||||
targetX: props.targetX,
|
||||
targetY: props.targetY,
|
||||
sourcePosition: props.sourcePosition,
|
||||
targetPosition: props.targetPosition,
|
||||
curvature: props.curvature,
|
||||
}),
|
||||
)
|
||||
const path = computed(() => {
|
||||
if (props.sourceX && props.sourceY)
|
||||
return getBezierPath({
|
||||
sourceX: props.sourceX,
|
||||
sourceY: props.sourceY,
|
||||
targetX: props.targetX,
|
||||
targetY: props.targetY,
|
||||
sourcePosition: props.sourcePosition,
|
||||
targetPosition: props.targetPosition,
|
||||
curvature: props.curvature,
|
||||
})
|
||||
else return ''
|
||||
})
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'BezierEdge',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<BaseEdge
|
||||
:path="path"
|
||||
:center-x="centered[0]"
|
||||
:center-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"
|
||||
:style="props.style"
|
||||
:marker-end="props.markerEnd"
|
||||
:marker-start="props.markerStart"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,49 @@
|
||||
<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,
|
||||
centerX: 0,
|
||||
centerY: 0,
|
||||
position: Position.Top,
|
||||
})
|
||||
|
||||
const cx = computed(() => {
|
||||
const val = shiftX(props.centerX, props.radius, props.position)
|
||||
if (isNaN(val)) return 0
|
||||
else return val
|
||||
})
|
||||
const cy = computed(() => {
|
||||
const val = shiftY(props.centerY, props.radius, props.position)
|
||||
if (isNaN(val)) return 0
|
||||
else return val
|
||||
})
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'EdgeAnchor',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<circle class="vue-flow__edgeupdater" :cx="cx" :cy="cy" :r="props.radius" stroke="transparent" fill="transparent" />
|
||||
</template>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EdgeTextProps } from '../../types/components'
|
||||
import { Rect } from '../../types'
|
||||
|
||||
const props = withDefaults(defineProps<EdgeTextProps>(), {
|
||||
labelStyle: () => ({}),
|
||||
labelShowBg: true,
|
||||
labelBgStyle: () => ({}),
|
||||
labelBgPadding: () => [2, 4],
|
||||
labelBgBorderRadius: 2,
|
||||
})
|
||||
|
||||
const edgeRef = templateRef<SVGTextElement>('edge-text', null)
|
||||
|
||||
const edgeRefBbox = ref<Rect>({ x: 0, y: 0, width: 0, height: 0 })
|
||||
|
||||
onMounted(() => {
|
||||
edgeRefBbox.value = edgeRef.value.getBBox()
|
||||
})
|
||||
const transform = computed(() => `translate(${props.x - edgeRefBbox.value.width / 2} ${props.y - edgeRefBbox.value.height / 2})`)
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'EdgeText',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<g :transform="transform" :class="props.class" class="vue-flow__edge-textwrapper">
|
||||
<rect
|
||||
v-if="props.labelShowBg"
|
||||
class="vue-flow__edge-textbg"
|
||||
:width="edgeRefBbox.width + 2 * props.labelBgPadding[0] + 'px'"
|
||||
:height="edgeRefBbox.height + 2 * props.labelBgPadding[1] + 'px'"
|
||||
:x="-props.labelBgPadding[0]"
|
||||
:y="-props.labelBgPadding[1]"
|
||||
:style="props.labelBgStyle"
|
||||
:rx="props.labelBgBorderRadius"
|
||||
:ry="props.labelBgBorderRadius"
|
||||
/>
|
||||
<text ref="edge-text" class="vue-flow__edge-text" :y="edgeRefBbox.height / 2" dy="0.3em" :style="props.labelStyle">
|
||||
<slot v-bind="props">
|
||||
<component
|
||||
:is="props.label?.component"
|
||||
v-if="typeof props.label !== 'string' && typeof props.label?.component !== 'undefined'"
|
||||
v-bind="props.label?.props"
|
||||
/>
|
||||
<template v-else v-html="props.label">
|
||||
{{ props.label }}
|
||||
</template>
|
||||
</slot>
|
||||
</text>
|
||||
</g>
|
||||
</template>
|
||||
@@ -0,0 +1,222 @@
|
||||
<script lang="ts" setup>
|
||||
import { useHandle, useVueFlow } from '../../composables'
|
||||
import { ConnectionMode, Position, EdgeComponent } from '../../types'
|
||||
import { getEdgePositions, getHandle, getMarkerId } from '../../utils'
|
||||
import EdgeAnchor from './EdgeAnchor.vue'
|
||||
|
||||
interface EdgeWrapper {
|
||||
id: string
|
||||
selectable?: boolean
|
||||
updatable?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<EdgeWrapper>()
|
||||
const { store } = useVueFlow()
|
||||
const edge = computed(() => store.getEdge(props.id)!)
|
||||
if (!edge.value) throw new Error(`Edge with ${props.id} not found!`)
|
||||
|
||||
const updating = ref(false)
|
||||
|
||||
const { onMouseDown } = useHandle()
|
||||
const onEdgeClick = (event: MouseEvent) => {
|
||||
const data = { event, edge: edge.value }
|
||||
if (props.selectable) {
|
||||
store.nodesSelectionActive = false
|
||||
store.addSelectedEdges([edge.value])
|
||||
}
|
||||
store.hooks.edgeClick.trigger(data)
|
||||
}
|
||||
const onEdgeContextMenu = (event: MouseEvent) => store.hooks.edgeContextMenu.trigger({ event, edge: edge.value })
|
||||
const onDoubleClick = (event: MouseEvent) => store.hooks.edgeDoubleClick.trigger({ event, edge: edge.value })
|
||||
const onEdgeMouseEnter = (event: MouseEvent) => store.hooks.edgeMouseEnter.trigger({ event, edge: edge.value })
|
||||
const onEdgeMouseMove = (event: MouseEvent) => store.hooks.edgeMouseMove.trigger({ event, edge: edge.value })
|
||||
const onEdgeMouseLeave = (event: MouseEvent) => store.hooks.edgeMouseLeave.trigger({ event, edge: edge.value })
|
||||
const onEdgeUpdaterMouseEnter = () => (updating.value = true)
|
||||
const onEdgeUpdaterMouseOut = () => (updating.value = false)
|
||||
const onEdgeUpdaterSourceMouseDown = (event: MouseEvent) => handleEdgeUpdater(event, true)
|
||||
const onEdgeUpdaterTargetMouseDown = (event: MouseEvent) => handleEdgeUpdater(event, false)
|
||||
|
||||
const handleEdgeUpdater = (event: MouseEvent, isSourceHandle: boolean) => {
|
||||
const nodeId = isSourceHandle ? edge.value.target : edge.value.source
|
||||
const handleId = (isSourceHandle ? edge.value.targetHandle : edge.value.sourceHandle) ?? ''
|
||||
|
||||
store.hooks.edgeUpdateStart.trigger({ event, edge: edge.value })
|
||||
onMouseDown(
|
||||
event,
|
||||
handleId,
|
||||
nodeId,
|
||||
isSourceHandle,
|
||||
undefined,
|
||||
isSourceHandle ? 'target' : 'source',
|
||||
(connection) => store.hooks.edgeUpdate.trigger({ edge: edge.value, connection }),
|
||||
() => store.hooks.edgeUpdateEnd.trigger({ event, edge: edge.value }),
|
||||
)
|
||||
}
|
||||
|
||||
// when connection type is loose we can define all handles as sources
|
||||
const targetNodeHandles = computed(() =>
|
||||
store.connectionMode === ConnectionMode.Strict
|
||||
? edge.value.targetNode.handleBounds.target
|
||||
: edge.value.targetNode.handleBounds.target ?? edge.value.targetNode.handleBounds.source,
|
||||
)
|
||||
const sourceHandle = controlledComputed(
|
||||
() => edge.value.sourceNode.handleBounds,
|
||||
() => getHandle(edge.value.sourceNode.handleBounds.source, edge.value.sourceHandle),
|
||||
)
|
||||
const targetHandle = computed(() => getHandle(targetNodeHandles.value, edge.value.targetHandle))
|
||||
const sourcePosition = computed(() => (sourceHandle.value ? sourceHandle.value.position : Position.Bottom))
|
||||
const targetPosition = computed(() => (targetHandle.value ? targetHandle.value.position : Position.Top))
|
||||
const edgeUpdaterRadius = computed(() => store.edgeUpdaterRadius)
|
||||
|
||||
onMounted(() => {
|
||||
watch(
|
||||
[
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
() => edge.value.sourceNode.position,
|
||||
() => edge.value.targetNode.position,
|
||||
() => edge.value.sourceNode.computedPosition,
|
||||
() => edge.value.targetNode.computedPosition,
|
||||
() => edge.value.sourceNode.dimensions,
|
||||
() => edge.value.targetNode.dimensions,
|
||||
],
|
||||
() => {
|
||||
const { sourceX, sourceY, targetY, targetX } = getEdgePositions(
|
||||
edge.value.sourceNode,
|
||||
sourceHandle.value,
|
||||
sourcePosition.value,
|
||||
edge.value.targetNode,
|
||||
targetHandle.value,
|
||||
targetPosition.value,
|
||||
)
|
||||
if (edge.value.sourceX !== sourceX) edge.value.sourceX = sourceX
|
||||
if (edge.value.sourceY !== sourceY) edge.value.sourceY = sourceY
|
||||
if (edge.value.targetX !== targetX) edge.value.targetX = targetX
|
||||
if (edge.value.targetY !== targetY) edge.value.targetY = targetY
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
)
|
||||
})
|
||||
|
||||
const name = ref(edge.value.type ?? 'default')
|
||||
const type = computed(() => {
|
||||
let edgeType = store.getEdgeTypes[name.value]
|
||||
if (typeof edgeType === 'string') edgeType = resolveComponent(name.value, false) as EdgeComponent
|
||||
if (typeof edgeType !== 'string') return edgeType
|
||||
|
||||
const slot = useSlots()?.[name.value]?.({})
|
||||
if (!slot || !slot[0].key?.toString().includes(name.value)) {
|
||||
console.warn(`Node type "${edge.value.type}" not found and no slot detected. Using fallback type "default".`)
|
||||
name.value = 'default'
|
||||
return store.getEdgeTypes.default
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'Edge',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<g
|
||||
:key="`edge-${edge.id}`"
|
||||
:class="[
|
||||
'vue-flow__edge',
|
||||
`vue-flow__edge-${name}`,
|
||||
store.noPanClassName,
|
||||
{
|
||||
selected: edge.selected,
|
||||
animated: edge.animated,
|
||||
inactive: !props.selectable,
|
||||
updating,
|
||||
},
|
||||
edge.class,
|
||||
]"
|
||||
@click="onEdgeClick"
|
||||
@dblClick="onDoubleClick"
|
||||
@contextmenu="onEdgeContextMenu"
|
||||
@mouseenter="onEdgeMouseEnter"
|
||||
@mousemove="onEdgeMouseMove"
|
||||
@mouseleave="onEdgeMouseLeave"
|
||||
>
|
||||
<slot
|
||||
v-bind="{
|
||||
id: edge.id,
|
||||
sourceNode: edge.sourceNode,
|
||||
targetNode: edge.targetNode,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
selected: edge.selected,
|
||||
animated: edge.animated,
|
||||
updatable: props.updatable,
|
||||
label: edge.label,
|
||||
labelStyle: edge.labelStyle,
|
||||
labelShowBg: edge.labelShowBg,
|
||||
labelBgStyle: edge.labelBgStyle,
|
||||
labelBgPadding: edge.labelBgPadding,
|
||||
labelBgBorderRadius: edge.labelBgBorderRadius,
|
||||
data: edge.data,
|
||||
style: edge.style,
|
||||
sourceX: edge.sourceX,
|
||||
sourceY: edge.sourceY,
|
||||
targetX: edge.targetX,
|
||||
targetY: edge.targetY,
|
||||
markerStart: `url(#${getMarkerId(edge.markerStart)})`,
|
||||
markerEnd: `url(#${getMarkerId(edge.markerEnd)})`,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
sourceHandleId: edge.sourceHandle,
|
||||
targetHandleId: edge.targetHandle,
|
||||
}"
|
||||
>
|
||||
<component
|
||||
:is="type"
|
||||
v-if="type"
|
||||
v-bind="{
|
||||
id: edge.id,
|
||||
sourceNode: edge.sourceNode,
|
||||
targetNode: edge.targetNode,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
updatable: props.updatable,
|
||||
selected: edge.selected,
|
||||
animated: edge.animated,
|
||||
label: edge.label,
|
||||
labelStyle: edge.labelStyle,
|
||||
labelShowBg: edge.labelShowBg,
|
||||
labelBgStyle: edge.labelBgStyle,
|
||||
labelBgPadding: edge.labelBgPadding,
|
||||
labelBgBorderRadius: edge.labelBgBorderRadius,
|
||||
data: edge.data,
|
||||
style: edge.style,
|
||||
markerStart: `url(#${getMarkerId(edge.markerStart)})`,
|
||||
markerEnd: `url(#${getMarkerId(edge.markerEnd)})`,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
sourceX: edge.sourceX,
|
||||
sourceY: edge.sourceY,
|
||||
targetX: edge.targetX,
|
||||
targetY: edge.targetY,
|
||||
sourceHandleId: edge.sourceHandle,
|
||||
targetHandleId: edge.targetHandle,
|
||||
}"
|
||||
/>
|
||||
</slot>
|
||||
<g
|
||||
v-if="props.updatable"
|
||||
@mousedown="onEdgeUpdaterSourceMouseDown"
|
||||
@mouseenter="onEdgeUpdaterMouseEnter"
|
||||
@mouseout="onEdgeUpdaterMouseOut"
|
||||
>
|
||||
<EdgeAnchor :position="sourcePosition" :center-x="edge.sourceX" :center-y="edge.sourceY" :radius="edgeUpdaterRadius" />
|
||||
</g>
|
||||
<g
|
||||
v-if="props.updatable"
|
||||
@mousedown="onEdgeUpdaterTargetMouseDown"
|
||||
@mouseenter="onEdgeUpdaterMouseEnter"
|
||||
@mouseout="onEdgeUpdaterMouseOut"
|
||||
>
|
||||
<EdgeAnchor :position="targetPosition" :center-x="edge.targetX" :center-y="edge.targetY" :radius="edgeUpdaterRadius" />
|
||||
</g>
|
||||
</g>
|
||||
</template>
|
||||
@@ -0,0 +1,60 @@
|
||||
<script lang="ts" setup>
|
||||
import { Position } from '../../types/flow'
|
||||
import type { EdgeProps } from '../../types/edge'
|
||||
import { getSimpleBezierPath, getSimpleBezierCenter } from './utils'
|
||||
import BaseEdge from './BaseEdge.vue'
|
||||
|
||||
const props = withDefaults(defineProps<EdgeProps>(), {
|
||||
selected: false,
|
||||
sourcePosition: 'bottom' as Position,
|
||||
targetPosition: 'top' as Position,
|
||||
labelStyle: () => ({}),
|
||||
label: () => '',
|
||||
labelShowBg: true,
|
||||
labelBgStyle: () => ({}),
|
||||
})
|
||||
|
||||
const centered = computed(() =>
|
||||
getSimpleBezierCenter({
|
||||
sourceX: props.sourceX,
|
||||
sourceY: props.sourceY,
|
||||
targetX: props.targetX,
|
||||
targetY: props.targetY,
|
||||
sourcePosition: props.sourcePosition,
|
||||
targetPosition: props.targetPosition,
|
||||
}),
|
||||
)
|
||||
const path = computed(() => {
|
||||
if (props.sourceX && props.sourceY)
|
||||
return getSimpleBezierPath({
|
||||
sourceX: props.sourceX,
|
||||
sourceY: props.sourceY,
|
||||
targetX: props.targetX,
|
||||
targetY: props.targetY,
|
||||
sourcePosition: props.sourcePosition,
|
||||
targetPosition: props.targetPosition,
|
||||
})
|
||||
else return ''
|
||||
})
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'SimpleBezierEdge',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<BaseEdge
|
||||
:path="path"
|
||||
:center-x="centered[0]"
|
||||
:center-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"
|
||||
:style="props.style"
|
||||
:marker-end="props.markerEnd"
|
||||
:marker-start="props.markerStart"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,62 @@
|
||||
<script lang="ts" setup>
|
||||
import { Position } from '../../types'
|
||||
import type { SmoothStepEdgeProps } from '../../types/edge'
|
||||
import { getCenter, getSmoothStepPath } from './utils'
|
||||
import BaseEdge from './BaseEdge.vue'
|
||||
|
||||
const props = withDefaults(defineProps<SmoothStepEdgeProps>(), {
|
||||
selected: false,
|
||||
sourcePosition: 'bottom' as Position,
|
||||
targetPosition: 'top' as Position,
|
||||
label: () => '',
|
||||
labelStyle: () => ({}),
|
||||
labelShowBg: true,
|
||||
labelBgStyle: () => ({}),
|
||||
})
|
||||
|
||||
const centered = computed(() =>
|
||||
getCenter({
|
||||
sourceX: props.sourceX,
|
||||
sourceY: props.sourceY,
|
||||
targetX: props.targetX,
|
||||
targetY: props.targetY,
|
||||
sourcePosition: props.sourcePosition,
|
||||
targetPosition: props.targetPosition,
|
||||
}),
|
||||
)
|
||||
|
||||
const path = computed(() => {
|
||||
if (props.sourceX && props.sourceY)
|
||||
return getSmoothStepPath({
|
||||
sourceX: props.sourceX,
|
||||
sourceY: props.sourceY,
|
||||
targetX: props.targetX,
|
||||
targetY: props.targetY,
|
||||
sourcePosition: props.sourcePosition,
|
||||
targetPosition: props.targetPosition,
|
||||
borderRadius: props.borderRadius,
|
||||
})
|
||||
else return ''
|
||||
})
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'SmoothStepEdge',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<BaseEdge
|
||||
:path="path"
|
||||
:center-x="centered[0]"
|
||||
:center-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"
|
||||
:style="props.style"
|
||||
:marker-end="props.markerEnd"
|
||||
:marker-start="props.markerStart"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts" setup>
|
||||
import { Position } from '../../types'
|
||||
import type { EdgeProps } from '../../types/edge'
|
||||
import SmoothStepEdge from './SmoothStepEdge.vue'
|
||||
|
||||
const props = withDefaults(defineProps<EdgeProps>(), {
|
||||
selected: false,
|
||||
sourcePosition: 'bottom' as Position,
|
||||
targetPosition: 'top' as Position,
|
||||
label: () => '',
|
||||
labelStyle: () => ({}),
|
||||
labelShowBg: true,
|
||||
labelBgStyle: () => ({}),
|
||||
})
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'StepEdge',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<SmoothStepEdge v-bind="props" :border-radius="0" />
|
||||
</template>
|
||||
@@ -0,0 +1,47 @@
|
||||
<script lang="ts" setup>
|
||||
import { Position } from '../../types'
|
||||
import type { EdgeProps } from '../../types/edge'
|
||||
import BaseEdge from './BaseEdge.vue'
|
||||
|
||||
const props = withDefaults(defineProps<EdgeProps>(), {
|
||||
selected: false,
|
||||
sourcePosition: 'bottom' as Position,
|
||||
targetPosition: 'top' as Position,
|
||||
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(() => `M ${props.sourceX},${props.sourceY}L ${props.targetX},${props.targetY}`)
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'StraightEdge',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<BaseEdge
|
||||
:path="path"
|
||||
:center-x="centerX"
|
||||
:center-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"
|
||||
:style="props.style"
|
||||
:marker-end="props.markerEnd"
|
||||
:marker-start="props.markerStart"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
export { default as BezierEdge } from './BezierEdge.vue'
|
||||
export { default as SimpleBezierEdge } from './SimpleBezierEdge.vue'
|
||||
export { default as StepEdge } from './StepEdge.vue'
|
||||
export { default as SmoothStepEdge } from './SmoothStepEdge.vue'
|
||||
export { default as StraightEdge } from './StraightEdge.vue'
|
||||
export { default as EdgeAnchor } from './EdgeAnchor.vue'
|
||||
export { default as EdgeText } from './EdgeText.vue'
|
||||
@@ -0,0 +1,115 @@
|
||||
import { Position } from '~/types'
|
||||
|
||||
interface GetControlWithCurvatureParams {
|
||||
pos: Position
|
||||
x1: number
|
||||
y1: number
|
||||
x2: number
|
||||
y2: number
|
||||
c: number
|
||||
}
|
||||
|
||||
export interface GetBezierPathParams {
|
||||
sourceX: number
|
||||
sourceY: number
|
||||
sourcePosition?: Position
|
||||
targetX: number
|
||||
targetY: number
|
||||
targetPosition?: Position
|
||||
curvature?: number
|
||||
centerX?: number
|
||||
centerY?: number
|
||||
}
|
||||
|
||||
function calculateControlOffset(distance: number, curvature: number): number {
|
||||
if (distance >= 0) {
|
||||
return 0.5 * distance
|
||||
} else {
|
||||
return curvature * 25 * Math.sqrt(-distance)
|
||||
}
|
||||
}
|
||||
|
||||
function getControlWithCurvature({ pos, x1, y1, x2, y2, c }: GetControlWithCurvatureParams): [number, number] {
|
||||
let ctX: number, ctY: number
|
||||
switch (pos) {
|
||||
case Position.Left:
|
||||
ctX = x1 - calculateControlOffset(x1 - x2, c)
|
||||
ctY = y1
|
||||
break
|
||||
case Position.Right:
|
||||
ctX = x1 + calculateControlOffset(x2 - x1, c)
|
||||
ctY = y1
|
||||
break
|
||||
case Position.Top:
|
||||
ctX = x1
|
||||
ctY = y1 - calculateControlOffset(y1 - y2, c)
|
||||
break
|
||||
case Position.Bottom:
|
||||
ctX = x1
|
||||
ctY = y1 + calculateControlOffset(y2 - y1, c)
|
||||
break
|
||||
}
|
||||
return [ctX, ctY]
|
||||
}
|
||||
|
||||
export function getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition = Position.Bottom,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition = Position.Top,
|
||||
curvature = 0.25,
|
||||
}: GetBezierPathParams): string {
|
||||
const [sourceControlX, sourceControlY] = getControlWithCurvature({
|
||||
pos: sourcePosition,
|
||||
x1: sourceX,
|
||||
y1: sourceY,
|
||||
x2: targetX,
|
||||
y2: targetY,
|
||||
c: curvature,
|
||||
})
|
||||
const [targetControlX, targetControlY] = getControlWithCurvature({
|
||||
pos: targetPosition,
|
||||
x1: targetX,
|
||||
y1: targetY,
|
||||
x2: sourceX,
|
||||
y2: sourceY,
|
||||
c: curvature,
|
||||
})
|
||||
return `M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`
|
||||
}
|
||||
|
||||
export function getBezierCenter({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition = Position.Bottom,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition = Position.Top,
|
||||
curvature = 0.25,
|
||||
}: GetBezierPathParams): [number, number, number, number] {
|
||||
const [sourceControlX, sourceControlY] = getControlWithCurvature({
|
||||
pos: sourcePosition,
|
||||
x1: sourceX,
|
||||
y1: sourceY,
|
||||
x2: targetX,
|
||||
y2: targetY,
|
||||
c: curvature,
|
||||
})
|
||||
const [targetControlX, targetControlY] = getControlWithCurvature({
|
||||
pos: targetPosition,
|
||||
x1: targetX,
|
||||
y1: targetY,
|
||||
x2: sourceX,
|
||||
y2: sourceY,
|
||||
c: curvature,
|
||||
})
|
||||
// cubic bezier t=0.5 mid point, not the actual mid point, but easy to calculate
|
||||
// https://stackoverflow.com/questions/67516101/how-to-find-distance-mid-point-of-bezier-curve
|
||||
const centerX = sourceX * 0.125 + sourceControlX * 0.375 + targetControlX * 0.375 + targetX * 0.125
|
||||
const centerY = sourceY * 0.125 + sourceControlY * 0.375 + targetControlY * 0.375 + targetY * 0.125
|
||||
const xOffset = Math.abs(centerX - sourceX)
|
||||
const yOffset = Math.abs(centerY - sourceY)
|
||||
return [centerX, centerY, xOffset, yOffset]
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Position } from '~/types'
|
||||
|
||||
export interface GetCenterParams {
|
||||
sourceX: number
|
||||
sourceY: number
|
||||
targetX: number
|
||||
targetY: number
|
||||
sourcePosition?: Position
|
||||
targetPosition?: Position
|
||||
}
|
||||
|
||||
const LeftOrRight = [Position.Left, Position.Right]
|
||||
|
||||
export const getCenter = ({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition = Position.Bottom,
|
||||
targetPosition = Position.Top,
|
||||
}: GetCenterParams): [number, number, number, number] => {
|
||||
const sourceIsLeftOrRight = LeftOrRight.includes(sourcePosition)
|
||||
const targetIsLeftOrRight = LeftOrRight.includes(targetPosition)
|
||||
|
||||
// we expect flows to be horizontal or vertical (all handles left or right respectively top or bottom)
|
||||
// a mixed edge is when one the source is on the left and the target is on the top for example.
|
||||
const mixedEdge = (sourceIsLeftOrRight && !targetIsLeftOrRight) || (targetIsLeftOrRight && !sourceIsLeftOrRight)
|
||||
|
||||
if (mixedEdge) {
|
||||
const xOffset = sourceIsLeftOrRight ? Math.abs(targetX - sourceX) : 0
|
||||
const centerX = sourceX > targetX ? sourceX - xOffset : sourceX + xOffset
|
||||
|
||||
const yOffset = sourceIsLeftOrRight ? 0 : Math.abs(targetY - sourceY)
|
||||
const centerY = sourceY < targetY ? sourceY + yOffset : sourceY - yOffset
|
||||
|
||||
return [centerX, centerY, xOffset, yOffset]
|
||||
}
|
||||
|
||||
const xOffset = Math.abs(targetX - sourceX) / 2
|
||||
const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset
|
||||
|
||||
const yOffset = Math.abs(targetY - sourceY) / 2
|
||||
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset
|
||||
|
||||
return [centerX, centerY, xOffset, yOffset]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './bezier'
|
||||
export * from './simple-bezier'
|
||||
export * from './smoothstep'
|
||||
export * from './general'
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Position } from '~/types'
|
||||
|
||||
export interface GetSimpleBezierPathParams {
|
||||
sourceX: number
|
||||
sourceY: number
|
||||
sourcePosition?: Position
|
||||
targetX: number
|
||||
targetY: number
|
||||
targetPosition?: Position
|
||||
}
|
||||
|
||||
interface GetControlParams {
|
||||
pos: Position
|
||||
x1: number
|
||||
y1: number
|
||||
x2: number
|
||||
y2: number
|
||||
}
|
||||
|
||||
function getControl({ pos, x1, y1, x2, y2 }: GetControlParams): [number, number] {
|
||||
let ctX: number, ctY: number
|
||||
switch (pos) {
|
||||
case Position.Left:
|
||||
case Position.Right:
|
||||
ctX = 0.5 * (x1 + x2)
|
||||
ctY = y1
|
||||
break
|
||||
case Position.Top:
|
||||
case Position.Bottom:
|
||||
ctX = x1
|
||||
ctY = 0.5 * (y1 + y2)
|
||||
break
|
||||
}
|
||||
return [ctX, ctY]
|
||||
}
|
||||
|
||||
export function getSimpleBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition = Position.Bottom,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition = Position.Top,
|
||||
}: GetSimpleBezierPathParams): string {
|
||||
const [sourceControlX, sourceControlY] = getControl({
|
||||
pos: sourcePosition,
|
||||
x1: sourceX,
|
||||
y1: sourceY,
|
||||
x2: targetX,
|
||||
y2: targetY,
|
||||
})
|
||||
const [targetControlX, targetControlY] = getControl({
|
||||
pos: targetPosition,
|
||||
x1: targetX,
|
||||
y1: targetY,
|
||||
x2: sourceX,
|
||||
y2: sourceY,
|
||||
})
|
||||
return `M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`
|
||||
}
|
||||
|
||||
// @TODO: this function will recalculate the control points
|
||||
// one option is to let getXXXPath() return center points
|
||||
// but will introduce breaking changes
|
||||
// the getCenter() of other types of edges might need to change, too
|
||||
export function getSimpleBezierCenter({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition = Position.Bottom,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition = Position.Top,
|
||||
}: GetSimpleBezierPathParams): [number, number, number, number] {
|
||||
const [sourceControlX, sourceControlY] = getControl({
|
||||
pos: sourcePosition,
|
||||
x1: sourceX,
|
||||
y1: sourceY,
|
||||
x2: targetX,
|
||||
y2: targetY,
|
||||
})
|
||||
const [targetControlX, targetControlY] = getControl({
|
||||
pos: targetPosition,
|
||||
x1: targetX,
|
||||
y1: targetY,
|
||||
x2: sourceX,
|
||||
y2: sourceY,
|
||||
})
|
||||
// cubic bezier t=0.5 mid point, not the actual mid point, but easy to calculate
|
||||
// https://stackoverflow.com/questions/67516101/how-to-find-distance-mid-point-of-bezier-curve
|
||||
const centerX = sourceX * 0.125 + sourceControlX * 0.375 + targetControlX * 0.375 + targetX * 0.125
|
||||
const centerY = sourceY * 0.125 + sourceControlY * 0.375 + targetControlY * 0.375 + targetY * 0.125
|
||||
const xOffset = Math.abs(centerX - sourceX)
|
||||
const yOffset = Math.abs(centerY - sourceY)
|
||||
return [centerX, centerY, xOffset, yOffset]
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { getCenter } from './general'
|
||||
import { Position } from '~/types'
|
||||
|
||||
export interface GetSmoothStepPathParams {
|
||||
sourceX: number
|
||||
sourceY: number
|
||||
sourcePosition?: Position
|
||||
targetX: number
|
||||
targetY: number
|
||||
targetPosition?: Position
|
||||
borderRadius?: number
|
||||
centerX?: number
|
||||
centerY?: number
|
||||
}
|
||||
|
||||
// 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}`
|
||||
|
||||
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) ||
|
||||
(sourcePosition === Position.Left && targetPosition === Position.Right) ||
|
||||
(sourcePosition === Position.Left && 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,53 @@
|
||||
<script lang="ts" setup>
|
||||
import { useHandle, useVueFlow } from '../../composables'
|
||||
import { Position } from '../../types'
|
||||
import { NodeId } from '../../context'
|
||||
import type { HandleProps } from '../../types/handle'
|
||||
|
||||
const { id, hooks, connectionStartHandle } = useVueFlow()
|
||||
const props = withDefaults(defineProps<HandleProps>(), {
|
||||
type: 'source',
|
||||
position: 'top' as Position,
|
||||
connectable: true,
|
||||
})
|
||||
|
||||
const nodeId = inject(NodeId, '')
|
||||
|
||||
const { onMouseDown, onClick } = useHandle()
|
||||
const onMouseDownHandler = (event: MouseEvent) =>
|
||||
onMouseDown(event, props.id ?? null, nodeId, props.type === 'target', props.isValidConnection, undefined, (connection) =>
|
||||
hooks.value.connect.trigger(connection),
|
||||
)
|
||||
const onClickHandler = (event: MouseEvent) => onClick(event, props.id ?? null, nodeId, props.type, props.isValidConnection)
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'Handle',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
:data-handleid="props.id"
|
||||
:data-nodeid="nodeId"
|
||||
:data-handlepos="props.position"
|
||||
:class="[
|
||||
'vue-flow__handle',
|
||||
`vue-flow__handle-${props.position}`,
|
||||
`vue-flow__handle-${id}`,
|
||||
'nodrag',
|
||||
{
|
||||
source: props.type !== 'target',
|
||||
target: props.type === 'target',
|
||||
connectable: props.connectable,
|
||||
connecting:
|
||||
connectionStartHandle?.nodeId === nodeId &&
|
||||
connectionStartHandle?.handleId === props.id &&
|
||||
connectionStartHandle?.type === props.type,
|
||||
},
|
||||
]"
|
||||
@mousedown="onMouseDownHandler"
|
||||
@click="onClickHandler"
|
||||
>
|
||||
<slot :node-id="nodeId" v-bind="props"></slot>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script lang="ts" setup>
|
||||
import Handle from '../Handle/Handle.vue'
|
||||
import { Position } from '../../types'
|
||||
import type { NodeProps } from '../../types/node'
|
||||
|
||||
const props = withDefaults(defineProps<NodeProps>(), {
|
||||
data: () => {},
|
||||
connectable: false,
|
||||
sourcePosition: 'bottom' as Position,
|
||||
targetPosition: 'top' as Position,
|
||||
})
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'DefaultNode',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Handle
|
||||
type="target"
|
||||
:position="props.targetPosition"
|
||||
:is-connectable="props.connectable"
|
||||
:is-valid-connection="props.isValidTargetPos"
|
||||
/>
|
||||
<component :is="props.label.component" v-bind="props.label.props" v-if="typeof props.label !== 'string' && props.label" />
|
||||
<span v-else v-html="props.label" />
|
||||
<Handle
|
||||
type="source"
|
||||
:position="props.sourcePosition"
|
||||
:is-connectable="props.connectable"
|
||||
:is-valid-connection="props.isValidSourcePos"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script lang="ts" setup>
|
||||
import Handle from '../Handle/Handle.vue'
|
||||
import { Position } from '../../types'
|
||||
import type { NodeProps } from '../../types/node'
|
||||
|
||||
const props = withDefaults(defineProps<NodeProps>(), {
|
||||
data: () => {},
|
||||
connectable: false,
|
||||
sourcePosition: 'bottom' as Position,
|
||||
})
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'InputNode',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<component :is="props.label.component" v-bind="props.label.props" v-if="typeof props.label !== 'string' && props.label" />
|
||||
<span v-else v-html="props.label" />
|
||||
<Handle
|
||||
type="source"
|
||||
:position="props.sourcePosition"
|
||||
:connectable="props.connectable"
|
||||
:is-valid-connection="props.isValidSourcePos"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,219 @@
|
||||
<script lang="ts" setup>
|
||||
import { DraggableEventListener, DraggableCore } from '@braks/revue-draggable'
|
||||
import { useVueFlow } from '../../composables'
|
||||
import { NodeComponent, SnapGrid } from '../../types'
|
||||
import { NodeId } from '../../context'
|
||||
import { getXYZPos } from '../../utils'
|
||||
|
||||
interface NodeWrapperProps {
|
||||
id: string
|
||||
draggable: boolean
|
||||
selectable: boolean
|
||||
connectable: boolean
|
||||
snapGrid?: SnapGrid
|
||||
}
|
||||
|
||||
const props = defineProps<NodeWrapperProps>()
|
||||
const { store } = useVueFlow()
|
||||
const node = computed(() => store.getNode(props.id)!)
|
||||
if (!node.value) throw new Error(`Node with ${props.id} not found!`)
|
||||
provide(NodeId, props.id)
|
||||
|
||||
const nodeElement = templateRef<HTMLDivElement>('node-element', null)
|
||||
|
||||
const onMouseEnterHandler = () =>
|
||||
node.value.dragging && ((event: MouseEvent) => store.hooks.nodeMouseEnter.trigger({ event, node: node.value }))
|
||||
const onMouseMoveHandler = () =>
|
||||
node.value.dragging && ((event: MouseEvent) => store.hooks.nodeMouseMove.trigger({ event, node: node.value }))
|
||||
const onMouseLeaveHandler = () =>
|
||||
node.value.dragging && ((event: MouseEvent) => store.hooks.nodeMouseLeave.trigger({ event, node: node.value }))
|
||||
const onContextMenuHandler = () => (event: MouseEvent) =>
|
||||
store.hooks.nodeContextMenu.trigger({
|
||||
event,
|
||||
node: node.value,
|
||||
})
|
||||
const onDoubleClick = () => (event: MouseEvent) => store.hooks.nodeDoubleClick.trigger({ event, node: node.value })
|
||||
const onSelectNodeHandler = (event: MouseEvent) => {
|
||||
if (!props.draggable) {
|
||||
if (props.selectable) {
|
||||
store.nodesSelectionActive = false
|
||||
if (!node.value.selected) store.addSelectedNodes([node.value])
|
||||
}
|
||||
store.hooks.nodeClick.trigger({ event, node: node.value })
|
||||
}
|
||||
}
|
||||
const onDragStart: DraggableEventListener = ({ event }) => {
|
||||
store.addSelectedNodes([])
|
||||
store.hooks.nodeDragStart.trigger({ event, node: node.value })
|
||||
|
||||
if (store.selectNodesOnDrag && props.selectable) {
|
||||
store.nodesSelectionActive = false
|
||||
|
||||
if (!node.value.selected) store.addSelectedNodes([node.value])
|
||||
} else if (!store.selectNodesOnDrag && !node.value.selected && props.selectable) {
|
||||
store.nodesSelectionActive = false
|
||||
store.addSelectedNodes([])
|
||||
}
|
||||
}
|
||||
const onDrag: DraggableEventListener = ({ event, data: { deltaX, deltaY } }) => {
|
||||
nextTick(() => store.updateNodePosition({ id: node.value.id, diff: { x: deltaX, y: deltaY }, dragging: true }))
|
||||
store.hooks.nodeDrag.trigger({ event, node: node.value })
|
||||
}
|
||||
const onDragStop: DraggableEventListener = ({ event, data: { deltaX, deltaY } }) => {
|
||||
// onDragStop also gets called when user just clicks on a node.
|
||||
// Because of that we set dragging to true inside the onDrag handler and handle the click here
|
||||
if (!node.value.dragging) {
|
||||
if (props.selectable && !store.selectNodesOnDrag && !node.value.selected) {
|
||||
store.addSelectedNodes([node.value])
|
||||
}
|
||||
store.hooks.nodeClick.trigger({ event, node: node.value })
|
||||
return
|
||||
}
|
||||
store.updateNodePosition({ id: node.value.id, diff: { x: deltaX, y: deltaY }, dragging: false })
|
||||
store.hooks.nodeDragStop.trigger({ event, node: node.value })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
useResizeObserver(nodeElement, () =>
|
||||
store.updateNodeDimensions([{ id: node.value.id, nodeElement: nodeElement.value, forceUpdate: true }]),
|
||||
)
|
||||
|
||||
watch([() => node.value.type, () => node.value.sourcePosition, () => node.value.targetPosition], () =>
|
||||
nextTick(() => store.updateNodeDimensions([{ id: node.value.id, nodeElement: nodeElement.value }])),
|
||||
)
|
||||
|
||||
store.updateNodeDimensions([{ id: node.value.id, nodeElement: nodeElement.value }])
|
||||
})
|
||||
|
||||
const scale = controlledComputed(
|
||||
() => store.transform[2],
|
||||
() => store.transform[2],
|
||||
)
|
||||
|
||||
watch(
|
||||
[() => node.value.position, () => store.getNode(node.value.parentNode!)],
|
||||
([pos, parent]) => {
|
||||
const xyzPos = {
|
||||
...pos,
|
||||
z: node.value.dragging || node.value.selected ? 1000 : node.value.computedPosition.z,
|
||||
}
|
||||
if (parent) {
|
||||
node.value.computedPosition = getXYZPos(parent, xyzPos)
|
||||
} else {
|
||||
node.value.computedPosition = xyzPos
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
store.updateNodePosition({ id: node.value.id, diff: { x: 0, y: 0 } })
|
||||
|
||||
const name = ref(node.value.type ?? 'default')
|
||||
const type = computed(() => {
|
||||
let nodeType = store.getNodeTypes[name.value]
|
||||
if (typeof nodeType === 'string') nodeType = resolveComponent(name.value, false) as NodeComponent
|
||||
if (typeof nodeType !== 'string') return nodeType
|
||||
|
||||
const slot = useSlots()?.[name.value]?.({})
|
||||
if (!slot || !slot[0].key?.toString().includes(name.value)) {
|
||||
console.warn(`Node type "${node.value.type}" not found and no slot detected. Using fallback type "default".`)
|
||||
name.value = 'default'
|
||||
return store.getNodeTypes.default
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'Node',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<DraggableCore
|
||||
:cancel="`.${store.noDragClassName}`"
|
||||
:handle="node.dragHandle"
|
||||
:disabled="!props.draggable"
|
||||
:scale="scale"
|
||||
:grid="props.snapGrid"
|
||||
:enable-user-select-hack="false"
|
||||
@start="onDragStart"
|
||||
@move="onDrag"
|
||||
@stop="onDragStop"
|
||||
>
|
||||
<div
|
||||
ref="node-element"
|
||||
:key="`node-${node.id}`"
|
||||
:class="[
|
||||
'vue-flow__node',
|
||||
`vue-flow__node-${name}`,
|
||||
store.noPanClassName,
|
||||
{
|
||||
dragging: node.dragging,
|
||||
selected: node.selected,
|
||||
selectable: props.selectable,
|
||||
},
|
||||
node.class,
|
||||
]"
|
||||
:style="{
|
||||
zIndex: node.computedPosition.z,
|
||||
transform: `translate(${node.computedPosition.x}px,${node.computedPosition.y}px)`,
|
||||
pointerEvents: props.selectable || props.draggable ? 'all' : 'none',
|
||||
...node.style,
|
||||
}"
|
||||
:data-id="node.id"
|
||||
@mouseenter="onMouseEnterHandler"
|
||||
@mousemove="onMouseMoveHandler"
|
||||
@mouseleave="onMouseLeaveHandler"
|
||||
@contextmenu="onContextMenuHandler"
|
||||
@click="onSelectNodeHandler"
|
||||
@dblclick="onDoubleClick"
|
||||
>
|
||||
<slot
|
||||
v-bind="{
|
||||
nodeElement,
|
||||
id: node.id,
|
||||
type: node.type,
|
||||
data: node.data,
|
||||
selected: !!node.selected,
|
||||
isConnectable: props.connectable,
|
||||
position: node.position,
|
||||
computedPosition: node.computedPosition,
|
||||
dimensions: node.dimensions,
|
||||
isValidTargetPos: node.isValidTargetPos,
|
||||
isValidSourcePos: node.isValidSourcePos,
|
||||
parentNode: node.parentNode,
|
||||
dragging: !!node.dragging,
|
||||
zIndex: node.dragging || node.selected ? 1000 : node.computedPosition.z,
|
||||
targetPosition: node.targetPosition,
|
||||
sourcePosition: node.sourcePosition,
|
||||
label: node.label,
|
||||
dragHandle: node.dragHandle,
|
||||
}"
|
||||
>
|
||||
<component
|
||||
:is="type"
|
||||
v-if="type"
|
||||
v-bind="{
|
||||
nodeElement,
|
||||
id: node.id,
|
||||
type: node.type,
|
||||
data: node.data,
|
||||
selected: !!node.selected,
|
||||
connectable: props.connectable,
|
||||
position: node.position,
|
||||
computedPosition: node.computedPosition,
|
||||
dimensions: node.dimensions,
|
||||
isValidTargetPos: node.isValidTargetPos,
|
||||
isValidSourcePos: node.isValidSourcePos,
|
||||
parentNode: node.parentNode,
|
||||
dragging: !!node.dragging,
|
||||
zIndex: node.dragging || node.selected ? 1000 : node.computedPosition.z,
|
||||
targetPosition: node.targetPosition,
|
||||
sourcePosition: node.sourcePosition,
|
||||
label: node.label,
|
||||
dragHandle: node.dragHandle,
|
||||
}"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
</DraggableCore>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script lang="ts" setup>
|
||||
import Handle from '../Handle/Handle.vue'
|
||||
import { Position } from '../../types'
|
||||
import type { NodeProps } from '../../types/node'
|
||||
|
||||
const props = withDefaults(defineProps<NodeProps>(), {
|
||||
data: () => {},
|
||||
connectable: false,
|
||||
targetPosition: 'top' as Position,
|
||||
})
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'OutputNode',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<component :is="props.label.component" v-bind="props.label.props" v-if="typeof props.label !== 'string' && props.label" />
|
||||
<span v-else v-html="props.label" />
|
||||
<Handle
|
||||
type="source"
|
||||
:position="props.targetPosition"
|
||||
:is-connectable="props.connectable"
|
||||
:is-valid-connection="props.isValidTargetPos"
|
||||
/>
|
||||
</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,51 @@
|
||||
<script lang="ts" setup>
|
||||
import { DraggableCore, DraggableEventListener } from '@braks/revue-draggable'
|
||||
import { useVueFlow } from '../../composables'
|
||||
import { getRectOfNodes } from '../../utils'
|
||||
|
||||
const { id, store } = useVueFlow()
|
||||
const selectedNodesBBox = computed(() => getRectOfNodes(store.getSelectedNodes))
|
||||
const innerStyle = computed(() => ({
|
||||
width: `${selectedNodesBBox.value.width}px`,
|
||||
height: `${selectedNodesBBox.value.height}px`,
|
||||
top: `${selectedNodesBBox.value.y}px`,
|
||||
left: `${selectedNodesBBox.value.x}px`,
|
||||
}))
|
||||
watch(selectedNodesBBox, (v) => (store.selectedNodesBbox = v))
|
||||
const onStart: DraggableEventListener = ({ event }) =>
|
||||
store.hooks.selectionDragStart.trigger({ event, nodes: store.getSelectedNodes })
|
||||
const onDrag: DraggableEventListener = ({ event, data: { deltaX, deltaY } }) => {
|
||||
store.hooks.selectionDrag.trigger({ event, nodes: store.getSelectedNodes })
|
||||
store.updateNodePosition({ diff: { x: deltaX, y: deltaY }, dragging: true })
|
||||
}
|
||||
const onStop: DraggableEventListener = ({ event }) => {
|
||||
store.hooks.selectionDragStop.trigger({ event, nodes: store.getSelectedNodes })
|
||||
store.getSelectedNodes.forEach((node) => (node.dragging = false))
|
||||
}
|
||||
const onContextMenu = (event: MouseEvent) => store.hooks.selectionContextMenu.trigger({ event, nodes: store.getSelectedNodes })
|
||||
|
||||
const transform = computed(() => `translate(${store.transform[0]}px,${store.transform[1]}px) scale(${store.transform[2]})`)
|
||||
const scale = controlledComputed(
|
||||
() => store.transform[2],
|
||||
() => store.transform[2],
|
||||
)
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'NodesSelection',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="vue-flow__nodesselection vue-flow__container" :class="store.noPanClassName" :style="{ transform }">
|
||||
<DraggableCore
|
||||
:grid="store.snapToGrid ? store.snapGrid : undefined"
|
||||
:enable-user-select-hack="false"
|
||||
:scale="scale"
|
||||
@start="onStart"
|
||||
@move="onDrag"
|
||||
@stop="onStop"
|
||||
>
|
||||
<div class="vue-flow__nodesselection-rect" :style="innerStyle" @contextmenu="onContextMenu" />
|
||||
</DraggableCore>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="ts" setup>
|
||||
interface SelectionRectProps {
|
||||
width: number
|
||||
height: number
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
const props = defineProps<SelectionRectProps>()
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'SelectionRect',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="vue-flow__selection"
|
||||
:style="{
|
||||
width: `${props.width}px`,
|
||||
height: `${props.height}px`,
|
||||
transform: `translate(${props.x}px, ${props.y}px)`,
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,96 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVueFlow } from '../../composables'
|
||||
import { SelectionRect as Rect } from '../../types'
|
||||
import { getConnectedEdges, getNodesInside } from '../../utils'
|
||||
import SelectionRect from './SelectionRect.vue'
|
||||
import { getMousePosition } from './utils'
|
||||
|
||||
const { store } = useVueFlow()
|
||||
const el = templateRef('user-selection', null)
|
||||
|
||||
const prevNodes = ref(0)
|
||||
const prevEdges = ref(0)
|
||||
const initialRect = () => ({
|
||||
width: 0,
|
||||
height: 0,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
draw: false,
|
||||
})
|
||||
const rect = ref<Rect>(initialRect())
|
||||
|
||||
const reset = () => {
|
||||
rect.value = initialRect()
|
||||
store.userSelectionActive = false
|
||||
prevNodes.value = 0
|
||||
prevEdges.value = 0
|
||||
}
|
||||
const onMouseDown = (event: MouseEvent) => {
|
||||
const mousePos = getMousePosition(event)
|
||||
if (!mousePos) return
|
||||
|
||||
rect.value = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
startX: mousePos.x,
|
||||
startY: mousePos.y,
|
||||
x: mousePos.x,
|
||||
y: mousePos.y,
|
||||
draw: true,
|
||||
}
|
||||
store.userSelectionActive = true
|
||||
store.nodesSelectionActive = false
|
||||
}
|
||||
|
||||
const onMouseMove = (event: MouseEvent) => {
|
||||
if (!store.userSelectionActive) return
|
||||
const mousePos = getMousePosition(event)
|
||||
if (!mousePos) return
|
||||
|
||||
const startX = rect.value.startX
|
||||
const startY = rect.value.startY
|
||||
|
||||
const nextUserSelectRect: Rect = {
|
||||
...rect.value,
|
||||
x: mousePos.x < startX ? mousePos.x : rect.value.x,
|
||||
y: mousePos.y < startY ? mousePos.y : rect.value.y,
|
||||
width: Math.abs(mousePos.x - startX),
|
||||
height: Math.abs(mousePos.y - startY),
|
||||
}
|
||||
const selectedNodes = getNodesInside(store.getNodes, rect.value, store.transform)
|
||||
const selectedEdges = getConnectedEdges(selectedNodes, store.getEdges)
|
||||
rect.value = nextUserSelectRect
|
||||
store.addSelectedNodes(selectedNodes)
|
||||
store.addSelectedEdges(selectedEdges)
|
||||
prevNodes.value = selectedNodes.length
|
||||
prevEdges.value = selectedEdges.length
|
||||
}
|
||||
|
||||
const onMouseUp = () => {
|
||||
store.nodesSelectionActive = prevNodes.value > 0
|
||||
reset()
|
||||
}
|
||||
|
||||
const onMouseLeave = () => {
|
||||
store.nodesSelectionActive = false
|
||||
reset()
|
||||
}
|
||||
|
||||
useEventListener(el, 'mousedown', onMouseDown)
|
||||
useEventListener(el, 'mousemove', onMouseMove)
|
||||
useEventListener(el, 'click', onMouseUp)
|
||||
useEventListener(el, 'mouseup', onMouseUp)
|
||||
useEventListener(el, 'mouseleave', onMouseLeave)
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'UserSelection',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div ref="user-selection" class="vue-flow__selectionpane vue-flow__container">
|
||||
<SelectionRect v-if="rect.draw" :width="rect.width" :height="rect.height" :x="rect.x" :y="rect.y" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,13 @@
|
||||
import { XYPosition } from '../../types'
|
||||
|
||||
export function getMousePosition(event: MouseEvent): XYPosition | void {
|
||||
const flowNode = (event.target as Element).closest('.vue-flow')
|
||||
if (!flowNode) return
|
||||
|
||||
const containerBounds = flowNode.getBoundingClientRect()
|
||||
|
||||
return {
|
||||
x: event.clientX - containerBounds.left,
|
||||
y: event.clientY - containerBounds.top,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './Nodes'
|
||||
export * from './Edges'
|
||||
export * from './ConnectionLine/ConnectionLine.vue'
|
||||
export * from './Handle/Handle.vue'
|
||||
export * from './NodesSelection/NodesSelection.vue'
|
||||
export * from './UserSelection/UserSelection.vue'
|
||||
@@ -0,0 +1,5 @@
|
||||
export { default as useHandle } from './useHandle'
|
||||
export { default as useKeyPress } from './useKeyPress'
|
||||
export { default as useZoomPanHelper } from './useZoomPanHelper'
|
||||
export { default as useWindow } from './useWindow'
|
||||
export { default as useVueFlow } from './useVueFlow'
|
||||
@@ -0,0 +1,224 @@
|
||||
import useVueFlow from './useVueFlow'
|
||||
import { getHostForElement } from '~/utils'
|
||||
import { Connection, ConnectionMode, HandleType, FlowStore, ValidConnectionFunc } from '~/types'
|
||||
|
||||
type Result = {
|
||||
elementBelow: Element | null
|
||||
isValid: boolean
|
||||
connection: Connection
|
||||
isHoveringHandle: boolean
|
||||
}
|
||||
|
||||
// checks if element below mouse is a handle and returns connection in form of an object { source: 123, target: 312 }
|
||||
export const checkElementBelowIsValid = (
|
||||
event: MouseEvent,
|
||||
connectionMode: ConnectionMode,
|
||||
isTarget: boolean,
|
||||
nodeId: string,
|
||||
handleId: string | null,
|
||||
isValidConnection: ValidConnectionFunc,
|
||||
doc: Document,
|
||||
) => {
|
||||
const elementBelow = doc.elementFromPoint(event.clientX, event.clientY)
|
||||
const elementBelowIsTarget = elementBelow?.classList.contains('target') || false
|
||||
const elementBelowIsSource = elementBelow?.classList.contains('source') || false
|
||||
|
||||
const result: Result = {
|
||||
elementBelow,
|
||||
isValid: false,
|
||||
connection: { source: null, target: null, sourceHandle: null, targetHandle: null },
|
||||
isHoveringHandle: false,
|
||||
}
|
||||
|
||||
if (elementBelow && (elementBelowIsTarget || elementBelowIsSource)) {
|
||||
result.isHoveringHandle = true
|
||||
|
||||
// in strict mode we don't allow target to target or source to source connections
|
||||
const isValid =
|
||||
connectionMode === ConnectionMode.Strict ? (isTarget && elementBelowIsSource) || (!isTarget && elementBelowIsTarget) : true
|
||||
|
||||
if (isValid) {
|
||||
const elementBelowNodeId = elementBelow.getAttribute('data-nodeid') ?? ''
|
||||
const elementBelowHandleId = elementBelow.getAttribute('data-handleid') ?? ''
|
||||
const connection: Connection = isTarget
|
||||
? {
|
||||
source: elementBelowNodeId,
|
||||
sourceHandle: elementBelowHandleId,
|
||||
target: nodeId,
|
||||
targetHandle: handleId,
|
||||
}
|
||||
: {
|
||||
source: nodeId,
|
||||
sourceHandle: handleId,
|
||||
target: elementBelowNodeId,
|
||||
targetHandle: elementBelowHandleId,
|
||||
}
|
||||
|
||||
result.connection = connection
|
||||
result.isValid = isValidConnection(connection)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const resetRecentHandle = (hoveredHandle: Element): void => {
|
||||
hoveredHandle?.classList.remove('vue-flow__handle-valid')
|
||||
hoveredHandle?.classList.remove('vue-flow__handle-connecting')
|
||||
}
|
||||
|
||||
export default (store: FlowStore = useVueFlow().store) => {
|
||||
let recentHoveredHandle: Element
|
||||
|
||||
const onMouseDown = (
|
||||
event: MouseEvent,
|
||||
handleId: string | null,
|
||||
nodeId: string,
|
||||
isTarget: boolean,
|
||||
isValidConnection?: ValidConnectionFunc,
|
||||
elementEdgeUpdaterType?: HandleType,
|
||||
onEdgeUpdate?: (connection: Connection) => void,
|
||||
onEdgeUpdateEnd?: () => void,
|
||||
) => {
|
||||
const flowNode = (event.target as Element).closest('.vue-flow')
|
||||
const doc = getHostForElement(event.target as HTMLElement)
|
||||
if (!doc) return
|
||||
|
||||
let validConnectFunc: ValidConnectionFunc = isValidConnection ?? (() => true)
|
||||
const node = store.getNode(nodeId)
|
||||
|
||||
if (node && (typeof node.connectable === 'undefined' ? store.nodesConnectable : node.connectable) === false) return
|
||||
if (!isValidConnection) {
|
||||
if (node) validConnectFunc = (!isTarget ? node.isValidTargetPos : node.isValidSourcePos) ?? (() => true)
|
||||
}
|
||||
const elementBelow = doc.elementFromPoint(event.clientX, event.clientY)
|
||||
const elementBelowIsTarget = elementBelow?.classList.contains('target')
|
||||
const elementBelowIsSource = elementBelow?.classList.contains('source')
|
||||
|
||||
if (!flowNode || (!elementBelowIsTarget && !elementBelowIsSource && !elementEdgeUpdaterType)) return
|
||||
|
||||
const handleType = elementEdgeUpdaterType ?? (elementBelowIsTarget ? 'target' : 'source')
|
||||
const containerBounds = flowNode.getBoundingClientRect()
|
||||
|
||||
store.setState({
|
||||
connectionPosition: {
|
||||
x: event.clientX - containerBounds.left,
|
||||
y: event.clientY - containerBounds.top,
|
||||
},
|
||||
connectionNodeId: nodeId,
|
||||
connectionHandleId: handleId,
|
||||
connectionHandleType: handleType,
|
||||
})
|
||||
|
||||
store.hooks.connectStart.trigger({ event, nodeId, handleId, handleType })
|
||||
|
||||
function onMouseMove(event: MouseEvent) {
|
||||
store.connectionPosition.x = event.clientX - containerBounds.left
|
||||
store.connectionPosition.y = event.clientY - containerBounds.top
|
||||
|
||||
const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid(
|
||||
event,
|
||||
store.connectionMode,
|
||||
isTarget,
|
||||
nodeId,
|
||||
handleId,
|
||||
validConnectFunc,
|
||||
doc,
|
||||
)
|
||||
|
||||
if (!isHoveringHandle) return resetRecentHandle(recentHoveredHandle)
|
||||
|
||||
const isOwnHandle = connection.source === connection.target
|
||||
|
||||
if (!isOwnHandle && elementBelow) {
|
||||
recentHoveredHandle = elementBelow
|
||||
elementBelow.classList.add('vue-flow__handle-connecting')
|
||||
elementBelow.classList.toggle('vue-flow__handle-valid', isValid)
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseUp(event: MouseEvent) {
|
||||
const { connection, isValid } = checkElementBelowIsValid(
|
||||
event,
|
||||
store.connectionMode,
|
||||
isTarget,
|
||||
nodeId,
|
||||
handleId,
|
||||
validConnectFunc,
|
||||
doc,
|
||||
)
|
||||
|
||||
store.hooks.connectStop.trigger(event)
|
||||
const isOwnHandle = connection.source === connection.target
|
||||
|
||||
if (isValid && !isOwnHandle) {
|
||||
onEdgeUpdate?.(connection)
|
||||
}
|
||||
|
||||
store.hooks.connectEnd.trigger(event)
|
||||
|
||||
if (elementEdgeUpdaterType) onEdgeUpdateEnd?.()
|
||||
|
||||
resetRecentHandle(recentHoveredHandle)
|
||||
store.setState({
|
||||
connectionNodeId: null,
|
||||
connectionHandleId: null,
|
||||
connectionHandleType: null,
|
||||
connectionPosition: { x: NaN, y: NaN },
|
||||
})
|
||||
|
||||
doc.removeEventListener('mousemove', onMouseMove as EventListenerOrEventListenerObject)
|
||||
doc.removeEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject)
|
||||
}
|
||||
|
||||
doc.addEventListener('mousemove', onMouseMove as EventListenerOrEventListenerObject)
|
||||
doc.addEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject)
|
||||
}
|
||||
|
||||
const onClick = (
|
||||
event: MouseEvent,
|
||||
handleId: string | null,
|
||||
nodeId: string,
|
||||
handleType: HandleType,
|
||||
isValidConnection?: ValidConnectionFunc,
|
||||
) => {
|
||||
if (!store.connectOnClick) return
|
||||
if (!store.connectionStartHandle) {
|
||||
store.hooks.connectStart.trigger({ event, nodeId, handleId, handleType })
|
||||
store.setState({ connectionStartHandle: { nodeId, type: handleType, handleId } })
|
||||
} else {
|
||||
let validConnectFunc: ValidConnectionFunc = isValidConnection ?? (() => true)
|
||||
const node = store.getNode(nodeId)
|
||||
|
||||
if (node && (typeof node.connectable === 'undefined' ? store.nodesConnectable : node.connectable) === false) return
|
||||
if (!isValidConnection) {
|
||||
if (node) validConnectFunc = (handleType !== 'target' ? node.isValidTargetPos : node.isValidSourcePos) ?? (() => true)
|
||||
}
|
||||
const doc = getHostForElement(event.target as HTMLElement)
|
||||
const { connection, isValid } = checkElementBelowIsValid(
|
||||
event as MouseEvent,
|
||||
store.connectionMode,
|
||||
store.connectionStartHandle.type === 'target',
|
||||
store.connectionStartHandle.nodeId,
|
||||
store.connectionStartHandle.handleId || null,
|
||||
validConnectFunc,
|
||||
doc,
|
||||
)
|
||||
|
||||
const isOwnHandle = connection.source === connection.target
|
||||
|
||||
store.hooks.connectStop.trigger(event)
|
||||
|
||||
if (isValid && !isOwnHandle) store.hooks.connect.trigger(connection)
|
||||
|
||||
store.hooks.connectEnd.trigger(event)
|
||||
|
||||
store.setState({ connectionStartHandle: null })
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
onMouseDown,
|
||||
onClick,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Ref } from 'vue'
|
||||
import { onKeyDown, onKeyPressed, onKeyUp } from '@vueuse/core'
|
||||
import useWindow from './useWindow'
|
||||
import { KeyCode } from '~/types'
|
||||
import { isInputDOMNode } from '~/utils'
|
||||
|
||||
export default (keyCode: KeyCode, onChange?: (keyPressed: boolean) => void): Ref<boolean> => {
|
||||
const window = useWindow()
|
||||
const isPressed = controlledRef<boolean>(false, {
|
||||
onBeforeChange(val, oldVal) {
|
||||
if (val === oldVal) return false
|
||||
},
|
||||
onChanged() {
|
||||
if (onChange && typeof onChange === 'function') onChange(isPressed.value)
|
||||
},
|
||||
})
|
||||
|
||||
onKeyPressed(
|
||||
(e) => !isInputDOMNode(e) && (e.key === keyCode || e.keyCode === keyCode),
|
||||
(e) => {
|
||||
e.preventDefault()
|
||||
isPressed.value = true
|
||||
},
|
||||
)
|
||||
|
||||
onKeyDown(
|
||||
(e) => !isInputDOMNode(e) && (e.key === keyCode || e.keyCode === keyCode),
|
||||
(e) => {
|
||||
e.preventDefault()
|
||||
isPressed.value = true
|
||||
},
|
||||
)
|
||||
|
||||
onKeyUp(
|
||||
(e) => !isInputDOMNode(e) && (e.key === keyCode || e.keyCode === keyCode),
|
||||
(e) => {
|
||||
e.preventDefault()
|
||||
isPressed.value = false
|
||||
},
|
||||
)
|
||||
|
||||
if (typeof window.addEventListener !== 'undefined') {
|
||||
useEventListener(window, 'blur', () => {
|
||||
isPressed.value = false
|
||||
})
|
||||
}
|
||||
|
||||
if (onChange && typeof onChange === 'function') onChange(isPressed.value)
|
||||
|
||||
return isPressed
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { EffectScope } from 'vue'
|
||||
import { FlowOptions, UseVueFlow } from '~/types'
|
||||
import { VueFlow } from '~/context'
|
||||
import { useStore } from '~/store'
|
||||
|
||||
export class Storage {
|
||||
public currentId = 0
|
||||
public flows = new Map<string, UseVueFlow>()
|
||||
static instance: Storage
|
||||
|
||||
public static getInstance(): Storage {
|
||||
if (!Storage.instance) {
|
||||
Storage.instance = new Storage()
|
||||
}
|
||||
|
||||
return Storage.instance
|
||||
}
|
||||
|
||||
public set(id: string, flow: UseVueFlow) {
|
||||
this.flows.set(id, flow)
|
||||
}
|
||||
|
||||
public get(id: string) {
|
||||
return this.flows.get(id)
|
||||
}
|
||||
|
||||
public remove(id: string) {
|
||||
this.flows.delete(id)
|
||||
}
|
||||
|
||||
public create(id: string, options?: Partial<FlowOptions>) {
|
||||
const store = useStore(options)
|
||||
const flow = {
|
||||
id,
|
||||
store: reactive(store),
|
||||
...toRefs(store.state),
|
||||
...store.getters,
|
||||
...store.actions,
|
||||
...store.hooksOn,
|
||||
} as unknown as UseVueFlow
|
||||
this.set(id, flow)
|
||||
return flow
|
||||
}
|
||||
|
||||
public getId() {
|
||||
return `vue-flow-${this.currentId++}`
|
||||
}
|
||||
}
|
||||
|
||||
type Injection = UseVueFlow | null | undefined
|
||||
type Scope = (EffectScope & { vueFlowId: string }) | undefined
|
||||
|
||||
export default <N = any, E = N>(options?: Partial<FlowOptions<N, E>>): UseVueFlow<N, E> => {
|
||||
const storage = Storage.getInstance()
|
||||
const scope = getCurrentScope() as Scope
|
||||
const vueFlowId = scope?.vueFlowId || options?.id
|
||||
|
||||
let vueFlow: Injection
|
||||
|
||||
if (scope) {
|
||||
const injection = inject(VueFlow, null)
|
||||
if (typeof injection !== 'undefined' && injection !== null) vueFlow = injection
|
||||
}
|
||||
|
||||
if (!vueFlow) {
|
||||
if (vueFlowId) vueFlow = storage.get(vueFlowId)
|
||||
}
|
||||
|
||||
if (!vueFlow || (vueFlow && options?.id && options.id !== vueFlow.id)) {
|
||||
const name = options?.id ?? storage.getId()
|
||||
|
||||
vueFlow = storage.create(name, options)
|
||||
|
||||
if (scope) {
|
||||
provide(VueFlow, storage.get(name))
|
||||
scope.vueFlowId = name
|
||||
|
||||
onScopeDispose(() => {
|
||||
storage.remove(name)
|
||||
vueFlow = null
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (options) vueFlow.setState(options)
|
||||
}
|
||||
|
||||
if (!vueFlow) throw new Error('vue flow store instance not found.')
|
||||
|
||||
return vueFlow
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
type UseWindow = Window & typeof globalThis & { chrome?: any }
|
||||
|
||||
export default (): UseWindow => {
|
||||
if (typeof window !== 'undefined') return window as UseWindow
|
||||
else return { chrome: false } as UseWindow
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { zoomIdentity } from 'd3-zoom'
|
||||
import useVueFlow from './useVueFlow'
|
||||
import useWindow from './useWindow'
|
||||
import { getRectOfNodes, pointToRendererPoint, getTransformForBounds } from '~/utils'
|
||||
import { GraphNode, FlowStore, UseZoomPanHelper, D3Selection } from '~/types'
|
||||
|
||||
const DEFAULT_PADDING = 0.1
|
||||
|
||||
const transition = (selection: D3Selection, ms = 0) => selection.transition().duration(ms)
|
||||
|
||||
export default (store: FlowStore = useVueFlow().store): UseZoomPanHelper => ({
|
||||
zoomIn: (options) => store.d3Selection && store.d3Zoom?.scaleBy(transition(store.d3Selection, options?.duration), 1.2),
|
||||
zoomOut: (options) => store.d3Selection && store.d3Zoom?.scaleBy(transition(store.d3Selection, options?.duration), 1 / 1.2),
|
||||
zoomTo: (zoomLevel, options) =>
|
||||
store.d3Selection && store.d3Zoom?.scaleTo(transition(store.d3Selection, options?.duration), zoomLevel),
|
||||
setTransform: (transform, options) => {
|
||||
const nextTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.zoom)
|
||||
store.d3Selection && store.d3Zoom?.transform(transition(store.d3Selection, options?.duration), nextTransform)
|
||||
},
|
||||
getTransform: () => ({
|
||||
x: store.transform[0],
|
||||
y: store.transform[1],
|
||||
zoom: store.transform[2],
|
||||
}),
|
||||
fitView: async (
|
||||
options = {
|
||||
padding: DEFAULT_PADDING,
|
||||
includeHiddenNodes: false,
|
||||
duration: 0,
|
||||
},
|
||||
) => {
|
||||
// if ssr we can't wait for dimensions, they'll never really exist
|
||||
const window = useWindow()
|
||||
if ('screen' in window)
|
||||
await until(store.dimensions).toMatch(({ height, width }) => !isNaN(width) && width > 0 && !isNaN(height) && height > 0)
|
||||
|
||||
if (!store.getNodes.length) return
|
||||
|
||||
let nodes: GraphNode[] = []
|
||||
if (options.nodes) {
|
||||
nodes = store.nodes.filter((n) => options.nodes?.includes(n.id))
|
||||
}
|
||||
if (!nodes || !nodes.length) {
|
||||
nodes = options.includeHiddenNodes ? store.nodes : store.getNodes
|
||||
}
|
||||
const bounds = getRectOfNodes(nodes)
|
||||
const [x, y, zoom] = getTransformForBounds(
|
||||
bounds,
|
||||
store.dimensions.width,
|
||||
store.dimensions.height,
|
||||
options.minZoom ?? store.minZoom,
|
||||
options.maxZoom ?? store.maxZoom,
|
||||
options.padding ?? DEFAULT_PADDING,
|
||||
options.offset,
|
||||
)
|
||||
const transform = zoomIdentity.translate(x, y).scale(zoom)
|
||||
|
||||
store.d3Selection && store.d3Zoom?.transform(transition(store.d3Selection, options?.duration), transform)
|
||||
},
|
||||
setCenter: (x, y, options) => {
|
||||
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : store.maxZoom
|
||||
const centerX = store.dimensions.width / 2 - x * nextZoom
|
||||
const centerY = store.dimensions.height / 2 - y * nextZoom
|
||||
const transform = zoomIdentity.translate(centerX, centerY).scale(nextZoom)
|
||||
|
||||
store.d3Selection && store.d3Zoom?.transform(transition(store.d3Selection, options?.duration), transform)
|
||||
},
|
||||
fitBounds: (bounds, options = { padding: DEFAULT_PADDING }) => {
|
||||
const [x, y, zoom] = getTransformForBounds(
|
||||
bounds,
|
||||
store.dimensions.width,
|
||||
store.dimensions.height,
|
||||
store.minZoom,
|
||||
store.maxZoom,
|
||||
options.padding,
|
||||
)
|
||||
const transform = zoomIdentity.translate(x, y).scale(zoom)
|
||||
|
||||
store.d3Selection && store.d3Zoom?.transform(transition(store.d3Selection, options.duration), transform)
|
||||
},
|
||||
project: (position) => pointToRendererPoint(position, store.transform, store.snapToGrid, store.snapGrid),
|
||||
})
|
||||
@@ -0,0 +1,62 @@
|
||||
<script lang="ts" setup>
|
||||
import EdgeWrapper from '../../components/Edges/EdgeWrapper.vue'
|
||||
import ConnectionLine from '../../components/ConnectionLine/ConnectionLine.vue'
|
||||
import { useVueFlow } from '../../composables'
|
||||
import { groupEdgesByZLevel } from '../../utils'
|
||||
import MarkerDefinitions from './MarkerDefinitions.vue'
|
||||
|
||||
const { store } = useVueFlow()
|
||||
|
||||
const sourceNode = controlledComputed(
|
||||
() => store.connectionNodeId,
|
||||
() => {
|
||||
if (store.connectionNodeId) return store.getNodes[store.getNodes.map((n) => n.id).indexOf(store.connectionNodeId)]
|
||||
},
|
||||
)
|
||||
const connectionLineVisible = controlledComputed(
|
||||
() => store.connectionNodeId,
|
||||
() =>
|
||||
!!(
|
||||
sourceNode.value &&
|
||||
(typeof sourceNode.value.connectable === 'undefined' ? store.nodesConnectable : sourceNode.value.connectable) &&
|
||||
store.connectionNodeId &&
|
||||
store.connectionHandleType
|
||||
),
|
||||
)
|
||||
const groups = computed(() => groupEdgesByZLevel(store.getEdges, store.getNodes))
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'Edges',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<svg
|
||||
v-for="group of groups"
|
||||
:key="group.level"
|
||||
:width="store.dimensions.width"
|
||||
:height="store.dimensions.height"
|
||||
class="vue-flow__edges vue-flow__container"
|
||||
:style="`z-index: ${group.level}`"
|
||||
>
|
||||
<MarkerDefinitions v-if="group.isMaxLevel" :default-color="store.defaultMarkerColor" />
|
||||
<g>
|
||||
<EdgeWrapper
|
||||
v-for="edge of group.edges"
|
||||
:id="edge.id"
|
||||
:key="edge.id"
|
||||
:selectable="typeof edge.selectable === 'undefined' ? store.elementsSelectable : edge.selectable"
|
||||
:updatable="typeof edge.updatable === 'undefined' ? store.edgesUpdatable : edge.updatable"
|
||||
>
|
||||
<template #default="edgeProps">
|
||||
<slot v-if="edge.type" :name="`edge-${edge.type}`" v-bind="edgeProps"></slot>
|
||||
</template>
|
||||
</EdgeWrapper>
|
||||
<ConnectionLine v-if="connectionLineVisible && sourceNode" :source-node="sourceNode">
|
||||
<template #default="customConnectionLineProps">
|
||||
<slot name="connection-line" v-bind="customConnectionLineProps"></slot>
|
||||
</template>
|
||||
</ConnectionLine>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,49 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MarkerProps } from '../../types/edge'
|
||||
|
||||
const props = withDefaults(defineProps<MarkerProps>(), {
|
||||
width: 12.5,
|
||||
height: 12.5,
|
||||
markerUnits: 'strokeWidth',
|
||||
orient: 'auto',
|
||||
strokeWidth: 1,
|
||||
color: 'none',
|
||||
})
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'Marker',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<marker
|
||||
:id="props.id"
|
||||
class="vue-flow__arrowhead"
|
||||
viewBox="-10 -10 20 20"
|
||||
refX="0"
|
||||
refY="0"
|
||||
:markerWidth="`${props.width}`"
|
||||
:markerHeight="`${props.height}`"
|
||||
:markerUnits="props.markerUnits"
|
||||
:orient="props.orient"
|
||||
>
|
||||
<polyline
|
||||
v-if="props.type === 'arrowclosed'"
|
||||
:stroke="props.color"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
:stroke-width="props.strokeWidth"
|
||||
:fill="props.color"
|
||||
points="-5,-4 0,0 -5,4 -5,-4"
|
||||
/>
|
||||
<polyline
|
||||
v-if="props.type === 'arrow'"
|
||||
:stroke="props.color"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
:stroke-width="props.strokeWidth"
|
||||
fill="none"
|
||||
points="-5,-4 0,0 -5,4"
|
||||
/>
|
||||
</marker>
|
||||
</template>
|
||||
@@ -0,0 +1,55 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVueFlow } from '../../composables'
|
||||
import type { MarkerProps } from '../../types/edge'
|
||||
import { getMarkerId } from '../../utils'
|
||||
import { MarkerType } from '../../types/edge'
|
||||
import Marker from './Marker.vue'
|
||||
|
||||
interface MarkerDefinitionsProps {
|
||||
defaultColor: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<MarkerDefinitionsProps>(), {
|
||||
defaultColor: '',
|
||||
})
|
||||
const { store } = useVueFlow()
|
||||
|
||||
const markers = computed(() => {
|
||||
const ids: string[] = []
|
||||
|
||||
return store.edges.reduce<MarkerProps[]>((markers, edge) => {
|
||||
;[edge.markerStart, edge.markerEnd].forEach((marker) => {
|
||||
if (marker) {
|
||||
const markerId = getMarkerId(marker)
|
||||
if (!ids.includes(markerId)) {
|
||||
if (typeof marker === 'object') markers.push({ ...marker, id: markerId, color: marker.color || props.defaultColor })
|
||||
else markers.push({ id: markerId, color: props.defaultColor, type: marker as MarkerType })
|
||||
ids.push(markerId)
|
||||
}
|
||||
}
|
||||
})
|
||||
return markers.sort((a, b) => a.id.localeCompare(b.id))
|
||||
}, [])
|
||||
})
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'MarkerDefinitions',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<defs>
|
||||
<Marker
|
||||
v-for="marker of markers"
|
||||
:id="marker.id"
|
||||
:key="marker.id"
|
||||
:type="marker.type"
|
||||
:color="marker.color"
|
||||
:width="marker.width"
|
||||
:height="marker.height"
|
||||
:markerUnits="marker.markerUnits"
|
||||
:stroke-width="marker.strokeWidth"
|
||||
:orient="marker.orient"
|
||||
/>
|
||||
</defs>
|
||||
</template>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="ts" setup>
|
||||
import NodeWrapper from '../../components/Nodes/NodeWrapper.vue'
|
||||
import { useVueFlow } from '../../composables'
|
||||
|
||||
const { store } = useVueFlow()
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'Nodes',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="vue-flow__nodes vue-flow__container">
|
||||
<NodeWrapper
|
||||
v-for="node of store.getNodes"
|
||||
:id="node.id"
|
||||
:key="node.id"
|
||||
:draggable="typeof node.draggable === 'undefined' ? store.nodesDraggable : !!node.draggable"
|
||||
:selectable="typeof node.selectable === 'undefined' ? store.elementsSelectable : !!node.selectable"
|
||||
:connectable="typeof node.connectable === 'undefined' ? store.nodesConnectable : !!node.connectable"
|
||||
:snap-grid="node.snapGrid ?? (store.snapToGrid ? store.snapGrid : undefined)"
|
||||
>
|
||||
<template #default="nodeProps">
|
||||
<slot v-if="node.type" :name="`node-${node.type}`" v-bind="nodeProps"></slot>
|
||||
</template>
|
||||
</NodeWrapper>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script lang="ts" setup>
|
||||
import { EdgeChange, NodeChange } from '../../types'
|
||||
import { useVueFlow, useKeyPress } from '../../composables'
|
||||
import { getConnectedEdges } from '../../utils'
|
||||
import NodesSelection from '../../components/NodesSelection/NodesSelection.vue'
|
||||
import UserSelection from '../../components/UserSelection/UserSelection.vue'
|
||||
|
||||
const { id, store } = useVueFlow()
|
||||
|
||||
const onClick = (event: MouseEvent) => {
|
||||
store.hooks.paneClick.trigger(event)
|
||||
store.nodesSelectionActive = false
|
||||
store.resetSelectedElements()
|
||||
}
|
||||
|
||||
const onContextMenu = (event: MouseEvent) => store.hooks.paneContextMenu.trigger(event)
|
||||
|
||||
const onWheel = (event: WheelEvent) => store.hooks.paneScroll.trigger(event)
|
||||
|
||||
useKeyPress(store.deleteKeyCode, (keyPressed) => {
|
||||
const selectedNodes = store.getSelectedNodes
|
||||
const selectedEdges = store.getSelectedEdges
|
||||
if (keyPressed && (selectedNodes || selectedEdges)) {
|
||||
const connectedEdges = (selectedNodes && getConnectedEdges(selectedNodes, store.edges)) ?? []
|
||||
|
||||
const nodeChanges: NodeChange[] = selectedNodes.map((n) => ({ id: n.id, type: 'remove' }))
|
||||
const edgeChanges: EdgeChange[] = [...selectedEdges, ...connectedEdges].map((e) => ({
|
||||
id: e.id,
|
||||
type: 'remove',
|
||||
}))
|
||||
|
||||
store.hooks.nodesChange.trigger(nodeChanges)
|
||||
store.hooks.edgesChange.trigger(edgeChanges)
|
||||
store.nodesSelectionActive = false
|
||||
|
||||
store.resetSelectedElements()
|
||||
}
|
||||
})
|
||||
|
||||
useKeyPress(store.multiSelectionKeyCode, (keyPressed) => {
|
||||
store.multiSelectionActive = keyPressed
|
||||
})
|
||||
|
||||
useKeyPress(store.selectionKeyCode, (keyPressed) => {
|
||||
if (store.userSelectionActive && keyPressed) return
|
||||
store.userSelectionActive = keyPressed && store.elementsSelectable
|
||||
})
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'SelectionPane',
|
||||
inheritAttrs: false,
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<UserSelection v-if="store.userSelectionActive" :key="`user-selection-${id}`" />
|
||||
<NodesSelection v-if="store.nodesSelectionActive" :key="`nodes-selection-${id}`" />
|
||||
<div
|
||||
:key="`flow-pane-${id}`"
|
||||
class="vue-flow__pane vue-flow__container"
|
||||
@click="onClick"
|
||||
@contextmenu="onContextMenu"
|
||||
@wheel="onWheel"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,60 @@
|
||||
<script lang="ts" setup>
|
||||
import NodeRenderer from '../NodeRenderer/NodeRenderer.vue'
|
||||
import EdgeRenderer from '../EdgeRenderer/EdgeRenderer.vue'
|
||||
import { useVueFlow, useZoomPanHelper } from '../../composables'
|
||||
import { FlowInstance } from '../../types'
|
||||
import { onLoadGetEdges, onLoadGetElements, onLoadGetNodes, onLoadProject, onLoadToObject } from '../../utils'
|
||||
|
||||
const { id, store } = useVueFlow()
|
||||
|
||||
onMounted(() => {
|
||||
const { zoomIn, zoomOut, zoomTo, setTransform, getTransform, fitView, fitBounds, setCenter } = useZoomPanHelper(store)
|
||||
const instance: FlowInstance = {
|
||||
fitView: (params = { padding: 0.1 }) => fitView(params),
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
zoomTo,
|
||||
setTransform,
|
||||
getTransform,
|
||||
setCenter,
|
||||
fitBounds,
|
||||
project: onLoadProject(store),
|
||||
getElements: onLoadGetElements(store),
|
||||
getNodes: onLoadGetNodes(store),
|
||||
getEdges: onLoadGetEdges(store),
|
||||
toObject: onLoadToObject(store),
|
||||
}
|
||||
store.instance = instance
|
||||
store.fitViewOnInit && instance.fitView()
|
||||
store.paneReady = true
|
||||
store.hooks.paneReady.trigger(instance)
|
||||
})
|
||||
|
||||
const transform = computed(() => `translate(${store.transform[0]}px,${store.transform[1]}px) scale(${store.transform[2]})`)
|
||||
</script>
|
||||
<template>
|
||||
<div :key="`transformation-pane-${id}`" class="vue-flow__transformation-pane vue-flow__container" :style="{ transform }">
|
||||
<NodeRenderer v-if="store.getNodes.length" :key="`node-renderer-${id}`">
|
||||
<template
|
||||
v-for="nodeName of Object.keys(store.getNodeTypes)"
|
||||
#[`node-${nodeName}`]="nodeProps"
|
||||
:key="`node-${nodeName}-${id}`"
|
||||
>
|
||||
<slot :name="`node-${nodeName}`" v-bind="nodeProps" />
|
||||
</template>
|
||||
</NodeRenderer>
|
||||
<EdgeRenderer :key="`edge-renderer-${id}`">
|
||||
<template
|
||||
v-for="edgeName of Object.keys(store.getEdgeTypes)"
|
||||
#[`edge-${edgeName}`]="edgeProps"
|
||||
:key="`edge-${edgeName}-${id}`"
|
||||
>
|
||||
<slot :name="`edge-${edgeName}`" v-bind="edgeProps" />
|
||||
</template>
|
||||
<template #connection-line="customConnectionLineProps">
|
||||
<slot name="connection-line" v-bind="customConnectionLineProps" />
|
||||
</template>
|
||||
</EdgeRenderer>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,102 @@
|
||||
<script lang="ts" setup>
|
||||
import ZoomPane from '../ZoomPane/ZoomPane.vue'
|
||||
import { createHooks, useHooks } from '../../store'
|
||||
import { useVueFlow } from '../../composables'
|
||||
import type { FlowProps } from '../../types/flow'
|
||||
import useWatch from './watch'
|
||||
|
||||
const props = withDefaults(defineProps<FlowProps>(), {
|
||||
snapToGrid: undefined,
|
||||
onlyRenderVisibleElements: undefined,
|
||||
edgesUpdatable: undefined,
|
||||
nodesConnectable: undefined,
|
||||
nodesDraggable: undefined,
|
||||
elementsSelectable: undefined,
|
||||
selectNodesOnDrag: undefined,
|
||||
preventScrolling: undefined,
|
||||
zoomOnScroll: undefined,
|
||||
zoomOnPinch: undefined,
|
||||
zoomOnDoubleClick: undefined,
|
||||
panOnScroll: undefined,
|
||||
panOnDrag: undefined,
|
||||
applyDefault: undefined,
|
||||
fitViewOnInit: undefined,
|
||||
connectOnClick: undefined,
|
||||
connectionLineStyle: () => null,
|
||||
})
|
||||
const emit = defineEmits([...Object.keys(createHooks()), 'update:modelValue', 'update:edges', 'update:nodes'])
|
||||
|
||||
const {
|
||||
id,
|
||||
store,
|
||||
hooks,
|
||||
applyDefault,
|
||||
setElements,
|
||||
setEdges,
|
||||
setNodes,
|
||||
getNodeTypes,
|
||||
getEdgeTypes,
|
||||
onNodesChange,
|
||||
onEdgesChange,
|
||||
applyNodeChanges,
|
||||
applyEdgeChanges,
|
||||
nodes: storedNodes,
|
||||
edges: storedEdges,
|
||||
} = useVueFlow(props)
|
||||
useHooks(hooks.value, emit)
|
||||
const { modelValue, nodes, edges } = useVModels(props, emit)
|
||||
onMounted(() => useWatch({ modelValue, nodes, edges }, props, store))
|
||||
|
||||
if (applyDefault.value) {
|
||||
onNodesChange(applyNodeChanges)
|
||||
onEdgesChange(applyEdgeChanges)
|
||||
}
|
||||
if (props.modelValue && !storedNodes.value.length) setElements(props.modelValue)
|
||||
if (props.nodes && !storedNodes.value.length) setNodes(props.nodes)
|
||||
if (props.edges && !storedEdges.value.length) setEdges(props.edges)
|
||||
|
||||
if (modelValue && modelValue.value) {
|
||||
watch([() => storedEdges.value.length, () => storedNodes.value.length], () => {
|
||||
modelValue.value = [...storedNodes.value, ...storedEdges.value]
|
||||
})
|
||||
}
|
||||
|
||||
if (nodes && nodes.value) {
|
||||
watch(
|
||||
() => storedNodes.value.length,
|
||||
() => (nodes.value = [...storedNodes.value]),
|
||||
)
|
||||
}
|
||||
|
||||
if (edges && edges.value) {
|
||||
watch(
|
||||
() => storedEdges.value.length,
|
||||
() => (edges.value = [...storedEdges.value]),
|
||||
)
|
||||
}
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'VueFlow',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="vue-flow">
|
||||
<ZoomPane :key="`renderer-${id}`">
|
||||
<template v-for="nodeName of Object.keys(getNodeTypes)" #[`node-${nodeName}`]="nodeProps" :key="`node-${nodeName}-${id}`">
|
||||
<slot :name="`node-${nodeName}`" v-bind="nodeProps" />
|
||||
</template>
|
||||
<template v-for="edgeName of Object.keys(getEdgeTypes)" #[`edge-${edgeName}`]="edgeProps" :key="`edge-${edgeName}-${id}`">
|
||||
<slot :name="`edge-${edgeName}`" v-bind="edgeProps" />
|
||||
</template>
|
||||
<template #connection-line="customConnectionLineProps">
|
||||
<slot name="connection-line" v-bind="customConnectionLineProps" />
|
||||
</template>
|
||||
<slot name="zoom-pane" />
|
||||
</ZoomPane>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
<style>
|
||||
@import '../../style.css';
|
||||
</style>
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Ref } from 'vue'
|
||||
import { FlowProps, FlowStore, Node, Edge, Elements } from '~/types'
|
||||
|
||||
const isDef = <T>(val: T): val is NonNullable<T> => typeof val !== 'undefined'
|
||||
export default (
|
||||
{
|
||||
modelValue,
|
||||
nodes,
|
||||
edges,
|
||||
}: {
|
||||
modelValue?: Ref<Elements | undefined>
|
||||
nodes?: Ref<Node[] | undefined>
|
||||
edges?: Ref<Edge[] | undefined>
|
||||
},
|
||||
props: FlowProps,
|
||||
store: FlowStore,
|
||||
) => {
|
||||
const scope = getCurrentScope()
|
||||
scope?.run(() => {
|
||||
if (isDefined(props.modelValue)) {
|
||||
const { pause, resume } = pausableWatch([() => props.modelValue, () => props.modelValue?.length], async ([v]) => {
|
||||
if (v && Array.isArray(v)) {
|
||||
pause()
|
||||
store.setElements(v)
|
||||
if (modelValue) modelValue.value = [...store.nodes, ...store.edges]
|
||||
await nextTick()
|
||||
resume()
|
||||
}
|
||||
})
|
||||
}
|
||||
if (isDefined(props.nodes)) {
|
||||
const { pause, resume } = pausableWatch([() => props.nodes, () => props.nodes?.length], async ([v]) => {
|
||||
if (v && Array.isArray(v)) {
|
||||
pause()
|
||||
store.setNodes(v)
|
||||
if (nodes) nodes.value = store.nodes
|
||||
await nextTick()
|
||||
resume()
|
||||
}
|
||||
})
|
||||
}
|
||||
if (isDefined(props.edges)) {
|
||||
const { pause, resume } = pausableWatch([() => props.edges, () => props.edges?.length], async ([v]) => {
|
||||
if (v && Array.isArray(v)) {
|
||||
pause()
|
||||
store.setEdges(v)
|
||||
if (edges) edges.value = store.edges
|
||||
await nextTick()
|
||||
resume()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.maxZoom,
|
||||
(v) => isDef(v) && store.setMaxZoom(v),
|
||||
{ immediate: isDef(props.maxZoom) },
|
||||
)
|
||||
watch(
|
||||
() => props.minZoom,
|
||||
(v) => isDef(v) && store.setMinZoom(v),
|
||||
{ immediate: isDef(props.minZoom) },
|
||||
)
|
||||
|
||||
const skip = ['modelValue', 'edges', 'nodes', 'maxZoom', 'minZoom']
|
||||
Object.keys(props).forEach((p) => {
|
||||
if (!skip.includes(p)) {
|
||||
const prop = props[p as keyof typeof props]
|
||||
watch(
|
||||
() => prop,
|
||||
(v) => isDef(v) && ((store as any)[p] = v),
|
||||
{ immediate: isDef(prop) },
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
<script lang="ts" setup>
|
||||
import { D3ZoomEvent, zoom, zoomIdentity, ZoomTransform } from 'd3-zoom'
|
||||
import { pointer, select } from 'd3-selection'
|
||||
import { FlowTransform, PanOnScrollMode } from '../../types'
|
||||
import { useKeyPress, useVueFlow } from '../../composables'
|
||||
import { clamp, clampPosition } from '../../utils'
|
||||
import SelectionPane from '../SelectionPane/SelectionPane.vue'
|
||||
import TransformationPane from '../TransformationPane/TransformationPane.vue'
|
||||
|
||||
const { id, store } = useVueFlow()
|
||||
const zoomPaneEl = templateRef<HTMLDivElement>('zoomPane', null)
|
||||
|
||||
const viewChanged = (prevTransform: FlowTransform, eventTransform: ZoomTransform): boolean =>
|
||||
(prevTransform.x !== eventTransform.x && !isNaN(eventTransform.x)) ||
|
||||
(prevTransform.y !== eventTransform.y && !isNaN(eventTransform.y)) ||
|
||||
(prevTransform.zoom !== eventTransform.k && !isNaN(eventTransform.k))
|
||||
|
||||
const eventToFlowTransform = (eventTransform: ZoomTransform): FlowTransform => ({
|
||||
x: eventTransform.x,
|
||||
y: eventTransform.y,
|
||||
zoom: eventTransform.k,
|
||||
})
|
||||
|
||||
const isWrappedWithClass = (event: Event, className: string | undefined) => (event.target as Element).closest(`.${className}`)
|
||||
|
||||
const clampedZoom = clamp(store.defaultZoom, store.minZoom, store.maxZoom)
|
||||
const transform = ref({
|
||||
...clampPosition({ x: store.defaultPosition[0], y: store.defaultPosition[1] }, store.translateExtent),
|
||||
zoom: clampedZoom,
|
||||
})
|
||||
const { width, height } = useElementBounding(zoomPaneEl)
|
||||
|
||||
onMounted(() => {
|
||||
const d3Zoom = zoom<HTMLDivElement, any>().scaleExtent([store.minZoom, store.maxZoom]).translateExtent(store.translateExtent)
|
||||
const d3Selection = select(zoomPaneEl.value).call(d3Zoom)
|
||||
const d3ZoomHandler = d3Selection.on('wheel.zoom')
|
||||
|
||||
const updatedTransform = zoomIdentity.translate(transform.value.x, transform.value.y).scale(transform.value.zoom)
|
||||
d3Zoom.transform(d3Selection, updatedTransform)
|
||||
|
||||
store.setState({
|
||||
d3Zoom,
|
||||
d3Selection,
|
||||
d3ZoomHandler,
|
||||
transform: [updatedTransform.x, updatedTransform.y, updatedTransform.k],
|
||||
})
|
||||
|
||||
const selectionKeyPressed = useKeyPress(store.selectionKeyCode, (keyPress) => {
|
||||
if (keyPress) {
|
||||
d3Zoom.on('zoom', null)
|
||||
} else {
|
||||
d3Zoom.on('zoom', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||
store.setState({ transform: [event.transform.x, event.transform.y, event.transform.k] })
|
||||
const flowTransform = eventToFlowTransform(event.transform)
|
||||
store.hooks.move.trigger({ event, flowTransform })
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const zoomKeyPressed = useKeyPress(store.zoomActivationKeyCode)
|
||||
|
||||
d3Zoom.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||
const flowTransform = eventToFlowTransform(event.transform)
|
||||
transform.value = flowTransform
|
||||
store.hooks.moveStart.trigger({ event, flowTransform })
|
||||
})
|
||||
|
||||
d3Zoom.on('end', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||
if (viewChanged(transform.value, event.transform)) {
|
||||
const flowTransform = eventToFlowTransform(event.transform)
|
||||
transform.value = flowTransform
|
||||
store.hooks.moveEnd.trigger({ event, flowTransform })
|
||||
}
|
||||
})
|
||||
|
||||
d3Selection
|
||||
?.on('wheel', (event: WheelEvent) => {
|
||||
if (store.panOnScroll && !zoomKeyPressed.value) {
|
||||
if (isWrappedWithClass(event, store.noWheelClassName)) return
|
||||
event.preventDefault()
|
||||
event.stopImmediatePropagation()
|
||||
|
||||
const currentZoom = d3Selection?.property('__zoom').k || 1
|
||||
|
||||
if (event.ctrlKey && store.zoomOnPinch) {
|
||||
const point = pointer(event)
|
||||
// taken from https://github.com/d3/d3-zoom/blob/master/src/zoom.js
|
||||
const pinchDelta = -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002) * 10
|
||||
const zoom = currentZoom * Math.pow(2, pinchDelta)
|
||||
if (d3Selection) d3Zoom.scaleTo(d3Selection, zoom, point)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// increase scroll speed in firefox
|
||||
// firefox: deltaMode === 1; chrome: deltaMode === 0
|
||||
const deltaNormalize = event.deltaMode === 1 ? 20 : 1
|
||||
const deltaX = store.panOnScrollMode === PanOnScrollMode.Vertical ? 0 : event.deltaX * deltaNormalize
|
||||
const deltaY = store.panOnScrollMode === PanOnScrollMode.Horizontal ? 0 : event.deltaY * deltaNormalize
|
||||
|
||||
if (d3Selection && store.panOnScrollSpeed)
|
||||
d3Zoom?.translateBy(
|
||||
d3Selection,
|
||||
-(deltaX / currentZoom) * store.panOnScrollSpeed,
|
||||
-(deltaY / currentZoom) * store.panOnScrollSpeed,
|
||||
)
|
||||
} else {
|
||||
if (
|
||||
(!store.zoomOnScroll && store.preventScrolling) ||
|
||||
!store.preventScrolling ||
|
||||
isWrappedWithClass(event, store.noWheelClassName)
|
||||
)
|
||||
return null
|
||||
event.preventDefault()
|
||||
}
|
||||
})
|
||||
.on('wheel.zoom', store.panOnScroll || typeof d3ZoomHandler === 'undefined' ? null : (d3ZoomHandler as any))
|
||||
|
||||
d3Zoom.filter((event: MouseEvent) => {
|
||||
const zoomScroll = zoomKeyPressed.value || store.zoomOnScroll
|
||||
const pinchZoom = store.zoomOnPinch && event.ctrlKey
|
||||
|
||||
// if all interactions are disabled, we prevent all zoom events
|
||||
if (!store.panOnDrag && !zoomScroll && !store.panOnScroll && !store.zoomOnDoubleClick && !store.zoomOnPinch) return false
|
||||
|
||||
// during a selection we prevent all other interactions
|
||||
if (selectionKeyPressed.value) return false
|
||||
|
||||
// if zoom on double click is disabled, we prevent the double click event
|
||||
if (!store.zoomOnDoubleClick && event.type === 'dblclick') return false
|
||||
|
||||
// if the target element is inside an element with the nowheel class, we prevent zooming
|
||||
if (isWrappedWithClass(event, store.noWheelClassName) && event.type === 'wheel') return false
|
||||
|
||||
// if the target element is inside an element with the nopan class, we prevent panning
|
||||
if (isWrappedWithClass(event, store.noPanClassName) && event.type !== 'wheel') return false
|
||||
|
||||
if (!store.zoomOnPinch && event.ctrlKey && event.type === 'wheel') return false
|
||||
|
||||
// when there is no scroll handling enabled, we prevent all wheel events
|
||||
if (!zoomScroll && !store.panOnScroll && !pinchZoom && event.type === 'wheel') return false
|
||||
|
||||
// if the pane is not movable, we prevent dragging it with mousestart or touchstart
|
||||
if (!store.panOnDrag && (event.type === 'mousedown' || event.type === 'touchstart')) return false
|
||||
|
||||
// default filter for d3-zoom
|
||||
return (!event.ctrlKey || event.type === 'wheel') && !event.button
|
||||
})
|
||||
})
|
||||
|
||||
watch(
|
||||
[width, height],
|
||||
([newWidth, newHeight]) => {
|
||||
store.dimensions.width = newWidth
|
||||
store.dimensions.height = newHeight
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ZoomPane',
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div ref="zoomPane" class="vue-flow__renderer vue-flow__container">
|
||||
<TransformationPane>
|
||||
<template
|
||||
v-for="nodeName of Object.keys(store.getNodeTypes)"
|
||||
#[`node-${nodeName}`]="nodeProps"
|
||||
:key="`node-${nodeName}-${id}`"
|
||||
>
|
||||
<slot :name="`node-${nodeName}`" v-bind="nodeProps" />
|
||||
</template>
|
||||
<template
|
||||
v-for="edgeName of Object.keys(store.getEdgeTypes)"
|
||||
#[`edge-${edgeName}`]="edgeProps"
|
||||
:key="`edge-${edgeName}-${id}`"
|
||||
>
|
||||
<slot :name="`edge-${edgeName}`" v-bind="edgeProps" />
|
||||
</template>
|
||||
<template #connection-line="customConnectionLineProps">
|
||||
<slot name="connection-line" v-bind="customConnectionLineProps" />
|
||||
</template>
|
||||
<slot />
|
||||
</TransformationPane>
|
||||
<SelectionPane :key="`selection-pane-${id}`" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
export { default as VueFlow } from './VueFlow/VueFlow.vue'
|
||||
export { default as EdgeRenderer } from './EdgeRenderer/EdgeRenderer.vue'
|
||||
export { default as Marker } from './EdgeRenderer/Marker.vue'
|
||||
export { default as MarkerDefinitions } from './EdgeRenderer/MarkerDefinitions.vue'
|
||||
export { default as NodeRenderer } from './NodeRenderer/NodeRenderer.vue'
|
||||
export { default as ZoomPane } from './ZoomPane/ZoomPane.vue'
|
||||
export { default as SelectionPane } from './SelectionPane/SelectionPane.vue'
|
||||
@@ -0,0 +1,5 @@
|
||||
import { InjectionKey } from 'vue'
|
||||
import { UseVueFlow } from '~/types'
|
||||
|
||||
export const VueFlow: InjectionKey<UseVueFlow> = Symbol('vueFlow')
|
||||
export const NodeId: InjectionKey<string> = Symbol('nodeId')
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
declare const __VUE_FLOW_VERSION__: string
|
||||
declare const __ENV__: string
|
||||
declare interface Window {
|
||||
chrome?: any
|
||||
}
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite-svg-loader" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import { DefineComponent } from 'vue'
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
export { default as VueFlow } from './container/VueFlow/VueFlow.vue'
|
||||
export { default as Handle } from './components/Handle/Handle.vue'
|
||||
export { default as EdgeText } from './components/Edges/EdgeText.vue'
|
||||
export { default as StraightEdge } from './components/Edges/StraightEdge.vue'
|
||||
export { default as StepEdge } from './components/Edges/StepEdge.vue'
|
||||
export { default as BezierEdge } from './components/Edges/BezierEdge.vue'
|
||||
export { default as SimpleBezierEdge } from './components/Edges/SimpleBezierEdge.vue'
|
||||
export { default as SmoothStepEdge } from './components/Edges/SmoothStepEdge.vue'
|
||||
export {
|
||||
getBezierPath,
|
||||
getBezierCenter,
|
||||
getSimpleBezierPath,
|
||||
getSimpleBezierCenter,
|
||||
getSmoothStepPath,
|
||||
getCenter as getEdgeCenter,
|
||||
} from './components/Edges/utils'
|
||||
|
||||
export {
|
||||
isNode,
|
||||
isEdge,
|
||||
addEdge,
|
||||
getOutgoers,
|
||||
getIncomers,
|
||||
getConnectedEdges,
|
||||
updateEdge,
|
||||
getTransformForBounds,
|
||||
getRectOfNodes,
|
||||
graphPosToZoomedPos,
|
||||
getNodesInside,
|
||||
getMarkerId,
|
||||
} from './utils/graph'
|
||||
export { defaultEdgeTypes, defaultNodeTypes } from './store'
|
||||
export { default as useZoomPanHelper } from './composables/useZoomPanHelper'
|
||||
export { default as useVueFlow } from './composables/useVueFlow'
|
||||
export { default as useHandle } from './composables/useHandle'
|
||||
|
||||
export * from './additional-components'
|
||||
export * from './types'
|
||||
export * from './context'
|
||||
@@ -0,0 +1,325 @@
|
||||
import useState from './state'
|
||||
import {
|
||||
Actions,
|
||||
ComputedGetters,
|
||||
Connection,
|
||||
CoordinateExtent,
|
||||
Edge,
|
||||
EdgeChange,
|
||||
Getters,
|
||||
GraphEdge,
|
||||
GraphNode,
|
||||
Node,
|
||||
NodeChange,
|
||||
NodeDimensionChange,
|
||||
NodePositionChange,
|
||||
State,
|
||||
} from '~/types'
|
||||
import {
|
||||
applyChanges,
|
||||
connectionExists,
|
||||
createPositionChange,
|
||||
createSelectionChange,
|
||||
getDimensions,
|
||||
getEdgeId,
|
||||
getHandleBounds,
|
||||
getSelectionChanges,
|
||||
isEdge,
|
||||
isGraphEdge,
|
||||
isGraphNode,
|
||||
isNode,
|
||||
isParentSelected,
|
||||
parseEdge,
|
||||
parseNode,
|
||||
} from '~/utils'
|
||||
|
||||
const isDef = <T>(val: T): val is NonNullable<T> => typeof val !== 'undefined'
|
||||
|
||||
const addEdge = (edgeParams: Edge | Connection, edges: Edge[]) => {
|
||||
if (!edgeParams.source || !edgeParams.target) {
|
||||
console.warn("Can't create edge. An edge needs a source and a target.")
|
||||
return false
|
||||
}
|
||||
|
||||
let edge
|
||||
if (isEdge(edgeParams)) {
|
||||
edge = { ...edgeParams }
|
||||
} else {
|
||||
edge = {
|
||||
...edgeParams,
|
||||
id: getEdgeId(edgeParams),
|
||||
} as Edge
|
||||
}
|
||||
edge = parseEdge(edge)
|
||||
if (connectionExists(edge, edges)) return false
|
||||
return edge
|
||||
}
|
||||
|
||||
const updateEdgeAction = (edge: GraphEdge, newConnection: Connection, edges: GraphEdge[], add: Actions['addEdges']): boolean => {
|
||||
if (!newConnection.source || !newConnection.target) {
|
||||
console.warn("Can't create new edge. An edge needs a source and a target.")
|
||||
return false
|
||||
}
|
||||
|
||||
const foundEdge = edges.find((e) => isGraphEdge(e) && e.id === edge.id)
|
||||
|
||||
if (!foundEdge) {
|
||||
console.warn(`The old edge with id=${edge.id} does not exist.`)
|
||||
return false
|
||||
}
|
||||
|
||||
edges.splice(edges.indexOf(edge), 1)
|
||||
add([
|
||||
{
|
||||
...edge,
|
||||
id: getEdgeId(newConnection),
|
||||
source: newConnection.source,
|
||||
target: newConnection.target,
|
||||
sourceHandle: newConnection.sourceHandle,
|
||||
targetHandle: newConnection.targetHandle,
|
||||
},
|
||||
])
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const applyEdgeChangesAction = (changes: EdgeChange[], edges: GraphEdge[], getNode: Getters['getNode']) =>
|
||||
applyChanges(changes, edges, getNode)
|
||||
const applyNodeChangesAction = (changes: NodeChange[], nodes: GraphNode[], getNode: Getters['getNode']) =>
|
||||
applyChanges(changes, nodes, getNode)
|
||||
|
||||
const createGraphNodes = (nodes: Node[], getNode: Getters['getNode'], currGraphNodes: GraphNode[], extent: CoordinateExtent) => {
|
||||
const parentNodes: Record<string, true> = {}
|
||||
|
||||
const graphNodes = nodes.map((node) => {
|
||||
const parsed = parseNode(node, extent, {
|
||||
...getNode(node.id),
|
||||
parentNode: node.parentNode,
|
||||
})
|
||||
if (node.parentNode) {
|
||||
parentNodes[node.parentNode] = true
|
||||
}
|
||||
|
||||
return parsed
|
||||
})
|
||||
|
||||
graphNodes.forEach((node) => {
|
||||
if (node.parentNode && ![...graphNodes, ...currGraphNodes].find((n) => n.id === node.parentNode)) {
|
||||
throw new Error(`Parent node ${node.parentNode} not found`)
|
||||
}
|
||||
|
||||
if (node.parentNode || parentNodes[node.id]) {
|
||||
if (parentNodes[node.id]) {
|
||||
node.isParent = true
|
||||
}
|
||||
const parent = node.parentNode ? getNode(node.parentNode) : undefined
|
||||
if (parent) parent.isParent = true
|
||||
}
|
||||
})
|
||||
|
||||
return graphNodes
|
||||
}
|
||||
|
||||
export default (state: State, getters: ComputedGetters): Actions => {
|
||||
const updateNodePosition: Actions['updateNodePosition'] = ({ id, diff = { x: 0, y: 0 }, dragging }) => {
|
||||
const changes: NodePositionChange[] = []
|
||||
|
||||
state.nodes.forEach((node) => {
|
||||
if (node.selected) {
|
||||
if (!node.parentNode) {
|
||||
changes.push(createPositionChange({ node, diff, nodeExtent: state.nodeExtent, dragging }, getters.getNode.value))
|
||||
} else if (!isParentSelected(node, getters.getNode.value)) {
|
||||
changes.push(createPositionChange({ node, diff, nodeExtent: state.nodeExtent, dragging }, getters.getNode.value))
|
||||
}
|
||||
} else if (node.id === id) {
|
||||
changes.push(createPositionChange({ node, diff, nodeExtent: state.nodeExtent, dragging }, getters.getNode.value))
|
||||
}
|
||||
})
|
||||
|
||||
if (changes.length) state.hooks.nodesChange.trigger(changes)
|
||||
}
|
||||
const updateNodeDimensions: Actions['updateNodeDimensions'] = (updates) => {
|
||||
const changes: NodeDimensionChange[] = updates.reduce<NodeDimensionChange[]>((res, update) => {
|
||||
const node = getters.getNode.value(update.id)
|
||||
|
||||
if (node) {
|
||||
const dimensions = getDimensions(update.nodeElement)
|
||||
const doUpdate = !!(
|
||||
dimensions.width &&
|
||||
dimensions.height &&
|
||||
(node.dimensions.width !== dimensions.width || node.dimensions.height !== dimensions.height || update.forceUpdate)
|
||||
)
|
||||
node.handleBounds = getHandleBounds(update.nodeElement, state.transform[2])
|
||||
|
||||
if (doUpdate) {
|
||||
node.dimensions = dimensions
|
||||
|
||||
res.push({
|
||||
id: node.id,
|
||||
type: 'dimensions',
|
||||
dimensions,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}, [])
|
||||
|
||||
if (changes.length) state.hooks.nodesChange.trigger(changes)
|
||||
}
|
||||
const addSelectedNodes: Actions['addSelectedNodes'] = (nodes) => {
|
||||
const selectedNodesIds = nodes.map((n) => n.id)
|
||||
|
||||
let changedNodes: NodeChange[]
|
||||
if (state.multiSelectionActive) changedNodes = selectedNodesIds.map((nodeId) => createSelectionChange(nodeId, true))
|
||||
else changedNodes = getSelectionChanges(state.nodes, selectedNodesIds, getters.getNode.value)
|
||||
|
||||
if (changedNodes.length) state.hooks.nodesChange.trigger(changedNodes)
|
||||
}
|
||||
const addSelectedEdges: Actions['addSelectedEdges'] = (edges) => {
|
||||
const selectedEdgesIds = edges.map((e) => e.id)
|
||||
|
||||
let changedEdges: EdgeChange[]
|
||||
if (state.multiSelectionActive) changedEdges = selectedEdgesIds.map((nodeId) => createSelectionChange(nodeId, true))
|
||||
else changedEdges = getSelectionChanges(state.edges, selectedEdgesIds, getters.getNode.value)
|
||||
|
||||
if (changedEdges.length) state.hooks.edgesChange.trigger(changedEdges)
|
||||
}
|
||||
const addSelectedElements: Actions['addSelectedElements'] = (elements) => {
|
||||
addSelectedNodes(elements.filter(isGraphNode))
|
||||
addSelectedEdges(elements.filter(isGraphEdge))
|
||||
}
|
||||
const setMinZoom: Actions['setMinZoom'] = (minZoom: any) => {
|
||||
state.d3Zoom?.scaleExtent([minZoom, state.maxZoom])
|
||||
state.minZoom = minZoom
|
||||
}
|
||||
const setMaxZoom: Actions['setMaxZoom'] = (maxZoom: any) => {
|
||||
state.d3Zoom?.scaleExtent([state.minZoom, maxZoom])
|
||||
state.maxZoom = maxZoom
|
||||
}
|
||||
const setTranslateExtent: Actions['setTranslateExtent'] = (translateExtent: any) => {
|
||||
state.d3Zoom?.translateExtent(translateExtent)
|
||||
state.translateExtent = translateExtent
|
||||
}
|
||||
const resetSelectedElements: Actions['resetSelectedElements'] = () => {
|
||||
addSelectedNodes([])
|
||||
addSelectedEdges([])
|
||||
}
|
||||
const setInteractive: Actions['setInteractive'] = (isInteractive) => {
|
||||
state.nodesDraggable = isInteractive
|
||||
state.nodesConnectable = isInteractive
|
||||
state.elementsSelectable = isInteractive
|
||||
}
|
||||
|
||||
const setNodes: Actions['setNodes'] = (nodes, extent?: CoordinateExtent) => {
|
||||
if (!state.initialized && !nodes.length) return
|
||||
state.nodes = createGraphNodes(nodes, getters.getNode.value, state.nodes, extent ?? state.nodeExtent)
|
||||
}
|
||||
const setEdges: Actions['setEdges'] = (edges) => {
|
||||
if (!state.initialized && !edges.length) return
|
||||
state.edges = edges.map((edge) => {
|
||||
const sourceNode = getters.getNode.value(edge.source)!
|
||||
const targetNode = getters.getNode.value(edge.target)!
|
||||
if (!sourceNode || typeof sourceNode === 'undefined')
|
||||
console.warn(`couldn't create edge for source id: ${edge.source}; edge id: ${edge.id}`)
|
||||
if (!targetNode || typeof targetNode === 'undefined')
|
||||
console.warn(`couldn't create edge for target id: ${edge.target}; edge id: ${edge.id}`)
|
||||
|
||||
return {
|
||||
...parseEdge(edge, {
|
||||
...state.defaultEdgeOptions,
|
||||
...getters.getEdge.value(edge.id),
|
||||
}),
|
||||
sourceNode,
|
||||
targetNode,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const setElements: Actions['setElements'] = (elements, extent) => {
|
||||
if ((!state.initialized && !elements.length) || !elements) return
|
||||
setNodes(elements.filter(isNode), extent)
|
||||
setEdges(elements.filter(isEdge))
|
||||
}
|
||||
|
||||
const addNodes: Actions['addNodes'] = (nodes, extent) => {
|
||||
state.nodes.push(...createGraphNodes(nodes, getters.getNode.value, state.nodes, extent ?? state.nodeExtent))
|
||||
}
|
||||
|
||||
const addEdges: Actions['addEdges'] = (params) => {
|
||||
params.forEach((param) => {
|
||||
const edge = addEdge(param, state.edges)
|
||||
if (edge) {
|
||||
const sourceNode = getters.getNode.value(edge.source)!
|
||||
const targetNode = getters.getNode.value(edge.target)!
|
||||
if (!sourceNode || typeof sourceNode === 'undefined')
|
||||
console.warn(`couldn't create edge for source id: ${edge.source}; edge id: ${edge.id}`)
|
||||
if (!targetNode || typeof targetNode === 'undefined')
|
||||
console.warn(`couldn't create edge for target id: ${edge.target}; edge id: ${edge.id}`)
|
||||
|
||||
state.edges.push({
|
||||
...state.defaultEdgeOptions,
|
||||
...edge,
|
||||
sourceNode,
|
||||
targetNode,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const updateEdge: Actions['updateEdge'] = (oldEdge, newConnection) =>
|
||||
updateEdgeAction(oldEdge, newConnection, state.edges, addEdges)
|
||||
const applyNodeChanges: Actions['applyNodeChanges'] = (changes) =>
|
||||
applyNodeChangesAction(changes, state.nodes, getters.getNode.value)
|
||||
const applyEdgeChanges: Actions['applyEdgeChanges'] = (changes) =>
|
||||
applyEdgeChangesAction(changes, state.edges, getters.getNode.value)
|
||||
|
||||
const setState: Actions['setState'] = (opts) => {
|
||||
const skip = ['modelValue', 'nodes', 'edges', 'maxZoom', 'minZoom', 'translateExtent']
|
||||
if (typeof opts.modelValue !== 'undefined') setElements(opts.modelValue, opts.nodeExtent ?? state.nodeExtent)
|
||||
if (typeof opts.nodes !== 'undefined') setNodes(opts.nodes, opts.nodeExtent ?? state.nodeExtent)
|
||||
if (typeof opts.edges !== 'undefined') setEdges(opts.edges)
|
||||
Object.keys(opts).forEach((o) => {
|
||||
const option = opts[o as keyof typeof opts]
|
||||
if (!skip.includes(o) && isDef(option)) (<any>state)[o] = option
|
||||
})
|
||||
if (!state.paneReady)
|
||||
until(() => state.d3Zoom)
|
||||
.not.toBeUndefined()
|
||||
.then(() => {
|
||||
if (typeof opts.maxZoom !== 'undefined') setMaxZoom(opts.maxZoom)
|
||||
if (typeof opts.minZoom !== 'undefined') setMinZoom(opts.minZoom)
|
||||
if (typeof opts.translateExtent !== 'undefined') setTranslateExtent(opts.translateExtent)
|
||||
})
|
||||
else {
|
||||
if (typeof opts.maxZoom !== 'undefined') setMaxZoom(opts.maxZoom)
|
||||
if (typeof opts.minZoom !== 'undefined') setMinZoom(opts.minZoom)
|
||||
if (typeof opts.translateExtent !== 'undefined') setTranslateExtent(opts.translateExtent)
|
||||
}
|
||||
if (!state.initialized) state.initialized = true
|
||||
}
|
||||
return {
|
||||
updateNodePosition,
|
||||
updateNodeDimensions,
|
||||
setElements,
|
||||
setNodes,
|
||||
setEdges,
|
||||
addNodes,
|
||||
addEdges,
|
||||
updateEdge,
|
||||
applyEdgeChanges,
|
||||
applyNodeChanges,
|
||||
addSelectedElements,
|
||||
addSelectedNodes,
|
||||
addSelectedEdges,
|
||||
setMinZoom,
|
||||
setMaxZoom,
|
||||
setTranslateExtent,
|
||||
resetSelectedElements,
|
||||
setInteractive,
|
||||
setState,
|
||||
$reset: () => {
|
||||
setState(useState())
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { defaultEdgeTypes, defaultNodeTypes } from './state'
|
||||
import { ComputedGetters, State, GraphEdge, GraphNode } from '~/types'
|
||||
import { getNodesInside, isEdgeVisible } from '~/utils'
|
||||
|
||||
export default (state: State): ComputedGetters => {
|
||||
const getEdgeTypes = computed(() => {
|
||||
const edgeTypes: Record<string, any> = {
|
||||
...defaultEdgeTypes,
|
||||
...state.edgeTypes,
|
||||
}
|
||||
const keys = Object.keys(edgeTypes)
|
||||
state.edges?.forEach((e) => e.type && !keys.includes(e.type) && (edgeTypes[e.type] = e.type))
|
||||
return edgeTypes
|
||||
})
|
||||
|
||||
const getNodeTypes = computed(() => {
|
||||
const nodeTypes: Record<string, any> = {
|
||||
...defaultNodeTypes,
|
||||
...state.nodeTypes,
|
||||
}
|
||||
const keys = Object.keys(nodeTypes)
|
||||
state.nodes?.forEach((n) => n.type && !keys.includes(n.type) && (nodeTypes[n.type] = n.type))
|
||||
return nodeTypes
|
||||
})
|
||||
|
||||
const getNodes = computed<GraphNode[]>(() => {
|
||||
if (state.paneReady && state.dimensions.width && state.dimensions.height) {
|
||||
const nodes = state.nodes.filter((n) => !n.hidden)
|
||||
return state.onlyRenderVisibleElements
|
||||
? nodes &&
|
||||
getNodesInside(
|
||||
nodes,
|
||||
{
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: state.dimensions.width,
|
||||
height: state.dimensions.height,
|
||||
},
|
||||
state.transform,
|
||||
true,
|
||||
)
|
||||
: nodes ?? []
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
const getEdges = computed<GraphEdge[]>(() => {
|
||||
if (state.paneReady && state.dimensions.width && state.dimensions.height) {
|
||||
if (!state.onlyRenderVisibleElements)
|
||||
return state.edges.filter(
|
||||
(e) => !e.hidden && e.targetNode && !e.targetNode.hidden && e.sourceNode && !e.sourceNode.hidden,
|
||||
)
|
||||
else
|
||||
return state.edges.filter(
|
||||
(e) =>
|
||||
!e.hidden &&
|
||||
e.sourceNode.dimensions.width &&
|
||||
e.sourceNode.dimensions.height &&
|
||||
e.targetNode.dimensions.width &&
|
||||
e.targetNode.dimensions.height &&
|
||||
!e.targetNode.hidden &&
|
||||
!e.sourceNode.hidden &&
|
||||
isEdgeVisible({
|
||||
sourcePos: e.sourceNode.computedPosition || { x: 0, y: 0 },
|
||||
targetPos: e.targetNode.computedPosition || { x: 0, y: 0 },
|
||||
sourceWidth: e.sourceNode.dimensions.width,
|
||||
sourceHeight: e.sourceNode.dimensions.height,
|
||||
targetWidth: e.targetNode.dimensions.width,
|
||||
targetHeight: e.targetNode.dimensions.height,
|
||||
width: state.dimensions.width,
|
||||
height: state.dimensions.height,
|
||||
transform: state.transform,
|
||||
}),
|
||||
)
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
const getSelectedNodes: ComputedGetters['getSelectedNodes'] = computed(() => state.nodes.filter((n) => n.selected))
|
||||
const getSelectedEdges: ComputedGetters['getSelectedEdges'] = computed(() => state.edges.filter((e) => e.selected))
|
||||
const getSelectedElements: ComputedGetters['getSelectedElements'] = computed(() => [
|
||||
...(getSelectedNodes.value ?? []),
|
||||
...(getSelectedEdges.value ?? []),
|
||||
])
|
||||
|
||||
const nodeIds = computed(() => state.nodes.map((n) => n.id))
|
||||
const edgeIds = computed(() => state.edges.map((e) => e.id))
|
||||
const getNode: ComputedGetters['getNode'] = computed(() => (id: string) => state.nodes[nodeIds.value.indexOf(id)])
|
||||
const getEdge: ComputedGetters['getEdge'] = computed(() => (id: string) => state.edges[edgeIds.value.indexOf(id)])
|
||||
|
||||
return {
|
||||
getNode,
|
||||
getEdge,
|
||||
getEdgeTypes,
|
||||
getNodeTypes,
|
||||
getEdges,
|
||||
getNodes,
|
||||
getSelectedElements,
|
||||
getSelectedNodes,
|
||||
getSelectedEdges,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { EmitFunc, FlowHooks, FlowEvents } from '~/types'
|
||||
|
||||
// flow event hooks
|
||||
export const createHooks = (): FlowHooks => ({
|
||||
edgesChange: createEventHook(),
|
||||
nodesChange: createEventHook(),
|
||||
nodeDoubleClick: createEventHook(),
|
||||
nodeClick: createEventHook(),
|
||||
nodeMouseEnter: createEventHook(),
|
||||
nodeMouseMove: createEventHook(),
|
||||
nodeMouseLeave: createEventHook(),
|
||||
nodeContextMenu: createEventHook(),
|
||||
nodeDragStart: createEventHook(),
|
||||
nodeDrag: createEventHook(),
|
||||
nodeDragStop: createEventHook(),
|
||||
connect: createEventHook(),
|
||||
connectStart: createEventHook(),
|
||||
connectStop: createEventHook(),
|
||||
connectEnd: createEventHook(),
|
||||
paneReady: createEventHook(),
|
||||
move: createEventHook(),
|
||||
moveStart: createEventHook(),
|
||||
moveEnd: createEventHook(),
|
||||
selectionDragStart: createEventHook(),
|
||||
selectionDrag: createEventHook(),
|
||||
selectionDragStop: createEventHook(),
|
||||
selectionContextMenu: createEventHook(),
|
||||
paneScroll: createEventHook(),
|
||||
paneClick: createEventHook(),
|
||||
paneContextMenu: createEventHook(),
|
||||
edgeContextMenu: createEventHook(),
|
||||
edgeMouseEnter: createEventHook(),
|
||||
edgeMouseMove: createEventHook(),
|
||||
edgeMouseLeave: createEventHook(),
|
||||
edgeDoubleClick: createEventHook(),
|
||||
edgeClick: createEventHook(),
|
||||
edgeUpdateStart: createEventHook(),
|
||||
edgeUpdate: createEventHook(),
|
||||
edgeUpdateEnd: createEventHook(),
|
||||
})
|
||||
|
||||
const bind = (emit: EmitFunc, hooks: FlowHooks) => {
|
||||
for (const [key, value] of Object.entries(hooks)) {
|
||||
value.on((data: FlowEvents[keyof FlowHooks]) => {
|
||||
emit(key as keyof FlowHooks, data)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default (hooks: FlowHooks, emit: EmitFunc) => bind(emit, hooks)
|
||||
@@ -0,0 +1,4 @@
|
||||
export { default as useHooks, createHooks } from './hooks'
|
||||
export { default as useStore } from './store'
|
||||
export * from './actions'
|
||||
export * from './state'
|
||||
@@ -0,0 +1,131 @@
|
||||
import { createHooks } from './hooks'
|
||||
import {
|
||||
ConnectionMode,
|
||||
State,
|
||||
PanOnScrollMode,
|
||||
DefaultNodeTypes,
|
||||
DefaultEdgeTypes,
|
||||
ConnectionLineType,
|
||||
FlowOptions,
|
||||
} from '~/types'
|
||||
import {
|
||||
DefaultNode,
|
||||
InputNode,
|
||||
OutputNode,
|
||||
BezierEdge,
|
||||
SmoothStepEdge,
|
||||
StepEdge,
|
||||
StraightEdge,
|
||||
SimpleBezierEdge,
|
||||
} from '~/components'
|
||||
|
||||
export const defaultNodeTypes: DefaultNodeTypes = {
|
||||
input: InputNode,
|
||||
default: DefaultNode,
|
||||
output: OutputNode,
|
||||
}
|
||||
|
||||
export const defaultEdgeTypes: DefaultEdgeTypes = {
|
||||
default: BezierEdge,
|
||||
straight: StraightEdge,
|
||||
step: StepEdge,
|
||||
smoothstep: SmoothStepEdge,
|
||||
simplebezier: SimpleBezierEdge,
|
||||
}
|
||||
|
||||
const isDef = <T>(val: T): val is NonNullable<T> => typeof val !== 'undefined'
|
||||
|
||||
const defaultState = (): State => ({
|
||||
nodes: [],
|
||||
edges: [],
|
||||
|
||||
paneReady: false,
|
||||
initialized: false,
|
||||
instance: null,
|
||||
|
||||
dimensions: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
transform: [0, 0, 1],
|
||||
|
||||
d3Zoom: null,
|
||||
d3Selection: null,
|
||||
d3ZoomHandler: null,
|
||||
minZoom: 0.5,
|
||||
maxZoom: 2,
|
||||
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],
|
||||
],
|
||||
preventScrolling: true,
|
||||
zoomOnScroll: true,
|
||||
zoomOnPinch: true,
|
||||
zoomOnDoubleClick: true,
|
||||
panOnScroll: false,
|
||||
panOnScrollSpeed: 0.5,
|
||||
panOnScrollMode: PanOnScrollMode.Free,
|
||||
panOnDrag: true,
|
||||
edgeUpdaterRadius: 10,
|
||||
onlyRenderVisibleElements: false,
|
||||
defaultZoom: 1,
|
||||
defaultPosition: [0, 0],
|
||||
|
||||
nodesSelectionActive: false,
|
||||
userSelectionActive: false,
|
||||
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
|
||||
|
||||
defaultMarkerColor: '#b1b1b7',
|
||||
connectionLineStyle: {},
|
||||
connectionLineType: ConnectionLineType.Bezier,
|
||||
connectionNodeId: null,
|
||||
connectionHandleId: null,
|
||||
connectionHandleType: null,
|
||||
connectionPosition: { x: NaN, y: NaN },
|
||||
connectionMode: ConnectionMode.Loose,
|
||||
connectionStartHandle: null,
|
||||
connectOnClick: true,
|
||||
|
||||
snapGrid: [15, 15],
|
||||
snapToGrid: false,
|
||||
|
||||
edgesUpdatable: false,
|
||||
nodesConnectable: true,
|
||||
nodesDraggable: true,
|
||||
elementsSelectable: true,
|
||||
selectNodesOnDrag: true,
|
||||
multiSelectionActive: false,
|
||||
selectionKeyCode: 'Shift',
|
||||
multiSelectionKeyCode: 'Meta',
|
||||
zoomActivationKeyCode: 'Meta',
|
||||
deleteKeyCode: 'Backspace',
|
||||
|
||||
hooks: createHooks(),
|
||||
|
||||
applyDefault: true,
|
||||
|
||||
fitViewOnInit: false,
|
||||
noDragClassName: 'nodrag',
|
||||
noWheelClassName: 'nowheel',
|
||||
noPanClassName: 'nopan',
|
||||
defaultEdgeOptions: undefined,
|
||||
|
||||
vueFlowVersion: typeof __VUE_FLOW_VERSION__ !== 'undefined' ? __VUE_FLOW_VERSION__ : '-',
|
||||
})
|
||||
|
||||
export default (opts?: FlowOptions): State => {
|
||||
const state = defaultState()
|
||||
if (opts) {
|
||||
Object.keys(opts).forEach((o) => {
|
||||
const option = opts[o as keyof typeof opts]
|
||||
if (isDef(option)) (state as any)[o] = option
|
||||
})
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import useState from './state'
|
||||
import useActions from './actions'
|
||||
import useGetters from './getters'
|
||||
import { FlowHooksOn, FlowOptions, State, Store } from '~/types'
|
||||
|
||||
export default (preloadedState?: FlowOptions): Store => {
|
||||
const state: State = useState(preloadedState)
|
||||
const reactiveState = reactive(state)
|
||||
const getters = useGetters(reactiveState)
|
||||
const actions = useActions(reactiveState, getters)
|
||||
const hooksOn: FlowHooksOn = <any>{}
|
||||
Object.entries(reactiveState.hooks).forEach(([n, h]) => {
|
||||
const name = `on${n.charAt(0).toUpperCase() + n.slice(1)}`
|
||||
hooksOn[<keyof FlowHooksOn>name] = h.on as any
|
||||
})
|
||||
actions.setState(reactiveState)
|
||||
if (preloadedState) {
|
||||
if (preloadedState.modelValue) actions.setElements(preloadedState.modelValue)
|
||||
if (preloadedState.nodes) actions.setNodes(preloadedState.nodes)
|
||||
if (preloadedState.edges) actions.setEdges(preloadedState.edges)
|
||||
}
|
||||
|
||||
return {
|
||||
state: reactiveState,
|
||||
actions,
|
||||
getters,
|
||||
hooksOn,
|
||||
...toRefs(reactiveState),
|
||||
...getters,
|
||||
...actions,
|
||||
} as unknown as Store
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
.vue-flow {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.vue-flow__container {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.vue-flow__transformation-pane {
|
||||
pointer-events: none;
|
||||
transform-origin: 0 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.vue-flow__pane {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.vue-flow__renderer {
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.vue-flow__selectionpane {
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.vue-flow__selection {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.vue-flow__edges {
|
||||
overflow: visible;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.vue-flow__edge {
|
||||
pointer-events: visibleStroke;
|
||||
|
||||
&.inactive {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dashdraw {
|
||||
from {
|
||||
stroke-dashoffset: 10;
|
||||
}
|
||||
}
|
||||
|
||||
.vue-flow__edge-path {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.vue-flow__edge-textwrapper {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.vue-flow__edge-text {
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.vue-flow__connection {
|
||||
pointer-events: none;
|
||||
|
||||
.animated {
|
||||
stroke-dasharray: 5;
|
||||
animation: dashdraw 0.5s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.vue-flow__connection-path {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.vue-flow__nodes {
|
||||
pointer-events: none;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.vue-flow__node {
|
||||
position: absolute;
|
||||
user-select: none;
|
||||
pointer-events: all;
|
||||
transform-origin: 0 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.vue-flow__nodesselection {
|
||||
z-index: 3;
|
||||
transform-origin: left top;
|
||||
pointer-events: none;
|
||||
|
||||
&-rect {
|
||||
position: absolute;
|
||||
pointer-events: all;
|
||||
cursor: grab;
|
||||
}
|
||||
}
|
||||
|
||||
.vue-flow__handle {
|
||||
pointer-events: none;
|
||||
|
||||
&.connectable {
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
.vue-flow__handle-bottom {
|
||||
top: auto;
|
||||
left: 50%;
|
||||
bottom: -4px;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
.vue-flow__handle-top {
|
||||
left: 50%;
|
||||
top: -4px;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
.vue-flow__handle-left {
|
||||
top: 50%;
|
||||
left: -4px;
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
|
||||
.vue-flow__handle-right {
|
||||
right: -4px;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
|
||||
.vue-flow__edgeupdater {
|
||||
cursor: move;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
/* additional components */
|
||||
.vue-flow__controls {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
|
||||
&-button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vue-flow__minimap {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
.vue-flow__selection {
|
||||
background: rgba(0, 89, 220, 0.08);
|
||||
border: 1px dotted rgba(0, 89, 220, 0.8);
|
||||
}
|
||||
|
||||
.vue-flow__edge {
|
||||
&.selected {
|
||||
.vue-flow__edge-path {
|
||||
stroke: #555;
|
||||
}
|
||||
}
|
||||
|
||||
&.animated path {
|
||||
stroke-dasharray: 5;
|
||||
animation: dashdraw 0.5s linear infinite;
|
||||
}
|
||||
|
||||
&.updating {
|
||||
.vue-flow__edge-path {
|
||||
stroke: #777;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vue-flow__edge-path {
|
||||
stroke: #b1b1b7;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.vue-flow__edge-text {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.vue-flow__edge-textbg {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.vue-flow__connection-path {
|
||||
stroke: #b1b1b7;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.vue-flow__node {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.vue-flow__node-default,
|
||||
.vue-flow__node-input,
|
||||
.vue-flow__node-output {
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
width: 150px;
|
||||
font-size: 12px;
|
||||
color: #222;
|
||||
text-align: center;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.vue-flow__node-default.selectable,
|
||||
.vue-flow__node-input.selectable,
|
||||
.vue-flow__node-output.selectable {
|
||||
&:hover {
|
||||
box-shadow: 0 1px 4px 1px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.vue-flow__node-input {
|
||||
background: #fff;
|
||||
border-color: #0041d0;
|
||||
|
||||
&.selected,
|
||||
&.selected:hover {
|
||||
box-shadow: 0 0 0 0.5px #0041d0;
|
||||
}
|
||||
|
||||
.vue-flow__handle {
|
||||
background: #0041d0;
|
||||
}
|
||||
}
|
||||
|
||||
.vue-flow__node-default {
|
||||
background: #fff;
|
||||
border-color: #1a192b;
|
||||
|
||||
&.selected,
|
||||
&.selected:hover {
|
||||
box-shadow: 0 0 0 0.5px #1a192b;
|
||||
}
|
||||
|
||||
.vue-flow__handle {
|
||||
background: #1a192b;
|
||||
}
|
||||
}
|
||||
|
||||
.vue-flow__node-output {
|
||||
background: #fff;
|
||||
border-color: #ff0072;
|
||||
|
||||
&.selected,
|
||||
&.selected:hover {
|
||||
box-shadow: 0 0 0 0.5px #ff0072;
|
||||
}
|
||||
|
||||
.vue-flow__handle {
|
||||
background: #ff0072;
|
||||
}
|
||||
}
|
||||
|
||||
.vue-flow__nodesselection-rect {
|
||||
background: rgba(0, 89, 220, 0.08);
|
||||
border: 1px dotted rgba(0, 89, 220, 0.8);
|
||||
}
|
||||
|
||||
.vue-flow__handle {
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: #555;
|
||||
border: 1px solid white;
|
||||
border-radius: 100%;
|
||||
|
||||
&.connectable {
|
||||
cursor: crosshair;
|
||||
}
|
||||
}
|
||||
|
||||
.vue-flow__minimap {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.vue-flow__controls {
|
||||
box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.08);
|
||||
|
||||
&-button {
|
||||
background: #fefefe;
|
||||
border-bottom: 1px solid #eee;
|
||||
box-sizing: content-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
padding: 5px;
|
||||
|
||||
svg {
|
||||
max-width: 12px;
|
||||
max-height: 12px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #f4f4f4;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { XYPosition, Dimensions } from './flow'
|
||||
import { NodeHandleBounds, Node } from './node'
|
||||
import { Edge } from './edge'
|
||||
|
||||
export type NodeDimensionChange = {
|
||||
id: string
|
||||
type: 'dimensions'
|
||||
dimensions: Dimensions
|
||||
handleBounds?: NodeHandleBounds
|
||||
}
|
||||
|
||||
export type NodePositionChange = {
|
||||
id: string
|
||||
type: 'position'
|
||||
position?: XYPosition
|
||||
dragging?: boolean
|
||||
}
|
||||
|
||||
export type NodeSelectionChange = {
|
||||
id: string
|
||||
type: 'select'
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
export type NodeRemoveChange = {
|
||||
id: string
|
||||
type: 'remove'
|
||||
}
|
||||
|
||||
export type NodeAddChange<NodeData = any> = {
|
||||
item: Node<NodeData>
|
||||
type: 'add'
|
||||
}
|
||||
|
||||
export type NodeResetChange<NodeData = any> = {
|
||||
item: Node<NodeData>
|
||||
type: 'reset'
|
||||
}
|
||||
|
||||
export type NodeChange =
|
||||
| NodeDimensionChange
|
||||
| NodePositionChange
|
||||
| NodeSelectionChange
|
||||
| NodeRemoveChange
|
||||
| NodeAddChange
|
||||
| NodeResetChange
|
||||
|
||||
export type EdgeSelectionChange = NodeSelectionChange
|
||||
export type EdgeRemoveChange = NodeRemoveChange
|
||||
export type EdgeAddChange<EdgeData = any> = {
|
||||
item: Edge<EdgeData>
|
||||
type: 'add'
|
||||
}
|
||||
export type EdgeResetChange<EdgeData = any> = {
|
||||
item: Edge<EdgeData>
|
||||
type: 'reset'
|
||||
}
|
||||
export type EdgeChange = EdgeSelectionChange | EdgeRemoveChange | EdgeAddChange | EdgeResetChange
|
||||
export type ElementChange = NodeChange | EdgeChange
|
||||
@@ -0,0 +1,109 @@
|
||||
import { Component, CSSProperties, DefineComponent, HTMLAttributes } from 'vue'
|
||||
import { BackgroundVariant, Dimensions, XYPosition } from './flow'
|
||||
import { GraphNode, Node, NodeProps } from './node'
|
||||
import { EdgeProps } from './edge'
|
||||
import { FitViewParams } from './zoom'
|
||||
|
||||
/** Global component names are components registered to the vue instance and are "autoloaded" by their string name */
|
||||
type GlobalComponentName = string
|
||||
|
||||
/** Node Components can either be a component definition or a string name */
|
||||
export type NodeComponent<N = any> =
|
||||
| Component<NodeProps<N>>
|
||||
| DefineComponent<NodeProps<N>, any, any, any, any>
|
||||
| GlobalComponentName
|
||||
|
||||
/** Edge Components can either be a component definition or a string name */
|
||||
export type EdgeComponent<E = any> =
|
||||
| Component<EdgeProps<E>>
|
||||
| DefineComponent<EdgeProps<E>, any, any, any, any, any>
|
||||
| GlobalComponentName
|
||||
|
||||
export type DefaultEdgeTypes = { [key in 'default' | 'straight' | 'smoothstep' | 'step' | 'simplebezier']: EdgeComponent }
|
||||
export type DefaultNodeTypes = { [key in 'input' | 'output' | 'default']: NodeComponent }
|
||||
|
||||
export interface BackgroundProps {
|
||||
/** The background pattern variant, {@link BackgroundVariant} */
|
||||
variant?: BackgroundVariant
|
||||
/** Background pattern gap */
|
||||
gap?: number
|
||||
/** Background pattern size */
|
||||
size?: number
|
||||
/** Background pattern color */
|
||||
patternColor?: string
|
||||
/** Background color */
|
||||
bgColor?: string
|
||||
/** Background height */
|
||||
height?: number
|
||||
/** Background width */
|
||||
width?: number
|
||||
/** Background x-coordinate (offset x) */
|
||||
x?: number
|
||||
/** Background y-coordinate (offset y) */
|
||||
y?: number
|
||||
}
|
||||
|
||||
export interface ControlProps {
|
||||
/** Show the zoom icon */
|
||||
showZoom?: boolean
|
||||
/** Show the fit-view icon */
|
||||
showFitView?: boolean
|
||||
/** Show the interactive icon */
|
||||
showInteractive?: boolean
|
||||
/** Params to use on fitView */
|
||||
fitViewParams?: FitViewParams
|
||||
}
|
||||
|
||||
export interface ControlEvents {
|
||||
(event: 'zoom-in'): void
|
||||
(event: 'zoom-out'): void
|
||||
(event: 'fit-view'): void
|
||||
(event: 'interaction-change', active: boolean): void
|
||||
}
|
||||
|
||||
/** expects a node and returns a color value */
|
||||
export type StringFunc<N = any> = (node: Node<N> | GraphNode<N>) => string
|
||||
export type ShapeRendering = 'inherit' | 'auto' | 'geometricPrecision' | 'optimizeSpeed' | 'crispEdges' | undefined
|
||||
|
||||
export interface MiniMapProps<N = any> {
|
||||
/** Node color, can be either a string or a string func that receives the current node */
|
||||
nodeColor?: string | (<NC = N>(node: Node<NC> | GraphNode<NC>) => string)
|
||||
/** Node stroke color, can be either a string or a string func that receives the current node */
|
||||
nodeStrokeColor?: string | (<NSC = N>(node: Node<NSC> | GraphNode<NSC>) => string)
|
||||
/** Additional node class name, can be either a string or a string func that receives the current node */
|
||||
nodeClassName?: string | (<NCM = N>(node: Node<NCM> | GraphNode<NCM>) => string)
|
||||
/** Node border radius */
|
||||
nodeBorderRadius?: number
|
||||
/** Node stroke width */
|
||||
nodeStrokeWidth?: number
|
||||
/** Background color of minimap */
|
||||
maskColor?: string
|
||||
}
|
||||
|
||||
/** these props are passed to mini map node slots */
|
||||
export interface MiniMapNodeProps {
|
||||
position: XYPosition
|
||||
dimensions: Dimensions
|
||||
borderRadius?: number
|
||||
color?: string
|
||||
shapeRendering?: CSSProperties['shapeRendering']
|
||||
strokeColor?: string
|
||||
strokeWidth?: number
|
||||
}
|
||||
|
||||
/** these props are passed to edge texts */
|
||||
export interface EdgeTextProps extends HTMLAttributes {
|
||||
x: number
|
||||
y: number
|
||||
label?:
|
||||
| string
|
||||
| {
|
||||
component: any
|
||||
props?: any
|
||||
}
|
||||
labelStyle?: CSSProperties
|
||||
labelShowBg?: boolean
|
||||
labelBgStyle?: CSSProperties
|
||||
labelBgPadding?: [number, number]
|
||||
labelBgBorderRadius?: number
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { CSSProperties } from 'vue'
|
||||
import { Position } from './flow'
|
||||
import { GraphNode } from './node'
|
||||
import { HandleElement, HandleType } from './handle'
|
||||
|
||||
/** Connection line types (same as default edge types */
|
||||
export enum ConnectionLineType {
|
||||
Bezier = 'default',
|
||||
Straight = 'straight',
|
||||
Step = 'step',
|
||||
SmoothStep = 'smoothstep',
|
||||
}
|
||||
|
||||
/** Connection params that are passed when onConnect is called */
|
||||
export interface Connection {
|
||||
/** Source node id */
|
||||
source: string | null
|
||||
/** Target node id */
|
||||
target: string | null
|
||||
/** Source handle id */
|
||||
sourceHandle: string | null
|
||||
/** Target handle id */
|
||||
targetHandle: string | null
|
||||
}
|
||||
|
||||
/** The source nodes params when connection is initiated */
|
||||
export type OnConnectStartParams = {
|
||||
/** Source node id */
|
||||
nodeId?: string
|
||||
/** Source handle id */
|
||||
handleId: string | null
|
||||
/** Source handle type */
|
||||
handleType?: HandleType
|
||||
}
|
||||
|
||||
/** Connection modes, when set to loose all handles are treated as source */
|
||||
export enum ConnectionMode {
|
||||
Strict = 'strict',
|
||||
Loose = 'loose',
|
||||
}
|
||||
|
||||
export interface ConnectionLineProps<N = any> {
|
||||
/** Source X position of the connection line */
|
||||
sourceX: number
|
||||
/** Source Y position of the connection line */
|
||||
sourceY: number
|
||||
/** Source position of the connection line */
|
||||
sourcePosition: Position
|
||||
/** Target X position of the connection line */
|
||||
targetX: number
|
||||
/** Target Y position of the connection line */
|
||||
targetY: number
|
||||
/** Target position of the connection line */
|
||||
targetPosition: Position
|
||||
/** the shape of the connection line when active */
|
||||
connectionLineType: ConnectionLineType
|
||||
/** extra styles */
|
||||
connectionLineStyle: CSSProperties
|
||||
/** All currently stored nodes */
|
||||
nodes: GraphNode<N>[]
|
||||
/** The source node of the connection line */
|
||||
sourceNode: GraphNode<N>
|
||||
/** The source handle element of the connection line */
|
||||
sourceHandle: HandleElement
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { CSSProperties } from 'vue'
|
||||
import { Position, Element } from './flow'
|
||||
import { GraphNode } from './node'
|
||||
|
||||
/** Edge markers */
|
||||
export enum MarkerType {
|
||||
Arrow = 'arrow',
|
||||
ArrowClosed = 'arrowclosed',
|
||||
}
|
||||
|
||||
/** Edge marker definition */
|
||||
export interface EdgeMarker {
|
||||
/** Unique marker id */
|
||||
id?: string
|
||||
/** Marker type */
|
||||
type: MarkerType
|
||||
/** Marker color */
|
||||
color?: string
|
||||
/** Marker width */
|
||||
width?: number
|
||||
/** Marker height */
|
||||
height?: number
|
||||
/** Marker units */
|
||||
markerUnits?: string
|
||||
/** Marker orientation */
|
||||
orient?: string
|
||||
/** Marker stroke width */
|
||||
strokeWidth?: number
|
||||
}
|
||||
|
||||
export interface MarkerProps {
|
||||
id: string
|
||||
type: MarkerType | string
|
||||
color?: string
|
||||
width?: number
|
||||
height?: number
|
||||
markerUnits?: string
|
||||
orient?: string
|
||||
strokeWidth?: number
|
||||
}
|
||||
|
||||
export type EdgeMarkerType = string | MarkerType | EdgeMarker
|
||||
|
||||
export interface Edge<T = any> extends Element<T> {
|
||||
/** Source node id */
|
||||
source: string
|
||||
/** Target node id */
|
||||
target: string
|
||||
/** Source handle id */
|
||||
sourceHandle?: string | null
|
||||
/** Target handle id */
|
||||
targetHandle?: string | null
|
||||
/** Source position */
|
||||
sourcePosition?: Position
|
||||
/** Target position */
|
||||
targetPosition?: Position
|
||||
/** Label styles (CSSProperties) */
|
||||
labelStyle?: CSSProperties
|
||||
/** Show label bg */
|
||||
labelShowBg?: boolean
|
||||
/** Label Bg styles (CSSProperties) */
|
||||
labelBgStyle?: CSSProperties
|
||||
/** Label Bg padding */
|
||||
labelBgPadding?: [number, number]
|
||||
/** Label Bg border radius */
|
||||
labelBgBorderRadius?: number
|
||||
/** Animated edge */
|
||||
animated?: boolean
|
||||
/** EdgeMarker */
|
||||
markerStart?: EdgeMarkerType
|
||||
/** EdgeMarker */
|
||||
markerEnd?: EdgeMarkerType
|
||||
/** Disable/enable updating edge */
|
||||
updatable?: boolean
|
||||
/** Disable/enable selecting edge */
|
||||
selectable?: boolean
|
||||
}
|
||||
|
||||
export type DefaultEdgeOptions = Omit<
|
||||
Edge,
|
||||
'id' | 'source' | 'target' | 'sourceHandle' | 'targetHandle' | 'sourceNode' | 'targetNode'
|
||||
>
|
||||
|
||||
export interface EdgePositions {
|
||||
sourceX: number
|
||||
sourceY: number
|
||||
targetX: number
|
||||
targetY: number
|
||||
}
|
||||
|
||||
/** Internal edge type */
|
||||
export type GraphEdge<T = any, N = T> = Edge<T> & {
|
||||
sourceNode: GraphNode<N>
|
||||
targetNode: GraphNode<N>
|
||||
selected?: boolean
|
||||
z?: number
|
||||
} & EdgePositions
|
||||
|
||||
/** these props are passed to edge components */
|
||||
export interface EdgeProps<Data = any> {
|
||||
id: string
|
||||
sourceNode: GraphNode
|
||||
targetNode: GraphNode
|
||||
label?:
|
||||
| string
|
||||
| {
|
||||
props?: any
|
||||
component: any
|
||||
}
|
||||
type?: string
|
||||
data?: Data
|
||||
style?: CSSProperties
|
||||
sourceX: number
|
||||
sourceY: number
|
||||
targetX: number
|
||||
targetY: number
|
||||
selected?: boolean
|
||||
sourcePosition: Position
|
||||
targetPosition: Position
|
||||
sourceHandleId?: string
|
||||
targetHandleId?: string
|
||||
source: string
|
||||
target: string
|
||||
sourceHandle?: string
|
||||
targetHandle?: string
|
||||
labelStyle?: CSSProperties
|
||||
labelShowBg?: boolean
|
||||
labelBgStyle?: any
|
||||
labelBgPadding?: [number, number]
|
||||
labelBgBorderRadius?: number
|
||||
animated?: boolean
|
||||
updatable?: boolean
|
||||
markerStart?: string
|
||||
markerEnd?: string
|
||||
curvature?: number
|
||||
}
|
||||
|
||||
/** these props are passed to smooth step edges */
|
||||
export interface SmoothStepEdgeProps<Data = any> extends EdgeProps<Data> {
|
||||
id: string
|
||||
sourceNode: GraphNode
|
||||
targetNode: GraphNode
|
||||
label?:
|
||||
| string
|
||||
| {
|
||||
props?: any
|
||||
component: any
|
||||
}
|
||||
type?: string
|
||||
data?: Data
|
||||
style?: CSSProperties
|
||||
sourceX: number
|
||||
sourceY: number
|
||||
targetX: number
|
||||
targetY: number
|
||||
selected?: boolean
|
||||
sourcePosition: Position
|
||||
targetPosition: Position
|
||||
sourceHandleId?: string
|
||||
targetHandleId?: string
|
||||
source: string
|
||||
target: string
|
||||
sourceHandle?: string
|
||||
targetHandle?: string
|
||||
labelStyle?: any
|
||||
labelShowBg?: boolean
|
||||
labelBgStyle?: any
|
||||
labelBgPadding?: [number, number]
|
||||
labelBgBorderRadius?: number
|
||||
animated?: boolean
|
||||
updatable?: boolean
|
||||
markerStart?: string
|
||||
markerEnd?: string
|
||||
borderRadius?: number
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { CSSProperties } from 'vue'
|
||||
import { GraphEdge, Edge, DefaultEdgeOptions } from './edge'
|
||||
import { GraphNode, CoordinateExtent, Node } from './node'
|
||||
import { ConnectionLineType, ConnectionMode } from './connection'
|
||||
import { KeyCode, PanOnScrollMode, UseZoomPanHelper } from './zoom'
|
||||
import { DefaultEdgeTypes, DefaultNodeTypes, EdgeComponent, NodeComponent } from './components'
|
||||
|
||||
/** an internal element */
|
||||
export type FlowElement<N = any, E = any> = GraphNode<N> | GraphEdge<E>
|
||||
export type FlowElements<N = any, E = any> = FlowElement<N, E>[]
|
||||
|
||||
/** base element props */
|
||||
export interface Element<Data = any> {
|
||||
id: string
|
||||
label?:
|
||||
| string
|
||||
| {
|
||||
props?: any
|
||||
component: any
|
||||
}
|
||||
type?: string
|
||||
data?: Data
|
||||
class?: string
|
||||
style?: CSSProperties
|
||||
hidden?: boolean
|
||||
}
|
||||
export type Elements<N = any, E = any> = (Node<N> | Edge<E>)[]
|
||||
|
||||
/** Transform x, y, z */
|
||||
export type Transform = [number, number, number]
|
||||
|
||||
/** Handle Positions */
|
||||
export enum Position {
|
||||
Left = 'left',
|
||||
Top = 'top',
|
||||
Right = 'right',
|
||||
Bottom = 'bottom',
|
||||
}
|
||||
|
||||
export interface XYPosition {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export type XYZPosition = XYPosition & { z: number }
|
||||
|
||||
export interface Dimensions {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export interface Box extends XYPosition {
|
||||
x2: number
|
||||
y2: number
|
||||
}
|
||||
|
||||
export interface Rect extends Dimensions, XYPosition {}
|
||||
|
||||
export type SnapGrid = [number, number]
|
||||
|
||||
export enum BackgroundVariant {
|
||||
Lines = 'lines',
|
||||
Dots = 'dots',
|
||||
}
|
||||
|
||||
export interface SelectionRect extends Rect {
|
||||
startX: number
|
||||
startY: number
|
||||
draw: boolean
|
||||
}
|
||||
|
||||
export type FlowExportObject<N = any, E = N> = {
|
||||
nodes: GraphNode<N>[]
|
||||
edges: GraphEdge<E>[]
|
||||
position: [number, number]
|
||||
zoom: number
|
||||
}
|
||||
|
||||
export type ToObject<N = any, E = N> = () => FlowExportObject<N, E>
|
||||
|
||||
export type FlowInstance<N = any, E = N> = {
|
||||
getElements: () => FlowElements<N, E>
|
||||
getNodes: () => GraphNode<N>[]
|
||||
getEdges: () => GraphEdge<E>[]
|
||||
toObject: ToObject<N, E>
|
||||
} & UseZoomPanHelper
|
||||
|
||||
export interface FlowProps<N = any, E = N> {
|
||||
id?: string
|
||||
modelValue?: Elements<N, E>
|
||||
nodes?: Node<N>[]
|
||||
edges?: Edge<E>[]
|
||||
/** either use the edgeTypes prop to define your edge-types or use slots (<template #edge-mySpecialType="props">) */
|
||||
edgeTypes?: { [key in keyof DefaultEdgeTypes]?: EdgeComponent } & { [key: string]: EdgeComponent }
|
||||
/** either use the nodeTypes prop to define your node-types or use slots (<template #node-mySpecialType="props">) */
|
||||
nodeTypes?: { [key in keyof DefaultNodeTypes]?: NodeComponent } & { [key: string]: NodeComponent }
|
||||
connectionMode?: ConnectionMode
|
||||
connectionLineType?: ConnectionLineType
|
||||
connectionLineStyle?: CSSProperties | null
|
||||
deleteKeyCode?: KeyCode
|
||||
selectionKeyCode?: KeyCode
|
||||
multiSelectionKeyCode?: KeyCode
|
||||
zoomActivationKeyCode?: KeyCode
|
||||
snapToGrid?: boolean
|
||||
snapGrid?: SnapGrid
|
||||
onlyRenderVisibleElements?: boolean
|
||||
edgesUpdatable?: boolean
|
||||
nodesDraggable?: boolean
|
||||
nodesConnectable?: boolean
|
||||
elementsSelectable?: boolean
|
||||
selectNodesOnDrag?: boolean
|
||||
/** move pane on drag, replaced prop `paneMovable` */
|
||||
panOnDrag?: boolean
|
||||
minZoom?: number
|
||||
maxZoom?: number
|
||||
defaultZoom?: number
|
||||
defaultPosition?: [number, number]
|
||||
translateExtent?: CoordinateExtent
|
||||
nodeExtent?: CoordinateExtent
|
||||
defaultMarkerColor?: string
|
||||
zoomOnScroll?: boolean
|
||||
zoomOnPinch?: boolean
|
||||
panOnScroll?: boolean
|
||||
panOnScrollSpeed?: number
|
||||
panOnScrollMode?: PanOnScrollMode
|
||||
zoomOnDoubleClick?: boolean
|
||||
/** enable this to prevent vue flow from scrolling inside the container, i.e. allow for the page to scroll */
|
||||
preventScrolling?: boolean
|
||||
edgeUpdaterRadius?: number
|
||||
fitViewOnInit?: boolean
|
||||
/** allow connection with click handlers, i.e. support touch devices */
|
||||
connectOnClick?: boolean
|
||||
/** apply default change handlers for position, dimensions, adding/removing nodes. set this to false if you want to apply the changes manually */
|
||||
applyDefault?: boolean
|
||||
noDragClassName?: string
|
||||
noWheelClassName?: string
|
||||
noPanClassName?: string
|
||||
/** does not work for the `addEdge` utility! */
|
||||
defaultEdgeOptions?: DefaultEdgeOptions
|
||||
}
|
||||
|
||||
export type FlowOptions<N = any, E = N> = FlowProps<N, E>
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Dimensions, Position, XYPosition } from './flow'
|
||||
import { Connection } from './connection'
|
||||
|
||||
export type HandleType = 'source' | 'target'
|
||||
|
||||
export interface HandleElement extends XYPosition, Dimensions {
|
||||
id?: string | null
|
||||
position: Position
|
||||
}
|
||||
|
||||
export interface StartHandle {
|
||||
nodeId: string
|
||||
type: HandleType
|
||||
handleId?: string | null
|
||||
}
|
||||
|
||||
/** A valid connection function can determine if an attempted connection is valid or not, i.e. abort creating a new edge */
|
||||
export type ValidConnectionFunc = (connection: Connection) => boolean
|
||||
|
||||
export interface HandleProps {
|
||||
/** Unique id of handle element */
|
||||
id?: string
|
||||
/** Handle type (source / target) {@link HandleType} */
|
||||
type?: HandleType
|
||||
/** Handle position (top, bottom, left, right) {@link Position} */
|
||||
position?: Position
|
||||
/** A valid connection func {@link ValidConnectionFunc} */
|
||||
isValidConnection?: ValidConnectionFunc
|
||||
/** Enable/disable connecting to handle */
|
||||
connectable?: boolean
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { EventHook, EventHookOn } from '@vueuse/core'
|
||||
import { MouseTouchEvent } from '@braks/revue-draggable'
|
||||
import { D3ZoomEvent } from 'd3-zoom'
|
||||
import { FlowInstance } from './flow'
|
||||
import { GraphEdge } from './edge'
|
||||
import { GraphNode } from './node'
|
||||
import { Connection, OnConnectStartParams } from './connection'
|
||||
import { FlowTransform } from './zoom'
|
||||
import { EdgeChange, NodeChange } from './changes'
|
||||
|
||||
export interface FlowEvents<N = any, E = N> {
|
||||
nodesChange: NodeChange[]
|
||||
edgesChange: EdgeChange[]
|
||||
nodeDoubleClick: { event: MouseTouchEvent; node: GraphNode<N> }
|
||||
nodeClick: { event: MouseTouchEvent; node: GraphNode<N> }
|
||||
nodeMouseEnter: { event: MouseEvent; node: GraphNode<N> }
|
||||
nodeMouseMove: { event: MouseEvent; node: GraphNode<N> }
|
||||
nodeMouseLeave: { event: MouseEvent; node: GraphNode<N> }
|
||||
nodeContextMenu: { event: MouseEvent; node: GraphNode<N> }
|
||||
nodeDragStart: { event: MouseTouchEvent; node: GraphNode<N> }
|
||||
nodeDrag: { event: MouseTouchEvent; node: GraphNode<N> }
|
||||
nodeDragStop: { event: MouseTouchEvent; node: GraphNode<N> }
|
||||
connect: Connection
|
||||
connectStart: {
|
||||
event: MouseEvent
|
||||
} & OnConnectStartParams
|
||||
connectStop: MouseEvent
|
||||
connectEnd: MouseEvent
|
||||
paneReady: FlowInstance<N, E>
|
||||
move: { event: D3ZoomEvent<HTMLDivElement, any>; flowTransform: FlowTransform }
|
||||
moveStart: { event: D3ZoomEvent<HTMLDivElement, any>; flowTransform: FlowTransform }
|
||||
moveEnd: { event: D3ZoomEvent<HTMLDivElement, any>; flowTransform: FlowTransform }
|
||||
selectionDragStart: { event: MouseTouchEvent; nodes: GraphNode<N>[] }
|
||||
selectionDrag: { event: MouseTouchEvent; nodes: GraphNode<N>[] }
|
||||
selectionDragStop: { event: MouseTouchEvent; nodes: GraphNode<N>[] }
|
||||
selectionContextMenu: { event: MouseEvent; nodes: GraphNode<N>[] }
|
||||
paneScroll: WheelEvent | undefined
|
||||
paneClick: MouseEvent
|
||||
paneContextMenu: MouseEvent
|
||||
edgeContextMenu: { event: MouseEvent; edge: GraphEdge<E> }
|
||||
edgeMouseEnter: { event: MouseEvent; edge: GraphEdge<E> }
|
||||
edgeMouseMove: { event: MouseEvent; edge: GraphEdge<E> }
|
||||
edgeMouseLeave: { event: MouseEvent; edge: GraphEdge<E> }
|
||||
edgeDoubleClick: { event: MouseEvent; edge: GraphEdge<E> }
|
||||
edgeClick: { event: MouseEvent; edge: GraphEdge<E> }
|
||||
edgeUpdateStart: { event: MouseEvent; edge: GraphEdge<E> }
|
||||
edgeUpdate: { edge: GraphEdge<E>; connection: Connection }
|
||||
edgeUpdateEnd: { event: MouseEvent; edge: GraphEdge<E> }
|
||||
}
|
||||
|
||||
export type FlowEvent = FlowEvents[keyof FlowEvents]
|
||||
export type FlowHook<T extends FlowEvent = any> = EventHook<T>
|
||||
export type FlowHookOn<T extends FlowEvent = any> = EventHookOn<T>
|
||||
|
||||
export type FlowHooks<N = any, E = N> = { [key in keyof FlowEvents<N, E>]: FlowHook<FlowEvents<N, E>[key]> }
|
||||
export type FlowHooksOn<N = any, E = N> = {
|
||||
[key in keyof FlowEvents<N, E> as `on${Capitalize<key>}`]: FlowHookOn<FlowEvents<N, E>[key]>
|
||||
}
|
||||
|
||||
export type EmitFunc<T extends FlowHooks = FlowHooks> = (name: keyof T, ...args: any[]) => void
|
||||
@@ -0,0 +1,10 @@
|
||||
export * from './flow'
|
||||
export * from './connection'
|
||||
export * from './edge'
|
||||
export * from './components'
|
||||
export * from './node'
|
||||
export * from './zoom'
|
||||
export * from './store'
|
||||
export * from './hooks'
|
||||
export * from './changes'
|
||||
export * from './handle'
|
||||
@@ -0,0 +1,81 @@
|
||||
import { XYPosition, Position, SnapGrid, Element, XYZPosition, Dimensions } from './flow'
|
||||
import { DefaultNodeTypes } from './components'
|
||||
import { HandleElement, ValidConnectionFunc } from './handle'
|
||||
|
||||
export type CoordinateExtent = [[number, number], [number, number]]
|
||||
|
||||
export type NodeHandleBounds = {
|
||||
source?: HandleElement[]
|
||||
target?: HandleElement[]
|
||||
}
|
||||
|
||||
export interface Node<T = any> extends Element<T> {
|
||||
/** node position x, y */
|
||||
position: XYPosition
|
||||
/** node type, can be a default type or a custom type */
|
||||
type?: keyof DefaultNodeTypes | string
|
||||
/** handle position */
|
||||
targetPosition?: Position
|
||||
/** handle position */
|
||||
sourcePosition?: Position
|
||||
draggable?: boolean
|
||||
selectable?: boolean
|
||||
connectable?: boolean
|
||||
dragHandle?: string
|
||||
/** move on grid */
|
||||
snapGrid?: SnapGrid
|
||||
/** called when used as target for new connection */
|
||||
isValidTargetPos?: ValidConnectionFunc
|
||||
/** called when used as source for new connection */
|
||||
isValidSourcePos?: ValidConnectionFunc
|
||||
/** define node extent, i.e. area in which node can be moved */
|
||||
extent?: 'parent' | CoordinateExtent
|
||||
/** expands parent area to fit child node */
|
||||
expandParent?: boolean
|
||||
/** define node as a child node by setting a parent node id */
|
||||
parentNode?: string
|
||||
}
|
||||
|
||||
export interface GraphNode<T = any> extends Node<T> {
|
||||
/** absolute position in relation to parent elements + z-index */
|
||||
computedPosition: XYZPosition
|
||||
handleBounds: NodeHandleBounds
|
||||
/** node width, height */
|
||||
dimensions: Dimensions
|
||||
isParent: boolean
|
||||
selected: boolean
|
||||
dragging: boolean
|
||||
}
|
||||
|
||||
/** these props are passed to node components */
|
||||
export interface NodeProps<T = any> {
|
||||
id: string
|
||||
/** node DOM-element */
|
||||
nodeElement: HTMLDivElement
|
||||
type: string
|
||||
data: T
|
||||
selected: boolean
|
||||
connectable: boolean
|
||||
/** absolute position in relation to parent elements + z-index */
|
||||
computedPosition: XYZPosition
|
||||
/** x, y position */
|
||||
position: XYPosition
|
||||
/** node dimensions (width, height) */
|
||||
dimensions: Dimensions
|
||||
label?:
|
||||
| string
|
||||
| {
|
||||
props?: any
|
||||
component: any
|
||||
}
|
||||
/** called when used as target for new connection */
|
||||
isValidTargetPos?: ValidConnectionFunc
|
||||
/** called when used as source for new connection */
|
||||
isValidSourcePos?: ValidConnectionFunc
|
||||
parentNode?: string
|
||||
dragging: boolean
|
||||
zIndex: number
|
||||
targetPosition?: Position
|
||||
sourcePosition?: Position
|
||||
dragHandle?: string
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { ComputedRef, CSSProperties, ToRefs } from 'vue'
|
||||
import { Dimensions, Elements, FlowElements, FlowInstance, FlowOptions, Rect, SnapGrid, Transform, XYPosition } from './flow'
|
||||
import { EdgeComponent, NodeComponent, DefaultNodeTypes, DefaultEdgeTypes } from './components'
|
||||
import { Connection, ConnectionLineType, ConnectionMode } from './connection'
|
||||
import { DefaultEdgeOptions, Edge, GraphEdge } from './edge'
|
||||
import { GraphNode, CoordinateExtent, Node } from './node'
|
||||
import { D3Selection, D3Zoom, D3ZoomHandler, KeyCode, PanOnScrollMode } from './zoom'
|
||||
import { FlowHooks, FlowHooksOn } from './hooks'
|
||||
import { NodeChange, EdgeChange } from './changes'
|
||||
import { StartHandle, HandleType } from './handle'
|
||||
|
||||
export interface State<N = any, E = N> extends Omit<FlowOptions<N, E>, 'id' | 'modelValue'> {
|
||||
/** Event hooks, you can manipulate the triggers on your own peril */
|
||||
hooks: FlowHooks<N, E>
|
||||
instance: FlowInstance<N, E> | null
|
||||
|
||||
/** all stored nodes */
|
||||
nodes: GraphNode<N>[]
|
||||
/** all stored edges */
|
||||
edges: GraphEdge<E>[]
|
||||
|
||||
d3Zoom: D3Zoom | null
|
||||
d3Selection: D3Selection | null
|
||||
d3ZoomHandler: D3ZoomHandler | null
|
||||
minZoom: number
|
||||
maxZoom: number
|
||||
defaultZoom: number
|
||||
translateExtent: CoordinateExtent
|
||||
nodeExtent: CoordinateExtent
|
||||
/** zoom pane dimensions */
|
||||
dimensions: Dimensions
|
||||
/** transform x, y, z */
|
||||
transform: Transform
|
||||
onlyRenderVisibleElements: boolean
|
||||
defaultPosition: [number, number]
|
||||
|
||||
selectedNodesBbox: Rect
|
||||
nodesSelectionActive: boolean
|
||||
userSelectionActive: boolean
|
||||
multiSelectionActive: boolean
|
||||
deleteKeyCode: KeyCode
|
||||
selectionKeyCode: KeyCode
|
||||
multiSelectionKeyCode: KeyCode
|
||||
zoomActivationKeyCode: KeyCode
|
||||
|
||||
connectionNodeId: string | null
|
||||
connectionHandleId: string | null
|
||||
connectionHandleType: HandleType | null
|
||||
connectionPosition: XYPosition
|
||||
connectionMode: ConnectionMode
|
||||
connectionLineType: ConnectionLineType
|
||||
connectionLineStyle: CSSProperties | null
|
||||
connectionStartHandle: StartHandle | null
|
||||
connectOnClick: boolean
|
||||
edgeUpdaterRadius: number
|
||||
|
||||
snapToGrid: boolean
|
||||
snapGrid: SnapGrid
|
||||
defaultMarkerColor: string
|
||||
|
||||
edgesUpdatable: boolean
|
||||
nodesDraggable: boolean
|
||||
nodesConnectable: boolean
|
||||
elementsSelectable: boolean
|
||||
selectNodesOnDrag: boolean
|
||||
panOnDrag: boolean
|
||||
zoomOnScroll: boolean
|
||||
zoomOnPinch: boolean
|
||||
panOnScroll: boolean
|
||||
panOnScrollSpeed: number
|
||||
panOnScrollMode: PanOnScrollMode
|
||||
zoomOnDoubleClick: boolean
|
||||
preventScrolling: boolean
|
||||
|
||||
paneReady: boolean
|
||||
initialized: boolean
|
||||
applyDefault: boolean
|
||||
|
||||
fitViewOnInit?: boolean
|
||||
noDragClassName?: string
|
||||
noWheelClassName?: string
|
||||
noPanClassName?: string
|
||||
defaultEdgeOptions?: DefaultEdgeOptions
|
||||
|
||||
vueFlowVersion: string
|
||||
}
|
||||
|
||||
export interface Actions<N = any, E = N> {
|
||||
/** @deprecated use setNodes / setEdges instead */
|
||||
setElements: (elements: Elements<N, E>, extent?: CoordinateExtent) => void
|
||||
/** parses nodes and re-sets the state */
|
||||
setNodes: (nodes: Node<N>[], extent?: CoordinateExtent) => void
|
||||
/** parses edges and re-sets the state */
|
||||
setEdges: (edges: Edge<E>[]) => void
|
||||
/** parses nodes and adds to state */
|
||||
addNodes: <NA = N>(nodes: Node<NA>[], extent?: CoordinateExtent) => void
|
||||
/** parses edges and adds to state */
|
||||
addEdges: <EA = E>(edgesOrConnections: (Edge<EA> | Connection)[]) => void
|
||||
updateEdge: <EU = E>(oldEdge: GraphEdge<EU>, newConnection: Connection) => boolean
|
||||
applyEdgeChanges: <ED = E>(changes: EdgeChange[]) => GraphEdge<ED>[]
|
||||
applyNodeChanges: <ND = N>(changes: NodeChange[]) => GraphNode<ND>[]
|
||||
addSelectedElements: (elements: FlowElements<N, E>) => void
|
||||
addSelectedEdges: (edges: GraphEdge<E>[]) => void
|
||||
addSelectedNodes: (nodes: GraphNode<N>[]) => void
|
||||
setMinZoom: (zoom: number) => void
|
||||
setMaxZoom: (zoom: number) => void
|
||||
setTranslateExtent: (translateExtent: CoordinateExtent) => void
|
||||
resetSelectedElements: () => void
|
||||
setInteractive: (isInteractive: boolean) => void
|
||||
setState: (state: Partial<FlowOptions<N, E> & Omit<State, 'nodes' | 'edges' | 'modelValue'>>) => void
|
||||
updateNodePosition: ({ id, diff, dragging }: { id?: string; diff?: XYPosition; dragging?: boolean }) => void
|
||||
updateNodeDimensions: (
|
||||
updates: {
|
||||
id: string
|
||||
nodeElement: HTMLDivElement
|
||||
forceUpdate?: boolean
|
||||
}[],
|
||||
) => void
|
||||
$reset: () => void
|
||||
}
|
||||
|
||||
export interface Getters<N = any, E = N> {
|
||||
getEdgeTypes: Record<keyof DefaultEdgeTypes | string, EdgeComponent>
|
||||
getNodeTypes: Record<keyof DefaultNodeTypes | string, NodeComponent>
|
||||
/** filters hidden nodes */
|
||||
getNodes: GraphNode<N>[]
|
||||
/** filters hidden edges */
|
||||
getEdges: GraphEdge<E>[]
|
||||
getNode: (id: string) => GraphNode<N> | undefined
|
||||
getEdge: (id: string) => GraphEdge<E> | undefined
|
||||
getSelectedElements: FlowElements<N, E>
|
||||
getSelectedNodes: GraphNode<N>[]
|
||||
getSelectedEdges: GraphEdge<E>[]
|
||||
}
|
||||
|
||||
export type ComputedGetters<N = any, E = N> = { [key in keyof Getters<N, E>]: ComputedRef<Getters<N, E>[key]> }
|
||||
|
||||
interface StoreBase<N = any, E = N> {
|
||||
state: State<N, E>
|
||||
actions: Actions<N, E>
|
||||
getters: ComputedGetters<N, E>
|
||||
hooksOn: FlowHooksOn<N, E>
|
||||
}
|
||||
|
||||
export type Store<N = any, E = N> = StoreBase<N, E> & ToRefs<State<N, E>> & Actions<N, E> & ComputedGetters<N, E>
|
||||
export type FlowStore<N = any, E = N> = StoreBase<N, E> & State<N, E> & Actions<N, E> & Getters<N, E>
|
||||
|
||||
export type UseVueFlow<N = any, E = N> = {
|
||||
id: string
|
||||
store: FlowStore<N, E>
|
||||
} & FlowHooksOn<N, E> &
|
||||
ToRefs<State<N, E>> &
|
||||
ComputedGetters<N, E> &
|
||||
Actions<N, E>
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Selection, ZoomBehavior } from 'd3'
|
||||
import { Rect, XYPosition } from './flow'
|
||||
|
||||
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 enum PanOnScrollMode {
|
||||
Free = 'free',
|
||||
Vertical = 'vertical',
|
||||
Horizontal = 'horizontal',
|
||||
}
|
||||
|
||||
export type UseZoomPanHelperOptions = {
|
||||
duration?: number
|
||||
}
|
||||
|
||||
export type FitViewParams = {
|
||||
padding?: number
|
||||
includeHiddenNodes?: boolean
|
||||
minZoom?: number
|
||||
maxZoom?: number
|
||||
offset?: {
|
||||
x?: number
|
||||
y?: number
|
||||
}
|
||||
nodes?: string[]
|
||||
} & UseZoomPanHelperOptions
|
||||
|
||||
export type FlowTransform = {
|
||||
x: number
|
||||
y: number
|
||||
zoom: number
|
||||
}
|
||||
|
||||
export type SetCenterOptions = UseZoomPanHelperOptions & {
|
||||
zoom?: number
|
||||
}
|
||||
|
||||
export type FitBoundsOptions = UseZoomPanHelperOptions & {
|
||||
padding?: number
|
||||
}
|
||||
|
||||
export type FitView = (fitViewOptions?: FitViewParams) => void
|
||||
export type Project = (position: XYPosition) => XYPosition
|
||||
export type SetCenter = (x: number, y: number, options?: SetCenterOptions) => void
|
||||
export type FitBounds = (bounds: Rect, options?: FitBoundsOptions) => void
|
||||
export type ZoomInOut = (options?: UseZoomPanHelperOptions) => void
|
||||
export type ZoomTo = (zoomLevel: number, options?: UseZoomPanHelperOptions) => void
|
||||
export type GetTransform = () => FlowTransform
|
||||
export type SetTransform = (transform: FlowTransform, options?: UseZoomPanHelperOptions) => void
|
||||
|
||||
export interface UseZoomPanHelper {
|
||||
zoomIn: ZoomInOut
|
||||
zoomOut: ZoomInOut
|
||||
zoomTo: ZoomTo
|
||||
setTransform: SetTransform
|
||||
getTransform: GetTransform
|
||||
fitView: FitView
|
||||
setCenter: SetCenter
|
||||
fitBounds: FitBounds
|
||||
project: Project
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import { clampPosition, isGraphNode } from './graph'
|
||||
import {
|
||||
EdgeChange,
|
||||
EdgeSelectionChange,
|
||||
ElementChange,
|
||||
FlowElement,
|
||||
FlowElements,
|
||||
GraphNode,
|
||||
NodeChange,
|
||||
NodeSelectionChange,
|
||||
NodePositionChange,
|
||||
Getters,
|
||||
CoordinateExtent,
|
||||
XYPosition,
|
||||
} from '~/types'
|
||||
|
||||
type CreatePositionChangeParams = {
|
||||
node: GraphNode
|
||||
nodeExtent: CoordinateExtent
|
||||
diff?: XYPosition
|
||||
dragging?: boolean
|
||||
}
|
||||
|
||||
function handleParentExpand(updateItem: GraphNode, getNode: Getters['getNode']) {
|
||||
const parent = updateItem.parentNode ? getNode(updateItem.parentNode) : undefined
|
||||
if (parent) {
|
||||
const extendWidth = updateItem.position.x + updateItem.dimensions.width - parent.dimensions.width
|
||||
const extendHeight = updateItem.position.y + updateItem.dimensions.height - parent.dimensions.height
|
||||
|
||||
if (extendWidth > 0 || extendHeight > 0 || updateItem.position.x < 0 || updateItem.position.y < 0) {
|
||||
parent.style = { ...parent.style } || {}
|
||||
|
||||
if (extendWidth > 0) {
|
||||
if (!parent.style.width) {
|
||||
parent.style.width = parent.dimensions.width
|
||||
}
|
||||
if (typeof parent.style.width === 'string') {
|
||||
const currWidth = parseInt(parent.style.width, 10)
|
||||
parent.style.width = `${currWidth + extendWidth}px`
|
||||
} else {
|
||||
parent.style.width += extendWidth
|
||||
}
|
||||
}
|
||||
|
||||
if (extendHeight > 0) {
|
||||
if (!parent.style.height) {
|
||||
parent.style.height = parent.dimensions.height
|
||||
}
|
||||
if (typeof parent.style.height === 'string') {
|
||||
const currWidth = parseInt(parent.style.height, 10)
|
||||
parent.style.height = `${currWidth + extendHeight}px`
|
||||
} else {
|
||||
parent.style.height += extendHeight
|
||||
}
|
||||
}
|
||||
|
||||
if (updateItem.position.x < 0) {
|
||||
const xDiff = Math.abs(updateItem.position.x)
|
||||
parent.position.x = parent.position.x - xDiff
|
||||
if (typeof parent.style.width === 'string') {
|
||||
const currWidth = parseInt(parent.style.width, 10)
|
||||
parent.style.width = `${currWidth + xDiff}px`
|
||||
} else {
|
||||
;(parent.style as any).width += xDiff
|
||||
}
|
||||
updateItem.position.x = 0
|
||||
}
|
||||
|
||||
if (updateItem.position.y < 0) {
|
||||
const yDiff = Math.abs(updateItem.position.y)
|
||||
parent.position.y = parent.position.y - yDiff
|
||||
if (typeof parent.style.height === 'string') {
|
||||
const currWidth = parseInt(parent.style.height, 10)
|
||||
parent.style.height = `${currWidth + yDiff}px`
|
||||
} else {
|
||||
;(parent.style as any).height += yDiff
|
||||
}
|
||||
updateItem.position.y = 0
|
||||
}
|
||||
|
||||
parent.dimensions.width = (
|
||||
typeof parent.style.width === 'string' ? parseInt((<string>parent.style.width)!, 10) : parent.style.width
|
||||
)!
|
||||
parent.dimensions.height = (
|
||||
typeof parent.style.height === 'string' ? parseInt((<string>parent.style.height)!, 10) : parent.style.height
|
||||
)!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const applyChanges = <
|
||||
T extends FlowElement = GraphNode,
|
||||
C extends ElementChange = T extends GraphNode ? NodeChange : EdgeChange,
|
||||
>(
|
||||
changes: C[],
|
||||
elements: T[],
|
||||
getNode: Getters['getNode'],
|
||||
): T[] => {
|
||||
let elementIds = elements.map((el) => el.id)
|
||||
changes.forEach((change) => {
|
||||
if (change.type === 'add') return elements.push(change.item as any)
|
||||
const i = elementIds.indexOf((<any>change).id)
|
||||
const el = elements[i]
|
||||
switch (change.type) {
|
||||
case 'select':
|
||||
el.selected = change.selected
|
||||
break
|
||||
case 'position':
|
||||
if (isGraphNode(el)) {
|
||||
if (typeof change.position !== 'undefined') el.position = change.position
|
||||
if (typeof change.dragging !== 'undefined') el.dragging = change.dragging
|
||||
if (el.expandParent && el.parentNode) handleParentExpand(el, getNode)
|
||||
}
|
||||
break
|
||||
case 'dimensions':
|
||||
if (isGraphNode(el)) {
|
||||
if (typeof change.dimensions !== 'undefined') el.dimensions = change.dimensions
|
||||
if (el.expandParent && el.parentNode) handleParentExpand(el, getNode)
|
||||
}
|
||||
break
|
||||
case 'remove':
|
||||
elements.splice(i, 1)
|
||||
elementIds = elements.map((el) => el.id)
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
return elements
|
||||
}
|
||||
|
||||
export const createSelectionChange = (id: string, selected: boolean): NodeSelectionChange | EdgeSelectionChange => ({
|
||||
id,
|
||||
type: 'select',
|
||||
selected,
|
||||
})
|
||||
|
||||
export const createPositionChange = (
|
||||
{ node, diff, dragging, nodeExtent }: CreatePositionChangeParams,
|
||||
getNode: Getters['getNode'],
|
||||
): NodePositionChange => {
|
||||
const parent = node.parentNode ? getNode(node.parentNode) : undefined
|
||||
const change: NodePositionChange = {
|
||||
id: node.id,
|
||||
type: 'position',
|
||||
dragging: !!dragging,
|
||||
}
|
||||
|
||||
if (diff) {
|
||||
const nextPosition = { x: node.position.x + diff.x, y: node.position.y + diff.y }
|
||||
let currentExtent = nodeExtent || node.extent
|
||||
|
||||
if (node.extent === 'parent' && parent && node.dimensions.width && node.dimensions.height) {
|
||||
currentExtent =
|
||||
parent.dimensions.width && parent.dimensions.height
|
||||
? [
|
||||
[0, 0],
|
||||
[parent.dimensions.width - node.dimensions.width, parent.dimensions.height - node.dimensions.height],
|
||||
]
|
||||
: currentExtent
|
||||
}
|
||||
|
||||
change.position = currentExtent ? clampPosition(nextPosition, currentExtent) : nextPosition
|
||||
}
|
||||
|
||||
return change
|
||||
}
|
||||
|
||||
const isParentSelected = (node: GraphNode, selectedIds: string[], getNode: Getters['getNode']): boolean => {
|
||||
const parent = node.parentNode ? getNode(node.parentNode) : undefined
|
||||
if (!node.parentNode || !parent) return false
|
||||
if (selectedIds.includes(node.parentNode)) return true
|
||||
return isParentSelected(parent, selectedIds, getNode)
|
||||
}
|
||||
|
||||
export const getSelectionChanges = (items: FlowElements, selectedIds: string[], getNode: Getters['getNode']) => {
|
||||
return items.reduce((res, item) => {
|
||||
const willBeSelected =
|
||||
selectedIds.includes(item.id) || !!(isGraphNode(item) && item.parentNode && isParentSelected(item, selectedIds, getNode))
|
||||
|
||||
if (!item.selected && willBeSelected) {
|
||||
res.push(createSelectionChange(item.id, true))
|
||||
} else if (item.selected && !willBeSelected) {
|
||||
res.push(createSelectionChange(item.id, false))
|
||||
}
|
||||
|
||||
return res
|
||||
}, [] as (NodeSelectionChange | EdgeSelectionChange)[])
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import { rectToBox } from './graph'
|
||||
import { EdgePositions, GraphEdge, GraphNode, HandleElement, Position, Rect, Transform, XYPosition } from '~/types'
|
||||
|
||||
export const getHandlePosition = (position: Position, rect: Rect, handle?: HandleElement): XYPosition => {
|
||||
const x = (handle?.x ?? 0) + rect.x
|
||||
const y = (handle?.y ?? 0) + rect.y
|
||||
const width = handle?.width ?? rect.width
|
||||
const height = handle?.height ?? rect.height
|
||||
|
||||
switch (position) {
|
||||
case Position.Top:
|
||||
return {
|
||||
x: x + width / 2,
|
||||
y,
|
||||
}
|
||||
case Position.Right:
|
||||
return {
|
||||
x: x + width,
|
||||
y: y + height / 2,
|
||||
}
|
||||
case Position.Bottom:
|
||||
return {
|
||||
x: x + width / 2,
|
||||
y: y + height,
|
||||
}
|
||||
case Position.Left:
|
||||
return {
|
||||
x,
|
||||
y: y + height / 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const getHandle = (bounds: HandleElement[] = [], handleId?: string | null): HandleElement | undefined => {
|
||||
if (!bounds.length) return undefined
|
||||
|
||||
// there is no handleId when there are no multiple handles/ handles with ids
|
||||
// so we just pick the first one
|
||||
let handle
|
||||
if (bounds.length === 1 ?? !handleId) handle = bounds[0]
|
||||
else if (handleId) handle = bounds.find((d) => d.id === handleId)
|
||||
|
||||
return handle
|
||||
}
|
||||
|
||||
export const getEdgePositions = (
|
||||
sourceNode: GraphNode,
|
||||
sourceHandle: HandleElement | undefined,
|
||||
sourcePosition: Position,
|
||||
targetNode: GraphNode,
|
||||
targetHandle: HandleElement | undefined,
|
||||
targetPosition: Position,
|
||||
): EdgePositions => {
|
||||
const sourceHandlePos = getHandlePosition(
|
||||
sourcePosition,
|
||||
{
|
||||
...sourceNode.dimensions,
|
||||
...sourceNode.computedPosition,
|
||||
},
|
||||
sourceHandle,
|
||||
)
|
||||
const targetHandlePos = getHandlePosition(
|
||||
targetPosition,
|
||||
{
|
||||
...targetNode.dimensions,
|
||||
...targetNode.computedPosition,
|
||||
},
|
||||
targetHandle,
|
||||
)
|
||||
|
||||
return {
|
||||
sourceX: sourceHandlePos.x,
|
||||
sourceY: sourceHandlePos.y,
|
||||
targetX: targetHandlePos.x,
|
||||
targetY: targetHandlePos.y,
|
||||
}
|
||||
}
|
||||
|
||||
interface IsEdgeVisibleParams {
|
||||
sourcePos: XYPosition
|
||||
targetPos: XYPosition
|
||||
sourceWidth: number
|
||||
sourceHeight: number
|
||||
targetWidth: number
|
||||
targetHeight: number
|
||||
width: number
|
||||
height: number
|
||||
transform: Transform
|
||||
}
|
||||
|
||||
export function isEdgeVisible({
|
||||
sourcePos,
|
||||
targetPos,
|
||||
sourceWidth,
|
||||
sourceHeight,
|
||||
targetWidth,
|
||||
targetHeight,
|
||||
width,
|
||||
height,
|
||||
transform,
|
||||
}: IsEdgeVisibleParams): boolean {
|
||||
const edgeBox = {
|
||||
x: Math.min(sourcePos.x, targetPos.x),
|
||||
y: Math.min(sourcePos.y, targetPos.y),
|
||||
x2: Math.max(sourcePos.x + sourceWidth, targetPos.x + targetWidth),
|
||||
y2: Math.max(sourcePos.y + sourceHeight, targetPos.y + targetHeight),
|
||||
}
|
||||
|
||||
if (edgeBox.x === edgeBox.x2) {
|
||||
edgeBox.x2 += 1
|
||||
}
|
||||
|
||||
if (edgeBox.y === edgeBox.y2) {
|
||||
edgeBox.y2 += 1
|
||||
}
|
||||
|
||||
const viewBox = rectToBox({
|
||||
x: (0 - transform[0]) / transform[2],
|
||||
y: (0 - transform[1]) / transform[2],
|
||||
width: width / transform[2],
|
||||
height: height / transform[2],
|
||||
})
|
||||
|
||||
const xOverlap = Math.max(0, Math.min(viewBox.x2, edgeBox.x2) - Math.max(viewBox.x, edgeBox.x))
|
||||
const yOverlap = Math.max(0, Math.min(viewBox.y2, edgeBox.y2) - Math.max(viewBox.y, edgeBox.y))
|
||||
const overlappingArea = Math.ceil(xOverlap * yOverlap)
|
||||
|
||||
return overlappingArea > 0
|
||||
}
|
||||
|
||||
export const groupEdgesByZLevel = (edges: GraphEdge[], nodes: GraphNode[]) => {
|
||||
let maxLevel = -1
|
||||
const nodeIds = nodes.map((n) => n.id)
|
||||
|
||||
const levelLookup = edges.reduce<Record<string, GraphEdge[]>>((tree, edge) => {
|
||||
const z = edge.z
|
||||
? edge.z
|
||||
: Math.max(
|
||||
nodes[nodeIds.indexOf(edge.source)]?.computedPosition.z || 0,
|
||||
nodes[nodeIds.indexOf(edge.target)]?.computedPosition.z || 0,
|
||||
)
|
||||
if (tree[z]) {
|
||||
tree[z].push(edge)
|
||||
} else {
|
||||
tree[z] = [edge]
|
||||
}
|
||||
|
||||
maxLevel = z > maxLevel ? z : maxLevel
|
||||
|
||||
return tree
|
||||
}, {})
|
||||
|
||||
return Object.entries(Object.keys(levelLookup).length ? levelLookup : { 0: [] }).map(([key, edges]) => {
|
||||
const level = +key
|
||||
|
||||
return {
|
||||
edges,
|
||||
level,
|
||||
isMaxLevel: level === maxLevel,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
import {
|
||||
Box,
|
||||
Connection,
|
||||
CoordinateExtent,
|
||||
Dimensions,
|
||||
Edge,
|
||||
EdgeMarkerType,
|
||||
Elements,
|
||||
FlowElements,
|
||||
FlowExportObject,
|
||||
State,
|
||||
FlowStore,
|
||||
GraphEdge,
|
||||
GraphNode,
|
||||
Node,
|
||||
Rect,
|
||||
Transform,
|
||||
XYPosition,
|
||||
XYZPosition,
|
||||
Getters,
|
||||
} from '~/types'
|
||||
import { useWindow } from '~/composables'
|
||||
|
||||
const isHTMLElement = (el: EventTarget): el is HTMLElement => ('nodeName' || 'hasAttribute') in el
|
||||
|
||||
export const isInputDOMNode = (e: KeyboardEvent | MouseEvent): boolean => {
|
||||
const target = e.target
|
||||
if (target && isHTMLElement(target)) {
|
||||
return ['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].includes(target.nodeName) || target.hasAttribute('contentEditable')
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export const getDimensions = (node: HTMLElement): Dimensions => ({
|
||||
width: node.offsetWidth,
|
||||
height: node.offsetHeight,
|
||||
})
|
||||
|
||||
export const clamp = (val: number, min = 0, max = 1): number => Math.min(Math.max(val, min), max)
|
||||
|
||||
export const clampPosition = (position: XYPosition, extent: CoordinateExtent): XYPosition => ({
|
||||
x: clamp(position.x, extent[0][0], extent[1][0]),
|
||||
y: clamp(position.y, extent[0][1], extent[1][1]),
|
||||
})
|
||||
|
||||
export const getHostForElement = (element: HTMLElement): Document => {
|
||||
const doc = element.getRootNode() as Document
|
||||
const window = useWindow()
|
||||
|
||||
if ('getElementFromPoint' in doc) return doc
|
||||
else return window.document
|
||||
}
|
||||
|
||||
export const isEdge = (element: Node | Edge | Connection): element is Edge =>
|
||||
'id' in element && 'source' in element && 'target' in element
|
||||
export const isGraphEdge = (element: any): element is GraphEdge =>
|
||||
isEdge(element) && 'sourceNode' in element && 'targetNode' in element
|
||||
|
||||
export const isNode = (element: Node | Edge | Connection): element is Node => 'id' in element && !isEdge(element)
|
||||
export const isGraphNode = (element: Node | Edge | Connection): element is GraphNode =>
|
||||
isNode(element) && 'computedPosition' in element
|
||||
|
||||
export const parseNode = (node: Node, nodeExtent: CoordinateExtent, defaults?: Partial<GraphNode>): GraphNode => {
|
||||
defaults = !isGraphNode(node)
|
||||
? {
|
||||
type: node.type ?? 'default',
|
||||
dimensions: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
handleBounds: {
|
||||
source: [],
|
||||
target: [],
|
||||
},
|
||||
computedPosition: {
|
||||
z: typeof node.style?.zIndex === 'string' ? parseInt(node.style?.zIndex as string) : node.style?.zIndex ?? 0,
|
||||
...clampPosition(node.position, nodeExtent),
|
||||
},
|
||||
dragging: false,
|
||||
draggable: undefined,
|
||||
selectable: undefined,
|
||||
connectable: undefined,
|
||||
...defaults,
|
||||
}
|
||||
: defaults
|
||||
|
||||
return {
|
||||
...(defaults as GraphNode),
|
||||
...node,
|
||||
id: node.id.toString(),
|
||||
}
|
||||
}
|
||||
|
||||
export const parseEdge = (edge: Edge, defaults?: Partial<GraphEdge>): GraphEdge => {
|
||||
defaults = !isGraphEdge(edge)
|
||||
? {
|
||||
sourceHandle: edge.sourceHandle ? edge.sourceHandle.toString() : undefined,
|
||||
targetHandle: edge.targetHandle ? edge.targetHandle.toString() : undefined,
|
||||
type: edge.type ?? 'default',
|
||||
source: edge.source.toString(),
|
||||
target: edge.target.toString(),
|
||||
z: typeof edge.style?.zIndex === 'string' ? parseInt(edge.style?.zIndex as string) : edge.style?.zIndex ?? 0,
|
||||
sourceX: 0,
|
||||
sourceY: 0,
|
||||
targetX: 0,
|
||||
targetY: 0,
|
||||
updatable: undefined,
|
||||
selectable: undefined,
|
||||
...defaults,
|
||||
}
|
||||
: defaults
|
||||
|
||||
return {
|
||||
...(defaults as GraphEdge),
|
||||
...edge,
|
||||
id: edge.id.toString(),
|
||||
}
|
||||
}
|
||||
|
||||
const getConnectedElements = (node: GraphNode, elements: Elements, dir: 'source' | 'target') => {
|
||||
if (!isNode(node)) return []
|
||||
const ids = elements.filter((e) => isEdge(e) && e.source === node.id).map((e) => isEdge(e) && e[dir])
|
||||
return elements.filter((e) => ids.includes(e.id))
|
||||
}
|
||||
export const getOutgoers = (node: GraphNode, elements: Elements) => getConnectedElements(node, elements, 'target')
|
||||
|
||||
export const getIncomers = (node: GraphNode, elements: Elements) => getConnectedElements(node, elements, 'source')
|
||||
|
||||
export const getEdgeId = ({ source, sourceHandle, target, targetHandle }: Connection) =>
|
||||
`vueflow__edge-${source}${sourceHandle ?? ''}-${target}${targetHandle ?? ''}`
|
||||
|
||||
export const connectionExists = (edge: Edge, elements: Elements) =>
|
||||
elements.some(
|
||||
(el) =>
|
||||
isEdge(el) &&
|
||||
el.source === edge.source &&
|
||||
el.target === edge.target &&
|
||||
(el.sourceHandle === edge.sourceHandle || (!el.sourceHandle && !edge.sourceHandle)) &&
|
||||
(el.targetHandle === edge.targetHandle || (!el.targetHandle && !edge.targetHandle)),
|
||||
)
|
||||
|
||||
export const addEdge = (edgeParams: Edge | Connection, elements: Elements) => {
|
||||
if (!edgeParams.source || !edgeParams.target) {
|
||||
console.warn("Can't create edge. An edge needs a source and a target.")
|
||||
return elements
|
||||
}
|
||||
|
||||
let edge
|
||||
if (isEdge(edgeParams)) {
|
||||
edge = { ...edgeParams }
|
||||
} else {
|
||||
edge = {
|
||||
...edgeParams,
|
||||
id: getEdgeId(edgeParams),
|
||||
} as Edge
|
||||
}
|
||||
edge = parseEdge(edge)
|
||||
if (connectionExists(edge, elements)) return elements
|
||||
elements.push(edge)
|
||||
return [...elements, edge]
|
||||
}
|
||||
|
||||
export const updateEdge = (oldEdge: Edge, newConnection: Connection, elements: Elements) => {
|
||||
if (!newConnection.source || !newConnection.target) {
|
||||
console.warn("Can't create new edge. An edge needs a source and a target.")
|
||||
return elements
|
||||
}
|
||||
|
||||
const foundEdge = elements.find((e) => isEdge(e) && e.id === oldEdge.id)
|
||||
|
||||
if (!foundEdge) {
|
||||
console.warn(`The old edge with id=${oldEdge.id} does not exist.`)
|
||||
return elements
|
||||
}
|
||||
|
||||
// Remove old edge and create the new edge with parameters of old edge.
|
||||
const edge: Edge = {
|
||||
...oldEdge,
|
||||
id: getEdgeId(newConnection),
|
||||
source: newConnection.source,
|
||||
target: newConnection.target,
|
||||
sourceHandle: newConnection.sourceHandle,
|
||||
targetHandle: newConnection.targetHandle,
|
||||
}
|
||||
elements.splice(elements.indexOf(foundEdge), 1, edge)
|
||||
return elements.filter((e) => e.id !== oldEdge.id).concat(edge)
|
||||
}
|
||||
|
||||
export const pointToRendererPoint = (
|
||||
{ x, y }: XYPosition,
|
||||
[tx, ty, tScale]: Transform,
|
||||
snapToGrid: boolean,
|
||||
[snapX, snapY]: [number, number],
|
||||
) => {
|
||||
const position: XYPosition = {
|
||||
x: (x - tx) / tScale,
|
||||
y: (y - ty) / tScale,
|
||||
}
|
||||
|
||||
if (snapToGrid) {
|
||||
return {
|
||||
x: snapX * Math.round(position.x / snapX),
|
||||
y: snapY * Math.round(position.y / snapY),
|
||||
}
|
||||
}
|
||||
|
||||
return position
|
||||
}
|
||||
|
||||
export const onLoadProject = (currentStore: FlowStore) => (position: XYPosition) =>
|
||||
pointToRendererPoint(position, currentStore.transform, currentStore.snapToGrid, currentStore.snapGrid)
|
||||
|
||||
const getBoundsOfBoxes = (box1: Box, box2: Box): Box => ({
|
||||
x: Math.min(box1.x, box2.x),
|
||||
y: Math.min(box1.y, box2.y),
|
||||
x2: Math.max(box1.x2, box2.x2),
|
||||
y2: Math.max(box1.y2, box2.y2),
|
||||
})
|
||||
|
||||
export const rectToBox = ({ x, y, width, height }: Rect): Box => ({
|
||||
x,
|
||||
y,
|
||||
x2: x + width,
|
||||
y2: y + height,
|
||||
})
|
||||
|
||||
export const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({
|
||||
x,
|
||||
y,
|
||||
width: x2 - x,
|
||||
height: y2 - y,
|
||||
})
|
||||
|
||||
export const getBoundsofRects = (rect1: Rect, rect2: Rect) => boxToRect(getBoundsOfBoxes(rectToBox(rect1), rectToBox(rect2)))
|
||||
|
||||
export const getRectOfNodes = (nodes: GraphNode[]) => {
|
||||
const box = nodes.reduce(
|
||||
(currBox, { computedPosition = { x: 0, y: 0 }, dimensions = { width: 0, height: 0 } } = {} as any) =>
|
||||
getBoundsOfBoxes(
|
||||
currBox,
|
||||
rectToBox({
|
||||
...computedPosition,
|
||||
...dimensions,
|
||||
} as Rect),
|
||||
),
|
||||
{ x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity },
|
||||
)
|
||||
|
||||
return boxToRect(box)
|
||||
}
|
||||
|
||||
export const graphPosToZoomedPos = ({ x, y }: XYPosition, [tx, ty, tScale]: Transform): XYPosition => ({
|
||||
x: x * tScale + tx,
|
||||
y: y * tScale + ty,
|
||||
})
|
||||
|
||||
export const getNodesInside = (nodes: GraphNode[], rect: Rect, [tx, ty, tScale]: Transform = [0, 0, 1], partially = false) => {
|
||||
const rBox = rectToBox({
|
||||
x: (rect.x - tx) / tScale,
|
||||
y: (rect.y - ty) / tScale,
|
||||
width: rect.width / tScale,
|
||||
height: rect.height / tScale,
|
||||
})
|
||||
|
||||
return nodes.filter((node) => {
|
||||
if (!node || node.selectable === false) return false
|
||||
const { computedPosition = { x: 0, y: 0 }, dimensions = { width: 0, height: 0 }, dragging = false } = node
|
||||
const nBox = rectToBox({ ...computedPosition, ...dimensions })
|
||||
const xOverlap = Math.max(0, Math.min(rBox.x2, nBox.x2) - Math.max(rBox.x, nBox.x))
|
||||
const yOverlap = Math.max(0, Math.min(rBox.y2, nBox.y2) - Math.max(rBox.y, nBox.y))
|
||||
const overlappingArea = Math.ceil(xOverlap * yOverlap)
|
||||
const notInitialized =
|
||||
typeof dimensions.width === 'undefined' ||
|
||||
typeof dimensions.height === 'undefined' ||
|
||||
dimensions.width === 0 ||
|
||||
dimensions.height === 0 ||
|
||||
dragging
|
||||
|
||||
const partiallyVisible = partially && overlappingArea > 0
|
||||
const area = dimensions.width * dimensions.height
|
||||
return notInitialized || partiallyVisible || overlappingArea >= area
|
||||
})
|
||||
}
|
||||
|
||||
export const getConnectedEdges = (nodes: GraphNode[], edges: GraphEdge[]) => {
|
||||
const nodeIds = nodes.map((node) => node.id)
|
||||
return edges.filter((edge) => nodeIds.includes(edge.source) || nodeIds.includes(edge.target))
|
||||
}
|
||||
|
||||
export const onLoadGetNodes = (store: FlowStore) => (): GraphNode[] => store.nodes
|
||||
export const onLoadGetEdges = (store: FlowStore) => (): GraphEdge[] => store.edges
|
||||
export const onLoadGetElements = (store: FlowStore) => (): FlowElements => [...store.nodes, ...store.edges]
|
||||
|
||||
export const onLoadToObject = (store: State) => (): FlowExportObject => {
|
||||
// we have to stringify/parse so objects containing refs (like nodes and edges) can potentially be saved in a storage
|
||||
return JSON.parse(
|
||||
JSON.stringify({
|
||||
nodes: store.nodes,
|
||||
edges: store.edges,
|
||||
position: [store.transform[0], store.transform[1]],
|
||||
zoom: store.transform[2],
|
||||
} as FlowExportObject),
|
||||
)
|
||||
}
|
||||
|
||||
export const getTransformForBounds = (
|
||||
bounds: Rect,
|
||||
width: number,
|
||||
height: number,
|
||||
minZoom: number,
|
||||
maxZoom: number,
|
||||
padding = 0.1,
|
||||
offset: {
|
||||
x?: number
|
||||
y?: number
|
||||
} = { x: 0, y: 0 },
|
||||
): Transform => {
|
||||
const xZoom = width / (bounds.width * (1 + padding))
|
||||
const yZoom = height / (bounds.height * (1 + padding))
|
||||
const zoom = Math.min(xZoom, yZoom)
|
||||
const clampedZoom = clamp(zoom, minZoom, maxZoom)
|
||||
const boundsCenterX = bounds.x + bounds.width / 2
|
||||
const boundsCenterY = bounds.y + bounds.height / 2
|
||||
const x = width / 2 - boundsCenterX * clampedZoom + (offset.x ?? 0)
|
||||
const y = height / 2 - boundsCenterY * clampedZoom + (offset.y ?? 0)
|
||||
|
||||
return [x, y, clampedZoom]
|
||||
}
|
||||
|
||||
export const getXYZPos = (parentNode: GraphNode, computedPosition: XYZPosition): XYZPosition => {
|
||||
return {
|
||||
x: computedPosition.x + parentNode.computedPosition.x,
|
||||
y: computedPosition.y + parentNode.computedPosition.y,
|
||||
z: parentNode.computedPosition.z > computedPosition.z ? parentNode.computedPosition.z : computedPosition.z,
|
||||
}
|
||||
}
|
||||
|
||||
export const isParentSelected = (node: GraphNode, getNode: Getters['getNode']): boolean => {
|
||||
if (!node.parentNode) return false
|
||||
const parent = getNode(node.parentNode)
|
||||
if (!parent) return false
|
||||
if (parent.selected) return true
|
||||
return isParentSelected(parent, getNode)
|
||||
}
|
||||
|
||||
export const getMarkerId = (marker: EdgeMarkerType | undefined): string => {
|
||||
if (typeof marker === 'undefined') return ''
|
||||
if (typeof marker === 'string') return marker
|
||||
|
||||
return Object.keys(marker)
|
||||
.sort()
|
||||
.map((key) => `${key}=${marker[<keyof EdgeMarkerType>key]}`)
|
||||
.join('&')
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './edge'
|
||||
export * from './graph'
|
||||
export * from './node'
|
||||
export * from './changes'
|
||||
@@ -0,0 +1,39 @@
|
||||
import { getDimensions } from './graph'
|
||||
import { HandleElement, Position } from '~/types'
|
||||
|
||||
export const getHandleBoundsByHandleType = (
|
||||
selector: string,
|
||||
nodeElement: HTMLDivElement,
|
||||
parentBounds: ClientRect | DOMRect,
|
||||
k: number,
|
||||
): HandleElement[] | null => {
|
||||
const handles = nodeElement.querySelectorAll(selector)
|
||||
|
||||
if (!handles || !handles.length) return null
|
||||
|
||||
const handlesArray = Array.from(handles) as HTMLDivElement[]
|
||||
|
||||
return handlesArray.map((handle): HandleElement => {
|
||||
const bounds = handle.getBoundingClientRect()
|
||||
const dimensions = getDimensions(handle)
|
||||
const handleId = handle.getAttribute('data-handleid') ?? undefined
|
||||
const handlePosition = handle.getAttribute('data-handlepos') as Position
|
||||
|
||||
return {
|
||||
id: handleId,
|
||||
position: handlePosition,
|
||||
x: (bounds.left - parentBounds.left) / k,
|
||||
y: (bounds.top - parentBounds.top) / k,
|
||||
...dimensions,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const getHandleBounds = (nodeElement: HTMLDivElement, scale: number, id?: string) => {
|
||||
const bounds = nodeElement.getBoundingClientRect()
|
||||
|
||||
return {
|
||||
source: getHandleBoundsByHandleType(`.source${id ? `.vue-flow__handle-${id}` : ''}`, nodeElement, bounds, scale) ?? undefined,
|
||||
target: getHandleBoundsByHandleType(`.target${id ? `.vue-flow__handle-${id}` : ''}`, nodeElement, bounds, scale) ?? undefined,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user