refactor(core): rename core pkg dir to core
This commit is contained in:
Vendored
+255
@@ -0,0 +1,255 @@
|
||||
// Generated by 'unplugin-auto-import'
|
||||
export {}
|
||||
declare global {
|
||||
const $$: typeof import('vue/macros')['$$']
|
||||
const $: typeof import('vue/macros')['$']
|
||||
const $computed: typeof import('vue/macros')['$computed']
|
||||
const $customRef: typeof import('vue/macros')['$customRef']
|
||||
const $ref: typeof import('vue/macros')['$ref']
|
||||
const $shallowRef: typeof import('vue/macros')['$shallowRef']
|
||||
const $toRef: typeof import('vue/macros')['$toRef']
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
||||
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const computedAsync: typeof import('@vueuse/core')['computedAsync']
|
||||
const computedEager: typeof import('@vueuse/core')['computedEager']
|
||||
const computedInject: typeof import('@vueuse/core')['computedInject']
|
||||
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
|
||||
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 createInjectionState: typeof import('@vueuse/core')['createInjectionState']
|
||||
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 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 isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const logicAnd: typeof import('@vueuse/core')['logicAnd']
|
||||
const logicNot: typeof import('@vueuse/core')['logicNot']
|
||||
const logicOr: typeof import('@vueuse/core')['logicOr']
|
||||
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 refAutoReset: typeof import('@vueuse/core')['refAutoReset']
|
||||
const refDebounced: typeof import('@vueuse/core')['refDebounced']
|
||||
const refDefault: typeof import('@vueuse/core')['refDefault']
|
||||
const refThrottled: typeof import('@vueuse/core')['refThrottled']
|
||||
const refWithControl: typeof import('@vueuse/core')['refWithControl']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const resolveRef: typeof import('@vueuse/core')['resolveRef']
|
||||
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
|
||||
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 syncRefs: typeof import('@vueuse/core')['syncRefs']
|
||||
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 tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
|
||||
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 useBluetooth: typeof import('@vueuse/core')['useBluetooth']
|
||||
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 useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
|
||||
const useCycleList: typeof import('@vueuse/core')['useCycleList']
|
||||
const useDark: typeof import('@vueuse/core')['useDark']
|
||||
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
|
||||
const useDebounce: typeof import('@vueuse/core')['useDebounce']
|
||||
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
|
||||
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
|
||||
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 useDropZone: typeof import('@vueuse/core')['useDropZone']
|
||||
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 useFileDialog: typeof import('@vueuse/core')['useFileDialog']
|
||||
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
|
||||
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 useGamepad: typeof import('@vueuse/core')['useGamepad']
|
||||
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
|
||||
const useIdle: typeof import('@vueuse/core')['useIdle']
|
||||
const useImage: typeof import('@vueuse/core')['useImage']
|
||||
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 useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
|
||||
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 useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
|
||||
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 useStepper: typeof import('@vueuse/core')['useStepper']
|
||||
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 useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
|
||||
const useThrottle: typeof import('@vueuse/core')['useThrottle']
|
||||
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
|
||||
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
|
||||
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
|
||||
const useTimeout: typeof import('@vueuse/core')['useTimeout']
|
||||
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
|
||||
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
|
||||
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 useVModel: typeof import('@vueuse/core')['useVModel']
|
||||
const useVModels: typeof import('@vueuse/core')['useVModels']
|
||||
const useVibrate: typeof import('@vueuse/core')['useVibrate']
|
||||
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
|
||||
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 watchArray: typeof import('@vueuse/core')['watchArray']
|
||||
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
|
||||
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
|
||||
const watchOnce: typeof import('@vueuse/core')['watchOnce']
|
||||
const watchPausable: typeof import('@vueuse/core')['watchPausable']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
|
||||
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
|
||||
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
|
||||
const whenever: typeof import('@vueuse/core')['whenever']
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<script lang="ts" setup>
|
||||
import { getBezierPath, getSimpleBezierPath, getSmoothStepPath } from '../Edges/utils'
|
||||
import type { GraphNode, HandleElement } from '../../types'
|
||||
import { ConnectionLineType, Position } from '../../types'
|
||||
import { useVueFlow } from '../../composables'
|
||||
import { Slots } from '../../context'
|
||||
import { getMarkerId } from '../../utils'
|
||||
|
||||
const { sourceNode } = defineProps<{
|
||||
sourceNode: GraphNode
|
||||
}>()
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
connectionStartHandle,
|
||||
connectionPosition,
|
||||
connectionLineType,
|
||||
connectionLineStyle,
|
||||
connectionLineOptions,
|
||||
viewport,
|
||||
} = $(useVueFlow())
|
||||
|
||||
const slots = inject(Slots)?.['connection-line']
|
||||
|
||||
const hasSlot = slots?.({})
|
||||
|
||||
const handleId = connectionStartHandle!.handleId
|
||||
const nodeId = connectionStartHandle!.nodeId
|
||||
const type = connectionStartHandle!.type
|
||||
|
||||
const sourceHandle =
|
||||
handleId && type
|
||||
? sourceNode.handleBounds[type]?.find((d: HandleElement) => d.id === handleId)
|
||||
: type && sourceNode.handleBounds[type ?? 'source']?.[0]
|
||||
|
||||
const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNode.dimensions.width / 2
|
||||
const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.dimensions.height
|
||||
|
||||
const sourceX = sourceNode.computedPosition.x + sourceHandleX
|
||||
const sourceY = sourceNode.computedPosition.y + sourceHandleY
|
||||
|
||||
const isRightOrLeft = sourceHandle?.position === Position.Left || sourceHandle?.position === Position.Right
|
||||
|
||||
const targetPosition = isRightOrLeft ? Position.Left : Position.Top
|
||||
|
||||
const targetX = $computed(() => (connectionPosition.x - viewport.x) / viewport.zoom)
|
||||
const targetY = $computed(() => (connectionPosition.y - viewport.y) / viewport.zoom)
|
||||
|
||||
const dAttr = computed(() => {
|
||||
let path = `M${sourceX},${sourceY} ${targetX},${targetY}`
|
||||
|
||||
const pathParams = {
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition: sourceHandle?.position,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
}
|
||||
|
||||
switch (connectionLineType || connectionLineOptions.type) {
|
||||
case ConnectionLineType.Bezier:
|
||||
;[path] = getBezierPath(pathParams)
|
||||
break
|
||||
case ConnectionLineType.Step:
|
||||
;[path] = getSmoothStepPath({
|
||||
...pathParams,
|
||||
borderRadius: 0,
|
||||
})
|
||||
break
|
||||
case ConnectionLineType.SmoothStep:
|
||||
;[path] = getSmoothStepPath(pathParams)
|
||||
break
|
||||
case ConnectionLineType.SimpleBezier:
|
||||
;[path] = getSimpleBezierPath(pathParams)
|
||||
break
|
||||
}
|
||||
|
||||
return path
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ConnectionLine',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<g class="vue-flow__connection">
|
||||
<component
|
||||
:is="slots"
|
||||
v-if="hasSlot"
|
||||
v-bind="{
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition: sourceHandle?.position,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
nodes: getNodes,
|
||||
sourceNode,
|
||||
sourceHandle,
|
||||
markerEnd: `url(#${getMarkerId(connectionLineOptions.markerEnd)})`,
|
||||
markerStart: `url(#${getMarkerId(connectionLineOptions.markerStart)})`,
|
||||
}"
|
||||
/>
|
||||
<path
|
||||
v-else
|
||||
:d="dAttr"
|
||||
class="vue-flow__connection-path"
|
||||
:class="connectionLineOptions.class"
|
||||
:style="connectionLineStyle || connectionLineOptions.style || {}"
|
||||
:marker-end="`url(#${getMarkerId(connectionLineOptions.markerEnd)})`"
|
||||
:marker-start="`url(#${getMarkerId(connectionLineOptions.markerStart)})`"
|
||||
/>
|
||||
</g>
|
||||
</template>
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { FunctionalComponent } from 'vue'
|
||||
import EdgeText from './EdgeText.vue'
|
||||
import type { BaseEdgeProps } from '~/types'
|
||||
|
||||
/**
|
||||
* The base edge is a simple wrapper for svg path
|
||||
* You can use the base edge in your custom edges and just pass down the necessary props
|
||||
*/
|
||||
const BaseEdge: FunctionalComponent<BaseEdgeProps> = function ({
|
||||
path,
|
||||
label,
|
||||
labelX,
|
||||
labelY,
|
||||
labelBgBorderRadius,
|
||||
labelBgPadding,
|
||||
labelBgStyle,
|
||||
labelShowBg,
|
||||
labelStyle,
|
||||
markerStart,
|
||||
markerEnd,
|
||||
style,
|
||||
}) {
|
||||
return [
|
||||
h('path', {
|
||||
'style': { ...style },
|
||||
'd': path,
|
||||
'class': 'vue-flow__edge-path',
|
||||
'marker-end': markerEnd,
|
||||
'marker-start': markerStart,
|
||||
}),
|
||||
label
|
||||
? h(EdgeText, {
|
||||
x: labelX,
|
||||
y: labelY,
|
||||
label,
|
||||
labelStyle,
|
||||
labelShowBg,
|
||||
labelBgStyle,
|
||||
labelBgPadding,
|
||||
labelBgBorderRadius,
|
||||
})
|
||||
: null,
|
||||
]
|
||||
}
|
||||
|
||||
BaseEdge.props = [
|
||||
'path',
|
||||
'labelX',
|
||||
'labelY',
|
||||
'label',
|
||||
'labelBgBorderRadius',
|
||||
'labelBgPadding',
|
||||
'labelBgStyle',
|
||||
'labelShowBg',
|
||||
'labelStyle',
|
||||
'markerStart',
|
||||
'markerEnd',
|
||||
'style',
|
||||
]
|
||||
BaseEdge.inheritAttrs = false
|
||||
|
||||
export default BaseEdge
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { FunctionalComponent } from 'vue'
|
||||
import { getBezierPath } from './utils'
|
||||
import BaseEdge from './BaseEdge'
|
||||
import { Position } from '~/types'
|
||||
import type { EdgeProps } from '~/types'
|
||||
|
||||
const BezierEdge: FunctionalComponent<EdgeProps> = function ({
|
||||
sourcePosition = Position.Bottom,
|
||||
targetPosition = Position.Top,
|
||||
label,
|
||||
labelStyle = {},
|
||||
labelShowBg = true,
|
||||
labelBgStyle = {},
|
||||
labelBgPadding,
|
||||
labelBgBorderRadius,
|
||||
sourceY,
|
||||
sourceX,
|
||||
targetX,
|
||||
targetY,
|
||||
curvature,
|
||||
markerEnd,
|
||||
markerStart,
|
||||
style,
|
||||
}) {
|
||||
const [path, labelX, labelY] = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
curvature,
|
||||
})
|
||||
|
||||
return h(BaseEdge, {
|
||||
path,
|
||||
labelX,
|
||||
labelY,
|
||||
label,
|
||||
labelStyle,
|
||||
labelShowBg,
|
||||
labelBgStyle,
|
||||
labelBgPadding,
|
||||
labelBgBorderRadius,
|
||||
style,
|
||||
markerEnd,
|
||||
markerStart,
|
||||
})
|
||||
}
|
||||
|
||||
BezierEdge.props = [
|
||||
'sourcePosition',
|
||||
'targetPosition',
|
||||
'label',
|
||||
'labelStyle',
|
||||
'labelShowBg',
|
||||
'labelBgStyle',
|
||||
'labelBgPadding',
|
||||
'labelBgBorderRadius',
|
||||
'sourceY',
|
||||
'sourceX',
|
||||
'targetX',
|
||||
'targetY',
|
||||
'curvature',
|
||||
'markerEnd',
|
||||
'markerStart',
|
||||
'style',
|
||||
]
|
||||
BezierEdge.inheritAttrs = false
|
||||
|
||||
export default BezierEdge
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { FunctionalComponent, HTMLAttributes } from 'vue'
|
||||
import { Position } from '~/types'
|
||||
|
||||
interface Props extends HTMLAttributes {
|
||||
position: Position
|
||||
centerX: number
|
||||
centerY: number
|
||||
radius?: number
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const EdgeAnchor: FunctionalComponent<Props> = function ({ radius = 10, centerX = 0, centerY = 0, position = Position.Top }) {
|
||||
const cx = computed(() => {
|
||||
const val = shiftX(centerX, radius, position)
|
||||
if (isNaN(val)) return 0
|
||||
else return val
|
||||
})
|
||||
const cy = computed(() => {
|
||||
const val = shiftY(centerY, radius, position)
|
||||
if (isNaN(val)) return 0
|
||||
else return val
|
||||
})
|
||||
|
||||
return h('circle', {
|
||||
class: 'vue-flow__edgeupdater',
|
||||
cx: cx.value,
|
||||
cy: cy.value,
|
||||
r: radius,
|
||||
stroke: 'transparent',
|
||||
fill: 'transparent',
|
||||
})
|
||||
}
|
||||
|
||||
EdgeAnchor.props = ['radius', 'centerX', 'centerY', 'position']
|
||||
|
||||
export default EdgeAnchor
|
||||
@@ -0,0 +1,55 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EdgeTextProps } from '../../types/components'
|
||||
import type { Rect as RectType } from '../../types'
|
||||
|
||||
const {
|
||||
x,
|
||||
y,
|
||||
label,
|
||||
labelStyle = {},
|
||||
labelShowBg = true,
|
||||
labelBgStyle = {},
|
||||
labelBgPadding = [2, 4],
|
||||
labelBgBorderRadius = 2,
|
||||
} = defineProps<EdgeTextProps>()
|
||||
|
||||
const el = ref<SVGTextElement>()
|
||||
|
||||
let box = $ref<RectType>({ x: 0, y: 0, width: 0, height: 0 })
|
||||
|
||||
onMounted(() => {
|
||||
box = el.value!.getBBox()
|
||||
})
|
||||
|
||||
const transform = computed(() => `translate(${x - box.width / 2} ${y - box.height / 2})`)
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'EdgeText',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<g :transform="transform" class="vue-flow__edge-textwrapper">
|
||||
<rect
|
||||
v-if="labelShowBg"
|
||||
class="vue-flow__edge-textbg"
|
||||
:width="`${box.width + 2 * labelBgPadding[0]}px`"
|
||||
:height="`${box.height + 2 * labelBgPadding[1]}px`"
|
||||
:x="-labelBgPadding[0]"
|
||||
:y="-labelBgPadding[1]"
|
||||
:style="labelBgStyle"
|
||||
:rx="labelBgBorderRadius"
|
||||
:ry="labelBgBorderRadius"
|
||||
/>
|
||||
<text v-bind="$attrs" ref="el" class="vue-flow__edge-text" :y="box.height / 2" dy="0.3em" :style="labelStyle">
|
||||
<slot>
|
||||
<component :is="label" v-if="typeof label !== 'string' && typeof label" />
|
||||
<template v-else>
|
||||
{{ label }}
|
||||
</template>
|
||||
</slot>
|
||||
</text>
|
||||
</g>
|
||||
</template>
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { FunctionalComponent } from 'vue'
|
||||
import { getSimpleBezierPath } from './utils'
|
||||
import BaseEdge from './BaseEdge'
|
||||
import { Position } from '~/types'
|
||||
import type { EdgeProps } from '~/types'
|
||||
|
||||
const SimpleBezierEdge: FunctionalComponent<EdgeProps> = function ({
|
||||
sourcePosition = Position.Bottom,
|
||||
targetPosition = Position.Top,
|
||||
label,
|
||||
labelStyle = {},
|
||||
labelShowBg = true,
|
||||
labelBgStyle = {},
|
||||
labelBgPadding,
|
||||
labelBgBorderRadius,
|
||||
sourceY,
|
||||
sourceX,
|
||||
targetX,
|
||||
targetY,
|
||||
markerEnd,
|
||||
markerStart,
|
||||
style,
|
||||
}) {
|
||||
const [path, labelX, labelY] = getSimpleBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
})
|
||||
|
||||
return h(BaseEdge, {
|
||||
path,
|
||||
labelX,
|
||||
labelY,
|
||||
label,
|
||||
labelStyle,
|
||||
labelShowBg,
|
||||
labelBgStyle,
|
||||
labelBgPadding,
|
||||
labelBgBorderRadius,
|
||||
style,
|
||||
markerEnd,
|
||||
markerStart,
|
||||
})
|
||||
}
|
||||
|
||||
SimpleBezierEdge.props = [
|
||||
'sourcePosition',
|
||||
'targetPosition',
|
||||
'label',
|
||||
'labelStyle',
|
||||
'labelShowBg',
|
||||
'labelBgStyle',
|
||||
'labelBgPadding',
|
||||
'labelBgBorderRadius',
|
||||
'sourceY',
|
||||
'sourceX',
|
||||
'targetX',
|
||||
'targetY',
|
||||
'markerEnd',
|
||||
'markerStart',
|
||||
'style',
|
||||
]
|
||||
SimpleBezierEdge.inheritAttrs = false
|
||||
|
||||
export default SimpleBezierEdge
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { FunctionalComponent } from 'vue'
|
||||
import { getSmoothStepPath } from './utils'
|
||||
import BaseEdge from './BaseEdge'
|
||||
import type { SmoothStepEdgeProps } from '~/types'
|
||||
import { Position } from '~/types'
|
||||
|
||||
const SmoothStepEdge: FunctionalComponent<SmoothStepEdgeProps> = function ({
|
||||
sourcePosition = Position.Bottom,
|
||||
targetPosition = Position.Top,
|
||||
label,
|
||||
labelStyle = {},
|
||||
labelShowBg = true,
|
||||
labelBgStyle = {},
|
||||
labelBgPadding,
|
||||
labelBgBorderRadius,
|
||||
sourceY,
|
||||
sourceX,
|
||||
targetX,
|
||||
targetY,
|
||||
markerEnd,
|
||||
markerStart,
|
||||
borderRadius,
|
||||
offset,
|
||||
style,
|
||||
}) {
|
||||
const [path, labelX, labelY] = getSmoothStepPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
borderRadius,
|
||||
offset,
|
||||
})
|
||||
|
||||
return h(BaseEdge, {
|
||||
path,
|
||||
labelX,
|
||||
labelY,
|
||||
label,
|
||||
labelStyle,
|
||||
labelShowBg,
|
||||
labelBgStyle,
|
||||
labelBgPadding,
|
||||
labelBgBorderRadius,
|
||||
style,
|
||||
markerEnd,
|
||||
markerStart,
|
||||
})
|
||||
}
|
||||
|
||||
SmoothStepEdge.props = [
|
||||
'sourcePosition',
|
||||
'targetPosition',
|
||||
'label',
|
||||
'labelStyle',
|
||||
'labelShowBg',
|
||||
'labelBgStyle',
|
||||
'labelBgPadding',
|
||||
'labelBgBorderRadius',
|
||||
'sourceY',
|
||||
'sourceX',
|
||||
'targetX',
|
||||
'targetY',
|
||||
'borderRadius',
|
||||
'offset',
|
||||
'markerEnd',
|
||||
'markerStart',
|
||||
'style',
|
||||
]
|
||||
SmoothStepEdge.inheritAttrs = false
|
||||
|
||||
export default SmoothStepEdge
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { FunctionalComponent } from 'vue'
|
||||
import SmoothStepEdge from './SmoothStepEdge'
|
||||
import type { EdgeProps } from '~/types'
|
||||
|
||||
const StepEdge: FunctionalComponent<EdgeProps> = function (props) {
|
||||
return h(SmoothStepEdge, { ...props, borderRadius: 0 })
|
||||
}
|
||||
|
||||
StepEdge.props = [
|
||||
'sourcePosition',
|
||||
'targetPosition',
|
||||
'label',
|
||||
'labelStyle',
|
||||
'labelShowBg',
|
||||
'labelBgStyle',
|
||||
'labelBgPadding',
|
||||
'labelBgBorderRadius',
|
||||
'sourceY',
|
||||
'sourceX',
|
||||
'targetX',
|
||||
'targetY',
|
||||
'markerEnd',
|
||||
'markerStart',
|
||||
'style',
|
||||
]
|
||||
StepEdge.inheritAttrs = false
|
||||
|
||||
export default StepEdge
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { FunctionalComponent } from 'vue'
|
||||
import BaseEdge from './BaseEdge'
|
||||
import { getStraightPath } from './utils'
|
||||
import type { EdgeProps } from '~/types'
|
||||
|
||||
const StraightEdge: FunctionalComponent<EdgeProps> = function ({
|
||||
label,
|
||||
labelStyle = {},
|
||||
labelShowBg = true,
|
||||
labelBgStyle = {},
|
||||
labelBgPadding,
|
||||
labelBgBorderRadius,
|
||||
sourceY,
|
||||
sourceX,
|
||||
targetX,
|
||||
targetY,
|
||||
markerEnd,
|
||||
markerStart,
|
||||
style,
|
||||
}) {
|
||||
const [path, labelX, labelY] = getStraightPath({ sourceX, sourceY, targetX, targetY })
|
||||
|
||||
return h(BaseEdge, {
|
||||
path,
|
||||
labelX,
|
||||
labelY,
|
||||
label,
|
||||
labelStyle,
|
||||
labelShowBg,
|
||||
labelBgStyle,
|
||||
labelBgPadding,
|
||||
labelBgBorderRadius,
|
||||
style,
|
||||
markerEnd,
|
||||
markerStart,
|
||||
})
|
||||
}
|
||||
|
||||
StraightEdge.props = [
|
||||
'label',
|
||||
'labelStyle',
|
||||
'labelShowBg',
|
||||
'labelBgStyle',
|
||||
'labelBgPadding',
|
||||
'labelBgBorderRadius',
|
||||
'sourceY',
|
||||
'sourceX',
|
||||
'targetX',
|
||||
'targetY',
|
||||
'markerEnd',
|
||||
'markerStart',
|
||||
'style',
|
||||
]
|
||||
StraightEdge.inheritAttrs = false
|
||||
|
||||
export default StraightEdge
|
||||
@@ -0,0 +1,218 @@
|
||||
import type { CSSProperties, Component, VNode } from 'vue'
|
||||
import EdgeAnchor from './EdgeAnchor'
|
||||
import type { EdgeComponent, EdgeEventsOn, EdgeMarkerType, EdgeTextProps, EdgeUpdatable, GraphNode } from '~/types'
|
||||
import { ConnectionMode, Position } from '~/types'
|
||||
import { getEdgePositions, getHandle, getMarkerId } from '~/utils'
|
||||
import { EdgeId, EdgeRef } from '~/context'
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
type: EdgeComponent | Function | Object | false
|
||||
name: string
|
||||
source: string
|
||||
target: string
|
||||
sourceNode?: GraphNode
|
||||
targetNode?: GraphNode
|
||||
targetHandleId?: string | null
|
||||
sourceHandleId?: string | null
|
||||
selectable?: boolean
|
||||
updatable?: EdgeUpdatable
|
||||
label?: string | VNode | Component<EdgeTextProps> | Object
|
||||
data?: any
|
||||
events: EdgeEventsOn
|
||||
labelStyle?: CSSProperties
|
||||
labelShowBg?: boolean
|
||||
labelBgStyle?: any
|
||||
labelBgPadding?: [number, number]
|
||||
labelBgBorderRadius?: number
|
||||
animated?: boolean
|
||||
selected?: boolean
|
||||
style: CSSProperties
|
||||
markerEnd?: EdgeMarkerType
|
||||
markerStart?: EdgeMarkerType
|
||||
connectionMode: ConnectionMode
|
||||
edgeUpdaterRadius: number
|
||||
updating?: boolean
|
||||
}
|
||||
|
||||
const Wrapper = defineComponent({
|
||||
props: [
|
||||
'name',
|
||||
'type',
|
||||
'id',
|
||||
'data',
|
||||
'events',
|
||||
'labelBgBorderRadius',
|
||||
'labelBgPadding',
|
||||
'labelBgStyle',
|
||||
'labelStyle',
|
||||
'labelShowBg',
|
||||
'style',
|
||||
'animated',
|
||||
'label',
|
||||
'updatable',
|
||||
'selectable',
|
||||
'target',
|
||||
'source',
|
||||
'sourceNode',
|
||||
'targetNode',
|
||||
'sourceHandleId',
|
||||
'targetHandleId',
|
||||
'selected',
|
||||
'markerEnd',
|
||||
'markerStart',
|
||||
'connectionMode',
|
||||
'edgeUpdaterRadius',
|
||||
'updating',
|
||||
],
|
||||
emits: ['source-mousedown', 'target-mousedown'],
|
||||
setup(props: Props, { emit }) {
|
||||
let updating = $ref(false)
|
||||
|
||||
const edgeEl = ref()
|
||||
|
||||
provide(EdgeId, props.id)
|
||||
provide(EdgeRef, edgeEl)
|
||||
|
||||
const onEdgeUpdaterMouseEnter = () => (updating = true)
|
||||
|
||||
const onEdgeUpdaterMouseOut = () => (updating = false)
|
||||
|
||||
const onEdgeUpdaterSourceMouseDown = (e: MouseEvent) => {
|
||||
emit('source-mousedown', e)
|
||||
}
|
||||
|
||||
const onEdgeUpdaterTargetMouseDown = (e: MouseEvent) => {
|
||||
emit('target-mousedown', e)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (!props.sourceNode || !props.targetNode) return null
|
||||
|
||||
let sourceNodeHandles
|
||||
if (props.connectionMode === ConnectionMode.Strict) {
|
||||
sourceNodeHandles = props.sourceNode.handleBounds.source
|
||||
} else {
|
||||
sourceNodeHandles = props.sourceNode.handleBounds.source ?? props.sourceNode.handleBounds.target
|
||||
}
|
||||
|
||||
const sourceHandle = getHandle(sourceNodeHandles, props.sourceHandleId)
|
||||
|
||||
let targetNodeHandles
|
||||
if (props.connectionMode === ConnectionMode.Strict) {
|
||||
targetNodeHandles = props.targetNode.handleBounds.target
|
||||
} else {
|
||||
targetNodeHandles = props.targetNode.handleBounds.target ?? props.targetNode.handleBounds.source
|
||||
}
|
||||
|
||||
const targetHandle = getHandle(targetNodeHandles, props.targetHandleId)
|
||||
|
||||
const sourcePosition = sourceHandle ? sourceHandle.position : Position.Bottom
|
||||
|
||||
const targetPosition = targetHandle ? targetHandle.position : Position.Top
|
||||
|
||||
const { sourceX, sourceY, targetY, targetX } = getEdgePositions(
|
||||
props.sourceNode,
|
||||
sourceHandle,
|
||||
sourcePosition,
|
||||
props.targetNode,
|
||||
targetHandle,
|
||||
targetPosition,
|
||||
)
|
||||
|
||||
return h(
|
||||
'g',
|
||||
{
|
||||
'ref': edgeEl,
|
||||
'data-id': props.id,
|
||||
'class': [
|
||||
'vue-flow__edge',
|
||||
`vue-flow__edge-${props.name}`,
|
||||
{
|
||||
updating,
|
||||
selected: props.selected,
|
||||
animated: props.animated,
|
||||
inactive: !props.selectable,
|
||||
},
|
||||
],
|
||||
},
|
||||
[
|
||||
props.updating
|
||||
? null
|
||||
: h(props.type as any, {
|
||||
id: props.id,
|
||||
sourceNode: props.sourceNode,
|
||||
targetNode: props.targetNode,
|
||||
source: props.source,
|
||||
target: props.target,
|
||||
updatable: props.updatable,
|
||||
selected: props.selected,
|
||||
animated: props.animated,
|
||||
label: props.label,
|
||||
labelStyle: props.labelStyle,
|
||||
labelShowBg: props.labelShowBg,
|
||||
labelBgStyle: props.labelBgStyle,
|
||||
labelBgPadding: props.labelBgPadding,
|
||||
labelBgBorderRadius: props.labelBgBorderRadius,
|
||||
data: props.data,
|
||||
events: props.events,
|
||||
style: props.style,
|
||||
markerStart: `url(#${getMarkerId(props.markerStart)})`,
|
||||
markerEnd: `url(#${getMarkerId(props.markerEnd)})`,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourceHandleId: props.sourceHandleId,
|
||||
targetHandleId: props.targetHandleId,
|
||||
}),
|
||||
|
||||
[
|
||||
props.updatable === 'source' || props.updatable === true
|
||||
? [
|
||||
h(
|
||||
'g',
|
||||
{
|
||||
onMousedown: onEdgeUpdaterSourceMouseDown,
|
||||
onMouseenter: onEdgeUpdaterMouseEnter,
|
||||
onMouseout: onEdgeUpdaterMouseOut,
|
||||
},
|
||||
h(EdgeAnchor, {
|
||||
'position': sourcePosition,
|
||||
'centerX': sourceX,
|
||||
'centerY': sourceY,
|
||||
'radius': props.edgeUpdaterRadius,
|
||||
'data-type': 'source',
|
||||
}),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
props.updatable === 'target' || props.updatable === true
|
||||
? [
|
||||
h(
|
||||
'g',
|
||||
{
|
||||
onMousedown: onEdgeUpdaterTargetMouseDown,
|
||||
onMouseenter: onEdgeUpdaterMouseEnter,
|
||||
onMouseout: onEdgeUpdaterMouseOut,
|
||||
},
|
||||
h(EdgeAnchor, {
|
||||
'position': targetPosition,
|
||||
'centerX': targetX,
|
||||
'centerY': targetY,
|
||||
'radius': props.edgeUpdaterRadius,
|
||||
'data-type': 'target',
|
||||
}),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
],
|
||||
],
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export default Wrapper
|
||||
@@ -0,0 +1,9 @@
|
||||
export { default as BaseEdge } from './BaseEdge'
|
||||
export { default as BezierEdge } from './BezierEdge'
|
||||
export { default as SimpleBezierEdge } from './SimpleBezierEdge'
|
||||
export { default as StepEdge } from './StepEdge'
|
||||
export { default as SmoothStepEdge } from './SmoothStepEdge'
|
||||
export { default as StraightEdge } from './StraightEdge'
|
||||
export { default as EdgeAnchor } from './EdgeAnchor'
|
||||
export { default as EdgeText } from './EdgeText.vue'
|
||||
export { default as EdgeWrapper } from './Wrapper'
|
||||
@@ -0,0 +1,98 @@
|
||||
import { getBezierEdgeCenter } from './general'
|
||||
import { Position } from '~/types'
|
||||
|
||||
export interface GetBezierPathParams {
|
||||
sourceX: number
|
||||
sourceY: number
|
||||
sourcePosition?: Position
|
||||
targetX: number
|
||||
targetY: number
|
||||
targetPosition?: Position
|
||||
curvature?: number
|
||||
}
|
||||
|
||||
interface GetControlWithCurvatureParams {
|
||||
pos: Position
|
||||
x1: number
|
||||
y1: number
|
||||
x2: number
|
||||
y2: number
|
||||
c: 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, 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,
|
||||
})
|
||||
|
||||
const [centerX, centerY, offsetX, offsetY] = getBezierEdgeCenter({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourceControlX,
|
||||
sourceControlY,
|
||||
targetControlX,
|
||||
targetControlY,
|
||||
})
|
||||
|
||||
return [
|
||||
`M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`,
|
||||
centerX,
|
||||
centerY,
|
||||
offsetX,
|
||||
offsetY,
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// this is used for straight edges and simple smoothstep edges (LTR, RTL, BTT, TTB)
|
||||
export function getSimpleEdgeCenter({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
}: {
|
||||
sourceX: number
|
||||
sourceY: number
|
||||
targetX: number
|
||||
targetY: number
|
||||
}): [number, number, number, number] {
|
||||
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]
|
||||
}
|
||||
|
||||
export function getBezierEdgeCenter({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourceControlX,
|
||||
sourceControlY,
|
||||
targetControlX,
|
||||
targetControlY,
|
||||
}: {
|
||||
sourceX: number
|
||||
sourceY: number
|
||||
targetX: number
|
||||
targetY: number
|
||||
sourceControlX: number
|
||||
sourceControlY: number
|
||||
targetControlX: number
|
||||
targetControlY: number
|
||||
}): [number, number, number, number] {
|
||||
// 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 offsetX = Math.abs(centerX - sourceX)
|
||||
const offsetY = Math.abs(centerY - sourceY)
|
||||
|
||||
return [centerX, centerY, offsetX, offsetY]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './bezier'
|
||||
export * from './general'
|
||||
export * from './simple-bezier'
|
||||
export * from './smoothstep'
|
||||
export * from './straight'
|
||||
@@ -0,0 +1,79 @@
|
||||
import { getBezierEdgeCenter } from './general'
|
||||
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, 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,
|
||||
})
|
||||
|
||||
const [centerX, centerY, offsetX, offsetY] = getBezierEdgeCenter({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourceControlX,
|
||||
sourceControlY,
|
||||
targetControlX,
|
||||
targetControlY,
|
||||
})
|
||||
|
||||
return [
|
||||
`M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`,
|
||||
centerX,
|
||||
centerY,
|
||||
offsetX,
|
||||
offsetY,
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import { getSimpleEdgeCenter } from './general'
|
||||
import type { XYPosition } from '~/types'
|
||||
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
|
||||
offset?: number
|
||||
}
|
||||
|
||||
const handleDirections = {
|
||||
[Position.Left]: { x: -1, y: 0 },
|
||||
[Position.Right]: { x: 1, y: 0 },
|
||||
[Position.Top]: { x: 0, y: -1 },
|
||||
[Position.Bottom]: { x: 0, y: 1 },
|
||||
}
|
||||
|
||||
const getDirection = ({
|
||||
source,
|
||||
sourcePosition = Position.Bottom,
|
||||
target,
|
||||
}: {
|
||||
source: XYPosition
|
||||
sourcePosition: Position
|
||||
target: XYPosition
|
||||
}): XYPosition => {
|
||||
if (sourcePosition === Position.Left || sourcePosition === Position.Right) {
|
||||
return source.x < target.x ? { x: 1, y: 0 } : { x: -1, y: 0 }
|
||||
}
|
||||
return source.y < target.y ? { x: 0, y: 1 } : { x: 0, y: -1 }
|
||||
}
|
||||
|
||||
const distance = (a: XYPosition, b: XYPosition) => Math.sqrt((b.x - a.x) ** 2 + (b.y - a.y) ** 2)
|
||||
|
||||
// With this function we try to mimic an orthogonal edge routing behaviour
|
||||
// It's not as good as a real orthogonal edge routing, but it's faster and good enough as a default for step and smooth step edges
|
||||
function getPoints({
|
||||
source,
|
||||
sourcePosition = Position.Bottom,
|
||||
target,
|
||||
targetPosition = Position.Top,
|
||||
center,
|
||||
offset,
|
||||
}: {
|
||||
source: XYPosition
|
||||
sourcePosition: Position
|
||||
target: XYPosition
|
||||
targetPosition: Position
|
||||
center: Partial<XYPosition>
|
||||
offset: number
|
||||
}): [XYPosition[], number, number, number, number] {
|
||||
const sourceDir = handleDirections[sourcePosition]
|
||||
const targetDir = handleDirections[targetPosition]
|
||||
const sourceGapped: XYPosition = { x: source.x + sourceDir.x * offset, y: source.y + sourceDir.y * offset }
|
||||
const targetGapped: XYPosition = { x: target.x + targetDir.x * offset, y: target.y + targetDir.y * offset }
|
||||
const dir = getDirection({
|
||||
source: sourceGapped,
|
||||
sourcePosition,
|
||||
target: targetGapped,
|
||||
})
|
||||
const dirAccessor = dir.x !== 0 ? 'x' : 'y'
|
||||
const currDir = dir[dirAccessor]
|
||||
|
||||
let points: XYPosition[] = []
|
||||
let centerX, centerY
|
||||
const [defaultCenterX, defaultCenterY, defaultOffsetX, defaultOffsetY] = getSimpleEdgeCenter({
|
||||
sourceX: source.x,
|
||||
sourceY: source.y,
|
||||
targetX: target.x,
|
||||
targetY: target.y,
|
||||
})
|
||||
|
||||
// opposite handle positions, default case
|
||||
if (sourceDir[dirAccessor] * targetDir[dirAccessor] === -1) {
|
||||
centerX = center.x || defaultCenterX
|
||||
centerY = center.y || defaultCenterY
|
||||
|
||||
// --->
|
||||
// |
|
||||
// >---
|
||||
const verticalSplit: XYPosition[] = [
|
||||
{ x: centerX, y: sourceGapped.y },
|
||||
{ x: centerX, y: targetGapped.y },
|
||||
]
|
||||
// |
|
||||
// ---
|
||||
// |
|
||||
const horizontalSplit: XYPosition[] = [
|
||||
{ x: sourceGapped.x, y: centerY },
|
||||
{ x: targetGapped.x, y: centerY },
|
||||
]
|
||||
|
||||
if (sourceDir[dirAccessor] === currDir) {
|
||||
points = dirAccessor === 'x' ? verticalSplit : horizontalSplit
|
||||
} else {
|
||||
points = dirAccessor === 'x' ? horizontalSplit : verticalSplit
|
||||
}
|
||||
} else {
|
||||
// sourceTarget means we take x from source and y from target, targetSource is the opposite
|
||||
const sourceTarget: XYPosition[] = [{ x: sourceGapped.x, y: targetGapped.y }]
|
||||
const targetSource: XYPosition[] = [{ x: targetGapped.x, y: sourceGapped.y }]
|
||||
// this handles edges with same handle positions
|
||||
if (dirAccessor === 'x') {
|
||||
points = sourceDir.x === currDir ? targetSource : sourceTarget
|
||||
} else {
|
||||
points = sourceDir.y === currDir ? sourceTarget : targetSource
|
||||
}
|
||||
|
||||
// these are conditions for handling mixed handle positions like Right -> Bottom for example
|
||||
if (sourcePosition !== targetPosition) {
|
||||
const dirAccessorOpposite = dirAccessor === 'x' ? 'y' : 'x'
|
||||
const isSameDir = sourceDir[dirAccessor] === targetDir[dirAccessorOpposite]
|
||||
const sourceGtTargetOppo = sourceGapped[dirAccessorOpposite] > targetGapped[dirAccessorOpposite]
|
||||
const sourceLtTargetOppo = sourceGapped[dirAccessorOpposite] < targetGapped[dirAccessorOpposite]
|
||||
const flipSourceTarget =
|
||||
(sourceDir[dirAccessor] === 1 && ((!isSameDir && sourceGtTargetOppo) || (isSameDir && sourceLtTargetOppo))) ||
|
||||
(sourceDir[dirAccessor] !== 1 && ((!isSameDir && sourceLtTargetOppo) || (isSameDir && sourceGtTargetOppo)))
|
||||
|
||||
if (flipSourceTarget) {
|
||||
points = dirAccessor === 'x' ? sourceTarget : targetSource
|
||||
}
|
||||
}
|
||||
|
||||
centerX = points[0].x
|
||||
centerY = points[0].y
|
||||
}
|
||||
|
||||
const pathPoints = [source, sourceGapped, ...points, targetGapped, target]
|
||||
|
||||
return [pathPoints, centerX, centerY, defaultOffsetX, defaultOffsetY]
|
||||
}
|
||||
|
||||
function getBend(a: XYPosition, b: XYPosition, c: XYPosition, size: number): string {
|
||||
const bendSize = Math.min(distance(a, b) / 2, distance(b, c) / 2, size)
|
||||
const { x, y } = b
|
||||
|
||||
// no bend
|
||||
if ((a.x === x && x === c.x) || (a.y === y && y === c.y)) {
|
||||
return `L${x} ${y}`
|
||||
}
|
||||
|
||||
// first segment is horizontal
|
||||
if (a.y === y) {
|
||||
const xDir = a.x < c.x ? -1 : 1
|
||||
const yDir = a.y < c.y ? 1 : -1
|
||||
return `L ${x + bendSize * xDir},${y}Q ${x},${y} ${x},${y + bendSize * yDir}`
|
||||
}
|
||||
|
||||
const xDir = a.x < c.x ? 1 : -1
|
||||
const yDir = a.y < c.y ? -1 : 1
|
||||
return `L ${x},${y + bendSize * yDir}Q ${x},${y} ${x + bendSize * xDir},${y}`
|
||||
}
|
||||
|
||||
export function getSmoothStepPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition = Position.Bottom,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition = Position.Top,
|
||||
borderRadius = 5,
|
||||
centerX,
|
||||
centerY,
|
||||
offset = 20,
|
||||
}: GetSmoothStepPathParams): [string, number, number, number, number] {
|
||||
const [points, labelX, labelY, offsetX, offsetY] = getPoints({
|
||||
source: { x: sourceX, y: sourceY },
|
||||
sourcePosition,
|
||||
target: { x: targetX, y: targetY },
|
||||
targetPosition,
|
||||
center: { x: centerX, y: centerY },
|
||||
offset,
|
||||
})
|
||||
|
||||
const path = points.reduce<string>((res, p, i) => {
|
||||
let segment = ''
|
||||
|
||||
if (i > 0 && i < points.length - 1) {
|
||||
segment = getBend(points[i - 1], p, points[i + 1], borderRadius)
|
||||
} else {
|
||||
segment = `${i === 0 ? 'M' : 'L'}${p.x} ${p.y}`
|
||||
}
|
||||
|
||||
res += segment
|
||||
|
||||
return res
|
||||
}, '')
|
||||
|
||||
return [path, labelX, labelY, offsetX, offsetY]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { getSimpleEdgeCenter } from './general'
|
||||
|
||||
export interface GetStraightPathParams {
|
||||
sourceX: number
|
||||
sourceY: number
|
||||
targetX: number
|
||||
targetY: number
|
||||
}
|
||||
|
||||
export function getStraightPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
}: GetStraightPathParams): [string, number, number, number, number] {
|
||||
const [centerX, centerY, offsetX, offsetY] = getSimpleEdgeCenter({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
})
|
||||
|
||||
return [`M ${sourceX},${sourceY}L ${targetX},${targetY}`, centerX, centerY, offsetX, offsetY]
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<script lang="ts" setup>
|
||||
import { useHandle, useNode, useVueFlow } from '../../composables'
|
||||
import type { Position } from '../../types'
|
||||
import { ConnectionMode } from '../../types'
|
||||
import type { HandleProps } from '../../types/handle'
|
||||
import { getDimensions } from '../../utils'
|
||||
|
||||
const { type = 'source', position = 'top' as Position, connectable = true, id, isValidConnection } = defineProps<HandleProps>()
|
||||
|
||||
const { connectionStartHandle, connectionMode, vueFlowRef } = $(useVueFlow())
|
||||
|
||||
const { id: nodeId, node, nodeEl } = useNode()
|
||||
|
||||
const handle = ref<HTMLDivElement>()
|
||||
|
||||
const handleId = $computed(() => id ?? (connectionMode === ConnectionMode.Strict ? null : `${nodeId}__handle-${position}`))
|
||||
|
||||
const { onMouseDown, onClick } = useHandle()
|
||||
|
||||
const onMouseDownHandler = (event: MouseEvent) => {
|
||||
onMouseDown(event, handleId, nodeId, type === 'target', isValidConnection, undefined)
|
||||
}
|
||||
|
||||
const onClickHandler = (event: MouseEvent) => {
|
||||
onClick(event, handleId ?? null, nodeId, type, isValidConnection)
|
||||
}
|
||||
|
||||
const getClasses = computed(() => {
|
||||
return [
|
||||
'vue-flow__handle',
|
||||
`vue-flow__handle-${position}`,
|
||||
`vue-flow__handle-${handleId}`,
|
||||
'nodrag',
|
||||
{
|
||||
source: type !== 'target',
|
||||
target: type === 'target',
|
||||
connectable,
|
||||
connecting:
|
||||
connectionStartHandle &&
|
||||
connectionStartHandle.nodeId === nodeId &&
|
||||
connectionStartHandle.handleId === handleId &&
|
||||
connectionStartHandle.type === type,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
const existingBounds = node.handleBounds[type]?.find((b) => b.id === handleId)
|
||||
if (!vueFlowRef || existingBounds) return
|
||||
|
||||
const viewportNode = vueFlowRef.querySelector('.vue-flow__transformationpane')
|
||||
|
||||
if (!nodeEl || !handle.value || !viewportNode) return
|
||||
|
||||
const nodeBounds = nodeEl.value.getBoundingClientRect()
|
||||
|
||||
const handleBounds = handle.value.getBoundingClientRect()
|
||||
|
||||
const style = window.getComputedStyle(viewportNode)
|
||||
const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform)
|
||||
|
||||
const nextBounds = {
|
||||
id: handleId,
|
||||
position,
|
||||
x: (handleBounds.left - nodeBounds.left) / zoom,
|
||||
y: (handleBounds.top - nodeBounds.top) / zoom,
|
||||
...getDimensions(handle.value),
|
||||
}
|
||||
|
||||
node.handleBounds[type] = [...(node.handleBounds[type] ?? []), nextBounds]
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'Handle',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="handle"
|
||||
:data-handleid="handleId"
|
||||
:data-nodeid="nodeId"
|
||||
:data-handlepos="position"
|
||||
:class="getClasses"
|
||||
@mousedown="onMouseDownHandler"
|
||||
@click="onClickHandler"
|
||||
>
|
||||
<slot :id="id" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { FunctionalComponent } from 'vue'
|
||||
import Handle from '../Handle/Handle.vue'
|
||||
import type { NodeProps } from '~/types'
|
||||
import { Position } from '~/types'
|
||||
|
||||
const DefaultNode: FunctionalComponent<NodeProps> = function ({
|
||||
sourcePosition = Position.Bottom,
|
||||
targetPosition = Position.Top,
|
||||
label,
|
||||
connectable = false,
|
||||
isValidTargetPos,
|
||||
isValidSourcePos,
|
||||
}) {
|
||||
return [
|
||||
h(Handle, { type: 'target', position: targetPosition, isConnectable: connectable, isValidConnection: isValidTargetPos }),
|
||||
typeof label !== 'string' && label ? h(label) : h('div', { innerHTML: label }),
|
||||
h(Handle, { type: 'source', position: sourcePosition, isConnectable: connectable, isValidConnection: isValidSourcePos }),
|
||||
]
|
||||
}
|
||||
|
||||
DefaultNode.props = ['sourcePosition', 'targetPosition', 'label', 'isValidTargetPos', 'isValidSourcePos', 'connectable']
|
||||
DefaultNode.inheritAttrs = false
|
||||
|
||||
export default DefaultNode
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { FunctionalComponent } from 'vue'
|
||||
import Handle from '../Handle/Handle.vue'
|
||||
import type { NodeProps } from '~/types'
|
||||
import { Position } from '~/types'
|
||||
|
||||
const InputNode: FunctionalComponent<NodeProps> = function ({
|
||||
sourcePosition = Position.Bottom,
|
||||
label,
|
||||
connectable = false,
|
||||
isValidSourcePos,
|
||||
}) {
|
||||
return [
|
||||
typeof label !== 'string' && label ? h(label) : h('div', { innerHTML: label }),
|
||||
h(Handle, { type: 'source', position: sourcePosition, isConnectable: connectable, isValidConnection: isValidSourcePos }),
|
||||
]
|
||||
}
|
||||
|
||||
InputNode.props = ['sourcePosition', 'label', 'isValidSourcePos', 'connectable']
|
||||
InputNode.inheritAttrs = false
|
||||
|
||||
export default InputNode
|
||||
@@ -0,0 +1,247 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import { useDrag, useNodeHooks, useVueFlow } from '../../composables'
|
||||
import type { GraphNode, NodeComponent, SnapGrid, XYZPosition } from '../../types'
|
||||
import { NodeId, NodeRef } from '../../context'
|
||||
import { getConnectedEdges, getXYZPos, handleNodeClick } from '../../utils'
|
||||
|
||||
const { id, type, name, draggable, selectable, connectable, snapGrid, ...props } = defineProps<{
|
||||
id: string
|
||||
draggable: boolean
|
||||
selectable: boolean
|
||||
connectable: boolean
|
||||
snapGrid?: SnapGrid
|
||||
type: NodeComponent | Function | Object | false
|
||||
name: string
|
||||
node: GraphNode
|
||||
resizeObserver: ResizeObserver
|
||||
}>()
|
||||
|
||||
provide(NodeId, id)
|
||||
|
||||
const {
|
||||
nodeExtent,
|
||||
edges,
|
||||
viewport,
|
||||
noPanClassName,
|
||||
selectNodesOnDrag,
|
||||
nodesSelectionActive,
|
||||
multiSelectionActive,
|
||||
emits,
|
||||
getNode,
|
||||
removeSelectedElements,
|
||||
addSelectedNodes,
|
||||
updateNodeDimensions,
|
||||
onUpdateNodeInternals,
|
||||
} = $(useVueFlow())
|
||||
|
||||
const node = $(useVModel(props, 'node'))
|
||||
|
||||
const parentNode = $computed(() => (node.parentNode ? getNode(node.parentNode) : undefined))
|
||||
|
||||
const nodeElement = ref()
|
||||
|
||||
provide(NodeRef, nodeElement)
|
||||
|
||||
const { emit, on } = useNodeHooks(node, emits)
|
||||
|
||||
const dragging = useDrag({
|
||||
id,
|
||||
el: nodeElement,
|
||||
disabled: computed(() => !draggable),
|
||||
onStart(event, node, nodes) {
|
||||
emit.dragStart({ event, node, nodes })
|
||||
},
|
||||
onDrag(event, node, nodes) {
|
||||
emit.drag({ event, node, nodes })
|
||||
},
|
||||
onStop(event, node, nodes) {
|
||||
emit.dragStop({ event, node, nodes })
|
||||
},
|
||||
})
|
||||
|
||||
const updatePosition = (nodePos: XYZPosition, parentPos?: XYZPosition) => {
|
||||
if (parentPos) {
|
||||
node.computedPosition = getXYZPos({ x: parentPos.x, y: parentPos.y, z: parentPos.z! }, nodePos)
|
||||
} else {
|
||||
node.computedPosition = nodePos
|
||||
}
|
||||
}
|
||||
|
||||
const updateInternals = () => {
|
||||
if (nodeElement.value) updateNodeDimensions([{ id, nodeElement: nodeElement.value, forceUpdate: true }])
|
||||
|
||||
updatePosition(
|
||||
{
|
||||
x: node.position.x,
|
||||
y: node.position.y,
|
||||
z: node.computedPosition.z ? node.computedPosition.z : node.selected ? 1000 : 0,
|
||||
},
|
||||
parentNode ? { ...parentNode.computedPosition } : undefined,
|
||||
)
|
||||
}
|
||||
|
||||
onUpdateNodeInternals((updateIds) => {
|
||||
if (updateIds.includes(id)) {
|
||||
updateInternals()
|
||||
}
|
||||
})
|
||||
|
||||
updatePosition(
|
||||
{
|
||||
x: node.position.x,
|
||||
y: node.position.y,
|
||||
z: node.computedPosition.z ? node.computedPosition.z : node.selected ? 1000 : 0,
|
||||
},
|
||||
parentNode ? { ...parentNode.computedPosition } : undefined,
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
props.resizeObserver.observe(nodeElement.value)
|
||||
})
|
||||
|
||||
watch(
|
||||
[() => node.type, () => node.sourcePosition, () => node.targetPosition],
|
||||
() => {
|
||||
updateNodeDimensions([{ id, nodeElement: nodeElement.value, forceUpdate: true }])
|
||||
},
|
||||
{ flush: 'post' },
|
||||
)
|
||||
|
||||
watch(
|
||||
[
|
||||
() => node.position.x,
|
||||
() => node.position.y,
|
||||
() => parentNode?.computedPosition.x,
|
||||
() => parentNode?.computedPosition.y,
|
||||
() => parentNode?.computedPosition.z,
|
||||
() => node.selected,
|
||||
() => node.dimensions,
|
||||
() => parentNode?.dimensions,
|
||||
],
|
||||
([newX, newY, parentX, parentY, parentZ]) => {
|
||||
const xyzPos = {
|
||||
x: newX,
|
||||
y: newY,
|
||||
z: node.selected ? 1000 : 0,
|
||||
}
|
||||
|
||||
updatePosition(xyzPos, parentX && parentY ? { x: parentX, y: parentY, z: parentZ! } : undefined)
|
||||
},
|
||||
{ flush: 'post' },
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
props.resizeObserver.unobserve(nodeElement.value)
|
||||
})
|
||||
|
||||
const onMouseEnter = (event: MouseEvent) => {
|
||||
if (!dragging?.value) {
|
||||
emit.mouseEnter({ event, node, connectedEdges: getConnectedEdges([node], edges) })
|
||||
}
|
||||
}
|
||||
|
||||
const onMouseMove = (event: MouseEvent) => {
|
||||
if (!dragging?.value) {
|
||||
emit.mouseMove({ event, node, connectedEdges: getConnectedEdges([node], edges) })
|
||||
}
|
||||
}
|
||||
|
||||
const onMouseLeave = (event: MouseEvent) => {
|
||||
if (!dragging?.value) {
|
||||
emit.mouseLeave({ event, node, connectedEdges: getConnectedEdges([node], edges) })
|
||||
}
|
||||
}
|
||||
|
||||
const onContextMenu = (event: MouseEvent) => {
|
||||
emit.contextMenu({
|
||||
event,
|
||||
node,
|
||||
connectedEdges: getConnectedEdges([node], edges),
|
||||
})
|
||||
}
|
||||
|
||||
const onDoubleClick = (event: MouseEvent) => {
|
||||
emit.doubleClick({ event, node, connectedEdges: getConnectedEdges([node], edges) })
|
||||
}
|
||||
|
||||
const onSelectNode = (event: MouseEvent) => {
|
||||
if (selectable && (!selectNodesOnDrag || !draggable)) {
|
||||
handleNodeClick(node, multiSelectionActive, addSelectedNodes, removeSelectedElements, $$(nodesSelectionActive))
|
||||
}
|
||||
emit.click({ event, node, connectedEdges: getConnectedEdges([node], edges) })
|
||||
}
|
||||
|
||||
const getClass = computed(() => {
|
||||
return node.class instanceof Function ? node.class(node) : node.class
|
||||
})
|
||||
|
||||
const getStyle = computed(() => {
|
||||
const styles = (node.style instanceof Function ? node.style(node) : node.style) || {}
|
||||
const width = node.width instanceof Function ? node.width(node) : node.width
|
||||
const height = node.height instanceof Function ? node.height(node) : node.height
|
||||
if (width) styles.width = typeof width === 'string' ? width : `${width}px`
|
||||
if (height) styles.height = typeof height === 'string' ? height : `${height}px`
|
||||
|
||||
return styles
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'Node',
|
||||
inheritAttrs: false,
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="nodeElement"
|
||||
class="vue-flow__node"
|
||||
:class="[
|
||||
`vue-flow__node-${name}`,
|
||||
noPanClassName,
|
||||
{
|
||||
dragging,
|
||||
selected: node.selected,
|
||||
selectable,
|
||||
},
|
||||
getClass,
|
||||
]"
|
||||
:style="{
|
||||
zIndex: node.computedPosition.z ?? 0,
|
||||
transform: `translate(${node.computedPosition.x}px,${node.computedPosition.y}px)`,
|
||||
pointerEvents: selectable || draggable ? 'all' : 'none',
|
||||
...getStyle,
|
||||
}"
|
||||
:data-id="node.id"
|
||||
@mouseenter="onMouseEnter"
|
||||
@mousemove="onMouseMove"
|
||||
@mouseleave="onMouseLeave"
|
||||
@contextmenu="onContextMenu"
|
||||
@click="onSelectNode"
|
||||
@dblclick="onDoubleClick"
|
||||
>
|
||||
<component
|
||||
:is="type"
|
||||
:id="node.id"
|
||||
:type="node.type"
|
||||
:data="node.data"
|
||||
:events="{ ...node.events, ...on }"
|
||||
:selected="!!node.selected"
|
||||
:connectable="connectable"
|
||||
:position="node.position"
|
||||
:dimensions="node.dimensions"
|
||||
:is-valid-target-pos="node.isValidTargetPos"
|
||||
:is-valid-source-pos="node.isValidSourcePos"
|
||||
:parent-node="node.parentNode"
|
||||
:dragging="dragging"
|
||||
:z-index="node.computedPosition.z"
|
||||
:target-position="node.targetPosition"
|
||||
:source-position="node.sourcePosition"
|
||||
:label="node.label"
|
||||
:drag-handle="node.dragHandle"
|
||||
@update-node-internals="updateInternals"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { FunctionalComponent } from 'vue'
|
||||
import Handle from '../Handle/Handle.vue'
|
||||
import type { NodeProps } from '~/types'
|
||||
import { Position } from '~/types'
|
||||
|
||||
const OutputNode: FunctionalComponent<NodeProps> = function ({
|
||||
targetPosition = Position.Top,
|
||||
label,
|
||||
connectable = false,
|
||||
isValidTargetPos,
|
||||
}) {
|
||||
return [
|
||||
h(Handle, { type: 'target', position: targetPosition, isConnectable: connectable, isValidConnection: isValidTargetPos }),
|
||||
typeof label !== 'string' && label ? h(label) : h('div', { innerHTML: label }),
|
||||
]
|
||||
}
|
||||
|
||||
OutputNode.props = ['targetPosition', 'label', 'isValidTargetPos', 'connectable']
|
||||
OutputNode.inheritAttrs = false
|
||||
|
||||
export default OutputNode
|
||||
@@ -0,0 +1,4 @@
|
||||
export { default as DefaultNode } from './DefaultNode'
|
||||
export { default as InputNode } from './InputNode'
|
||||
export { default as OutputNode } from './OutputNode'
|
||||
export { default as NodeWrapper } from './NodeWrapper.vue'
|
||||
@@ -0,0 +1,48 @@
|
||||
<script lang="ts" setup>
|
||||
import { useDrag, useVueFlow } from '../../composables'
|
||||
import { getRectOfNodes } from '../../utils'
|
||||
|
||||
const { emits, setState, viewport, getSelectedNodes, snapToGrid, snapGrid, noPanClassName } = $(useVueFlow())
|
||||
|
||||
const el = ref()
|
||||
|
||||
useDrag({
|
||||
el,
|
||||
onStart(event, node, nodes) {
|
||||
emits.selectionDragStart({ event, node, nodes })
|
||||
},
|
||||
onDrag(event, node, nodes) {
|
||||
emits.selectionDrag({ event, node, nodes })
|
||||
},
|
||||
onStop(event, node, nodes) {
|
||||
emits.selectionDragStop({ event, node, nodes })
|
||||
},
|
||||
})
|
||||
|
||||
const selectedNodesBBox = $computed(() => getRectOfNodes(getSelectedNodes))
|
||||
|
||||
const innerStyle = computed(() => ({
|
||||
width: `${selectedNodesBBox.width}px`,
|
||||
height: `${selectedNodesBBox.height}px`,
|
||||
top: `${selectedNodesBBox.y}px`,
|
||||
left: `${selectedNodesBBox.x}px`,
|
||||
}))
|
||||
|
||||
const onContextMenu = (event: MouseEvent) => emits.selectionContextMenu({ event, nodes: getSelectedNodes })
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'NodesSelection',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="vue-flow__nodesselection vue-flow__container"
|
||||
:class="noPanClassName"
|
||||
:style="{ transform: `translate(${viewport.x}px,${viewport.y}px) scale(${viewport.zoom})` }"
|
||||
>
|
||||
<div ref="el" class="vue-flow__nodesselection-rect" :style="innerStyle" @contextmenu="onContextMenu" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="ts" setup>
|
||||
const { width, height, x, y } = defineProps<{
|
||||
width: number
|
||||
height: number
|
||||
x: number
|
||||
y: number
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'SelectionRect',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="vue-flow__selection"
|
||||
:style="{
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
transform: `translate(${x}px, ${y}px)`,
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,115 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVueFlow } from '../../composables'
|
||||
import type { SelectionRect as Rect } from '../../types'
|
||||
import { getConnectedEdges, getNodesInside } from '../../utils'
|
||||
import SelectionRect from './SelectionRect.vue'
|
||||
import { getMousePosition } from './utils'
|
||||
|
||||
const { userSelectionActive, nodesSelectionActive, getNodes, getEdges, viewport, addSelectedEdges, addSelectedNodes } =
|
||||
useVueFlow()
|
||||
|
||||
let prevNodes = $ref(0)
|
||||
|
||||
let prevEdges = $ref(0)
|
||||
|
||||
const initialRect = () => ({
|
||||
width: 0,
|
||||
height: 0,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
draw: false,
|
||||
})
|
||||
|
||||
let rect = $ref<Rect>(initialRect())
|
||||
|
||||
const reset = () => {
|
||||
rect = initialRect()
|
||||
prevNodes = 0
|
||||
prevEdges = 0
|
||||
|
||||
userSelectionActive.value = false
|
||||
}
|
||||
|
||||
const onMouseDown = (event: MouseEvent) => {
|
||||
const mousePos = getMousePosition(event)
|
||||
if (!mousePos) return
|
||||
|
||||
rect = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
startX: mousePos.x,
|
||||
startY: mousePos.y,
|
||||
x: mousePos.x,
|
||||
y: mousePos.y,
|
||||
draw: true,
|
||||
}
|
||||
|
||||
userSelectionActive.value = true
|
||||
nodesSelectionActive.value = true
|
||||
}
|
||||
|
||||
const onMouseMove = (event: MouseEvent) => {
|
||||
if (!userSelectionActive || !rect.draw) return
|
||||
|
||||
const mousePos = getMousePosition(event)
|
||||
if (!mousePos) return
|
||||
|
||||
const startX = rect.startX
|
||||
const startY = rect.startY
|
||||
|
||||
const nextUserSelectRect: Rect = {
|
||||
...rect,
|
||||
x: mousePos.x < startX ? mousePos.x : rect.x,
|
||||
y: mousePos.y < startY ? mousePos.y : rect.y,
|
||||
width: Math.abs(mousePos.x - startX),
|
||||
height: Math.abs(mousePos.y - startY),
|
||||
}
|
||||
|
||||
const selectedNodes = getNodesInside(getNodes.value, rect, viewport.value)
|
||||
const selectedEdges = getConnectedEdges(selectedNodes, getEdges.value)
|
||||
|
||||
rect = nextUserSelectRect
|
||||
|
||||
addSelectedNodes(selectedNodes)
|
||||
addSelectedEdges(selectedEdges)
|
||||
|
||||
prevNodes = selectedNodes.length
|
||||
prevEdges = selectedEdges.length
|
||||
}
|
||||
|
||||
const onMouseUp = () => {
|
||||
rect = initialRect()
|
||||
|
||||
nodesSelectionActive.value = prevNodes > 0
|
||||
userSelectionActive.value = false
|
||||
}
|
||||
|
||||
const onMouseLeave = () => {
|
||||
nodesSelectionActive.value = prevNodes > 0
|
||||
|
||||
reset()
|
||||
}
|
||||
|
||||
onBeforeUnmount(reset)
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'UserSelection',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="vue-flow__selectionpane vue-flow__container"
|
||||
@mousedown="onMouseDown"
|
||||
@mousemove="onMouseMove"
|
||||
@click="onMouseUp"
|
||||
@mouseup="onMouseUp"
|
||||
@mouseleave="onMouseLeave"
|
||||
>
|
||||
<SelectionRect v-if="rect.draw" :width="rect.width" :height="rect.height" :x="rect.x" :y="rect.y" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { 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 { default as ConnectionLine } from './ConnectionLine/ConnectionLine.vue'
|
||||
export { default as Handle } from './Handle/Handle.vue'
|
||||
export { default as NodesSelection } from './NodesSelection/NodesSelection.vue'
|
||||
export { default as UserSelection } from './UserSelection/UserSelection.vue'
|
||||
@@ -0,0 +1,10 @@
|
||||
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'
|
||||
export { default as useDrag } from './useDrag'
|
||||
export { default as useNodeHooks } from './useNodeHooks'
|
||||
export { default as useEdgeHooks } from './useEdgeHooks'
|
||||
export { default as useEdge } from './useEdge'
|
||||
export { default as useNode } from './useNode'
|
||||
@@ -0,0 +1,167 @@
|
||||
import type { D3DragEvent, SubjectPosition } from 'd3-drag'
|
||||
import { drag } from 'd3-drag'
|
||||
import { select } from 'd3-selection'
|
||||
import type { Ref } from 'vue'
|
||||
import type { MaybeRef } from '@vueuse/core'
|
||||
import useVueFlow from './useVueFlow'
|
||||
import { handleNodeClick, pointToRendererPoint } from '~/utils'
|
||||
import type { NodeDragEvent, NodeDragItem, SnapGrid, XYPosition } from '~/types'
|
||||
import { getDragItems, getEventHandlerParams, hasSelector, updatePosition } from '~/utils/drag'
|
||||
|
||||
export type UseDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>
|
||||
|
||||
interface UseDragParams {
|
||||
onStart: (event: NodeDragEvent['event'], currentNode: NodeDragEvent['node'], nodes: NodeDragEvent['nodes']) => void
|
||||
onDrag: (event: NodeDragEvent['event'], currentNode: NodeDragEvent['node'], nodes: NodeDragEvent['nodes']) => void
|
||||
onStop: (event: NodeDragEvent['event'], currentNode: NodeDragEvent['node'], nodes: NodeDragEvent['nodes']) => void
|
||||
el: Ref<Element>
|
||||
disabled?: MaybeRef<boolean>
|
||||
id?: string
|
||||
}
|
||||
|
||||
function useDrag(params: UseDragParams) {
|
||||
const scope = effectScope()
|
||||
|
||||
const dragging = scope.run(() => {
|
||||
const {
|
||||
viewport,
|
||||
snapToGrid,
|
||||
snapGrid: globalSnapGrid,
|
||||
noDragClassName,
|
||||
nodes,
|
||||
nodeExtent,
|
||||
getNode,
|
||||
multiSelectionActive,
|
||||
nodesSelectionActive,
|
||||
selectNodesOnDrag,
|
||||
removeSelectedElements,
|
||||
addSelectedNodes,
|
||||
updateNodePositions,
|
||||
} = $(useVueFlow())
|
||||
|
||||
const { onStart, onDrag, onStop, el, disabled = false, id } = $(params)
|
||||
|
||||
const dragging = ref(false)
|
||||
let dragItems = $ref<NodeDragItem[]>()
|
||||
let lastPos = $ref<Partial<XYPosition>>({ x: undefined, y: undefined })
|
||||
let dragHandler = $ref<any>()
|
||||
|
||||
const hasSnapGrid = (sg?: SnapGrid) => (sg ?? snapToGrid ? globalSnapGrid : undefined)
|
||||
|
||||
const getMousePosition = (event: UseDragEvent, snapGrid: SnapGrid) => {
|
||||
const x = event.sourceEvent.touches ? event.sourceEvent.touches[0].clientX : event.sourceEvent.clientX
|
||||
const y = event.sourceEvent.touches ? event.sourceEvent.touches[0].clientY : event.sourceEvent.clientY
|
||||
|
||||
return pointToRendererPoint(
|
||||
{
|
||||
x,
|
||||
y,
|
||||
},
|
||||
viewport,
|
||||
!!snapGrid ?? snapToGrid,
|
||||
snapGrid ?? globalSnapGrid,
|
||||
)
|
||||
}
|
||||
|
||||
watch([() => disabled, () => el], () => {
|
||||
if (el) {
|
||||
const selection = select(el)
|
||||
const node = id ? getNode(id) : undefined
|
||||
|
||||
if (disabled) {
|
||||
selection.on('.drag', null)
|
||||
} else {
|
||||
dragHandler = drag()
|
||||
.on('start', (event: UseDragEvent) => {
|
||||
if (!selectNodesOnDrag && !multiSelectionActive && id) {
|
||||
if (!node?.selected) {
|
||||
removeSelectedElements()
|
||||
}
|
||||
}
|
||||
|
||||
if (node && !disabled && selectNodesOnDrag) {
|
||||
handleNodeClick(node, multiSelectionActive, addSelectedNodes, removeSelectedElements, $$(nodesSelectionActive))
|
||||
}
|
||||
|
||||
const mousePos = getMousePosition(event, hasSnapGrid(node?.snapGrid) as SnapGrid)
|
||||
dragItems = getDragItems(nodes, mousePos, getNode, id)
|
||||
|
||||
if (onStart && dragItems) {
|
||||
const [currentNode, nodes] = getEventHandlerParams({
|
||||
id,
|
||||
dragItems,
|
||||
getNode: $$(getNode),
|
||||
})
|
||||
onStart(event.sourceEvent, currentNode, nodes)
|
||||
}
|
||||
})
|
||||
.on('drag', (event: UseDragEvent) => {
|
||||
const snapGrid = hasSnapGrid(node?.snapGrid) as SnapGrid
|
||||
|
||||
const mousePos = getMousePosition(event, snapGrid)
|
||||
|
||||
// skip events without movement
|
||||
if ((lastPos.x !== mousePos.x || lastPos.y !== mousePos.y) && dragItems) {
|
||||
lastPos = mousePos
|
||||
dragItems = dragItems.map((n) =>
|
||||
updatePosition(
|
||||
n,
|
||||
mousePos,
|
||||
!!snapGrid ?? snapToGrid,
|
||||
snapGrid ?? globalSnapGrid,
|
||||
n.parentNode ? getNode(n.parentNode) : undefined,
|
||||
nodeExtent,
|
||||
),
|
||||
)
|
||||
|
||||
updateNodePositions(dragItems, true, true)
|
||||
dragging.value = true
|
||||
|
||||
if (onDrag) {
|
||||
const [currentNode, nodes] = getEventHandlerParams({
|
||||
id,
|
||||
dragItems,
|
||||
getNode: $$(getNode),
|
||||
})
|
||||
onDrag(event.sourceEvent, currentNode, nodes)
|
||||
}
|
||||
}
|
||||
|
||||
event.on('end', (event) => {
|
||||
dragging.value = false
|
||||
if (onStop && dragItems) {
|
||||
updateNodePositions(dragItems, false, false)
|
||||
|
||||
const [currentNode, nodes] = getEventHandlerParams({
|
||||
id,
|
||||
dragItems,
|
||||
getNode: $$(getNode),
|
||||
})
|
||||
onStop(event.sourceEvent, currentNode, nodes)
|
||||
}
|
||||
})
|
||||
})
|
||||
.filter((event: D3DragEvent<HTMLDivElement, null, SubjectPosition>['sourceEvent']) => {
|
||||
const target = event.target as HTMLDivElement
|
||||
return (
|
||||
!event.button &&
|
||||
(!noDragClassName ||
|
||||
(!hasSelector(target, `.${noDragClassName}`, $$(el)) &&
|
||||
(!node?.dragHandle || hasSelector(target, node.dragHandle, $$(el)))))
|
||||
)
|
||||
})
|
||||
|
||||
selection.call(dragHandler)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return dragging
|
||||
})
|
||||
|
||||
tryOnScopeDispose(() => scope.stop())
|
||||
|
||||
return dragging
|
||||
}
|
||||
|
||||
export default useDrag
|
||||
@@ -0,0 +1,29 @@
|
||||
import useVueFlow from './useVueFlow'
|
||||
import { EdgeId, EdgeRef } from '~/context'
|
||||
import type { CustomEvent, ElementData } from '~/types'
|
||||
|
||||
/**
|
||||
* Access an edge
|
||||
*
|
||||
* If no edge id is provided, the edge id is injected from context
|
||||
*
|
||||
* Meaning if you do not provide an id, this composable has to be called in a child of your custom edge component, or it will throw
|
||||
*/
|
||||
export default function useEdge<Data = ElementData, CustomEvents extends Record<string, CustomEvent> = any>(id?: string) {
|
||||
const edgeId = id ?? inject(EdgeId, '')
|
||||
const edgeEl = inject(EdgeRef, null)
|
||||
|
||||
const { findEdge } = useVueFlow()
|
||||
|
||||
const edge = findEdge<Data, CustomEvents>(edgeId)
|
||||
|
||||
if (!edge) {
|
||||
throw new Error(`[vue-flow]: useEdge - Edge with id ${edgeId} not found!`)
|
||||
}
|
||||
|
||||
return {
|
||||
id: edgeId,
|
||||
edge,
|
||||
edgeEl,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { EdgeEventsEmit, EdgeEventsOn, GraphEdge, VueFlowStore } from '~/types'
|
||||
|
||||
const createEdgeHooks = () => ({
|
||||
doubleClick: createEventHook(),
|
||||
click: createEventHook(),
|
||||
mouseEnter: createEventHook(),
|
||||
mouseMove: createEventHook(),
|
||||
mouseLeave: createEventHook(),
|
||||
contextMenu: createEventHook(),
|
||||
updateStart: createEventHook(),
|
||||
update: createEventHook(),
|
||||
updateEnd: createEventHook(),
|
||||
})
|
||||
|
||||
export default function useEdgeHooks(edge: GraphEdge, emits: VueFlowStore['emits']): { emit: EdgeEventsEmit; on: EdgeEventsOn } {
|
||||
const edgeHooks = createEdgeHooks()
|
||||
|
||||
edgeHooks.doubleClick.on((event) => {
|
||||
emits.edgeDoubleClick(event)
|
||||
edge.events?.doubleClick?.(event)
|
||||
})
|
||||
|
||||
edgeHooks.click.on((event) => {
|
||||
emits.edgeClick(event)
|
||||
edge.events?.click?.(event)
|
||||
})
|
||||
|
||||
edgeHooks.mouseEnter.on((event) => {
|
||||
emits.edgeMouseEnter(event)
|
||||
edge.events?.mouseEnter?.(event)
|
||||
})
|
||||
|
||||
edgeHooks.mouseMove.on((event) => {
|
||||
emits.edgeMouseMove(event)
|
||||
edge.events?.mouseMove?.(event)
|
||||
})
|
||||
|
||||
edgeHooks.mouseLeave.on((event) => {
|
||||
emits.edgeMouseLeave(event)
|
||||
edge.events?.mouseLeave?.(event)
|
||||
})
|
||||
|
||||
edgeHooks.contextMenu.on((event) => {
|
||||
emits.edgeContextMenu(event)
|
||||
edge.events?.contextMenu?.(event)
|
||||
})
|
||||
|
||||
edgeHooks.updateStart.on((event) => {
|
||||
emits.edgeUpdateStart(event)
|
||||
edge.events?.updateStart?.(event)
|
||||
})
|
||||
|
||||
edgeHooks.update.on((event) => {
|
||||
emits.edgeUpdate(event)
|
||||
edge.events?.update?.(event)
|
||||
})
|
||||
|
||||
edgeHooks.updateEnd.on((event) => {
|
||||
emits.edgeUpdateEnd(event)
|
||||
edge.events?.updateEnd?.(event)
|
||||
})
|
||||
|
||||
return Object.entries(edgeHooks).reduce(
|
||||
(hooks, [key, value]) => {
|
||||
hooks.emit[key as keyof EdgeEventsEmit] = value.trigger
|
||||
hooks.on[key as keyof EdgeEventsOn] = value.on
|
||||
|
||||
return hooks
|
||||
},
|
||||
{ emit: {} as EdgeEventsEmit, on: {} as EdgeEventsOn },
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
import { isFunction } from '@vueuse/core'
|
||||
import useVueFlow from './useVueFlow'
|
||||
import { getHostForElement } from '~/utils'
|
||||
import type { Connection, Getters, GraphEdge, HandleType, ValidConnectionFunc } from '~/types'
|
||||
import { ConnectionMode } from '~/types'
|
||||
|
||||
interface 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 | undefined,
|
||||
doc: Document,
|
||||
edges: GraphEdge[],
|
||||
getNode: Getters['getNode'],
|
||||
) => {
|
||||
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: '', target: '', 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 sourceId = isTarget ? elementBelowNodeId : nodeId
|
||||
const sourceHandleId = isTarget ? elementBelowHandleId : handleId
|
||||
const targetId = isTarget ? nodeId : elementBelowNodeId
|
||||
const targetHandleId = isTarget ? handleId : elementBelowHandleId
|
||||
|
||||
const connection: Connection = {
|
||||
source: sourceId,
|
||||
sourceHandle: sourceHandleId,
|
||||
target: targetId,
|
||||
targetHandle: targetHandleId,
|
||||
}
|
||||
|
||||
result.connection = connection
|
||||
|
||||
result.isValid =
|
||||
(isFunction(isValidConnection)
|
||||
? isValidConnection(connection, { edges, sourceNode: getNode(sourceId)!, targetNode: getNode(targetId)! })
|
||||
: true) ||
|
||||
!result.connection.target ||
|
||||
!result.connection.source
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const resetRecentHandle = (hoveredHandle: Element): void => {
|
||||
hoveredHandle?.classList.remove('vue-flow__handle-valid')
|
||||
hoveredHandle?.classList.remove('vue-flow__handle-connecting')
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const {
|
||||
edges,
|
||||
connectOnClick,
|
||||
nodesConnectable,
|
||||
connectionStartHandle,
|
||||
connectionMode,
|
||||
emits,
|
||||
startConnection,
|
||||
updateConnection,
|
||||
endConnection,
|
||||
getNode,
|
||||
vueFlowRef,
|
||||
} = $(useVueFlow())
|
||||
|
||||
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 doc = getHostForElement(event.target as HTMLElement)
|
||||
if (!doc) return
|
||||
|
||||
let validConnectFunc = isValidConnection
|
||||
|
||||
const node = getNode(nodeId)
|
||||
|
||||
if (node && (typeof node.connectable === 'undefined' ? nodesConnectable : node.connectable) === false) return
|
||||
|
||||
if (!isValidConnection) {
|
||||
if (node) validConnectFunc = !isTarget ? node.isValidTargetPos : node.isValidSourcePos
|
||||
}
|
||||
|
||||
const elementBelow = doc.elementFromPoint(event.clientX, event.clientY)
|
||||
const elementBelowIsTarget = elementBelow?.classList.contains('target')
|
||||
const elementBelowIsSource = elementBelow?.classList.contains('source')
|
||||
|
||||
if (!vueFlowRef || (!elementBelowIsTarget && !elementBelowIsSource && !elementEdgeUpdaterType)) return
|
||||
|
||||
const handleType = elementEdgeUpdaterType ?? (elementBelowIsTarget ? 'target' : 'source')
|
||||
|
||||
const containerBounds = vueFlowRef.getBoundingClientRect()
|
||||
|
||||
startConnection(
|
||||
{
|
||||
nodeId,
|
||||
handleId,
|
||||
type: handleType,
|
||||
},
|
||||
{
|
||||
x: event.clientX - containerBounds.left,
|
||||
y: event.clientY - containerBounds.top,
|
||||
},
|
||||
event,
|
||||
)
|
||||
|
||||
function onMouseMove(event: MouseEvent) {
|
||||
updateConnection({
|
||||
x: event.clientX - containerBounds.left,
|
||||
y: event.clientY - containerBounds.top,
|
||||
})
|
||||
|
||||
const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid(
|
||||
event,
|
||||
connectionMode,
|
||||
isTarget,
|
||||
nodeId,
|
||||
handleId,
|
||||
validConnectFunc,
|
||||
doc,
|
||||
edges,
|
||||
getNode,
|
||||
)
|
||||
|
||||
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,
|
||||
connectionMode,
|
||||
isTarget,
|
||||
nodeId,
|
||||
handleId,
|
||||
validConnectFunc,
|
||||
doc,
|
||||
edges,
|
||||
getNode,
|
||||
)
|
||||
|
||||
const isOwnHandle = connection.source === connection.target
|
||||
|
||||
if (isValid && !isOwnHandle) {
|
||||
if (!onEdgeUpdate) emits.connect(connection)
|
||||
else onEdgeUpdate(connection)
|
||||
}
|
||||
|
||||
if (elementEdgeUpdaterType) onEdgeUpdateEnd?.()
|
||||
|
||||
resetRecentHandle(recentHoveredHandle)
|
||||
|
||||
endConnection(event)
|
||||
|
||||
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 (!connectOnClick) return
|
||||
if (!connectionStartHandle) {
|
||||
startConnection({ nodeId, type: handleType, handleId }, undefined, event)
|
||||
} else {
|
||||
let validConnectFunc: ValidConnectionFunc = isValidConnection ?? (() => true)
|
||||
|
||||
const node = getNode(nodeId)
|
||||
|
||||
if (node && (typeof node.connectable === 'undefined' ? 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,
|
||||
connectionMode,
|
||||
connectionStartHandle.type === 'target',
|
||||
connectionStartHandle.nodeId,
|
||||
connectionStartHandle.handleId || null,
|
||||
validConnectFunc,
|
||||
doc,
|
||||
edges,
|
||||
getNode,
|
||||
)
|
||||
|
||||
const isOwnHandle = connection.source === connection.target
|
||||
|
||||
if (isValid && !isOwnHandle) emits.connect(connection)
|
||||
|
||||
endConnection(event)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
onMouseDown,
|
||||
onClick,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { Ref } from 'vue'
|
||||
import type { KeyFilter, MaybeRef } from '@vueuse/core'
|
||||
import { isBoolean, isFunction } from '@vueuse/core'
|
||||
import useWindow from './useWindow'
|
||||
|
||||
function isInputDOMNode(event: KeyboardEvent): boolean {
|
||||
const target = event.target as HTMLElement
|
||||
const hasAttribute = isFunction(target.hasAttribute) ? target.hasAttribute('contenteditable') : false
|
||||
const closest = isFunction(target.closest) ? target.closest('.nokey') : null
|
||||
|
||||
return ['INPUT', 'SELECT', 'TEXTAREA'].includes(target?.nodeName) || hasAttribute || !!closest
|
||||
}
|
||||
|
||||
export default (keyFilter: MaybeRef<KeyFilter>, onChange?: (keyPressed: boolean) => void): Ref<boolean> => {
|
||||
const window = useWindow()
|
||||
|
||||
let isPressed = $ref(unref(keyFilter) === true)
|
||||
|
||||
watch($$(isPressed), () => {
|
||||
if (onChange && typeof onChange === 'function') onChange(isPressed)
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
const unrefKeyFilter = unref(keyFilter)
|
||||
|
||||
if (isBoolean(unrefKeyFilter)) {
|
||||
isPressed = unrefKeyFilter
|
||||
return
|
||||
}
|
||||
|
||||
onKeyStroke(
|
||||
unrefKeyFilter,
|
||||
(e) => {
|
||||
if (isInputDOMNode(e)) return
|
||||
|
||||
e.preventDefault()
|
||||
isPressed = true
|
||||
},
|
||||
{ eventName: 'keydown' },
|
||||
)
|
||||
|
||||
onKeyStroke(
|
||||
unrefKeyFilter,
|
||||
(e) => {
|
||||
if (isInputDOMNode(e)) return
|
||||
|
||||
e.preventDefault()
|
||||
isPressed = false
|
||||
},
|
||||
{ eventName: 'keyup' },
|
||||
)
|
||||
})
|
||||
|
||||
if (typeof window.addEventListener !== 'undefined') {
|
||||
useEventListener(window, 'blur', () => {
|
||||
isPressed = false
|
||||
})
|
||||
}
|
||||
|
||||
return $$(isPressed)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import useVueFlow from './useVueFlow'
|
||||
import { NodeId, NodeRef } from '~/context'
|
||||
import type { CustomEvent, ElementData } from '~/types'
|
||||
import { getConnectedEdges } from '~/utils'
|
||||
|
||||
/**
|
||||
* Access a node, it's parent (if one exists) and connected edges
|
||||
*
|
||||
* If no node id is provided, the node id is injected from context
|
||||
*
|
||||
* Meaning if you do not provide an id, this composable has to be called in a child of your custom node component, or it will throw
|
||||
*/
|
||||
export default function useNode<Data = ElementData, CustomEvents extends Record<string, CustomEvent> = any>(id?: string) {
|
||||
const nodeId = id ?? inject(NodeId, '')
|
||||
const nodeEl = inject(NodeRef, null)
|
||||
|
||||
const { findNode, getEdges } = useVueFlow()
|
||||
|
||||
const node = findNode<Data, CustomEvents>(nodeId)
|
||||
|
||||
if (!node) {
|
||||
throw new Error(`[vue-flow]: useNode - Node with id ${nodeId} not found!`)
|
||||
}
|
||||
|
||||
return {
|
||||
id: nodeId,
|
||||
node,
|
||||
nodeEl,
|
||||
parentNode: node.parentNode ? findNode(node.parentNode) : undefined,
|
||||
connectedEdges: computed(() => getConnectedEdges([node], getEdges.value)),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { GraphNode, NodeEventsEmit, NodeEventsOn, VueFlowStore } from '~/types'
|
||||
|
||||
const createNodeHooks = () => ({
|
||||
doubleClick: createEventHook(),
|
||||
click: createEventHook(),
|
||||
mouseEnter: createEventHook(),
|
||||
mouseMove: createEventHook(),
|
||||
mouseLeave: createEventHook(),
|
||||
contextMenu: createEventHook(),
|
||||
dragStart: createEventHook(),
|
||||
drag: createEventHook(),
|
||||
dragStop: createEventHook(),
|
||||
})
|
||||
|
||||
export default function useNodeHooks(node: GraphNode, emits: VueFlowStore['emits']): { emit: NodeEventsEmit; on: NodeEventsOn } {
|
||||
const nodeHooks = createNodeHooks()
|
||||
|
||||
nodeHooks.doubleClick.on((event) => {
|
||||
emits.nodeDoubleClick(event)
|
||||
node.events?.doubleClick?.(event)
|
||||
})
|
||||
|
||||
nodeHooks.click.on((event) => {
|
||||
emits.nodeClick(event)
|
||||
node.events?.click?.(event)
|
||||
})
|
||||
|
||||
nodeHooks.mouseEnter.on((event) => {
|
||||
emits.nodeMouseEnter(event)
|
||||
node.events?.mouseEnter?.(event)
|
||||
})
|
||||
|
||||
nodeHooks.mouseMove.on((event) => {
|
||||
emits.nodeMouseMove(event)
|
||||
node.events?.mouseMove?.(event)
|
||||
})
|
||||
|
||||
nodeHooks.mouseLeave.on((event) => {
|
||||
emits.nodeMouseLeave(event)
|
||||
node.events?.mouseLeave?.(event)
|
||||
})
|
||||
|
||||
nodeHooks.contextMenu.on((event) => {
|
||||
emits.nodeContextMenu(event)
|
||||
node.events?.contextMenu?.(event)
|
||||
})
|
||||
|
||||
nodeHooks.dragStart.on((event) => {
|
||||
emits.nodeDragStart(event)
|
||||
node.events?.dragStart?.(event)
|
||||
})
|
||||
|
||||
nodeHooks.drag.on((event) => {
|
||||
emits.nodeDrag(event)
|
||||
node.events?.drag?.(event)
|
||||
})
|
||||
|
||||
nodeHooks.dragStop.on((event) => {
|
||||
emits.nodeDragStop(event)
|
||||
node.events?.dragStop?.(event)
|
||||
})
|
||||
|
||||
return Object.entries(nodeHooks).reduce(
|
||||
(hooks, [key, value]) => {
|
||||
hooks.emit[key as keyof NodeEventsEmit] = value.trigger
|
||||
hooks.on[key as keyof NodeEventsOn] = value.on
|
||||
return hooks
|
||||
},
|
||||
{ emit: {} as NodeEventsEmit, on: {} as NodeEventsOn },
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import type { EffectScope } from 'vue'
|
||||
import type { FlowOptions, FlowProps, State, VueFlowStore } from '~/types'
|
||||
import { VueFlow } from '~/context'
|
||||
import { useActions, useGetters, useState } from '~/store'
|
||||
|
||||
/**
|
||||
* Stores all currently created store instances
|
||||
*/
|
||||
export class Storage {
|
||||
public currentId = 0
|
||||
public flows = new Map<string, VueFlowStore>()
|
||||
static instance: Storage
|
||||
|
||||
public static getInstance(): Storage {
|
||||
if (!Storage.instance) {
|
||||
Storage.instance = new Storage()
|
||||
}
|
||||
|
||||
return Storage.instance
|
||||
}
|
||||
|
||||
public set(id: string, flow: VueFlowStore) {
|
||||
return this.flows.set(id, flow)
|
||||
}
|
||||
|
||||
public get(id: string) {
|
||||
return this.flows.get(id)
|
||||
}
|
||||
|
||||
public remove(id: string) {
|
||||
return this.flows.delete(id)
|
||||
}
|
||||
|
||||
public create(id: string, preloadedState?: FlowOptions): VueFlowStore {
|
||||
const state: State = useState(preloadedState)
|
||||
|
||||
const reactiveState = reactive(state)
|
||||
|
||||
const getters = useGetters(reactiveState)
|
||||
|
||||
const actions = useActions(reactiveState, getters)
|
||||
|
||||
const hooksOn = <any>{}
|
||||
Object.entries(reactiveState.hooks).forEach(([n, h]) => {
|
||||
const name = `on${n.charAt(0).toUpperCase() + n.slice(1)}`
|
||||
hooksOn[name] = h.on
|
||||
})
|
||||
|
||||
const emits = <any>{}
|
||||
Object.entries(reactiveState.hooks).forEach(([n, h]) => {
|
||||
emits[n] = h.trigger
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
const flow: VueFlowStore = {
|
||||
...hooksOn,
|
||||
...getters,
|
||||
...actions,
|
||||
...toRefs(reactiveState),
|
||||
emits,
|
||||
id,
|
||||
$destroy: () => {
|
||||
this.remove(id)
|
||||
},
|
||||
}
|
||||
|
||||
this.set(id, flow)
|
||||
|
||||
return flow
|
||||
}
|
||||
|
||||
public getId() {
|
||||
return `vue-flow-${this.currentId++}`
|
||||
}
|
||||
}
|
||||
|
||||
type Injection = VueFlowStore | null | undefined
|
||||
type Scope = (EffectScope & { vueFlowId: string }) | undefined
|
||||
|
||||
export default (options?: FlowProps): VueFlowStore => {
|
||||
const storage = Storage.getInstance()
|
||||
|
||||
const scope = getCurrentScope() as Scope
|
||||
|
||||
const id = options?.id
|
||||
const vueFlowId = scope?.vueFlowId || id
|
||||
|
||||
let vueFlow: Injection
|
||||
|
||||
let isParentScope = false
|
||||
|
||||
/**
|
||||
* check if we can get a store instance through injections
|
||||
* this should be the regular way after initialization
|
||||
*/
|
||||
if (scope) {
|
||||
const injection = inject(VueFlow, null)
|
||||
if (typeof injection !== 'undefined' && injection !== null) vueFlow = injection
|
||||
}
|
||||
|
||||
/**
|
||||
* check if we can get a store instance through storage
|
||||
* this requires options id or an id on the current scope
|
||||
*/
|
||||
if (!vueFlow) {
|
||||
if (vueFlowId) vueFlow = storage.get(vueFlowId)
|
||||
}
|
||||
|
||||
/**
|
||||
* If we cannot find any store instance in the previous steps
|
||||
* _or_ if the store instance we found does not match up with provided ids
|
||||
* create a new store instance and register it in storage
|
||||
*/
|
||||
if (!vueFlow || (vueFlow && id && id !== vueFlow.id)) {
|
||||
const name = id ?? storage.getId()
|
||||
|
||||
vueFlow = storage.create(name, options)
|
||||
|
||||
if (scope) {
|
||||
isParentScope = true
|
||||
}
|
||||
} else {
|
||||
// if composable was called with additional options after initialization, overwrite state with the options values
|
||||
if (options) vueFlow.setState(options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Vue flow wasn't able to find any store instance - we can't proceed
|
||||
*/
|
||||
if (!vueFlow) throw new Error('[vueflow]: store instance not found.')
|
||||
|
||||
// always provide a fresh instance into context on call
|
||||
if (scope) {
|
||||
provide(VueFlow, vueFlow)
|
||||
|
||||
scope.vueFlowId = vueFlow.id
|
||||
|
||||
if (isParentScope) {
|
||||
// dispose of state values and storage entry
|
||||
tryOnScopeDispose(() => {
|
||||
if (storage.get(vueFlow!.id)) {
|
||||
vueFlow!.$destroy()
|
||||
}
|
||||
|
||||
vueFlow = null
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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,154 @@
|
||||
import { zoomIdentity } from 'd3-zoom'
|
||||
import useVueFlow from './useVueFlow'
|
||||
import useWindow from './useWindow'
|
||||
import { clampPosition, getRectOfNodes, getTransformForBounds, pointToRendererPoint } from '~/utils'
|
||||
import type { D3Selection, Dimensions, Getters, GraphNode, ViewportFunctions } from '~/types'
|
||||
|
||||
const DEFAULT_PADDING = 0.1
|
||||
|
||||
const transition = (selection: D3Selection, ms = 0) => selection.transition().duration(ms)
|
||||
|
||||
const untilDimensions = async (dimensions: Dimensions, getNodes: Getters['getNodes']) => {
|
||||
// if ssr we can't wait for dimensions, they'll never really exist
|
||||
const window = useWindow()
|
||||
if ('screen' in window) {
|
||||
// wait until viewport dimensions has been established
|
||||
await until(dimensions).toMatch(({ height, width }) => !isNaN(width) && width > 0 && !isNaN(height) && height > 0)
|
||||
|
||||
// if initial nodes are present, wait until the node dimensions have been established
|
||||
if (getNodes.length > 0) {
|
||||
await until(getNodes).toMatch(
|
||||
(nodes) =>
|
||||
!!nodes.filter(({ dimensions: { width, height } }) => !isNaN(width) && width > 0 && !isNaN(height) && height > 0)
|
||||
.length,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use {@link useVueFlow} instead (all viewport functions are also available in {@link useVueFlow})
|
||||
*/
|
||||
export default (vueFlowId?: string): ViewportFunctions => {
|
||||
const {
|
||||
onPaneReady,
|
||||
nodes,
|
||||
d3Zoom,
|
||||
d3Selection,
|
||||
dimensions,
|
||||
translateExtent,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
viewport,
|
||||
snapToGrid,
|
||||
snapGrid,
|
||||
getNodes,
|
||||
} = $(useVueFlow({ id: vueFlowId }))
|
||||
|
||||
let hasDimensions = $ref(false)
|
||||
|
||||
onPaneReady(() => (hasDimensions = true))
|
||||
|
||||
const zoomTo: ViewportFunctions['zoomTo'] = async (zoomLevel, options) => {
|
||||
if (!hasDimensions) await untilDimensions(dimensions, getNodes)
|
||||
|
||||
if (d3Selection && d3Zoom) {
|
||||
d3Zoom.scaleTo(transition(d3Selection, options?.duration), zoomLevel)
|
||||
}
|
||||
}
|
||||
|
||||
const zoom = async (scale: number, duration?: number) => {
|
||||
if (!hasDimensions) await untilDimensions(dimensions, getNodes)
|
||||
|
||||
if (d3Selection && d3Zoom) {
|
||||
d3Zoom.scaleBy(transition(d3Selection, duration), scale)
|
||||
}
|
||||
}
|
||||
|
||||
const zoomIn: ViewportFunctions['zoomIn'] = async (options) => {
|
||||
await zoom(1.2, options?.duration)
|
||||
}
|
||||
|
||||
const zoomOut: ViewportFunctions['zoomOut'] = async (options) => {
|
||||
await zoom(1 / 1.2, options?.duration)
|
||||
}
|
||||
|
||||
const transformViewport = (x: number, y: number, zoom: number, duration?: number) => {
|
||||
// enforce translate extent
|
||||
const { x: clampedX, y: clampedY } = clampPosition({ x: -x, y: -y }, translateExtent)
|
||||
|
||||
const nextTransform = zoomIdentity.translate(-clampedX, -clampedY).scale(zoom)
|
||||
|
||||
if (d3Selection && d3Zoom) {
|
||||
d3Zoom.transform(transition(d3Selection, duration), nextTransform)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
zoomTo,
|
||||
setTransform: async (transform, options) => {
|
||||
if (!hasDimensions) await untilDimensions(dimensions, getNodes)
|
||||
transformViewport(transform.x, transform.y, transform.zoom, options?.duration)
|
||||
},
|
||||
getTransform: () => ({
|
||||
x: viewport.x,
|
||||
y: viewport.y,
|
||||
zoom: viewport.zoom,
|
||||
}),
|
||||
fitView: async (
|
||||
options = {
|
||||
padding: DEFAULT_PADDING,
|
||||
includeHiddenNodes: false,
|
||||
duration: 0,
|
||||
},
|
||||
) => {
|
||||
if (!hasDimensions) await untilDimensions(dimensions, getNodes)
|
||||
|
||||
if (!getNodes.length) return
|
||||
|
||||
let nodeBounds: GraphNode[] = []
|
||||
if (options.nodes) {
|
||||
nodeBounds = nodes.filter((n) => options.nodes?.includes(n.id))
|
||||
}
|
||||
|
||||
if (!nodeBounds || !nodeBounds.length) {
|
||||
nodeBounds = options.includeHiddenNodes ? nodeBounds : getNodes
|
||||
}
|
||||
|
||||
const bounds = getRectOfNodes(nodeBounds)
|
||||
|
||||
const { x, y, zoom } = getTransformForBounds(
|
||||
bounds,
|
||||
dimensions.width,
|
||||
dimensions.height,
|
||||
options.minZoom ?? minZoom,
|
||||
options.maxZoom ?? maxZoom,
|
||||
options.padding ?? DEFAULT_PADDING,
|
||||
options.offset,
|
||||
)
|
||||
|
||||
transformViewport(x, y, zoom, options?.duration)
|
||||
},
|
||||
setCenter: async (x, y, options) => {
|
||||
if (!hasDimensions) await untilDimensions(dimensions, getNodes)
|
||||
|
||||
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : maxZoom
|
||||
const centerX = dimensions.width / 2 - x * nextZoom
|
||||
const centerY = dimensions.height / 2 - y * nextZoom
|
||||
|
||||
transformViewport(centerX, centerY, nextZoom, options?.duration)
|
||||
},
|
||||
fitBounds: async (bounds, options = { padding: DEFAULT_PADDING }) => {
|
||||
if (!hasDimensions) await untilDimensions(dimensions, getNodes)
|
||||
|
||||
const { x, y, zoom } = getTransformForBounds(bounds, dimensions.width, dimensions.height, minZoom, maxZoom, options.padding)
|
||||
|
||||
transformViewport(x, y, zoom, options?.duration)
|
||||
},
|
||||
project: (position) => pointToRendererPoint(position, viewport, snapToGrid, snapGrid),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CSSProperties, EffectScope } from 'vue'
|
||||
import EdgeWrapper from '../../components/Edges/Wrapper'
|
||||
import ConnectionLine from '../../components/ConnectionLine/ConnectionLine.vue'
|
||||
import { useEdgeHooks, useHandle, useVueFlow } from '../../composables'
|
||||
import { connectionExists, groupEdgesByZLevel } from '../../utils'
|
||||
import type { EdgeComponent, EdgeUpdatable, GraphEdge } from '../../types'
|
||||
import { Slots } from '../../context'
|
||||
import MarkerDefinitions from './MarkerDefinitions.vue'
|
||||
|
||||
const slots = inject(Slots)
|
||||
|
||||
const {
|
||||
emits,
|
||||
connectionMode,
|
||||
edgeUpdaterRadius,
|
||||
onPaneReady,
|
||||
connectionStartHandle,
|
||||
nodesConnectable,
|
||||
edgesUpdatable,
|
||||
elementsSelectable,
|
||||
getSelectedNodes,
|
||||
nodesSelectionActive,
|
||||
getNode,
|
||||
getNodes,
|
||||
getEdges,
|
||||
getEdgeTypes,
|
||||
addSelectedEdges,
|
||||
noPanClassName,
|
||||
elevateEdgesOnSelect,
|
||||
} = $(useVueFlow())
|
||||
|
||||
const selectable = (s?: boolean) => (typeof s === 'undefined' ? elementsSelectable : s)
|
||||
|
||||
const updatable = (u?: EdgeUpdatable) => (typeof u === 'undefined' ? edgesUpdatable : u)
|
||||
|
||||
const updating = ref<string>()
|
||||
|
||||
const sourceNode = $(
|
||||
controlledComputed(
|
||||
() => connectionStartHandle?.nodeId,
|
||||
() => {
|
||||
if (connectionStartHandle?.nodeId) return getNode(connectionStartHandle.nodeId)
|
||||
return false
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
const connectionLineVisible = $(
|
||||
controlledComputed(
|
||||
() => connectionStartHandle?.nodeId,
|
||||
() =>
|
||||
!!(
|
||||
sourceNode &&
|
||||
(typeof sourceNode.connectable === 'undefined' ? nodesConnectable : sourceNode.connectable) &&
|
||||
connectionStartHandle?.nodeId &&
|
||||
connectionStartHandle?.type
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
const hooks = $ref<Record<string, ReturnType<typeof useEdgeHooks>>>({})
|
||||
|
||||
let groups = $ref<ReturnType<typeof groupEdgesByZLevel>>([])
|
||||
|
||||
let scope: EffectScope | null = effectScope()
|
||||
|
||||
onPaneReady(() => {
|
||||
if (!scope) scope = effectScope()
|
||||
|
||||
scope.run(() => {
|
||||
watch(
|
||||
[$$(getSelectedNodes), $$(getEdges)],
|
||||
() => {
|
||||
getEdges.forEach((edge) => {
|
||||
if (hooks[edge.id]) return
|
||||
hooks[edge.id] = useEdgeHooks(edge, emits)
|
||||
})
|
||||
|
||||
if (elevateEdgesOnSelect) {
|
||||
nextTick(() => (groups = groupEdgesByZLevel(getEdges, getNode)))
|
||||
} else {
|
||||
groups = [
|
||||
{
|
||||
isMaxLevel: true,
|
||||
edges: getEdges,
|
||||
level: 0,
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
scope?.stop()
|
||||
scope = null
|
||||
})
|
||||
|
||||
const getType = (type?: string, template?: GraphEdge['template']) => {
|
||||
const name = type || 'default'
|
||||
let edgeType = template ?? getEdgeTypes[name]
|
||||
const instance = getCurrentInstance()
|
||||
|
||||
if (typeof edgeType === 'string') {
|
||||
if (instance) {
|
||||
const components = Object.keys(instance.appContext.components)
|
||||
if (components && components.includes(name)) {
|
||||
edgeType = resolveComponent(name, false) as EdgeComponent
|
||||
}
|
||||
}
|
||||
}
|
||||
if (edgeType && typeof edgeType !== 'string') return edgeType
|
||||
|
||||
const slot = slots?.[`edge-${name}`]
|
||||
if (!slot?.({})) {
|
||||
console.warn(`[vueflow]: Edge type "${type}" not found and no edge-slot detected. Using fallback type "default".`)
|
||||
return false
|
||||
}
|
||||
|
||||
return slot
|
||||
}
|
||||
|
||||
const onEdgeClick = (event: MouseEvent, edge: GraphEdge) => {
|
||||
const data = { event, edge }
|
||||
if (selectable(edge.selectable)) {
|
||||
$$(nodesSelectionActive).value = false
|
||||
|
||||
addSelectedEdges([edge])
|
||||
}
|
||||
hooks[edge.id].emit.click(data)
|
||||
}
|
||||
|
||||
const onEdgeContextMenu = (event: MouseEvent, edge: GraphEdge) => hooks[edge.id].emit.contextMenu({ event, edge })
|
||||
|
||||
const onDoubleClick = (event: MouseEvent, edge: GraphEdge) => hooks[edge.id].emit.doubleClick({ event, edge })
|
||||
|
||||
const onEdgeMouseEnter = (event: MouseEvent, edge: GraphEdge) => hooks[edge.id].emit.mouseEnter({ event, edge })
|
||||
|
||||
const onEdgeMouseMove = (event: MouseEvent, edge: GraphEdge) => hooks[edge.id].emit.mouseMove({ event, edge })
|
||||
|
||||
const onEdgeMouseLeave = (event: MouseEvent, edge: GraphEdge) => hooks[edge.id].emit.mouseLeave({ event, edge })
|
||||
|
||||
const onEdgeUpdaterSourceMouseDown = (event: MouseEvent, edge: GraphEdge) => {
|
||||
updating.value = edge.id
|
||||
handleEdgeUpdater(event, edge, true)
|
||||
}
|
||||
|
||||
const onEdgeUpdaterTargetMouseDown = (event: MouseEvent, edge: GraphEdge) => {
|
||||
updating.value = edge.id
|
||||
handleEdgeUpdater(event, edge, false)
|
||||
}
|
||||
|
||||
const { onMouseDown } = useHandle()
|
||||
|
||||
const handleEdgeUpdater = (event: MouseEvent, edge: GraphEdge, isSourceHandle: boolean) => {
|
||||
const nodeId = isSourceHandle ? edge.target : edge.source
|
||||
const handleId = (isSourceHandle ? edge.targetHandle : edge.sourceHandle) ?? ''
|
||||
|
||||
hooks[edge.id].emit.updateStart({ event, edge })
|
||||
|
||||
onMouseDown(
|
||||
event,
|
||||
handleId,
|
||||
nodeId,
|
||||
isSourceHandle,
|
||||
undefined,
|
||||
isSourceHandle ? 'target' : 'source',
|
||||
(connection) => {
|
||||
if (!connectionExists(connection, getEdges)) hooks[edge.id].emit.update({ edge, connection })
|
||||
},
|
||||
() => {
|
||||
hooks[edge.id].emit.updateEnd({ event, edge })
|
||||
updating.value = ''
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const getClass = (edge: GraphEdge) => {
|
||||
const extraClass = edge.class instanceof Function ? edge.class(edge) : edge.class
|
||||
return [noPanClassName, extraClass]
|
||||
}
|
||||
|
||||
const getStyle = (edge: GraphEdge) => (edge.style instanceof Function ? edge.style(edge) : edge.style) as CSSProperties
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'Edges',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg v-for="group of groups" :key="group.level" class="vue-flow__edges vue-flow__container" :style="`z-index: ${group.level}`">
|
||||
<MarkerDefinitions v-if="group.isMaxLevel" />
|
||||
<g>
|
||||
<EdgeWrapper
|
||||
v-for="edge of group.edges"
|
||||
:id="edge.id"
|
||||
:key="edge.id"
|
||||
:type="getType(edge.type, edge.template)"
|
||||
:name="edge.type || 'default'"
|
||||
:source="edge.source"
|
||||
:target="edge.target"
|
||||
:target-handle-id="edge.targetHandle"
|
||||
:source-handle-id="edge.sourceHandle"
|
||||
:source-node="getNode(edge.source)"
|
||||
:target-node="getNode(edge.target)"
|
||||
:label="edge.label"
|
||||
:data="edge.data"
|
||||
:events="{ ...edge.events, ...hooks[edge.id].on }"
|
||||
:animated="edge.animated"
|
||||
:selectable="selectable(edge.selectable)"
|
||||
:selected="edge.selected"
|
||||
:updatable="updatable(edge.updatable)"
|
||||
:updating="edge.id === updating"
|
||||
:label-style="edge.labelStyle"
|
||||
:label-show-bg="edge.labelShowBg"
|
||||
:label-bg-style="edge.labelBgStyle"
|
||||
:label-bg-padding="edge.labelBgPadding"
|
||||
:label-bg-border-radius="edge.labelBgBorderRadius"
|
||||
:connection-mode="connectionMode"
|
||||
:edge-updater-radius="edgeUpdaterRadius"
|
||||
:marker-end="edge.markerEnd"
|
||||
:marker-start="edge.markerStart"
|
||||
:style="getStyle(edge)"
|
||||
:class="getClass(edge)"
|
||||
@click="onEdgeClick($event, edge)"
|
||||
@dblclick="onDoubleClick($event, edge)"
|
||||
@contextmenu="onEdgeContextMenu($event, edge)"
|
||||
@mouseenter="onEdgeMouseEnter($event, edge)"
|
||||
@mousemove="onEdgeMouseMove($event, edge)"
|
||||
@mouseleave="onEdgeMouseLeave($event, edge)"
|
||||
@source-mousedown="onEdgeUpdaterSourceMouseDown($event, edge)"
|
||||
@target-mousedown="onEdgeUpdaterTargetMouseDown($event, edge)"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
<svg v-if="connectionLineVisible && !!sourceNode" class="vue-flow__edges vue-flow__connectionline vue-flow__container">
|
||||
<ConnectionLine :source-node="sourceNode" />
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,54 @@
|
||||
<script lang="ts" setup>
|
||||
import { MarkerType } from '../../types/edge'
|
||||
import type { MarkerProps } from '../../types/edge'
|
||||
|
||||
const {
|
||||
id,
|
||||
type,
|
||||
width = 12.5,
|
||||
height = 12.5,
|
||||
markerUnits = 'strokeWidth',
|
||||
orient = 'auto-start-reverse',
|
||||
strokeWidth = 1,
|
||||
color = 'none',
|
||||
} = defineProps<MarkerProps>()
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'MarkerType',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<marker
|
||||
:id="id"
|
||||
class="vue-flow__arrowhead"
|
||||
viewBox="-10 -10 20 20"
|
||||
refX="0"
|
||||
refY="0"
|
||||
:markerWidth="`${width}`"
|
||||
:markerHeight="`${height}`"
|
||||
:markerUnits="markerUnits"
|
||||
:orient="orient"
|
||||
>
|
||||
<polyline
|
||||
v-if="type === MarkerType.ArrowClosed"
|
||||
:stroke="color"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
:stroke-width="strokeWidth"
|
||||
:fill="color"
|
||||
points="-5,-4 0,0 -5,4 -5,-4"
|
||||
/>
|
||||
<polyline
|
||||
v-if="type === MarkerType.Arrow"
|
||||
:stroke="color"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
:stroke-width="strokeWidth"
|
||||
fill="none"
|
||||
points="-5,-4 0,0 -5,4"
|
||||
/>
|
||||
</marker>
|
||||
</template>
|
||||
@@ -0,0 +1,56 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVueFlow } from '../../composables'
|
||||
import type { EdgeMarkerType, MarkerProps, MarkerType } from '../../types/edge'
|
||||
import { getMarkerId } from '../../utils'
|
||||
import Marker from './Marker.vue'
|
||||
|
||||
const { edges, connectionLineOptions, defaultMarkerColor: defaultColor } = $(useVueFlow())
|
||||
|
||||
const markers = computed(() => {
|
||||
const ids: string[] = []
|
||||
const markers: MarkerProps[] = []
|
||||
|
||||
const createMarkers = (marker?: EdgeMarkerType) => {
|
||||
if (marker) {
|
||||
const markerId = getMarkerId(marker)
|
||||
if (!ids.includes(markerId)) {
|
||||
if (typeof marker === 'object') markers.push({ ...marker, id: markerId, color: marker.color || defaultColor })
|
||||
else markers.push({ id: markerId, color: defaultColor, type: marker as MarkerType })
|
||||
ids.push(markerId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
;[connectionLineOptions.markerEnd, connectionLineOptions.markerStart].forEach(createMarkers)
|
||||
|
||||
edges.reduce<MarkerProps[]>((markers, edge) => {
|
||||
;[edge.markerStart, edge.markerEnd].forEach(createMarkers)
|
||||
return markers.sort((a, b) => a.id.localeCompare(b.id))
|
||||
}, markers)
|
||||
|
||||
return markers
|
||||
})
|
||||
</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,90 @@
|
||||
<script lang="ts" setup>
|
||||
import { NodeWrapper } from '../../components'
|
||||
import type { GraphNode, NodeComponent, SnapGrid } from '../../types'
|
||||
import { useVueFlow } from '../../composables'
|
||||
import { Slots } from '../../context'
|
||||
|
||||
const slots = inject(Slots)
|
||||
|
||||
const {
|
||||
nodesDraggable,
|
||||
elementsSelectable,
|
||||
nodesConnectable,
|
||||
noPanClassName,
|
||||
snapToGrid,
|
||||
snapGrid,
|
||||
getNode,
|
||||
getNodes,
|
||||
getNodeTypes,
|
||||
updateNodeDimensions,
|
||||
} = $(useVueFlow())
|
||||
|
||||
const draggable = (d?: boolean) => (typeof d === 'undefined' ? nodesDraggable : d)
|
||||
const selectable = (s?: boolean) => (typeof s === 'undefined' ? elementsSelectable : s)
|
||||
const connectable = (c?: boolean) => (typeof c === 'undefined' ? nodesConnectable : c)
|
||||
|
||||
const resizeObserver = ref<ResizeObserver>()
|
||||
|
||||
onMounted(() => {
|
||||
resizeObserver.value = new ResizeObserver((entries) => {
|
||||
const updates = entries.map((entry: ResizeObserverEntry) => ({
|
||||
id: entry.target.getAttribute('data-id') as string,
|
||||
nodeElement: entry.target as HTMLDivElement,
|
||||
forceUpdate: true,
|
||||
}))
|
||||
|
||||
updateNodeDimensions(updates)
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => resizeObserver.value?.disconnect())
|
||||
|
||||
const getType = (type?: string, template?: GraphNode['template']) => {
|
||||
const name = type || 'default'
|
||||
let nodeType = template ?? getNodeTypes[name]
|
||||
const instance = getCurrentInstance()
|
||||
|
||||
if (typeof nodeType === 'string') {
|
||||
if (instance) {
|
||||
const components = Object.keys(instance.appContext.components)
|
||||
if (components && components.includes(name)) {
|
||||
nodeType = resolveComponent(name, false) as NodeComponent
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof nodeType !== 'string') return nodeType
|
||||
|
||||
const slot = slots?.[`node-${name}`]
|
||||
if (!slot?.({})) {
|
||||
console.warn(`[vueflow]: Node type "${type}" not found and no node-slot detected. Using fallback type "default".`)
|
||||
return false
|
||||
}
|
||||
|
||||
return slot
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'Nodes',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vue-flow__nodes vue-flow__container">
|
||||
<template v-if="resizeObserver">
|
||||
<NodeWrapper
|
||||
v-for="node of getNodes"
|
||||
:id="node.id"
|
||||
:key="node.id"
|
||||
:resize-observer="resizeObserver"
|
||||
:type="getType(node.type, node.template)"
|
||||
:name="node.type || 'default'"
|
||||
:draggable="draggable(node.draggable)"
|
||||
:selectable="selectable(node.selectable)"
|
||||
:connectable="connectable(node.connectable)"
|
||||
:node="node"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,99 @@
|
||||
<script lang="ts" setup>
|
||||
import type { GraphNode } from '../../types'
|
||||
import { useKeyPress, useVueFlow } from '../../composables'
|
||||
import { NodesSelection, UserSelection } from '../../components'
|
||||
|
||||
const {
|
||||
id,
|
||||
deleteKeyCode,
|
||||
selectionKeyCode,
|
||||
multiSelectionKeyCode,
|
||||
emits,
|
||||
nodesSelectionActive,
|
||||
userSelectionActive,
|
||||
multiSelectionActive,
|
||||
elementsSelectable,
|
||||
getNodes,
|
||||
getSelectedEdges,
|
||||
removeSelectedElements,
|
||||
removeNodes,
|
||||
removeEdges,
|
||||
} = useVueFlow()
|
||||
|
||||
const onClick = (event: MouseEvent) => {
|
||||
emits.paneClick(event)
|
||||
nodesSelectionActive.value = false
|
||||
|
||||
removeSelectedElements()
|
||||
}
|
||||
|
||||
const onContextMenu = (event: MouseEvent) => emits.paneContextMenu(event)
|
||||
|
||||
const onWheel = (event: WheelEvent) => emits.paneScroll(event)
|
||||
|
||||
const onMouseEnter = (event: MouseEvent) => emits.paneMouseEnter(event)
|
||||
|
||||
const onMouseLeave = (event: MouseEvent) => emits.paneMouseLeave(event)
|
||||
|
||||
const onMouseMove = (event: MouseEvent) => emits.paneMouseMove(event)
|
||||
|
||||
useKeyPress(deleteKeyCode, (keyPressed) => {
|
||||
if (!keyPressed) return
|
||||
|
||||
const nodesToRemove = getNodes.value.reduce<GraphNode[]>((res, node) => {
|
||||
if (!node.selected && node.parentNode && res.find((n) => n.id === node.parentNode)) {
|
||||
res.push(node)
|
||||
} else if (node.selected) {
|
||||
res.push(node)
|
||||
}
|
||||
|
||||
return res
|
||||
}, [])
|
||||
|
||||
if (nodesToRemove || getSelectedEdges.value) {
|
||||
if (getSelectedEdges.value.length > 0) {
|
||||
removeEdges(getSelectedEdges.value)
|
||||
}
|
||||
|
||||
if (nodesToRemove.length > 0) {
|
||||
removeNodes(nodesToRemove)
|
||||
}
|
||||
|
||||
nodesSelectionActive.value = false
|
||||
|
||||
removeSelectedElements()
|
||||
}
|
||||
})
|
||||
|
||||
useKeyPress(multiSelectionKeyCode, (keyPressed) => {
|
||||
multiSelectionActive.value = keyPressed
|
||||
})
|
||||
|
||||
const selectionKeyPressed = useKeyPress(selectionKeyCode, (keyPressed) => {
|
||||
if (userSelectionActive.value && keyPressed) return
|
||||
|
||||
userSelectionActive.value = keyPressed && elementsSelectable.value
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'SelectionPane',
|
||||
inheritAttrs: false,
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UserSelection v-if="selectionKeyPressed" :key="`user-selection-${id}`" />
|
||||
<NodesSelection v-if="nodesSelectionActive" :key="`nodes-selection-${id}`" />
|
||||
<div
|
||||
:key="`pane-${id}`"
|
||||
class="vue-flow__pane vue-flow__container"
|
||||
@click="onClick"
|
||||
@contextmenu="onContextMenu"
|
||||
@wheel="onWheel"
|
||||
@mouseenter="onMouseEnter"
|
||||
@mousemove="onMouseMove"
|
||||
@mouseleave="onMouseLeave"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,59 @@
|
||||
<script lang="ts" setup>
|
||||
import NodeRenderer from '../NodeRenderer/NodeRenderer.vue'
|
||||
import EdgeRenderer from '../EdgeRenderer/EdgeRenderer.vue'
|
||||
import { useVueFlow, useWindow } from '../../composables'
|
||||
import type { Dimensions } from '../../types'
|
||||
|
||||
const { id, viewport, dimensions, fitViewOnInit, emits, fitView, ...rest } = useVueFlow()
|
||||
|
||||
const untilDimensions = async (dim: Dimensions) => {
|
||||
// if ssr we can't wait for dimensions, they'll never really exist
|
||||
const window = useWindow()
|
||||
if ('screen' in window) {
|
||||
// wait until viewport dimensions has been established
|
||||
await until(dim).toMatch(({ height, width }) => !isNaN(width) && width > 0 && !isNaN(height) && height > 0)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
let ready = $ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
// wait until proper dimensions have been established, otherwise fitView will have wrong bounds when called at paneReady
|
||||
await untilDimensions(dimensions.value)
|
||||
|
||||
// hide graph until dimensions are ready, so we don't have jumping graphs (ssr for example)
|
||||
ready = true
|
||||
|
||||
emits.paneReady({
|
||||
id,
|
||||
viewport,
|
||||
dimensions,
|
||||
fitViewOnInit,
|
||||
fitView,
|
||||
emits,
|
||||
...rest,
|
||||
})
|
||||
|
||||
fitViewOnInit?.value && fitView()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'Transform',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:key="`transform-${id}`"
|
||||
class="vue-flow__transformationpane vue-flow__container"
|
||||
:style="{ transform: `translate(${viewport.x}px,${viewport.y}px) scale(${viewport.zoom})`, opacity: ready ? undefined : 0 }"
|
||||
>
|
||||
<EdgeRenderer />
|
||||
<NodeRenderer />
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,222 @@
|
||||
<script lang="ts" setup>
|
||||
import type { D3ZoomEvent, ZoomTransform } from 'd3-zoom'
|
||||
import { zoom, zoomIdentity } from 'd3-zoom'
|
||||
import { pointer, select } from 'd3-selection'
|
||||
import type { ViewpaneTransform } from '../../types'
|
||||
import { PanOnScrollMode } from '../../types'
|
||||
import { useKeyPress, useVueFlow, useWindow } from '../../composables'
|
||||
import { clamp, clampPosition, getDimensions } from '../../utils'
|
||||
import SelectionPane from '../SelectionPane/SelectionPane.vue'
|
||||
import Transform from './Transform.vue'
|
||||
|
||||
const {
|
||||
id,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
defaultZoom,
|
||||
defaultPosition,
|
||||
translateExtent,
|
||||
dimensions,
|
||||
zoomActivationKeyCode,
|
||||
selectionKeyCode,
|
||||
panOnScroll,
|
||||
panOnScrollMode,
|
||||
panOnScrollSpeed,
|
||||
panOnDrag,
|
||||
zoomOnDoubleClick,
|
||||
zoomOnPinch,
|
||||
zoomOnScroll,
|
||||
preventScrolling,
|
||||
noWheelClassName,
|
||||
noPanClassName,
|
||||
setState,
|
||||
emits,
|
||||
} = $(useVueFlow())
|
||||
|
||||
const viewportEl = templateRef<HTMLDivElement>('viewport', null)
|
||||
|
||||
let isZoomingOrPanning = $ref(false)
|
||||
|
||||
const viewChanged = (prevTransform: ViewpaneTransform, 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): ViewpaneTransform => ({
|
||||
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(defaultZoom, minZoom, maxZoom)
|
||||
|
||||
let transform = $ref({
|
||||
...clampPosition({ x: defaultPosition[0], y: defaultPosition[1] }, translateExtent),
|
||||
zoom: clampedZoom,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
useResizeObserver(viewportEl, () => {
|
||||
if (!viewportEl.value) return
|
||||
|
||||
const { width, height } = getDimensions(viewportEl.value)
|
||||
dimensions.width = width
|
||||
dimensions.height = height
|
||||
})
|
||||
|
||||
const window = useWindow()
|
||||
useEventListener(window, 'resize', () => {
|
||||
if (!viewportEl.value) return
|
||||
|
||||
const { width, height } = getDimensions(viewportEl.value)
|
||||
dimensions.width = width
|
||||
dimensions.height = height
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
const d3Zoom = zoom<HTMLDivElement, any>().scaleExtent([minZoom, maxZoom]).translateExtent(translateExtent)
|
||||
const d3Selection = select(viewportEl.value).call(d3Zoom)
|
||||
const d3ZoomHandler = d3Selection.on('wheel.zoom')
|
||||
|
||||
const updatedTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.zoom)
|
||||
d3Zoom.transform(d3Selection, updatedTransform)
|
||||
|
||||
setState({
|
||||
d3Zoom,
|
||||
d3Selection,
|
||||
d3ZoomHandler,
|
||||
viewport: { x: updatedTransform.x, y: updatedTransform.y, zoom: updatedTransform.k },
|
||||
viewportRef: viewportEl.value,
|
||||
})
|
||||
|
||||
const onKeyPress = (keyPress: boolean) => {
|
||||
if (keyPress && !isZoomingOrPanning) {
|
||||
d3Zoom.on('zoom', null)
|
||||
} else if (!keyPress) {
|
||||
d3Zoom.on('zoom', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||
setState({ viewport: { x: event.transform.x, y: event.transform.y, zoom: event.transform.k } })
|
||||
const flowTransform = eventToFlowTransform(event.transform)
|
||||
emits.move({ event, flowTransform })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const selectionKeyPressed = useKeyPress(selectionKeyCode, onKeyPress)
|
||||
// initialize
|
||||
onKeyPress(false)
|
||||
|
||||
const zoomKeyPressed = useKeyPress(zoomActivationKeyCode)
|
||||
|
||||
d3Zoom.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||
isZoomingOrPanning = true
|
||||
|
||||
const flowTransform = eventToFlowTransform(event.transform)
|
||||
transform = flowTransform
|
||||
emits.moveStart({ event, flowTransform })
|
||||
})
|
||||
|
||||
d3Zoom.on('end', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||
isZoomingOrPanning = false
|
||||
|
||||
if (viewChanged(transform, event.transform)) {
|
||||
const flowTransform = eventToFlowTransform(event.transform)
|
||||
transform = flowTransform
|
||||
emits.moveEnd({ event, flowTransform })
|
||||
}
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (panOnScroll && !zoomKeyPressed.value) {
|
||||
d3Selection
|
||||
.on('wheel', (event: WheelEvent) => {
|
||||
if (isWrappedWithClass(event, noWheelClassName?.value)) {
|
||||
return false
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopImmediatePropagation()
|
||||
|
||||
const currentZoom = d3Selection.property('__zoom').k || 1
|
||||
|
||||
if (event.ctrlKey && zoomOnPinch) {
|
||||
const point = pointer(event)
|
||||
// taken from https://github.com/d3/d3-zoom/blob/master/src/zoom.js
|
||||
const pinchDelta = -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002) * 10
|
||||
const zoom = currentZoom * 2 ** pinchDelta
|
||||
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 = panOnScrollMode === PanOnScrollMode.Vertical ? 0 : event.deltaX * deltaNormalize
|
||||
const deltaY = panOnScrollMode === PanOnScrollMode.Horizontal ? 0 : event.deltaY * deltaNormalize
|
||||
|
||||
d3Zoom.translateBy(d3Selection, -(deltaX / currentZoom) * panOnScrollSpeed, -(deltaY / currentZoom) * panOnScrollSpeed)
|
||||
})
|
||||
.on('wheel.zoom', null)
|
||||
} else if (typeof d3ZoomHandler !== 'undefined') {
|
||||
d3Selection
|
||||
.on('wheel', (event: WheelEvent) => {
|
||||
if (!preventScrolling || isWrappedWithClass(event, noWheelClassName?.value)) {
|
||||
return null
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
})
|
||||
.on('wheel.zoom', d3ZoomHandler)
|
||||
}
|
||||
})
|
||||
|
||||
d3Zoom.filter((event: MouseEvent) => {
|
||||
const zoomScroll = zoomKeyPressed.value || zoomOnScroll
|
||||
const pinchZoom = zoomOnPinch && event.ctrlKey
|
||||
|
||||
// if all interactions are disabled, we prevent all zoom events
|
||||
if (!panOnDrag && !zoomScroll && !panOnScroll && !zoomOnDoubleClick && !zoomOnPinch) return false
|
||||
|
||||
// during a selection we prevent all other interactions
|
||||
if (selectionKeyPressed.value && !selectionKeyCode !== true) return false
|
||||
|
||||
// if zoom on double click is disabled, we prevent the double click event
|
||||
if (!zoomOnDoubleClick && event.type === 'dblclick') return false
|
||||
|
||||
// if the target element is inside an element with the nowheel class, we prevent zooming
|
||||
if (isWrappedWithClass(event, noWheelClassName as any) && event.type === 'wheel') return false
|
||||
|
||||
// if the target element is inside an element with the nopan class, we prevent panning
|
||||
if (isWrappedWithClass(event, noPanClassName as any) && event.type !== 'wheel') return false
|
||||
|
||||
if (!zoomOnPinch && event.ctrlKey && event.type === 'wheel') return false
|
||||
|
||||
// when there is no scroll handling enabled, we prevent all wheel events
|
||||
if (!zoomScroll && !panOnScroll && !pinchZoom && event.type === 'wheel') return false
|
||||
|
||||
// if the pane is not movable, we prevent dragging it with mousestart or touchstart
|
||||
if (!panOnDrag && (event.type === 'mousedown' || event.type === 'touchstart')) return false
|
||||
|
||||
// default filter for d3-zoom
|
||||
return (!event.ctrlKey || event.type === 'wheel') && !event.button
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'Viewport',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="viewport" :key="`viewport-${id}`" class="vue-flow__viewport vue-flow__container">
|
||||
<Transform>
|
||||
<slot />
|
||||
</Transform>
|
||||
<SelectionPane />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,173 @@
|
||||
<script lang="ts" setup>
|
||||
import type { D3ZoomEvent } from 'd3-zoom'
|
||||
import Viewport from '../Viewport/Viewport.vue'
|
||||
import { useHooks } from '../../store'
|
||||
import { useVueFlow } from '../../composables'
|
||||
import type { FlowElements, FlowProps } from '../../types/flow'
|
||||
import { Slots } from '../../context'
|
||||
import type {
|
||||
Connection,
|
||||
EdgeChange,
|
||||
EdgeMouseEvent,
|
||||
EdgeUpdateEvent,
|
||||
GraphEdge,
|
||||
GraphNode,
|
||||
NodeChange,
|
||||
NodeDragEvent,
|
||||
NodeMouseEvent,
|
||||
OnConnectStartParams,
|
||||
ViewpaneTransform,
|
||||
VueFlowStore,
|
||||
} from '../../types'
|
||||
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: undefined,
|
||||
autoConnect: undefined,
|
||||
elevateEdgesOnSelect: undefined,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'nodesChange', changes: NodeChange[]): void
|
||||
(event: 'edgesChange', changes: EdgeChange[]): void
|
||||
(event: 'nodeDoubleClick', nodeMouseEvent: NodeMouseEvent): void
|
||||
(event: 'nodeClick', nodeMouseEvent: NodeMouseEvent): void
|
||||
(event: 'nodeMouseEnter', nodeMouseEvent: NodeMouseEvent): void
|
||||
(event: 'nodeMouseMove', nodeMouseEvent: NodeMouseEvent): void
|
||||
(event: 'nodeMouseLeave', nodeMouseEvent: NodeMouseEvent): void
|
||||
(event: 'nodeContextMenu', nodeMouseEvent: NodeMouseEvent): void
|
||||
(event: 'nodeDragStart', nodeDragEvent: NodeDragEvent): void
|
||||
(event: 'nodeDrag', nodeDragEvent: NodeDragEvent): void
|
||||
(event: 'nodeDragStop', nodeDragEvent: NodeDragEvent): void
|
||||
(event: 'miniMapNodeClick', nodeMouseEvent: NodeMouseEvent): void
|
||||
(event: 'miniMapNodeDoubleClick', nodeMouseEvent: NodeMouseEvent): void
|
||||
(event: 'miniMapNodeMouseEnter', nodeMouseEvent: NodeMouseEvent): void
|
||||
(event: 'miniMapNodeMouseMove', nodeMouseEvent: NodeMouseEvent): void
|
||||
(event: 'miniMapNodeMouseLeave', nodeMouseEvent: NodeMouseEvent): void
|
||||
(event: 'connect', connectionEvent: Connection): void
|
||||
(
|
||||
event: 'connectStart',
|
||||
connectionEvent: {
|
||||
event?: MouseEvent
|
||||
} & OnConnectStartParams,
|
||||
): void
|
||||
(event: 'connectEnd', connectionEvent?: MouseEvent): void
|
||||
(event: 'moveStart', moveEvent: { event: D3ZoomEvent<HTMLDivElement, any>; flowTransform: ViewpaneTransform }): void
|
||||
(event: 'move', moveEvent: { event: D3ZoomEvent<HTMLDivElement, any>; flowTransform: ViewpaneTransform }): void
|
||||
(event: 'moveEnd', moveEvent: { event: D3ZoomEvent<HTMLDivElement, any>; flowTransform: ViewpaneTransform }): void
|
||||
(event: 'selectionDragStart', selectionEvent: NodeDragEvent): void
|
||||
(event: 'selectionDrag', selectionEvent: NodeDragEvent): void
|
||||
(event: 'selectionDragStop', selectionEvent: NodeDragEvent): void
|
||||
(event: 'selectionContextMenu', selectionEvent: { event: MouseEvent; nodes: GraphNode[] }): void
|
||||
(event: 'paneReady', paneEvent: VueFlowStore): void
|
||||
(event: 'paneScroll', paneEvent: WheelEvent | undefined): void
|
||||
(event: 'paneClick', paneEvent: MouseEvent): void
|
||||
(event: 'paneContextMenu', paneEvent: MouseEvent): void
|
||||
(event: 'paneMouseEnter', paneEvent: MouseEvent): void
|
||||
(event: 'paneMouseMove', paneEvent: MouseEvent): void
|
||||
(event: 'paneMouseLeave', paneEvent: MouseEvent): void
|
||||
(event: 'edgeContextMenu', edgeMouseEvent: EdgeMouseEvent): void
|
||||
(event: 'edgeMouseEnter', edgeMouseEvent: EdgeMouseEvent): void
|
||||
(event: 'edgeMouseMove', edgeMouseEvent: EdgeMouseEvent): void
|
||||
(event: 'edgeMouseLeave', edgeMouseEvent: EdgeMouseEvent): void
|
||||
(event: 'edgeDoubleClick', edgeMouseEvent: EdgeMouseEvent): void
|
||||
(event: 'edgeClick', edgeMouseEvent: EdgeMouseEvent): void
|
||||
(event: 'edgeUpdateStart', edgeMouseEvent: EdgeMouseEvent): void
|
||||
(event: 'edgeUpdate', edgeUpdateEvent: EdgeUpdateEvent): void
|
||||
(event: 'edgeUpdateEnd', edgeMouseEvent: EdgeMouseEvent): void
|
||||
(event: 'updateNodeInternals'): void
|
||||
|
||||
/** v-model event definitions */
|
||||
(event: 'update:modelValue', value: FlowElements): void
|
||||
(event: 'update:nodes', value: GraphNode[]): void
|
||||
(event: 'update:edges', value: GraphEdge[]): void
|
||||
}>()
|
||||
|
||||
const modelValue = useVModel(props, 'modelValue', emit)
|
||||
const modelNodes = useVModel(props, 'nodes', emit)
|
||||
const modelEdges = useVModel(props, 'edges', emit)
|
||||
|
||||
const { vueFlowRef, id, hooks, getNodeTypes, getEdgeTypes, $reset, ...rest } = useVueFlow({ id: props.id })
|
||||
|
||||
const dispose = useWatch({ modelValue, nodes: modelNodes, edges: modelEdges }, props, {
|
||||
vueFlowRef,
|
||||
id,
|
||||
hooks,
|
||||
getNodeTypes,
|
||||
getEdgeTypes,
|
||||
$reset,
|
||||
...rest,
|
||||
})
|
||||
|
||||
const el = ref<HTMLDivElement>()
|
||||
|
||||
onUnmounted(() => {
|
||||
dispose()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
vueFlowRef.value = el.value!
|
||||
})
|
||||
|
||||
useHooks(emit, hooks)
|
||||
|
||||
provide(Slots, useSlots())
|
||||
|
||||
defineExpose<VueFlowStore>({
|
||||
vueFlowRef,
|
||||
id,
|
||||
hooks,
|
||||
getNodeTypes,
|
||||
getEdgeTypes,
|
||||
$reset,
|
||||
...rest,
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'VueFlow',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="el" class="vue-flow">
|
||||
<Viewport>
|
||||
<template #nodes>
|
||||
<template v-for="nodeName of Object.keys(getNodeTypes)">
|
||||
<slot :name="`node-${nodeName}`" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template #edges>
|
||||
<template v-for="edgeName of Object.keys(getEdgeTypes)">
|
||||
<slot :name="`edge-${edgeName}`" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template #connection-name>
|
||||
<slot name="connection-line" />
|
||||
</template>
|
||||
|
||||
<slot name="zoom-pane" />
|
||||
</Viewport>
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,300 @@
|
||||
import type { ToRefs } from 'vue'
|
||||
import type { WatchPausableReturn } from '@vueuse/core'
|
||||
import { isFunction } from '@vueuse/core'
|
||||
import type { Connection, FlowProps, GraphEdge, GraphNode, VueFlowStore } from '~/types'
|
||||
|
||||
const isDef = <T>(val: T): val is NonNullable<T> => typeof unref(val) !== 'undefined'
|
||||
|
||||
export default (models: ToRefs<Pick<FlowProps, 'nodes' | 'edges' | 'modelValue'>>, props: FlowProps, store: VueFlowStore) => {
|
||||
const scope = effectScope()
|
||||
|
||||
scope.run(() => {
|
||||
const watchModelValue = () => {
|
||||
scope.run(() => {
|
||||
let pauseModel: WatchPausableReturn
|
||||
let pauseStore: WatchPausableReturn
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
pauseModel = watchPausable(
|
||||
[models.modelValue, () => models.modelValue?.value?.length],
|
||||
([v]) => {
|
||||
if (v && Array.isArray(v)) {
|
||||
if (pauseStore) pauseStore.pause()
|
||||
if (pauseModel) pauseModel.pause()
|
||||
|
||||
store.setElements(v)
|
||||
|
||||
pauseStore = watchPausable(
|
||||
[store.edges, store.nodes, () => store.edges.value.length, () => store.nodes.value.length],
|
||||
([e, n]) => {
|
||||
if (pauseModel) pauseModel.pause()
|
||||
const val = [...(n as GraphNode[]), ...(e as GraphEdge[])]
|
||||
if (val.length) models.modelValue!.value = val
|
||||
|
||||
nextTick(() => {
|
||||
if (pauseModel) pauseModel.resume()
|
||||
})
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
nextTick(() => {
|
||||
if (pauseStore) pauseStore.resume()
|
||||
if (pauseModel) pauseModel.resume()
|
||||
})
|
||||
}
|
||||
},
|
||||
{ immediate: !!(models.modelValue && models.modelValue.value) },
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const watchNodesValue = () => {
|
||||
scope.run(() => {
|
||||
let pauseModel: WatchPausableReturn
|
||||
let pauseStore: WatchPausableReturn
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
pauseModel = watchPausable(
|
||||
[models.nodes, () => models.nodes?.value?.length],
|
||||
async ([v]) => {
|
||||
if (v && Array.isArray(v)) {
|
||||
if (pauseStore) pauseStore.pause()
|
||||
if (pauseModel) pauseModel.pause()
|
||||
|
||||
store.setNodes(v)
|
||||
|
||||
pauseStore = watchPausable(
|
||||
() => store.nodes.value.length,
|
||||
() => {
|
||||
if (store.nodes.value.length) models.nodes!.value = [...store.nodes.value]
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
nextTick(() => {
|
||||
if (pauseStore) pauseStore.resume()
|
||||
if (pauseModel) pauseModel.resume()
|
||||
})
|
||||
}
|
||||
},
|
||||
{ immediate: !!(models.nodes && models.nodes.value) },
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const watchEdgesValue = () => {
|
||||
scope.run(() => {
|
||||
let pauseModel: WatchPausableReturn
|
||||
let pauseStore: WatchPausableReturn
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
pauseModel = watchPausable(
|
||||
[models.edges, () => models.edges?.value?.length],
|
||||
async ([v]) => {
|
||||
if (v && Array.isArray(v)) {
|
||||
if (pauseStore) pauseStore.pause()
|
||||
if (pauseModel) pauseModel.pause()
|
||||
|
||||
store.setEdges(v)
|
||||
|
||||
pauseStore = watchPausable(
|
||||
() => store.edges.value.length,
|
||||
() => {
|
||||
if (store.edges.value.length) models.edges!.value = [...store.edges.value]
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
nextTick(() => {
|
||||
if (pauseStore) pauseStore.resume()
|
||||
if (pauseModel) pauseModel.resume()
|
||||
})
|
||||
}
|
||||
},
|
||||
{ immediate: !!(models.edges && models.edges.value) },
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const watchMaxZoom = () => {
|
||||
scope.run(() => {
|
||||
watch(
|
||||
() => props.maxZoom,
|
||||
() => {
|
||||
if (props.maxZoom && isDef(props.maxZoom)) {
|
||||
store.setMaxZoom(props.maxZoom)
|
||||
}
|
||||
},
|
||||
{ immediate: isDef(props.maxZoom) },
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const watchMinZoom = () => {
|
||||
scope.run(() => {
|
||||
watch(
|
||||
() => props.minZoom,
|
||||
() => {
|
||||
if (props.minZoom && isDef(props.minZoom)) {
|
||||
store.setMinZoom(props.minZoom)
|
||||
}
|
||||
},
|
||||
{ immediate: isDef(props.minZoom) },
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const watchTranslateExtent = () => {
|
||||
scope.run(() => {
|
||||
watch(
|
||||
() => props.translateExtent,
|
||||
() => {
|
||||
if (props.translateExtent && isDef(props.translateExtent)) {
|
||||
store.setTranslateExtent(props.translateExtent)
|
||||
}
|
||||
},
|
||||
{ immediate: isDef(props.translateExtent) },
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const watchNodeExtent = () => {
|
||||
scope.run(() => {
|
||||
watch(
|
||||
() => props.nodeExtent,
|
||||
() => {
|
||||
if (props.nodeExtent && isDef(props.nodeExtent)) {
|
||||
store.setNodeExtent(props.nodeExtent)
|
||||
}
|
||||
},
|
||||
{ immediate: isDef(props.nodeExtent) },
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const watchApplyDefault = () => {
|
||||
scope.run(() => {
|
||||
watch(
|
||||
() => props.applyDefault,
|
||||
() => {
|
||||
if (isDef(props.applyDefault)) {
|
||||
store.applyDefault.value = props.applyDefault
|
||||
}
|
||||
},
|
||||
{ immediate: isDef(props.applyDefault) },
|
||||
)
|
||||
|
||||
watch(
|
||||
store.applyDefault,
|
||||
(_, __, onCleanup) => {
|
||||
if (store.applyDefault.value) {
|
||||
store.onNodesChange(store.applyNodeChanges)
|
||||
store.onEdgesChange(store.applyEdgeChanges)
|
||||
} else {
|
||||
store.hooks.value.nodesChange.off(store.applyNodeChanges)
|
||||
store.hooks.value.edgesChange.off(store.applyEdgeChanges)
|
||||
}
|
||||
|
||||
onCleanup(() => {
|
||||
store.hooks.value.nodesChange.off(store.applyNodeChanges)
|
||||
store.hooks.value.edgesChange.off(store.applyEdgeChanges)
|
||||
})
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const watchAutoConnect = () => {
|
||||
scope.run(() => {
|
||||
const autoConnector = async (params: Connection) => {
|
||||
let connection: boolean | Connection = params
|
||||
|
||||
if (isFunction(props.autoConnect)) {
|
||||
connection = await props.autoConnect(params)
|
||||
}
|
||||
|
||||
if (connection !== false) {
|
||||
store.addEdges([connection])
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.autoConnect,
|
||||
() => {
|
||||
if (isDef(props.autoConnect)) {
|
||||
store.autoConnect.value = props.autoConnect
|
||||
}
|
||||
},
|
||||
{ immediate: isDef(props.autoConnect) },
|
||||
)
|
||||
|
||||
watch(
|
||||
store.autoConnect,
|
||||
(autoConnectEnabled, _, onCleanup) => {
|
||||
if (autoConnectEnabled) {
|
||||
store.onConnect(autoConnector)
|
||||
} else {
|
||||
store.hooks.value.connect.off(autoConnector)
|
||||
}
|
||||
|
||||
onCleanup(() => {
|
||||
store.hooks.value.connect.off(autoConnector)
|
||||
})
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const watchRest = () => {
|
||||
const skip: (keyof typeof props)[] = [
|
||||
'id',
|
||||
'modelValue',
|
||||
'translateExtent',
|
||||
'nodeExtent',
|
||||
'edges',
|
||||
'nodes',
|
||||
'maxZoom',
|
||||
'minZoom',
|
||||
'applyDefault',
|
||||
'autoConnect',
|
||||
]
|
||||
|
||||
Object.keys(props).forEach((prop) => {
|
||||
if (!skip.includes(prop as keyof typeof props)) {
|
||||
const model = toRef(props, prop as keyof typeof props)
|
||||
const storedValue = store[prop as keyof typeof store] as typeof model
|
||||
|
||||
scope.run(() => {
|
||||
watch(
|
||||
model,
|
||||
() => {
|
||||
if (isDef(model)) {
|
||||
storedValue.value = model.value
|
||||
}
|
||||
},
|
||||
{ immediate: isDef(model) },
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
;[
|
||||
watchModelValue,
|
||||
watchNodesValue,
|
||||
watchEdgesValue,
|
||||
watchMinZoom,
|
||||
watchMaxZoom,
|
||||
watchTranslateExtent,
|
||||
watchNodeExtent,
|
||||
watchApplyDefault,
|
||||
watchAutoConnect,
|
||||
watchRest,
|
||||
].forEach((watch) => watch())
|
||||
})
|
||||
|
||||
return () => scope.stop()
|
||||
}
|
||||
@@ -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 Viewport } from './Viewport/Viewport.vue'
|
||||
export { default as SelectionPane } from './SelectionPane/SelectionPane.vue'
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { InjectionKey, Ref, Slots as TSlots } from 'vue'
|
||||
import type { VueFlowStore } from '~/types'
|
||||
|
||||
export const VueFlow: InjectionKey<VueFlowStore> = Symbol('vueFlow')
|
||||
export const NodeId: InjectionKey<string> = Symbol('nodeId')
|
||||
export const NodeRef: InjectionKey<Ref<HTMLDivElement>> = Symbol('nodeRef')
|
||||
export const EdgeId: InjectionKey<string> = Symbol('edgeId')
|
||||
export const EdgeRef: InjectionKey<Ref<HTMLDivElement>> = Symbol('edgeRef')
|
||||
export const Slots: InjectionKey<TSlots> = Symbol('slots')
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
declare const __VUE_FLOW_VERSION__: string
|
||||
declare const __ENV__: string
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="vue/macros-global" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Vue Flow
|
||||
* @module vue-flow
|
||||
*/
|
||||
|
||||
export { default as VueFlow } from './container/VueFlow/VueFlow.vue'
|
||||
|
||||
export { default as Handle } from './components/Handle/Handle.vue'
|
||||
|
||||
export { StraightEdge, StepEdge, BezierEdge, SimpleBezierEdge, SmoothStepEdge, BaseEdge, EdgeText } from './components/Edges'
|
||||
|
||||
export { getBezierPath, getSimpleBezierPath, getSmoothStepPath, getStraightPath } from './components/Edges/utils'
|
||||
|
||||
export {
|
||||
isNode,
|
||||
isEdge,
|
||||
addEdge,
|
||||
updateEdge,
|
||||
getOutgoers,
|
||||
getIncomers,
|
||||
getConnectedEdges,
|
||||
getTransformForBounds,
|
||||
getRectOfNodes,
|
||||
graphPosToZoomedPos,
|
||||
getNodesInside,
|
||||
getMarkerId,
|
||||
getBoundsofRects,
|
||||
} from './utils/graph'
|
||||
|
||||
/**
|
||||
* Intended for options API
|
||||
* In composition API you can access apply utilities from `useVueFlow`
|
||||
*/
|
||||
export { applyChanges, applyEdgeChanges, applyNodeChanges } from './utils/changes'
|
||||
|
||||
export { defaultEdgeTypes, defaultNodeTypes } from './store'
|
||||
|
||||
export { VueFlow as VueFlowInjection, NodeId as NodeIdInjection } from './context'
|
||||
|
||||
export { default as useZoomPanHelper } from './composables/useZoomPanHelper'
|
||||
|
||||
export { default as useVueFlow, Storage as GlobalVueFlowStorage } from './composables/useVueFlow'
|
||||
|
||||
export { default as useHandle } from './composables/useHandle'
|
||||
|
||||
export { default as useNode } from './composables/useNode'
|
||||
|
||||
export { default as useEdge } from './composables/useEdge'
|
||||
|
||||
export * from './types'
|
||||
@@ -0,0 +1,519 @@
|
||||
import useState from './state'
|
||||
import type {
|
||||
Actions,
|
||||
ComputedGetters,
|
||||
CoordinateExtent,
|
||||
EdgeChange,
|
||||
EdgeRemoveChange,
|
||||
FlowExportObject,
|
||||
GraphEdge,
|
||||
GraphNode,
|
||||
NodeChange,
|
||||
NodeDimensionChange,
|
||||
NodePositionChange,
|
||||
NodeRemoveChange,
|
||||
State,
|
||||
} from '~/types'
|
||||
import {
|
||||
addEdgeToStore,
|
||||
applyChanges,
|
||||
createAdditionChange,
|
||||
createGraphNodes,
|
||||
createRemoveChange,
|
||||
createSelectionChange,
|
||||
getConnectedEdges,
|
||||
getDimensions,
|
||||
getHandleBounds,
|
||||
getSelectionChanges,
|
||||
isDef,
|
||||
isEdge,
|
||||
isGraphEdge,
|
||||
isGraphNode,
|
||||
isNode,
|
||||
parseEdge,
|
||||
pointToRendererPoint,
|
||||
updateEdgeAction,
|
||||
} from '~/utils'
|
||||
import { useZoomPanHelper } from '~/composables'
|
||||
|
||||
export default (state: State, getters: ComputedGetters): Actions => {
|
||||
const updateNodeInternals: Actions['updateNodeInternals'] = (ids) => {
|
||||
state.hooks.updateNodeInternals.trigger(ids)
|
||||
}
|
||||
|
||||
const zoomPanHelper = ref<ReturnType<typeof useZoomPanHelper>>()
|
||||
|
||||
state.hooks.paneReady.on(({ id }) => {
|
||||
zoomPanHelper.value = useZoomPanHelper(id)
|
||||
})
|
||||
|
||||
const paneReady = async () => {
|
||||
return new Promise<ReturnType<typeof useZoomPanHelper>>((resolve) => {
|
||||
if (!zoomPanHelper.value) {
|
||||
until(zoomPanHelper)
|
||||
.not.toBeUndefined()
|
||||
.then(() => resolve(zoomPanHelper.value!))
|
||||
} else {
|
||||
resolve(zoomPanHelper.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const updateNodePositions: Actions['updateNodePositions'] = (dragItems, changed, dragging) => {
|
||||
const changes: NodePositionChange[] = []
|
||||
|
||||
dragItems.forEach((node) => {
|
||||
const change: Partial<NodePositionChange> = {
|
||||
id: node.id,
|
||||
type: 'position',
|
||||
dragging,
|
||||
from: node.from,
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
change.computedPosition = node.position
|
||||
change.position = node.position
|
||||
|
||||
if (node.parentNode) {
|
||||
const parentNode = getters.getNode.value(node.parentNode)
|
||||
|
||||
change.position = {
|
||||
x: change.position.x - (parentNode?.computedPosition?.x ?? 0),
|
||||
y: change.position.y - (parentNode?.computedPosition?.y ?? 0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changes.push(change as NodePositionChange)
|
||||
})
|
||||
|
||||
if (changes?.length) {
|
||||
state.hooks.nodesChange.trigger(changes)
|
||||
}
|
||||
}
|
||||
|
||||
const updateNodeDimensions: Actions['updateNodeDimensions'] = (updates) => {
|
||||
if (!state.vueFlowRef) return
|
||||
|
||||
const viewportNode = state.vueFlowRef.querySelector('.vue-flow__transformationpane')
|
||||
|
||||
if (!viewportNode) return
|
||||
|
||||
const changes: NodeDimensionChange[] = updates.reduce<NodeDimensionChange[]>((res, update) => {
|
||||
const node = getters.getNode.value(update.id)
|
||||
|
||||
if (node) {
|
||||
const dimensions = getDimensions(update.nodeElement)
|
||||
|
||||
const style = window.getComputedStyle(viewportNode)
|
||||
const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform)
|
||||
|
||||
const doUpdate =
|
||||
!!(
|
||||
dimensions.width &&
|
||||
dimensions.height &&
|
||||
(node.dimensions.width !== dimensions.width || node.dimensions.height !== dimensions.height)
|
||||
) || update.forceUpdate
|
||||
|
||||
if (doUpdate) {
|
||||
node.handleBounds.source = getHandleBounds('.source', update.nodeElement, zoom)
|
||||
node.handleBounds.target = getHandleBounds('.target', update.nodeElement, zoom)
|
||||
|
||||
node.dimensions = dimensions
|
||||
|
||||
res.push({
|
||||
id: node.id,
|
||||
type: 'dimensions',
|
||||
dimensions,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}, [])
|
||||
|
||||
if (changes.length) state.hooks.nodesChange.trigger(changes)
|
||||
}
|
||||
|
||||
const nodeSelectionHandler = (nodes: GraphNode[], selected: boolean) => {
|
||||
const nodeIds = nodes.map((n) => n.id)
|
||||
|
||||
let changedNodes: NodeChange[]
|
||||
if (state.multiSelectionActive) changedNodes = nodeIds.map((nodeId) => createSelectionChange(nodeId, selected))
|
||||
else changedNodes = getSelectionChanges(state.nodes, nodeIds)
|
||||
|
||||
if (changedNodes.length) state.hooks.nodesChange.trigger(changedNodes)
|
||||
}
|
||||
|
||||
const edgeSelectionHandler = (edges: GraphEdge[], selected: boolean) => {
|
||||
const edgeIds = edges.map((e) => e.id)
|
||||
|
||||
let changedEdges: EdgeChange[]
|
||||
if (state.multiSelectionActive) changedEdges = edgeIds.map((edgeId) => createSelectionChange(edgeId, selected))
|
||||
else changedEdges = getSelectionChanges(state.edges, edgeIds)
|
||||
|
||||
if (changedEdges.length) state.hooks.edgesChange.trigger(changedEdges)
|
||||
}
|
||||
|
||||
const addSelectedNodes: Actions['addSelectedNodes'] = (nodes) => {
|
||||
nodeSelectionHandler(nodes, true)
|
||||
}
|
||||
|
||||
const addSelectedEdges: Actions['addSelectedEdges'] = (edges) => {
|
||||
edgeSelectionHandler(edges, true)
|
||||
}
|
||||
|
||||
const addSelectedElements: Actions['addSelectedElements'] = (elements) => {
|
||||
addSelectedNodes(elements.filter(isGraphNode))
|
||||
addSelectedEdges(elements.filter(isGraphEdge))
|
||||
}
|
||||
|
||||
const removeSelectedNodes: Actions['removeSelectedNodes'] = (nodes) => {
|
||||
if (!nodes.length) return nodeSelectionHandler(nodes, false)
|
||||
|
||||
const nodeIds = nodes.map((n) => n.id)
|
||||
|
||||
const changedNodes = nodeIds.map((nodeId) => createSelectionChange(nodeId, false))
|
||||
|
||||
if (changedNodes.length) state.hooks.nodesChange.trigger(changedNodes)
|
||||
}
|
||||
|
||||
const removeSelectedEdges: Actions['removeSelectedEdges'] = (edges) => {
|
||||
if (!edges.length) edgeSelectionHandler(edges, false)
|
||||
|
||||
const edgeIds = edges.map((e) => e.id)
|
||||
|
||||
const changedEdges = edgeIds.map((edgeId) => createSelectionChange(edgeId, false))
|
||||
|
||||
if (changedEdges.length) state.hooks.edgesChange.trigger(changedEdges)
|
||||
}
|
||||
|
||||
const removeSelectedElements: Actions['removeSelectedElements'] = (elements) => {
|
||||
if (!elements) {
|
||||
removeSelectedNodes([])
|
||||
removeSelectedEdges([])
|
||||
} else {
|
||||
if (elements.nodes) removeSelectedNodes(elements.nodes.filter(isGraphNode))
|
||||
if (elements.edges) removeSelectedEdges(elements.edges.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) => {
|
||||
state.d3Zoom?.translateExtent(translateExtent)
|
||||
state.translateExtent = translateExtent
|
||||
}
|
||||
|
||||
const setNodeExtent: Actions['setNodeExtent'] = async (nodeExtent) => {
|
||||
state.nodeExtent = nodeExtent
|
||||
|
||||
if (zoomPanHelper.value) {
|
||||
const nodeIds = getters.getNodes.value.map((n) => n.id)
|
||||
updateNodeInternals(nodeIds)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
if (!state.nodes) state.nodes = []
|
||||
const curr = nodes instanceof Function ? nodes(state.nodes) : nodes
|
||||
state.nodes = createGraphNodes(curr, getters.getNode.value, state.nodes, extent ?? state.nodeExtent)
|
||||
}
|
||||
|
||||
const setEdges: Actions['setEdges'] = (edges) => {
|
||||
if (!state.initialized && !edges.length) return
|
||||
const curr = edges instanceof Function ? edges(state.edges) : edges
|
||||
|
||||
state.edges = curr.reduce<GraphEdge[]>((res, edge) => {
|
||||
const sourceNode = getters.getNode.value(edge.source)!
|
||||
const targetNode = getters.getNode.value(edge.target)!
|
||||
|
||||
const missingSource = !sourceNode || typeof sourceNode === 'undefined'
|
||||
const missingTarget = !targetNode || typeof targetNode === 'undefined'
|
||||
if (missingSource) console.warn(`[vue-flow]: Couldn't create edge for source id: ${edge.source}; edge id: ${edge.id}`)
|
||||
if (missingTarget) console.warn(`[vue-flow]: Couldn't create edge for target id: ${edge.target}; edge id: ${edge.id}`)
|
||||
if (missingSource || missingTarget) return res
|
||||
|
||||
const storedEdge = getters.getEdge.value(edge.id)
|
||||
|
||||
res.push(
|
||||
shallowReactive({
|
||||
...parseEdge(edge, Object.assign({}, storedEdge, state.defaultEdgeOptions)),
|
||||
sourceNode,
|
||||
targetNode,
|
||||
}),
|
||||
)
|
||||
|
||||
return res
|
||||
}, [])
|
||||
}
|
||||
|
||||
const setElements: Actions['setElements'] = (elements, extent) => {
|
||||
if ((!state.initialized && !elements.length) || !elements) return
|
||||
const curr = elements instanceof Function ? elements([...state.nodes, ...state.edges]) : elements
|
||||
|
||||
setNodes(curr.filter(isNode), extent)
|
||||
setEdges(curr.filter(isEdge))
|
||||
}
|
||||
|
||||
const addNodes: Actions['addNodes'] = (nodes, extent) => {
|
||||
const curr = nodes instanceof Function ? nodes(state.nodes) : nodes
|
||||
|
||||
const graphNodes = createGraphNodes(curr, getters.getNode.value, state.nodes, extent ?? state.nodeExtent)
|
||||
const changes = graphNodes.map(createAdditionChange)
|
||||
|
||||
if (changes.length) state.hooks.nodesChange.trigger(changes)
|
||||
}
|
||||
|
||||
const addEdges: Actions['addEdges'] = (params) => {
|
||||
const curr = params instanceof Function ? params(state.edges) : params
|
||||
|
||||
const changes = curr.reduce((acc, param) => {
|
||||
const edge = addEdgeToStore(
|
||||
{
|
||||
...param,
|
||||
...state.defaultEdgeOptions,
|
||||
},
|
||||
state.edges,
|
||||
)
|
||||
if (edge) {
|
||||
const sourceNode = getters.getNode.value(edge.source)!
|
||||
const targetNode = getters.getNode.value(edge.target)!
|
||||
|
||||
const missingSource = !sourceNode || typeof sourceNode === 'undefined'
|
||||
const missingTarget = !targetNode || typeof targetNode === 'undefined'
|
||||
if (missingSource) console.warn(`[vueflow]: Couldn't create edge for source id: ${edge.source}; edge id: ${edge.id}`)
|
||||
if (missingTarget) console.warn(`[vueflow]: Couldn't create edge for target id: ${edge.target}; edge id: ${edge.id}`)
|
||||
if (missingTarget || missingSource) return acc
|
||||
|
||||
acc.push(
|
||||
createAdditionChange<GraphEdge>(
|
||||
shallowReactive({
|
||||
...edge,
|
||||
sourceNode,
|
||||
targetNode,
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [] as EdgeChange[])
|
||||
|
||||
if (changes.length) state.hooks.edgesChange.trigger(changes)
|
||||
}
|
||||
|
||||
const removeNodes: Actions['removeNodes'] = (nodes, removeConnectedEdges = true) => {
|
||||
const curr = nodes instanceof Function ? nodes(state.nodes) : nodes
|
||||
const nodeChanges: NodeRemoveChange[] = []
|
||||
const edgeChanges: EdgeRemoveChange[] = []
|
||||
curr.forEach((item) => {
|
||||
nodeChanges.push(createRemoveChange(typeof item === 'string' ? item : item.id))
|
||||
if (removeConnectedEdges) {
|
||||
const connections = getConnectedEdges([typeof item === 'string' ? ({ id: item } as any) : item], state.edges)
|
||||
edgeChanges.push(...connections.map((connection) => createRemoveChange(connection.id)))
|
||||
}
|
||||
})
|
||||
|
||||
if (nodeChanges.length) {
|
||||
state.hooks.nodesChange.trigger(nodeChanges)
|
||||
}
|
||||
|
||||
if (edgeChanges.length) {
|
||||
state.hooks.edgesChange.trigger(edgeChanges)
|
||||
}
|
||||
}
|
||||
|
||||
const removeEdges: Actions['removeEdges'] = (edges) => {
|
||||
const curr = edges instanceof Function ? edges(state.edges) : edges
|
||||
const changes: EdgeRemoveChange[] = []
|
||||
curr.forEach((item) => {
|
||||
changes.push(createRemoveChange(typeof item === 'string' ? item : item.id))
|
||||
})
|
||||
|
||||
state.hooks.edgesChange.trigger(changes)
|
||||
}
|
||||
|
||||
const nodeIds = $computed(() => state.nodes.map((n) => n.id))
|
||||
const edgeIds = $computed(() => state.edges.map((e) => e.id))
|
||||
|
||||
const findNode: Actions['findNode'] = (id) => {
|
||||
if (state.nodes && !nodeIds.length) return state.nodes.find((node) => node.id === id)
|
||||
|
||||
return state.nodes[nodeIds.indexOf(id)]
|
||||
}
|
||||
|
||||
const findEdge: Actions['findEdge'] = (id) => {
|
||||
if (state.edges && !edgeIds.length) return state.edges.find((edge) => edge.id === id)
|
||||
|
||||
return state.edges[edgeIds.indexOf(id)]
|
||||
}
|
||||
|
||||
const updateEdge: Actions['updateEdge'] = (oldEdge, newConnection) => updateEdgeAction(oldEdge, newConnection, state.edges)
|
||||
|
||||
const applyNodeChanges: Actions['applyNodeChanges'] = (changes) => applyChanges(changes, state.nodes)
|
||||
|
||||
const applyEdgeChanges: Actions['applyEdgeChanges'] = (changes) => applyChanges(changes, state.edges)
|
||||
|
||||
const startConnection: Actions['startConnection'] = (startHandle, position, event) => {
|
||||
state.connectionStartHandle = startHandle
|
||||
|
||||
if (position) state.connectionPosition = position
|
||||
|
||||
state.hooks.connectStart.trigger({
|
||||
event,
|
||||
nodeId: startHandle.nodeId,
|
||||
handleId: startHandle.handleId,
|
||||
handleType: startHandle.type,
|
||||
})
|
||||
}
|
||||
|
||||
const updateConnection: Actions['updateConnection'] = (position) => {
|
||||
state.connectionPosition = position
|
||||
}
|
||||
|
||||
const endConnection: Actions['endConnection'] = (event) => {
|
||||
state.connectionPosition = { x: NaN, y: NaN }
|
||||
state.connectionStartHandle = null
|
||||
state.hooks.connectEnd.trigger(event)
|
||||
}
|
||||
|
||||
const setState: Actions['setState'] = (options) => {
|
||||
const opts = options instanceof Function ? options(state) : options
|
||||
const skip: (keyof typeof opts)[] = [
|
||||
'modelValue',
|
||||
'nodes',
|
||||
'edges',
|
||||
'maxZoom',
|
||||
'minZoom',
|
||||
'translateExtent',
|
||||
'nodeExtent',
|
||||
'hooks',
|
||||
]
|
||||
|
||||
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)
|
||||
|
||||
const setSkippedOptions = () => {
|
||||
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 (typeof opts.nodeExtent !== 'undefined') setNodeExtent(opts.nodeExtent)
|
||||
}
|
||||
|
||||
Object.keys(opts).forEach((o) => {
|
||||
const option = opts[o as keyof typeof opts]
|
||||
if (!skip.includes(o as keyof typeof opts) && isDef(option)) (<any>state)[o] = option
|
||||
})
|
||||
|
||||
if (!state.d3Zoom) {
|
||||
until(() => state.d3Zoom)
|
||||
.not.toBeUndefined()
|
||||
.then(setSkippedOptions)
|
||||
} else {
|
||||
setSkippedOptions()
|
||||
}
|
||||
|
||||
if (!state.initialized) state.initialized = true
|
||||
}
|
||||
|
||||
const toObject = () => {
|
||||
// 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: state.nodes,
|
||||
edges: state.edges,
|
||||
position: [state.viewport.x, state.viewport.y],
|
||||
zoom: state.viewport.zoom,
|
||||
} as FlowExportObject),
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
updateNodePositions,
|
||||
updateNodeDimensions,
|
||||
setElements,
|
||||
setNodes,
|
||||
setEdges,
|
||||
addNodes,
|
||||
addEdges,
|
||||
removeNodes,
|
||||
removeEdges,
|
||||
findNode,
|
||||
findEdge,
|
||||
updateEdge,
|
||||
applyEdgeChanges,
|
||||
applyNodeChanges,
|
||||
addSelectedElements,
|
||||
addSelectedNodes,
|
||||
addSelectedEdges,
|
||||
setMinZoom,
|
||||
setMaxZoom,
|
||||
setTranslateExtent,
|
||||
setNodeExtent,
|
||||
removeSelectedElements,
|
||||
removeSelectedNodes,
|
||||
removeSelectedEdges,
|
||||
startConnection,
|
||||
updateConnection,
|
||||
endConnection,
|
||||
setInteractive,
|
||||
setState,
|
||||
fitView: async (params = { padding: 0.1 }) => {
|
||||
const { fitView } = await paneReady()
|
||||
fitView(params)
|
||||
},
|
||||
zoomIn: async (options) => {
|
||||
const { zoomIn } = await paneReady()
|
||||
zoomIn(options)
|
||||
},
|
||||
zoomOut: async (options) => {
|
||||
const { zoomOut } = await paneReady()
|
||||
zoomOut(options)
|
||||
},
|
||||
zoomTo: async (zoomLevel, options) => {
|
||||
const { zoomTo } = await paneReady()
|
||||
zoomTo(zoomLevel, options)
|
||||
},
|
||||
setTransform: async (transform, options) => {
|
||||
const { setTransform } = await paneReady()
|
||||
setTransform(transform, options)
|
||||
},
|
||||
getTransform: () => ({
|
||||
x: state.viewport.x,
|
||||
y: state.viewport.y,
|
||||
zoom: state.viewport.zoom,
|
||||
}),
|
||||
setCenter: async (x, y, options) => {
|
||||
const { setCenter } = await paneReady()
|
||||
setCenter(x, y, options)
|
||||
},
|
||||
fitBounds: async (bounds, options) => {
|
||||
const { fitBounds } = await paneReady()
|
||||
fitBounds(bounds, options)
|
||||
},
|
||||
project: (position) => pointToRendererPoint(position, state.viewport, state.snapToGrid, state.snapGrid),
|
||||
toObject,
|
||||
updateNodeInternals,
|
||||
$reset: () => {
|
||||
setState(useState())
|
||||
},
|
||||
$destroy: () => {},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { defaultEdgeTypes, defaultNodeTypes } from './state'
|
||||
import type { ComputedGetters, GraphEdge, GraphNode, State } from '~/types'
|
||||
import { getNodesInside, isEdgeVisible } from '~/utils'
|
||||
|
||||
export default (state: State): ComputedGetters => {
|
||||
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) => {
|
||||
if (state.nodes && !nodeIds.value.length) return state.nodes.find((node) => node.id === id)
|
||||
return state.nodes[nodeIds.value.indexOf(id)]
|
||||
})
|
||||
|
||||
const getEdge: ComputedGetters['getEdge'] = computed(() => (id: string) => {
|
||||
if (state.edges && !edgeIds.value.length) return state.edges.find((edge) => edge.id === id)
|
||||
return state.edges[edgeIds.value.indexOf(id)]
|
||||
})
|
||||
|
||||
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[]>(() => {
|
||||
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.viewport,
|
||||
true,
|
||||
)
|
||||
: nodes ?? []
|
||||
})
|
||||
|
||||
const edgeHidden = (e: GraphEdge, source?: GraphNode, target?: GraphNode) => {
|
||||
source = source ?? getNode.value(e.source)
|
||||
target = target ?? getNode.value(e.target)
|
||||
|
||||
if (!source || !target) {
|
||||
state.edges = state.edges.filter((edge) => edge.id !== e.id)
|
||||
|
||||
console.warn(`[vue-flow]: Orphaned edge ${e.id} removed.`)
|
||||
return
|
||||
}
|
||||
|
||||
return (
|
||||
!e.hidden &&
|
||||
target &&
|
||||
!target.hidden &&
|
||||
source &&
|
||||
!source.hidden &&
|
||||
source.dimensions.width &&
|
||||
source.dimensions.height &&
|
||||
target.dimensions.width &&
|
||||
target.dimensions.height
|
||||
)
|
||||
}
|
||||
|
||||
const getEdges = computed<GraphEdge[]>(() => {
|
||||
if (!state.onlyRenderVisibleElements) return state.edges.filter((edge) => edgeHidden(edge))
|
||||
|
||||
return state.edges.filter((e) => {
|
||||
const source = getNode.value(e.source)!
|
||||
const target = getNode.value(e.target)!
|
||||
|
||||
return (
|
||||
edgeHidden(e, source, target) &&
|
||||
isEdgeVisible({
|
||||
sourcePos: source.computedPosition || { x: 0, y: 0 },
|
||||
targetPos: target.computedPosition || { x: 0, y: 0 },
|
||||
sourceWidth: source.dimensions.width,
|
||||
sourceHeight: source.dimensions.height,
|
||||
targetWidth: target.dimensions.width,
|
||||
targetHeight: target.dimensions.height,
|
||||
width: state.dimensions.width,
|
||||
height: state.dimensions.height,
|
||||
viewport: state.viewport,
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const getElements: ComputedGetters['getElements'] = computed(() => [...getNodes.value, ...getEdges.value])
|
||||
|
||||
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 ?? []),
|
||||
])
|
||||
|
||||
return {
|
||||
getNode,
|
||||
getEdge,
|
||||
getElements,
|
||||
getEdgeTypes,
|
||||
getNodeTypes,
|
||||
getEdges,
|
||||
getNodes,
|
||||
getSelectedElements,
|
||||
getSelectedNodes,
|
||||
getSelectedEdges,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { Ref } from 'vue'
|
||||
import type { Emits, FlowHooks } 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(),
|
||||
miniMapNodeClick: createEventHook(),
|
||||
miniMapNodeDoubleClick: createEventHook(),
|
||||
miniMapNodeMouseEnter: createEventHook(),
|
||||
miniMapNodeMouseMove: createEventHook(),
|
||||
miniMapNodeMouseLeave: createEventHook(),
|
||||
connect: createEventHook(),
|
||||
connectStart: 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(),
|
||||
paneMouseEnter: createEventHook(),
|
||||
paneMouseMove: createEventHook(),
|
||||
paneMouseLeave: createEventHook(),
|
||||
edgeContextMenu: createEventHook(),
|
||||
edgeMouseEnter: createEventHook(),
|
||||
edgeMouseMove: createEventHook(),
|
||||
edgeMouseLeave: createEventHook(),
|
||||
edgeDoubleClick: createEventHook(),
|
||||
edgeClick: createEventHook(),
|
||||
edgeUpdateStart: createEventHook(),
|
||||
edgeUpdate: createEventHook(),
|
||||
edgeUpdateEnd: createEventHook(),
|
||||
updateNodeInternals: createEventHook(),
|
||||
})
|
||||
|
||||
export default (emit: Emits, hooks: Ref<FlowHooks>) => {
|
||||
onBeforeMount(() => {
|
||||
for (const [key, value] of Object.entries(hooks.value)) {
|
||||
const listener = (data: any) => {
|
||||
emit(key as Emits extends (event: infer Event) => void ? Event : never, data)
|
||||
}
|
||||
|
||||
value.on(listener)
|
||||
|
||||
tryOnScopeDispose(() => {
|
||||
value.off(listener)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export { default as useHooks, createHooks } from './hooks'
|
||||
export { default as useActions } from './actions'
|
||||
export { default as useState, defaultEdgeTypes, defaultNodeTypes } from './state'
|
||||
export { default as useGetters } from './getters'
|
||||
@@ -0,0 +1,129 @@
|
||||
import { createHooks } from './hooks'
|
||||
import type { DefaultEdgeTypes, DefaultNodeTypes, FlowOptions, State } from '~/types'
|
||||
import { ConnectionLineType, ConnectionMode, PanOnScrollMode } from '~/types'
|
||||
import {
|
||||
BezierEdge,
|
||||
DefaultNode,
|
||||
InputNode,
|
||||
OutputNode,
|
||||
SimpleBezierEdge,
|
||||
SmoothStepEdge,
|
||||
StepEdge,
|
||||
StraightEdge,
|
||||
} 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 => ({
|
||||
vueFlowRef: null,
|
||||
viewportRef: null,
|
||||
nodes: [],
|
||||
edges: [],
|
||||
nodeTypes: {},
|
||||
edgeTypes: {},
|
||||
|
||||
initialized: false,
|
||||
|
||||
dimensions: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
viewport: { x: 0, y: 0, zoom: 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,
|
||||
|
||||
defaultMarkerColor: '#b1b1b7',
|
||||
connectionLineStyle: {},
|
||||
connectionLineType: ConnectionLineType.Bezier,
|
||||
connectionLineOptions: {
|
||||
type: ConnectionLineType.Bezier,
|
||||
style: {},
|
||||
},
|
||||
connectionMode: ConnectionMode.Loose,
|
||||
connectionStartHandle: null,
|
||||
connectionPosition: { x: NaN, y: NaN },
|
||||
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,
|
||||
autoConnect: false,
|
||||
|
||||
fitViewOnInit: false,
|
||||
noDragClassName: 'nodrag',
|
||||
noWheelClassName: 'nowheel',
|
||||
noPanClassName: 'nopan',
|
||||
defaultEdgeOptions: undefined,
|
||||
elevateEdgesOnSelect: false,
|
||||
|
||||
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,176 @@
|
||||
.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__transformationpane {
|
||||
pointer-events: none;
|
||||
transform-origin: 0 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.vue-flow__pane {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.vue-flow__viewport {
|
||||
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 .vue-flow__connectionline {
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.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 {
|
||||
position: absolute;
|
||||
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 @@
|
||||
:root {
|
||||
--vf-node-bg: #fff;
|
||||
--vf-node-text: #222;
|
||||
--vf-connection-path: #b1b1b7;
|
||||
--vf-handle: #555;
|
||||
}
|
||||
|
||||
.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: #fff;
|
||||
}
|
||||
|
||||
.vue-flow__connection-path {
|
||||
stroke: var(--vf-connection-path);
|
||||
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: var(--vf-node-text);
|
||||
text-align: center;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
|
||||
background: var(--vf-node-bg);
|
||||
border-color: var(--vf-node-color);
|
||||
|
||||
&.selected,
|
||||
&.selected:hover {
|
||||
box-shadow: 0 0 0 0.5px var(--vf-box-shadow);
|
||||
}
|
||||
|
||||
.vue-flow__handle {
|
||||
background: var(--vf-handle);
|
||||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
--vf-node-color: #0041d0;
|
||||
--vf-handle: var(--vf-node-color);
|
||||
--vf-box-shadow: var(--vf-node-color);
|
||||
|
||||
background: var(--vf-node-bg);
|
||||
border-color: var(--vf-node-color);
|
||||
}
|
||||
|
||||
.vue-flow__node-default {
|
||||
--vf-node-color: #1a192b;
|
||||
--vf-handle: var(--vf-node-color);
|
||||
--vf-box-shadow: var(--vf-node-color);
|
||||
|
||||
background: var(--vf-node-bg);
|
||||
border-color: var(--vf-node-color);
|
||||
}
|
||||
|
||||
.vue-flow__node-output {
|
||||
--vf-node-color: #ff0072;
|
||||
--vf-handle: var(--vf-node-color);
|
||||
--vf-box-shadow: var(--vf-node-color);
|
||||
}
|
||||
|
||||
.vue-flow__nodesselection-rect {
|
||||
background: rgba(0, 89, 220, 0.08);
|
||||
border: 1px dotted rgba(0, 89, 220, 0.8);
|
||||
}
|
||||
|
||||
.vue-flow__handle {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: var(--vf-handle);
|
||||
border: 1px solid #fff;
|
||||
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,62 @@
|
||||
import type { Dimensions, ElementData, XYPosition } from './flow'
|
||||
import type { GraphNode, Node, NodeHandleBounds } from './node'
|
||||
import type { GraphEdge } from './edge'
|
||||
|
||||
export interface NodeDragItem {
|
||||
id: string
|
||||
// relative node position
|
||||
position: XYPosition
|
||||
// distance from the mouse cursor to the node when start dragging
|
||||
distance: XYPosition
|
||||
dimensions: Dimensions
|
||||
from: XYPosition
|
||||
extent?: Node['extent']
|
||||
parentNode?: string
|
||||
}
|
||||
|
||||
export interface NodeDimensionChange {
|
||||
id: string
|
||||
type: 'dimensions'
|
||||
dimensions: Dimensions
|
||||
handleBounds?: NodeHandleBounds
|
||||
}
|
||||
|
||||
export interface NodePositionChange {
|
||||
id: string
|
||||
type: 'position'
|
||||
position: XYPosition
|
||||
computedPosition: XYPosition
|
||||
from: XYPosition
|
||||
dragging: boolean
|
||||
}
|
||||
|
||||
export interface NodeSelectionChange {
|
||||
id: string
|
||||
type: 'select'
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
export interface NodeRemoveChange {
|
||||
id: string
|
||||
type: 'remove'
|
||||
}
|
||||
|
||||
export interface NodeAddChange<Data = ElementData> {
|
||||
item: GraphNode<Data>
|
||||
type: 'add'
|
||||
}
|
||||
|
||||
export type NodeChange = NodeDimensionChange | NodePositionChange | NodeSelectionChange | NodeRemoveChange | NodeAddChange
|
||||
|
||||
export type EdgeSelectionChange = NodeSelectionChange
|
||||
|
||||
export type EdgeRemoveChange = NodeRemoveChange
|
||||
|
||||
export interface EdgeAddChange<Data = ElementData> {
|
||||
item: GraphEdge<Data>
|
||||
type: 'add'
|
||||
}
|
||||
|
||||
export type EdgeChange = EdgeSelectionChange | EdgeRemoveChange | EdgeAddChange
|
||||
|
||||
export type ElementChange = NodeChange | EdgeChange
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { CSSProperties, Component, DefineComponent, SVGAttributes, VNode } from 'vue'
|
||||
import type { NodeProps } from './node'
|
||||
import type { EdgeProps } from './edge'
|
||||
|
||||
/** 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 = Component<NodeProps> | DefineComponent<NodeProps, any, any, any, any> | GlobalComponentName
|
||||
|
||||
export type NodeTypesObject = { [key in keyof DefaultNodeTypes]?: NodeComponent } & Record<string, NodeComponent>
|
||||
|
||||
export type EdgeTypesObject = { [key in keyof DefaultEdgeTypes]?: EdgeComponent } & Record<string, EdgeComponent>
|
||||
|
||||
/** Edge Components can either be a component definition or a string name */
|
||||
export type EdgeComponent = Component<EdgeProps> | DefineComponent<EdgeProps, 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 }
|
||||
|
||||
/** these props are passed to edge texts */
|
||||
export interface EdgeTextProps extends SVGAttributes {
|
||||
x: number
|
||||
y: number
|
||||
label?: string | VNode | Object
|
||||
labelStyle?: CSSProperties
|
||||
labelShowBg?: boolean
|
||||
labelBgStyle?: CSSProperties
|
||||
labelBgPadding?: [number, number]
|
||||
labelBgBorderRadius?: number
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { CSSProperties } from 'vue'
|
||||
import type { Position } from './flow'
|
||||
import type { GraphNode } from './node'
|
||||
import type { HandleElement, HandleType } from './handle'
|
||||
import type { Edge, EdgeMarkerType } from './edge'
|
||||
|
||||
/** Connection line types (same as default edge types */
|
||||
export enum ConnectionLineType {
|
||||
Bezier = 'default',
|
||||
SimpleBezier = 'simple-bezier',
|
||||
Straight = 'straight',
|
||||
Step = 'step',
|
||||
SmoothStep = 'smoothstep',
|
||||
}
|
||||
|
||||
export interface ConnectionLineOptions {
|
||||
type?: ConnectionLineType
|
||||
style?: CSSProperties
|
||||
class?: string
|
||||
markerEnd?: EdgeMarkerType
|
||||
markerStart?: EdgeMarkerType
|
||||
}
|
||||
|
||||
/** Connection params that are passed when onConnect is called */
|
||||
export interface Connection {
|
||||
/** Source node id */
|
||||
source: string
|
||||
/** Target node id */
|
||||
target: string
|
||||
/** Source handle id */
|
||||
sourceHandle: string | null
|
||||
/** Target handle id */
|
||||
targetHandle: string | null
|
||||
}
|
||||
|
||||
export type Connector = (
|
||||
params: Connection,
|
||||
) => Promise<(Connection & Partial<Edge>) | false> | ((Connection & Partial<Edge>) | false)
|
||||
|
||||
/** The source nodes params when connection is initiated */
|
||||
export interface 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 {
|
||||
/** 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[]
|
||||
/** The source node of the connection line */
|
||||
sourceNode: GraphNode
|
||||
/** The source handle element of the connection line */
|
||||
sourceHandle: HandleElement
|
||||
/** marker url */
|
||||
markerStart: string
|
||||
/** marker url */
|
||||
markerEnd: string
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import type { CSSProperties, Component, VNode } from 'vue'
|
||||
import type { ClassFunc, ElementData, Position, StyleFunc, Styles } from './flow'
|
||||
import type { GraphNode } from './node'
|
||||
import type { DefaultEdgeTypes, EdgeComponent, EdgeTextProps } from './components'
|
||||
import type { CustomEvent, EdgeEventsHandler, EdgeEventsOn } from './hooks'
|
||||
|
||||
/** 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 type EdgeUpdatable = boolean | 'target' | 'source'
|
||||
|
||||
export interface Edge<Data = ElementData, CustomEvents extends Record<string, CustomEvent> = any> {
|
||||
/** Unique edge id */
|
||||
id: string
|
||||
/** An edge label */
|
||||
label?: string | VNode | Component<EdgeTextProps>
|
||||
/** Edge type, can be a default type or a custom type */
|
||||
type?: keyof DefaultEdgeTypes | string
|
||||
/** Source node id */
|
||||
source: string
|
||||
/** Target node id */
|
||||
target: string
|
||||
/** Source handle id */
|
||||
sourceHandle?: string | null
|
||||
/** Target handle id */
|
||||
targetHandle?: string | null
|
||||
/** 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?: EdgeUpdatable
|
||||
/** Disable/enable selecting edge */
|
||||
selectable?: boolean
|
||||
/** Additional class names, can be a string or a callback returning a string (receives current flow element) */
|
||||
class?: string | ClassFunc<GraphEdge<Data, CustomEvents>>
|
||||
/** Additional styles, can be an object or a callback returning an object (receives current flow element) */
|
||||
style?: Styles | StyleFunc<GraphEdge<Data, CustomEvents>>
|
||||
/** Is edge hidden */
|
||||
hidden?: boolean
|
||||
/** Overwrites current edge type */
|
||||
template?: EdgeComponent
|
||||
/** Additional data that is passed to your custom components */
|
||||
data?: Data
|
||||
/** contextual and custom events of edge */
|
||||
events?: Partial<EdgeEventsHandler<CustomEvents>>
|
||||
}
|
||||
|
||||
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<Data = ElementData, CustomEvents extends Record<string, CustomEvent> = any> = Edge<Data, CustomEvents> & {
|
||||
selected?: boolean
|
||||
z?: number
|
||||
sourceNode: GraphNode
|
||||
targetNode: GraphNode
|
||||
} & EdgePositions
|
||||
|
||||
/** these props are passed to edge components */
|
||||
export interface EdgeProps<Data = ElementData, CustomEvents = {}> {
|
||||
id: string
|
||||
sourceNode: GraphNode
|
||||
targetNode: GraphNode
|
||||
label?: string | VNode | Component<EdgeTextProps> | Object
|
||||
type?: string
|
||||
style?: CSSProperties
|
||||
sourceX: number
|
||||
sourceY: number
|
||||
targetX: number
|
||||
targetY: number
|
||||
selected?: boolean
|
||||
sourcePosition: Position
|
||||
targetPosition: Position
|
||||
sourceHandleId?: string
|
||||
targetHandleId?: string
|
||||
source: string
|
||||
target: string
|
||||
labelStyle?: CSSProperties
|
||||
labelShowBg?: boolean
|
||||
labelBgStyle?: any
|
||||
labelBgPadding?: [number, number]
|
||||
labelBgBorderRadius?: number
|
||||
animated?: boolean
|
||||
updatable?: boolean
|
||||
markerStart?: string
|
||||
markerEnd?: string
|
||||
curvature?: number
|
||||
data?: Data
|
||||
/** contextual and custom events of edge */
|
||||
events?: Partial<EdgeEventsOn<CustomEvents>>
|
||||
}
|
||||
|
||||
/** these props are passed to smooth step edges */
|
||||
export interface SmoothStepEdgeProps<Data = ElementData, CustomEvents = {}> extends EdgeProps<Data, CustomEvents> {
|
||||
id: string
|
||||
sourceNode: GraphNode
|
||||
targetNode: GraphNode
|
||||
label?: string | VNode | Component<EdgeTextProps> | Object
|
||||
type?: string
|
||||
style?: CSSProperties
|
||||
sourceX: number
|
||||
sourceY: number
|
||||
targetX: number
|
||||
targetY: number
|
||||
selected?: boolean
|
||||
sourcePosition: Position
|
||||
targetPosition: Position
|
||||
sourceHandleId?: string
|
||||
targetHandleId?: string
|
||||
source: string
|
||||
target: string
|
||||
labelStyle?: any
|
||||
labelShowBg?: boolean
|
||||
labelBgStyle?: any
|
||||
labelBgPadding?: [number, number]
|
||||
labelBgBorderRadius?: number
|
||||
animated?: boolean
|
||||
updatable?: boolean
|
||||
markerStart?: string
|
||||
markerEnd?: string
|
||||
borderRadius?: number
|
||||
offset?: number
|
||||
data?: Data
|
||||
/** contextual and custom events of edge */
|
||||
events?: Partial<EdgeEventsOn<CustomEvents>>
|
||||
}
|
||||
|
||||
export interface BaseEdgeProps {
|
||||
labelX: number
|
||||
labelY: number
|
||||
path: string
|
||||
label?: any
|
||||
style?: CSSProperties
|
||||
labelStyle?: any
|
||||
labelShowBg?: boolean
|
||||
labelBgStyle?: any
|
||||
labelBgPadding?: [number, number]
|
||||
labelBgBorderRadius?: number
|
||||
markerStart?: string
|
||||
markerEnd?: string
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import type { CSSProperties } from 'vue'
|
||||
import type { KeyFilter } from '@vueuse/core'
|
||||
import type { DefaultEdgeOptions, Edge, EdgeUpdatable, GraphEdge } from './edge'
|
||||
import type { CoordinateExtent, GraphNode, Node } from './node'
|
||||
import type { ConnectionLineOptions, ConnectionLineType, ConnectionMode, Connector } from './connection'
|
||||
import type { PanOnScrollMode } from './zoom'
|
||||
import type { EdgeTypesObject, NodeTypesObject } from './components'
|
||||
|
||||
export type ElementData = any
|
||||
|
||||
/** A flow element (after parsing into state) */
|
||||
export type FlowElement<Data = ElementData> = GraphNode<Data> | GraphEdge<Data>
|
||||
export type FlowElements<Data = ElementData> = FlowElement<Data>[]
|
||||
|
||||
/** Initial elements (before parsing into state) */
|
||||
export type Element<Data = ElementData> = Node<Data> | Edge<Data>
|
||||
export type Elements<Data = ElementData> = Element<Data>[]
|
||||
|
||||
export type CustomThemeVars = Record<string, string | number>
|
||||
export type CSSVars =
|
||||
| '--vf-node-color'
|
||||
| '--vf-box-shadow'
|
||||
| '--vf-node-bg'
|
||||
| '--vf-node-text'
|
||||
| '--vf-connection-path'
|
||||
| '--vf-handle'
|
||||
export type ThemeVars = { [key in CSSVars]?: CSSProperties['color'] }
|
||||
export type Styles = CSSProperties & ThemeVars & CustomThemeVars
|
||||
export type ClassFunc<ElementType extends FlowElement = FlowElement> = (element: ElementType) => string | void
|
||||
export type StyleFunc<ElementType extends FlowElement = FlowElement> = (element: ElementType) => Styles | void
|
||||
|
||||
/** 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 interface SelectionRect extends Rect {
|
||||
startX: number
|
||||
startY: number
|
||||
draw: boolean
|
||||
}
|
||||
|
||||
export interface FlowExportObject {
|
||||
nodes: GraphNode[]
|
||||
edges: GraphEdge[]
|
||||
position: [number, number]
|
||||
zoom: number
|
||||
}
|
||||
|
||||
export interface FlowProps {
|
||||
id?: string
|
||||
modelValue?: Elements
|
||||
nodes?: Node[]
|
||||
edges?: Edge[]
|
||||
/** either use the edgeTypes prop to define your edge-types or use slots (<template #edge-mySpecialType="props">) */
|
||||
edgeTypes?: EdgeTypesObject
|
||||
/** either use the nodeTypes prop to define your node-types or use slots (<template #node-mySpecialType="props">) */
|
||||
nodeTypes?: NodeTypesObject
|
||||
connectionMode?: ConnectionMode
|
||||
/** @deprecated use {@link ConnectionLineOptions.type} */
|
||||
connectionLineType?: ConnectionLineType
|
||||
/** @deprecated use {@link ConnectionLineOptions.style} */
|
||||
connectionLineStyle?: CSSProperties | null
|
||||
connectionLineOptions?: ConnectionLineOptions
|
||||
deleteKeyCode?: KeyFilter
|
||||
selectionKeyCode?: KeyFilter
|
||||
multiSelectionKeyCode?: KeyFilter
|
||||
zoomActivationKeyCode?: KeyFilter
|
||||
snapToGrid?: boolean
|
||||
snapGrid?: SnapGrid
|
||||
onlyRenderVisibleElements?: boolean
|
||||
edgesUpdatable?: EdgeUpdatable
|
||||
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
|
||||
/** automatically create an edge when connection is triggered */
|
||||
autoConnect?: boolean | Connector
|
||||
noDragClassName?: string
|
||||
noWheelClassName?: string
|
||||
noPanClassName?: string
|
||||
/** does not work for the `addEdge` utility! */
|
||||
defaultEdgeOptions?: DefaultEdgeOptions
|
||||
/** elevates edges when selected and applies z-Index to put them above their nodes */
|
||||
elevateEdgesOnSelect?: boolean
|
||||
}
|
||||
|
||||
export type FlowOptions = FlowProps
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { Dimensions, Position, XYPosition } from './flow'
|
||||
import type { Connection } from './connection'
|
||||
import type { GraphEdge } from './edge'
|
||||
import type { GraphNode } from './node'
|
||||
|
||||
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,
|
||||
elements: { edges: GraphEdge[]; sourceNode: GraphNode; targetNode: GraphNode },
|
||||
) => 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,201 @@
|
||||
import type { EventHook, EventHookOn, EventHookTrigger } from '@vueuse/core'
|
||||
import type { D3ZoomEvent } from 'd3-zoom'
|
||||
import type { GraphEdge } from './edge'
|
||||
import type { GraphNode } from './node'
|
||||
import type { Connection, OnConnectStartParams } from './connection'
|
||||
import type { ViewpaneTransform } from './zoom'
|
||||
import type { EdgeChange, NodeChange } from './changes'
|
||||
import type { VueFlowStore } from './store'
|
||||
import type { FlowElements } from './flow'
|
||||
|
||||
export type MouseTouchEvent = MouseEvent | TouchEvent | PointerEvent
|
||||
|
||||
export interface NodeMouseEvent {
|
||||
event: MouseTouchEvent
|
||||
node: GraphNode
|
||||
connectedEdges: GraphEdge[]
|
||||
}
|
||||
|
||||
export interface NodeDragEvent {
|
||||
event: MouseEvent
|
||||
node: GraphNode
|
||||
nodes: GraphNode[]
|
||||
}
|
||||
|
||||
export interface EdgeMouseEvent {
|
||||
event: MouseEvent
|
||||
edge: GraphEdge
|
||||
}
|
||||
|
||||
export interface EdgeUpdateEvent {
|
||||
edge: GraphEdge
|
||||
connection: Connection
|
||||
}
|
||||
|
||||
export interface FlowEvents {
|
||||
nodesChange: NodeChange[]
|
||||
edgesChange: EdgeChange[]
|
||||
nodeDoubleClick: NodeMouseEvent
|
||||
nodeClick: NodeMouseEvent
|
||||
nodeMouseEnter: NodeMouseEvent
|
||||
nodeMouseMove: NodeMouseEvent
|
||||
nodeMouseLeave: NodeMouseEvent
|
||||
nodeContextMenu: NodeMouseEvent
|
||||
nodeDragStart: NodeDragEvent
|
||||
nodeDrag: NodeDragEvent
|
||||
nodeDragStop: NodeDragEvent
|
||||
updateNodeInternals: string[]
|
||||
miniMapNodeClick: NodeMouseEvent
|
||||
miniMapNodeDoubleClick: NodeMouseEvent
|
||||
miniMapNodeMouseEnter: NodeMouseEvent
|
||||
miniMapNodeMouseMove: NodeMouseEvent
|
||||
miniMapNodeMouseLeave: NodeMouseEvent
|
||||
connect: Connection
|
||||
connectStart: {
|
||||
event?: MouseEvent
|
||||
} & OnConnectStartParams
|
||||
connectEnd: MouseEvent | undefined
|
||||
paneReady: VueFlowStore
|
||||
move: { event: D3ZoomEvent<HTMLDivElement, any>; flowTransform: ViewpaneTransform }
|
||||
moveStart: { event: D3ZoomEvent<HTMLDivElement, any>; flowTransform: ViewpaneTransform }
|
||||
moveEnd: { event: D3ZoomEvent<HTMLDivElement, any>; flowTransform: ViewpaneTransform }
|
||||
selectionDragStart: NodeDragEvent
|
||||
selectionDrag: NodeDragEvent
|
||||
selectionDragStop: NodeDragEvent
|
||||
selectionContextMenu: { event: MouseEvent; nodes: GraphNode[] }
|
||||
paneScroll: WheelEvent | undefined
|
||||
paneClick: MouseEvent
|
||||
paneContextMenu: MouseEvent
|
||||
paneMouseEnter: MouseEvent
|
||||
paneMouseMove: MouseEvent
|
||||
paneMouseLeave: MouseEvent
|
||||
edgeContextMenu: EdgeMouseEvent
|
||||
edgeMouseEnter: EdgeMouseEvent
|
||||
edgeMouseMove: EdgeMouseEvent
|
||||
edgeMouseLeave: EdgeMouseEvent
|
||||
edgeDoubleClick: EdgeMouseEvent
|
||||
edgeClick: EdgeMouseEvent
|
||||
edgeUpdateStart: EdgeMouseEvent
|
||||
edgeUpdate: EdgeUpdateEvent
|
||||
edgeUpdateEnd: EdgeMouseEvent
|
||||
}
|
||||
|
||||
export type FlowHooks = Readonly<{
|
||||
[key in keyof FlowEvents]: EventHook<FlowEvents[key]>
|
||||
}>
|
||||
|
||||
export type FlowHooksOn = Readonly<{
|
||||
[key in keyof FlowEvents as `on${Capitalize<key>}`]: EventHookOn<FlowEvents[key]>
|
||||
}>
|
||||
|
||||
export type FlowHooksEmit = Readonly<{
|
||||
[key in keyof FlowEvents]: EventHookTrigger<FlowEvents[key]>
|
||||
}>
|
||||
|
||||
/**
|
||||
* Vue Flow component event emitter definitions
|
||||
*/
|
||||
export interface Emits {
|
||||
(event: 'nodesChange', changes: NodeChange[]): void
|
||||
(event: 'edgesChange', changes: EdgeChange[]): void
|
||||
(event: 'nodeDoubleClick', nodeMouseEvent: NodeMouseEvent): void
|
||||
(event: 'nodeClick', nodeMouseEvent: NodeMouseEvent): void
|
||||
(event: 'nodeMouseEnter', nodeMouseEvent: NodeMouseEvent): void
|
||||
(event: 'nodeMouseMove', nodeMouseEvent: NodeMouseEvent): void
|
||||
(event: 'nodeMouseLeave', nodeMouseEvent: NodeMouseEvent): void
|
||||
(event: 'nodeContextMenu', nodeMouseEvent: NodeMouseEvent): void
|
||||
(event: 'nodeDragStart', nodeDragEvent: NodeDragEvent): void
|
||||
(event: 'nodeDrag', nodeDragEvent: NodeDragEvent): void
|
||||
(event: 'nodeDragStop', nodeDragEvent: NodeDragEvent): void
|
||||
(event: 'miniMapNodeClick', nodeMouseEvent: NodeMouseEvent): void
|
||||
(event: 'miniMapNodeDoubleClick', nodeMouseEvent: NodeMouseEvent): void
|
||||
(event: 'miniMapNodeMouseEnter', nodeMouseEvent: NodeMouseEvent): void
|
||||
(event: 'miniMapNodeMouseMove', nodeMouseEvent: NodeMouseEvent): void
|
||||
(event: 'miniMapNodeMouseLeave', nodeMouseEvent: NodeMouseEvent): void
|
||||
(event: 'connect', connectionEvent: Connection): void
|
||||
(
|
||||
event: 'connectStart',
|
||||
connectionEvent: {
|
||||
event: MouseEvent
|
||||
} & OnConnectStartParams,
|
||||
): void
|
||||
(event: 'connectEnd', connectionEvent: MouseEvent): void
|
||||
(event: 'moveStart', moveEvent: { event: D3ZoomEvent<HTMLDivElement, any>; flowTransform: ViewpaneTransform }): void
|
||||
(event: 'move', moveEvent: { event: D3ZoomEvent<HTMLDivElement, any>; flowTransform: ViewpaneTransform }): void
|
||||
(event: 'moveEnd', moveEvent: { event: D3ZoomEvent<HTMLDivElement, any>; flowTransform: ViewpaneTransform }): void
|
||||
(event: 'selectionDragStart', selectionEvent: NodeDragEvent): void
|
||||
(event: 'selectionDrag', selectionEvent: NodeDragEvent): void
|
||||
(event: 'selectionDragStop', selectionEvent: NodeDragEvent): void
|
||||
(event: 'selectionContextMenu', selectionEvent: { event: MouseEvent; nodes: GraphNode[] }): void
|
||||
(event: 'paneReady', paneEvent: VueFlowStore): void
|
||||
(event: 'paneScroll', paneEvent: WheelEvent | undefined): void
|
||||
(event: 'paneClick', paneEvent: MouseEvent): void
|
||||
(event: 'paneContextMenu', paneEvent: MouseEvent): void
|
||||
(event: 'paneMouseEnter', paneEvent: MouseEvent): void
|
||||
(event: 'paneMouseMove', paneEvent: MouseEvent): void
|
||||
(event: 'paneMouseLeave', paneEvent: MouseEvent): void
|
||||
(event: 'edgeContextMenu', edgeMouseEvent: EdgeMouseEvent): void
|
||||
(event: 'edgeMouseEnter', edgeMouseEvent: EdgeMouseEvent): void
|
||||
(event: 'edgeMouseMove', edgeMouseEvent: EdgeMouseEvent): void
|
||||
(event: 'edgeMouseLeave', edgeMouseEvent: EdgeMouseEvent): void
|
||||
(event: 'edgeDoubleClick', edgeMouseEvent: EdgeMouseEvent): void
|
||||
(event: 'edgeClick', edgeMouseEvent: EdgeMouseEvent): void
|
||||
(event: 'edgeUpdateStart', edgeMouseEvent: EdgeMouseEvent): void
|
||||
(event: 'edgeUpdate', edgeUpdateEvent: EdgeUpdateEvent): void
|
||||
(event: 'edgeUpdateEnd', edgeMouseEvent: EdgeMouseEvent): void
|
||||
|
||||
/** v-model event definitions */
|
||||
(event: 'update:modelValue', value: FlowElements): void
|
||||
(event: 'update:nodes', value: GraphNode[]): void
|
||||
(event: 'update:edges', value: GraphEdge[]): void
|
||||
}
|
||||
|
||||
/**
|
||||
* To type `Args` (the event callback arguments) pass an array as argument list as first generic type
|
||||
* To type `Return` (the event callback return value) pass a value to the second generic type
|
||||
*/
|
||||
export type CustomEvent<Args extends any[] = any[], Return = any> = (...args: Args) => Return
|
||||
|
||||
type CustomEventHandlers<CustomEvents = {}> = {
|
||||
[key in keyof CustomEvents]: CustomEvents[key]
|
||||
}
|
||||
|
||||
export type NodeEventsHandler<CustomEvents = {}> = {
|
||||
doubleClick: (event: NodeMouseEvent) => void | { off: () => void }
|
||||
click: (event: NodeMouseEvent) => void | { off: () => void }
|
||||
mouseEnter: (event: NodeMouseEvent) => void | { off: () => void }
|
||||
mouseMove: (event: NodeMouseEvent) => void | { off: () => void }
|
||||
mouseLeave: (event: NodeMouseEvent) => void | { off: () => void }
|
||||
contextMenu: (event: NodeMouseEvent) => void | { off: () => void }
|
||||
dragStart: (event: NodeDragEvent) => void | { off: () => void }
|
||||
drag: (event: NodeDragEvent) => void | { off: () => void }
|
||||
dragStop: (event: NodeDragEvent) => void | { off: () => void }
|
||||
} & CustomEventHandlers<CustomEvents>
|
||||
|
||||
export type NodeEventsOn<CustomEvents = {}> = {
|
||||
[key in keyof NodeEventsHandler]: EventHookOn<NodeEventsHandler[key] extends (event: infer Event) => any ? Event : never>
|
||||
} & CustomEventHandlers<CustomEvents>
|
||||
|
||||
export type NodeEventsEmit<CustomEvents = {}> = {
|
||||
[key in keyof NodeEventsHandler]: EventHookTrigger<NodeEventsHandler[key] extends (event: infer Event) => any ? Event : never>
|
||||
} & CustomEventHandlers<CustomEvents>
|
||||
|
||||
export type EdgeEventsHandler<CustomEvents = {}> = {
|
||||
doubleClick: (event: EdgeMouseEvent) => void | { off: () => void }
|
||||
click: (event: EdgeMouseEvent) => void | { off: () => void }
|
||||
mouseEnter: (event: EdgeMouseEvent) => void | { off: () => void }
|
||||
mouseMove: (event: EdgeMouseEvent) => void | { off: () => void }
|
||||
mouseLeave: (event: EdgeMouseEvent) => void | { off: () => void }
|
||||
contextMenu: (event: EdgeMouseEvent) => void | { off: () => void }
|
||||
updateStart: (event: EdgeMouseEvent) => void | { off: () => void }
|
||||
update: (event: EdgeUpdateEvent) => void | { off: () => void }
|
||||
updateEnd: (event: EdgeMouseEvent) => void | { off: () => void }
|
||||
} & CustomEventHandlers<CustomEvents>
|
||||
|
||||
export type EdgeEventsOn<CustomEvents = {}> = {
|
||||
[key in keyof EdgeEventsHandler]: EventHookOn<EdgeEventsHandler[key] extends (event: infer Event) => any ? Event : never>
|
||||
} & CustomEventHandlers<CustomEvents>
|
||||
|
||||
export type EdgeEventsEmit<CustomEvents = {}> = {
|
||||
[key in keyof EdgeEventsHandler]: EventHookTrigger<EdgeEventsHandler[key] extends (event: infer Event) => any ? Event : never>
|
||||
} & CustomEventHandlers<CustomEvents>
|
||||
@@ -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,129 @@
|
||||
import type { Component, VNode } from 'vue'
|
||||
import type { ClassFunc, Dimensions, ElementData, Position, SnapGrid, StyleFunc, Styles, XYPosition, XYZPosition } from './flow'
|
||||
import type { DefaultNodeTypes, NodeComponent } from './components'
|
||||
import type { HandleElement, ValidConnectionFunc } from './handle'
|
||||
import type { CustomEvent, NodeEventsHandler, NodeEventsOn } from './hooks'
|
||||
|
||||
/** Defined as [[x-from, y-from], [x-to, y-to]] **/
|
||||
export type CoordinateExtent = [[number, number], [number, number]]
|
||||
|
||||
export interface NodeHandleBounds {
|
||||
source?: HandleElement[]
|
||||
target?: HandleElement[]
|
||||
}
|
||||
|
||||
export type WidthFunc = (node: GraphNode) => number | string | void
|
||||
|
||||
export type HeightFunc = (node: GraphNode) => number | string | void
|
||||
|
||||
export interface Node<Data = ElementData, CustomEvents extends Record<string, CustomEvent> = any> {
|
||||
/** Unique node id */
|
||||
id: string
|
||||
/** A node label */
|
||||
label?: string | VNode | Component
|
||||
/** initial 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
|
||||
|
||||
/**
|
||||
* Fixed width of node, applied as style
|
||||
* You can pass a number which will be used in pixel values (width: 300 -> width: 300px)
|
||||
* or pass a string with units (width: `10rem` -> width: 10rem)
|
||||
*/
|
||||
width?: number | string | WidthFunc
|
||||
|
||||
/**
|
||||
* Fixed height of node, applied as style
|
||||
* You can pass a number which will be used in pixel values (height: 300 -> height: 300px)
|
||||
* or pass a string with units (height: `10rem` -> height: 10rem)
|
||||
*/
|
||||
height?: number | string | HeightFunc
|
||||
|
||||
/** Additional class names, can be a string or a callback returning a string (receives current flow element) */
|
||||
class?: string | ClassFunc<GraphNode<Data, CustomEvents>>
|
||||
/** Additional styles, can be an object or a callback returning an object (receives current flow element) */
|
||||
style?: Styles | StyleFunc<GraphNode<Data, CustomEvents>>
|
||||
/** Is node hidden */
|
||||
hidden?: boolean
|
||||
/** overwrites current node type */
|
||||
template?: NodeComponent
|
||||
/** Additional data that is passed to your custom components */
|
||||
data?: Data
|
||||
/** contextual and custom events that are passed to your custom components */
|
||||
events?: Partial<NodeEventsHandler<CustomEvents>>
|
||||
}
|
||||
|
||||
export interface GraphNode<Data = ElementData, CustomEvents extends Record<string, CustomEvent> = any>
|
||||
extends Node<Data, CustomEvents> {
|
||||
/** absolute position in relation to parent elements + z-index */
|
||||
computedPosition: XYZPosition
|
||||
handleBounds: NodeHandleBounds
|
||||
/** node width, height */
|
||||
dimensions: Dimensions
|
||||
isParent: boolean
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
/** these props are passed to node components */
|
||||
export interface NodeProps<Data = ElementData, CustomEvents = {}> {
|
||||
/** unique node id */
|
||||
id: string
|
||||
/** node type */
|
||||
type: keyof DefaultNodeTypes | string
|
||||
/** is node selected */
|
||||
selected: boolean
|
||||
/** can node be connected */
|
||||
connectable: boolean
|
||||
/** node x, y (relative) position on graph */
|
||||
position: XYPosition
|
||||
/** dom element dimensions (width, height) */
|
||||
dimensions: Dimensions
|
||||
/**
|
||||
* node label, either pass a string or a VNode
|
||||
* For example like this: `h('div', props, children)`)
|
||||
* Object is just a type-hack for Vue, ignore that
|
||||
*/
|
||||
label?: string | VNode | Component | Object
|
||||
/** called when used as target for new connection */
|
||||
isValidTargetPos?: ValidConnectionFunc
|
||||
/** called when used as source for new connection */
|
||||
isValidSourcePos?: ValidConnectionFunc
|
||||
/** parent node id */
|
||||
parentNode?: string
|
||||
/** is node currently dragging */
|
||||
dragging: boolean
|
||||
/** node z-index */
|
||||
zIndex: number
|
||||
/** handle position */
|
||||
targetPosition?: Position
|
||||
/** handle position */
|
||||
sourcePosition?: Position
|
||||
/** drag handle query selector */
|
||||
dragHandle?: string
|
||||
|
||||
/** additional data of node */
|
||||
data?: Data
|
||||
/** contextual and custom events of node */
|
||||
events?: Partial<NodeEventsOn<CustomEvents>>
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
import type { CSSProperties, ComputedRef, ToRefs } from 'vue'
|
||||
import type { KeyFilter } from '@vueuse/core'
|
||||
import type { Dimensions, ElementData, Elements, FlowElements, FlowExportObject, FlowOptions, SnapGrid, XYPosition } from './flow'
|
||||
import type { DefaultEdgeTypes, DefaultNodeTypes, EdgeComponent, NodeComponent } from './components'
|
||||
import type { Connection, ConnectionLineOptions, ConnectionLineType, ConnectionMode, Connector } from './connection'
|
||||
import type { DefaultEdgeOptions, Edge, EdgeUpdatable, GraphEdge } from './edge'
|
||||
import type { CoordinateExtent, GraphNode, Node } from './node'
|
||||
import type { D3Selection, D3Zoom, D3ZoomHandler, PanOnScrollMode, Viewport, ViewportFunctions } from './zoom'
|
||||
import type { CustomEvent, FlowHooks, FlowHooksEmit, FlowHooksOn } from './hooks'
|
||||
import type { EdgeChange, NodeChange, NodeDragItem } from './changes'
|
||||
import type { StartHandle } from './handle'
|
||||
|
||||
export interface UpdateNodeDimensionsParams {
|
||||
id: string
|
||||
nodeElement: HTMLDivElement
|
||||
forceUpdate?: boolean
|
||||
}
|
||||
|
||||
export interface State extends Omit<FlowOptions, 'id' | 'modelValue'> {
|
||||
/** Vue flow element ref */
|
||||
vueFlowRef: HTMLDivElement | null
|
||||
/** Vue flow viewport element */
|
||||
viewportRef: HTMLDivElement | null
|
||||
|
||||
/** Event hooks, you can manipulate the triggers at your own peril */
|
||||
readonly hooks: FlowHooks
|
||||
|
||||
/** all stored nodes */
|
||||
nodes: GraphNode[]
|
||||
/** all stored edges */
|
||||
edges: GraphEdge[]
|
||||
|
||||
readonly d3Zoom: D3Zoom | null
|
||||
readonly d3Selection: D3Selection | null
|
||||
readonly d3ZoomHandler: D3ZoomHandler | null
|
||||
|
||||
/** use setMinZoom action to change minZoom */
|
||||
minZoom: number
|
||||
/** use setMaxZoom action to change maxZoom */
|
||||
maxZoom: number
|
||||
defaultZoom: number
|
||||
/** use setTranslateExtent action to change translateExtent */
|
||||
translateExtent: CoordinateExtent
|
||||
nodeExtent: CoordinateExtent
|
||||
|
||||
/** viewport dimensions - do not change! */
|
||||
readonly dimensions: Dimensions
|
||||
/** viewport transform x, y, z - do not change! */
|
||||
readonly viewport: Viewport
|
||||
/** if true will skip rendering any elements currently not inside viewport until they become visible */
|
||||
onlyRenderVisibleElements: boolean
|
||||
defaultPosition: [number, number]
|
||||
|
||||
nodesSelectionActive: boolean
|
||||
userSelectionActive: boolean
|
||||
multiSelectionActive: boolean
|
||||
|
||||
deleteKeyCode: KeyFilter
|
||||
selectionKeyCode: KeyFilter
|
||||
multiSelectionKeyCode: KeyFilter
|
||||
zoomActivationKeyCode: KeyFilter
|
||||
|
||||
connectionMode: ConnectionMode
|
||||
connectionLineOptions: ConnectionLineOptions
|
||||
/** @deprecated use {@link ConnectionLineOptions.type} */
|
||||
connectionLineType: ConnectionLineType
|
||||
/** @deprecated use {@link ConnectionLineOptions.style} */
|
||||
connectionLineStyle: CSSProperties | null
|
||||
connectionStartHandle: StartHandle | null
|
||||
connectionPosition: XYPosition
|
||||
|
||||
connectOnClick: boolean
|
||||
edgeUpdaterRadius: number
|
||||
|
||||
snapToGrid: boolean
|
||||
snapGrid: SnapGrid
|
||||
defaultMarkerColor: string
|
||||
|
||||
edgesUpdatable: EdgeUpdatable
|
||||
nodesDraggable: boolean
|
||||
nodesConnectable: boolean
|
||||
|
||||
elementsSelectable: boolean
|
||||
selectNodesOnDrag: boolean
|
||||
|
||||
panOnDrag: boolean
|
||||
zoomOnScroll: boolean
|
||||
zoomOnPinch: boolean
|
||||
panOnScroll: boolean
|
||||
panOnScrollSpeed: number
|
||||
panOnScrollMode: PanOnScrollMode
|
||||
zoomOnDoubleClick: boolean
|
||||
preventScrolling: boolean
|
||||
|
||||
initialized: boolean
|
||||
applyDefault: boolean
|
||||
autoConnect: boolean | Connector
|
||||
|
||||
fitViewOnInit?: boolean
|
||||
|
||||
noDragClassName?: 'nodrag' | string
|
||||
noWheelClassName?: 'nowheel' | string
|
||||
noPanClassName?: 'nopan' | string
|
||||
|
||||
defaultEdgeOptions?: DefaultEdgeOptions
|
||||
|
||||
/** current vue flow version you're using */
|
||||
readonly vueFlowVersion: string
|
||||
}
|
||||
|
||||
export type SetElements = (elements: Elements | ((elements: FlowElements) => Elements), extent?: CoordinateExtent) => void
|
||||
export type SetNodes = (nodes: Node[] | ((nodes: GraphNode[]) => Node[]), extent?: CoordinateExtent) => void
|
||||
export type SetEdges = (edges: Edge[] | ((edges: GraphEdge[]) => Edge[])) => void
|
||||
export type AddNodes = (nodes: Node[] | ((nodes: GraphNode[]) => Node[]), extent?: CoordinateExtent) => void
|
||||
export type RemoveNodes = (
|
||||
nodes: (Node[] | string[]) | ((nodes: GraphNode[]) => Node[] | string[]),
|
||||
removeConnectedEdges?: boolean,
|
||||
) => void
|
||||
export type RemoveEdges = (edges: (Edge[] | string[]) | ((edges: GraphEdge[]) => Edge[] | string[])) => void
|
||||
export type AddEdges = (edgesOrConnections: (Edge | Connection)[] | ((edges: GraphEdge[]) => (Edge | Connection)[])) => void
|
||||
export type UpdateEdge = (oldEdge: GraphEdge, newConnection: Connection) => GraphEdge | false
|
||||
export type SetState = (
|
||||
state:
|
||||
| Partial<FlowOptions & Omit<State, 'nodes' | 'edges' | 'modelValue'>>
|
||||
| ((state: State) => Partial<FlowOptions & Omit<State, 'nodes' | 'edges' | 'modelValue'>>),
|
||||
) => void
|
||||
export type UpdateNodePosition = (dragItems: NodeDragItem[], changed: boolean, dragging: boolean) => void
|
||||
export type UpdateNodeDimensions = (updates: UpdateNodeDimensionsParams[]) => void
|
||||
export type UpdateNodeInternals = (nodeIds: string[]) => void
|
||||
export type FindNode = <Data = ElementData, CustomEvents extends Record<string, CustomEvent> = any>(
|
||||
id: string,
|
||||
) => GraphNode<Data, CustomEvents> | undefined
|
||||
export type FindEdge = <Data = ElementData, CustomEvents extends Record<string, CustomEvent> = any>(
|
||||
id: string,
|
||||
) => GraphEdge<Data, CustomEvents> | undefined
|
||||
|
||||
export interface Actions extends ViewportFunctions {
|
||||
/** parses elements (nodes + edges) and re-sets the state */
|
||||
setElements: SetElements
|
||||
/** parses nodes and re-sets the state */
|
||||
setNodes: SetNodes
|
||||
/** parses edges and re-sets the state */
|
||||
setEdges: SetEdges
|
||||
/** parses nodes and adds to state */
|
||||
addNodes: AddNodes
|
||||
/** parses edges and adds to state */
|
||||
addEdges: AddEdges
|
||||
/** remove nodes (and possibly connected edges) from state */
|
||||
removeNodes: RemoveNodes
|
||||
/** remove edges from state */
|
||||
removeEdges: RemoveEdges
|
||||
/** find a node by id */
|
||||
findNode: FindNode
|
||||
/** find an edge by id */
|
||||
findEdge: FindEdge
|
||||
/** updates an edge */
|
||||
updateEdge: UpdateEdge
|
||||
/** applies default edge change handler */
|
||||
applyEdgeChanges: (changes: EdgeChange[]) => GraphEdge[]
|
||||
/** applies default node change handler */
|
||||
applyNodeChanges: (changes: NodeChange[]) => GraphNode[]
|
||||
/** manually select elements and add to state */
|
||||
addSelectedElements: (elements: FlowElements) => void
|
||||
/** manually select edges and add to state */
|
||||
addSelectedEdges: (edges: GraphEdge[]) => void
|
||||
/** manually select nodes and add to state */
|
||||
addSelectedNodes: (nodes: GraphNode[]) => void
|
||||
/** manually unselect edges and remove from state */
|
||||
removeSelectedEdges: (edges: GraphEdge[]) => void
|
||||
/** manually unselect nodes and remove from state */
|
||||
removeSelectedNodes: (nodes: GraphNode[]) => void
|
||||
/** unselect selected elements (if none are passed, all elements are unselected) */
|
||||
removeSelectedElements: (elements?: { nodes?: GraphNode[]; edges?: GraphEdge[] }) => void
|
||||
/** apply min zoom value to d3 */
|
||||
setMinZoom: (zoom: number) => void
|
||||
/** apply max zoom value to d3 */
|
||||
setMaxZoom: (zoom: number) => void
|
||||
/** apply translate extent to d3 */
|
||||
setTranslateExtent: (translateExtent: CoordinateExtent) => void
|
||||
/** apply extent to nodes */
|
||||
setNodeExtent: (nodeExtent: CoordinateExtent) => void
|
||||
/** enable/disable node interaction (dragging, selecting etc) */
|
||||
setInteractive: (isInteractive: boolean) => void
|
||||
/** set new state */
|
||||
setState: SetState
|
||||
/** return an object of graph values (elements, viewpane transform) for storage and re-loading a graph */
|
||||
toObject: () => FlowExportObject
|
||||
/** force update node internal data, if handle bounds are incorrect, you might want to use this */
|
||||
updateNodeInternals: UpdateNodeInternals
|
||||
/** start a connection */
|
||||
startConnection: (startHandle: StartHandle, position?: XYPosition, event?: MouseEvent) => void
|
||||
/** update connection position */
|
||||
updateConnection: (position: XYPosition) => void
|
||||
/** end (or cancel) a connection */
|
||||
endConnection: (event?: MouseEvent) => void
|
||||
|
||||
/** internal position updater, you probably don't want to use this */
|
||||
updateNodePositions: UpdateNodePosition
|
||||
/** internal dimensions' updater, you probably don't want to use this */
|
||||
updateNodeDimensions: UpdateNodeDimensions
|
||||
|
||||
/** reset state to defaults */
|
||||
$reset: () => void
|
||||
|
||||
/** remove store instance from global storage and destroy it (will invalidate effect scopes) */
|
||||
$destroy: () => void
|
||||
}
|
||||
|
||||
export interface Getters {
|
||||
/** returns object containing current edge types */
|
||||
getEdgeTypes: Record<keyof DefaultEdgeTypes | string, EdgeComponent>
|
||||
/** returns object containing current node types */
|
||||
getNodeTypes: Record<keyof DefaultNodeTypes | string, NodeComponent>
|
||||
/** get all elements (filters hidden elements) */
|
||||
getElements: FlowElements
|
||||
/** filters hidden nodes */
|
||||
getNodes: GraphNode[]
|
||||
/** filters hidden edges */
|
||||
getEdges: GraphEdge[]
|
||||
/** @deprecated use {@link Actions.findNode} instead; returns a node by id */
|
||||
getNode: (id: string) => GraphNode | undefined
|
||||
/** @deprecated use {@link Actions.findEdge} instead; returns an edge by id */
|
||||
getEdge: (id: string) => GraphEdge | undefined
|
||||
/** returns all currently selected elements */
|
||||
getSelectedElements: FlowElements
|
||||
/** returns all currently selected nodes */
|
||||
getSelectedNodes: GraphNode[]
|
||||
/** returns all currently selected edges */
|
||||
getSelectedEdges: GraphEdge[]
|
||||
}
|
||||
|
||||
export type ComputedGetters = {
|
||||
[key in keyof Getters]: ComputedRef<Getters[key]>
|
||||
}
|
||||
|
||||
export type VueFlowStore = {
|
||||
readonly id: string
|
||||
readonly emits: FlowHooksEmit
|
||||
} & FlowHooksOn &
|
||||
ToRefs<State> &
|
||||
Readonly<ComputedGetters> &
|
||||
Readonly<Actions>
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { Selection, ZoomBehavior } from 'd3'
|
||||
import type { 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
|
||||
|
||||
/** Transform x, y, z */
|
||||
export interface Viewport {
|
||||
x: number
|
||||
y: number
|
||||
zoom: number
|
||||
}
|
||||
|
||||
export enum PanOnScrollMode {
|
||||
Free = 'free',
|
||||
Vertical = 'vertical',
|
||||
Horizontal = 'horizontal',
|
||||
}
|
||||
|
||||
export interface TransitionOptions {
|
||||
duration?: number
|
||||
}
|
||||
|
||||
export type FitViewParams = {
|
||||
padding?: number
|
||||
includeHiddenNodes?: boolean
|
||||
minZoom?: number
|
||||
maxZoom?: number
|
||||
offset?: {
|
||||
x?: number
|
||||
y?: number
|
||||
}
|
||||
nodes?: string[]
|
||||
} & TransitionOptions
|
||||
|
||||
export interface ViewpaneTransform {
|
||||
x: number
|
||||
y: number
|
||||
zoom: number
|
||||
}
|
||||
|
||||
export type SetCenterOptions = TransitionOptions & {
|
||||
zoom?: number
|
||||
}
|
||||
|
||||
export type FitBoundsOptions = TransitionOptions & {
|
||||
padding?: number
|
||||
}
|
||||
|
||||
/** Fit the viewpane around visible nodes */
|
||||
export type FitView = (fitViewOptions?: FitViewParams) => void
|
||||
|
||||
/** project a position onto the viewpane, i.e. a mouse event clientX/clientY onto graph coordinates */
|
||||
export type Project = (position: XYPosition) => XYPosition
|
||||
|
||||
/** set center of viewpane */
|
||||
export type SetCenter = (x: number, y: number, options?: SetCenterOptions) => void
|
||||
|
||||
/** fit the viewpane around bounds */
|
||||
export type FitBounds = (bounds: Rect, options?: FitBoundsOptions) => void
|
||||
|
||||
/** zoom in/out */
|
||||
export type ZoomInOut = (options?: TransitionOptions) => void
|
||||
|
||||
/** zoom to a specific level */
|
||||
export type ZoomTo = (zoomLevel: number, options?: TransitionOptions) => void
|
||||
|
||||
/** get current viewpane transform */
|
||||
export type GetTransform = () => ViewpaneTransform
|
||||
|
||||
/** set current viewpane transform */
|
||||
export type SetTransform = (transform: ViewpaneTransform, options?: TransitionOptions) => void
|
||||
|
||||
export interface ViewportFunctions {
|
||||
zoomIn: ZoomInOut
|
||||
zoomOut: ZoomInOut
|
||||
zoomTo: ZoomTo
|
||||
setTransform: SetTransform
|
||||
getTransform: GetTransform
|
||||
fitView: FitView
|
||||
setCenter: SetCenter
|
||||
fitBounds: FitBounds
|
||||
project: Project
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import { isGraphEdge, isGraphNode } from './graph'
|
||||
import type {
|
||||
Edge,
|
||||
EdgeAddChange,
|
||||
EdgeChange,
|
||||
EdgeRemoveChange,
|
||||
EdgeSelectionChange,
|
||||
ElementChange,
|
||||
FlowElement,
|
||||
FlowElements,
|
||||
GraphEdge,
|
||||
GraphNode,
|
||||
Node,
|
||||
NodeAddChange,
|
||||
NodeChange,
|
||||
NodeRemoveChange,
|
||||
NodeSelectionChange,
|
||||
} from '~/types'
|
||||
|
||||
function handleParentExpand(updateItem: GraphNode, parent: GraphNode) {
|
||||
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 } || {}
|
||||
|
||||
parent.style.width = parent.style.width ?? parent.dimensions.width
|
||||
parent.style.height = parent.style.height ?? parent.dimensions.height
|
||||
|
||||
if (extendWidth > 0) {
|
||||
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 (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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const applyChanges = <
|
||||
T extends Node | Edge | FlowElement = Node,
|
||||
C extends ElementChange = T extends GraphNode ? NodeChange : EdgeChange,
|
||||
>(
|
||||
changes: C[],
|
||||
elements: T[],
|
||||
): T[] => {
|
||||
let elementIds = elements.map((el) => el.id)
|
||||
changes.forEach((change) => {
|
||||
if (change.type === 'add') {
|
||||
const item = <T>change.item
|
||||
return elements.push(item)
|
||||
}
|
||||
|
||||
const i = elementIds.indexOf((<any>change).id)
|
||||
const el = elements[i]
|
||||
switch (change.type) {
|
||||
case 'select':
|
||||
if (isGraphNode(el) || isGraphEdge(el)) el.selected = change.selected
|
||||
break
|
||||
case 'position':
|
||||
if (isGraphNode(el)) {
|
||||
if (typeof change.position !== 'undefined') el.position = change.position
|
||||
|
||||
if (typeof change.computedPosition !== 'undefined') {
|
||||
el.computedPosition = { ...el.computedPosition, ...change.computedPosition }
|
||||
}
|
||||
|
||||
if (el.expandParent && el.parentNode) {
|
||||
const parent = elements[elementIds.indexOf(el.parentNode)]
|
||||
|
||||
if (parent && isGraphNode(parent)) {
|
||||
handleParentExpand(el, parent)
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'dimensions':
|
||||
if (isGraphNode(el)) {
|
||||
if (typeof change.dimensions !== 'undefined') el.dimensions = change.dimensions
|
||||
if (el.expandParent && el.parentNode) {
|
||||
const parent = elements[elementIds.indexOf(el.parentNode)]
|
||||
|
||||
if (parent && isGraphNode(parent)) {
|
||||
handleParentExpand(el, parent)
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'remove':
|
||||
if (elementIds.includes(change.id)) {
|
||||
elements.splice(i, 1)
|
||||
elementIds = elements.map((el) => el.id)
|
||||
}
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
return elements
|
||||
}
|
||||
|
||||
export const applyEdgeChanges = (changes: EdgeChange[], edges: GraphEdge[]) => applyChanges(changes, edges)
|
||||
export const applyNodeChanges = (changes: NodeChange[], nodes: GraphNode[]) => applyChanges(changes, nodes)
|
||||
|
||||
export const createSelectionChange = (id: string, selected: boolean): NodeSelectionChange | EdgeSelectionChange => ({
|
||||
id,
|
||||
type: 'select',
|
||||
selected,
|
||||
})
|
||||
|
||||
export const createAdditionChange = <
|
||||
T extends GraphNode | GraphEdge = GraphNode,
|
||||
C extends NodeAddChange | EdgeAddChange = T extends GraphNode ? NodeAddChange : EdgeAddChange,
|
||||
>(
|
||||
item: T,
|
||||
): C =>
|
||||
<C>{
|
||||
item,
|
||||
type: 'add',
|
||||
}
|
||||
|
||||
export const createRemoveChange = (id: string): NodeRemoveChange | EdgeRemoveChange => ({
|
||||
id,
|
||||
type: 'remove',
|
||||
})
|
||||
|
||||
export const getSelectionChanges = (items: FlowElements, selectedIds: string[]) => {
|
||||
return items.reduce((res, item) => {
|
||||
const willBeSelected = selectedIds.includes(item.id)
|
||||
|
||||
if (!item.selected && willBeSelected) {
|
||||
item.selected = true
|
||||
res.push(createSelectionChange(item.id, true))
|
||||
} else if (item.selected && !willBeSelected) {
|
||||
item.selected = false
|
||||
res.push(createSelectionChange(item.id, false))
|
||||
}
|
||||
|
||||
return res
|
||||
}, [] as (NodeSelectionChange | EdgeSelectionChange)[])
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import type { Ref } from 'vue'
|
||||
import { clampPosition, isParentSelected } from './graph'
|
||||
import type { ComputedGetters, CoordinateExtent, Getters, GraphNode, NodeDragItem, SnapGrid, XYPosition } from '~/types'
|
||||
|
||||
export function hasSelector(target: Element, selector: string, node: Ref<Element>): boolean {
|
||||
let current = target
|
||||
|
||||
do {
|
||||
if (current && current.matches(selector)) return true
|
||||
else if (current === node.value) return false
|
||||
|
||||
current = current.parentElement as Element
|
||||
} while (current)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export function getDragItems(
|
||||
nodes: GraphNode[],
|
||||
mousePos: XYPosition,
|
||||
getNode: Getters['getNode'],
|
||||
nodeId?: string,
|
||||
): NodeDragItem[] {
|
||||
return nodes
|
||||
.filter((n) => (n.selected || n.id === nodeId) && (!n.parentNode || !isParentSelected(n, getNode)))
|
||||
.map((n) =>
|
||||
markRaw({
|
||||
id: n.id,
|
||||
position: n.computedPosition || { x: 0, y: 0, z: 0 },
|
||||
distance: {
|
||||
x: mousePos.x - n.computedPosition?.x || 0,
|
||||
y: mousePos.y - n.computedPosition?.y || 0,
|
||||
},
|
||||
from: n.computedPosition,
|
||||
extent: n.extent,
|
||||
parentNode: n.parentNode,
|
||||
dimensions: n.dimensions,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export function getEventHandlerParams({
|
||||
id,
|
||||
dragItems,
|
||||
getNode,
|
||||
}: {
|
||||
id?: string
|
||||
dragItems: NodeDragItem[]
|
||||
getNode: ComputedGetters['getNode']
|
||||
}): [GraphNode, GraphNode[]] {
|
||||
const extendedDragItems: GraphNode[] = dragItems.map((n) => {
|
||||
const node = getNode.value(n.id)!
|
||||
|
||||
return {
|
||||
...node,
|
||||
}
|
||||
})
|
||||
|
||||
return [id ? extendedDragItems.find((n) => n.id === id)! : extendedDragItems[0], extendedDragItems]
|
||||
}
|
||||
|
||||
export function updatePosition(
|
||||
dragItem: NodeDragItem,
|
||||
mousePos: XYPosition,
|
||||
snapToGrid?: boolean,
|
||||
snapGrid?: SnapGrid,
|
||||
parent?: GraphNode,
|
||||
nodeExtent?: CoordinateExtent,
|
||||
): NodeDragItem {
|
||||
const nextPosition = { x: mousePos.x - dragItem.distance.x, y: mousePos.y - dragItem.distance.y }
|
||||
if (snapToGrid && snapGrid) {
|
||||
const [snapX, snapY] = snapGrid
|
||||
nextPosition.x = snapX * Math.round(nextPosition.x / snapX)
|
||||
nextPosition.y = snapY * Math.round(nextPosition.y / snapY)
|
||||
}
|
||||
|
||||
const currentExtent = applyExtent(dragItem, nodeExtent, parent)
|
||||
|
||||
dragItem.position = currentExtent ? clampPosition(nextPosition, currentExtent as CoordinateExtent) : nextPosition
|
||||
|
||||
return dragItem
|
||||
}
|
||||
|
||||
export function applyExtent<T extends NodeDragItem | GraphNode>(item: T, extent?: CoordinateExtent, parent?: GraphNode) {
|
||||
let currentExtent = item.extent || extent
|
||||
|
||||
if (item.extent === 'parent' && parent) {
|
||||
if (item.parentNode && item.dimensions.width && item.dimensions.height) {
|
||||
currentExtent =
|
||||
parent.computedPosition && parent.dimensions.width && parent.dimensions.height
|
||||
? [
|
||||
[parent.computedPosition.x, parent.computedPosition.y],
|
||||
[
|
||||
parent.computedPosition.x + parent.dimensions.width - item.dimensions.width,
|
||||
parent.computedPosition.y + parent.dimensions.height - item.dimensions.height,
|
||||
],
|
||||
]
|
||||
: currentExtent
|
||||
}
|
||||
} else if (item.extent && item.parentNode) {
|
||||
const itemExtent = item.extent as CoordinateExtent
|
||||
const parentX = parent?.computedPosition?.x ?? 0
|
||||
const parentY = parent?.computedPosition?.y ?? 0
|
||||
|
||||
currentExtent = [
|
||||
[itemExtent[0][0] + parentX, itemExtent[0][1] + parentY],
|
||||
[itemExtent[1][0] + parentX, itemExtent[1][1] + parentY],
|
||||
]
|
||||
}
|
||||
|
||||
return currentExtent
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import { rectToBox } from './graph'
|
||||
import type { EdgePositions, Getters, GraphEdge, GraphNode, HandleElement, Rect, Viewport, XYPosition } from '~/types'
|
||||
import { Position } 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
|
||||
|
||||
let handle
|
||||
if (!handleId && bounds.length === 1) handle = bounds[0]
|
||||
else if (handleId) handle = bounds.find((d) => d.id === handleId)
|
||||
|
||||
return handle || bounds[0]
|
||||
}
|
||||
|
||||
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
|
||||
viewport: Viewport
|
||||
}
|
||||
|
||||
export function isEdgeVisible({
|
||||
sourcePos,
|
||||
targetPos,
|
||||
sourceWidth,
|
||||
sourceHeight,
|
||||
targetWidth,
|
||||
targetHeight,
|
||||
width,
|
||||
height,
|
||||
viewport,
|
||||
}: 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 - viewport.x) / viewport.zoom,
|
||||
y: (0 - viewport.y) / viewport.zoom,
|
||||
width: width / viewport.zoom,
|
||||
height: height / viewport.zoom,
|
||||
})
|
||||
|
||||
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[], getNode: Getters['getNode']) => {
|
||||
let maxLevel = -1
|
||||
|
||||
const levelLookup = edges.reduce<Record<string, GraphEdge[]>>((tree, edge) => {
|
||||
const source = getNode(edge.source)
|
||||
const target = getNode(edge.target)
|
||||
|
||||
if (!source || !target) return tree
|
||||
|
||||
const z = edge.z ? edge.z : Math.max(source.computedPosition.z || 0, 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,334 @@
|
||||
import type {
|
||||
Box,
|
||||
Connection,
|
||||
CoordinateExtent,
|
||||
DefaultEdgeOptions,
|
||||
Dimensions,
|
||||
Edge,
|
||||
EdgeMarkerType,
|
||||
Elements,
|
||||
FlowElement,
|
||||
Getters,
|
||||
GraphEdge,
|
||||
GraphNode,
|
||||
Node,
|
||||
Rect,
|
||||
Viewport,
|
||||
XYPosition,
|
||||
XYZPosition,
|
||||
} from '~/types'
|
||||
import { useWindow } from '~/composables'
|
||||
|
||||
export const getDimensions = (node: HTMLElement): Dimensions => ({
|
||||
width: node.offsetWidth,
|
||||
height: node.offsetHeight,
|
||||
})
|
||||
|
||||
export const clamp = (val: number, min = 0, max = 1) => 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 ('elementFromPoint' in doc) return doc
|
||||
else return window.document
|
||||
}
|
||||
|
||||
type MaybeElement = Node | Edge | Connection | FlowElement
|
||||
export const isEdge = (element: MaybeElement): element is Edge =>
|
||||
element && 'id' in element && 'source' in element && 'target' in element
|
||||
export const isGraphEdge = (element: MaybeElement): element is GraphEdge =>
|
||||
isEdge(element) && 'sourceNode' in element && 'targetNode' in element
|
||||
|
||||
export const isNode = (element: MaybeElement): element is Node => element && 'id' in element && !isEdge(element)
|
||||
export const isGraphNode = (element: MaybeElement): element is GraphNode => isNode(element) && 'computedPosition' in element
|
||||
|
||||
export const parseNode = (node: Node, nodeExtent: CoordinateExtent, defaults?: Partial<GraphNode>): GraphNode => {
|
||||
let defaultValues = defaults
|
||||
if (!isGraphNode(node)) {
|
||||
defaultValues = {
|
||||
type: node.type ?? 'default',
|
||||
dimensions: markRaw({
|
||||
width: 0,
|
||||
height: 0,
|
||||
}),
|
||||
handleBounds: shallowReactive({
|
||||
source: [],
|
||||
target: [],
|
||||
}),
|
||||
computedPosition: markRaw({
|
||||
z: 0,
|
||||
...node.position,
|
||||
}),
|
||||
draggable: undefined,
|
||||
selectable: undefined,
|
||||
connectable: undefined,
|
||||
...defaults,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...defaultValues,
|
||||
...(node as GraphNode),
|
||||
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,
|
||||
source: edge.source.toString(),
|
||||
target: edge.target.toString(),
|
||||
z: 0,
|
||||
sourceX: 0,
|
||||
sourceY: 0,
|
||||
targetX: 0,
|
||||
targetY: 0,
|
||||
updatable: edge.updatable,
|
||||
selectable: edge.selectable,
|
||||
data: edge.data,
|
||||
...defaults,
|
||||
} as GraphEdge)
|
||||
: defaults
|
||||
|
||||
return Object.assign({ id: edge.id.toString() }, edge, defaults) as GraphEdge
|
||||
}
|
||||
|
||||
const getConnectedElements = (node: GraphNode, elements: Elements, dir: 'source' | 'target') => {
|
||||
if (!isNode(node)) return []
|
||||
const origin = dir === 'source' ? 'target' : 'source'
|
||||
const ids = elements.filter((e) => isEdge(e) && e[origin] === 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 | Connection, 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)),
|
||||
)
|
||||
|
||||
/**
|
||||
* Intended for options API
|
||||
* In composition API you can access utilities from `useVueFlow`
|
||||
*/
|
||||
export const addEdge = (edgeParams: Edge | Connection, elements: Elements, defaults?: DefaultEdgeOptions) => {
|
||||
if (!edgeParams.source || !edgeParams.target) {
|
||||
console.warn("[vueflow]: 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, defaults)
|
||||
if (connectionExists(edge, elements)) return elements
|
||||
elements.push(edge)
|
||||
return elements
|
||||
}
|
||||
|
||||
/**
|
||||
* Intended for options API
|
||||
* In composition API you can access utilities from `useVueFlow`
|
||||
*/
|
||||
export const updateEdge = (oldEdge: Edge, newConnection: Connection, elements: Elements) => {
|
||||
if (!newConnection.source || !newConnection.target) {
|
||||
console.warn("[vueflow]: 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(`[vueflow]: 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)
|
||||
}
|
||||
|
||||
export const pointToRendererPoint = (
|
||||
{ x, y }: XYPosition,
|
||||
{ x: tx, y: ty, zoom: tScale }: Viewport,
|
||||
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
|
||||
}
|
||||
|
||||
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, { x: tx, y: ty, zoom: tScale }: Viewport): XYPosition => ({
|
||||
x: x * tScale + tx,
|
||||
y: y * tScale + ty,
|
||||
})
|
||||
|
||||
export const getNodesInside = (
|
||||
nodes: GraphNode[],
|
||||
rect: Rect,
|
||||
{ x: tx, y: ty, zoom: tScale }: Viewport = { x: 0, y: 0, zoom: 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 } } = 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
|
||||
|
||||
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 getTransformForBounds = (
|
||||
bounds: Rect,
|
||||
width: number,
|
||||
height: number,
|
||||
minZoom: number,
|
||||
maxZoom: number,
|
||||
padding = 0.1,
|
||||
offset: {
|
||||
x?: number
|
||||
y?: number
|
||||
} = { x: 0, y: 0 },
|
||||
): Viewport => {
|
||||
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, zoom: clampedZoom }
|
||||
}
|
||||
|
||||
export const getXYZPos = (parentPos: XYZPosition, computedPosition: XYZPosition): XYZPosition => {
|
||||
return {
|
||||
x: computedPosition.x + parentPos.x,
|
||||
y: computedPosition.y + parentPos.y,
|
||||
z: parentPos.z > computedPosition.z ? parentPos.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,5 @@
|
||||
export * from './edge'
|
||||
export * from './graph'
|
||||
export * from './node'
|
||||
export * from './changes'
|
||||
export * from './store'
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { Ref } from 'vue'
|
||||
import { getDimensions } from './graph'
|
||||
import type { Actions, GraphNode, HandleElement, Position } from '~/types'
|
||||
|
||||
export const getHandleBounds = (selector: string, nodeElement: HTMLDivElement, zoom: number): HandleElement[] | undefined => {
|
||||
const handles = nodeElement.querySelectorAll(selector)
|
||||
|
||||
if (!handles || !handles.length) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const handlesArray = Array.from(handles) as HTMLDivElement[]
|
||||
const nodeBounds = nodeElement.getBoundingClientRect()
|
||||
|
||||
return handlesArray.map((handle): HandleElement => {
|
||||
const handleBounds = handle.getBoundingClientRect()
|
||||
|
||||
return {
|
||||
id: handle.getAttribute('data-handleid'),
|
||||
position: handle.getAttribute('data-handlepos') as unknown as Position,
|
||||
x: (handleBounds.left - nodeBounds.left) / zoom,
|
||||
y: (handleBounds.top - nodeBounds.top) / zoom,
|
||||
...getDimensions(handle),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const handleNodeClick = (
|
||||
node: GraphNode,
|
||||
multiSelectionActive: boolean,
|
||||
addSelectedNodes: Actions['addSelectedNodes'],
|
||||
removeSelectedElements: Actions['removeSelectedElements'],
|
||||
nodesSelectionActive: Ref<boolean>,
|
||||
) => {
|
||||
nodesSelectionActive.value = false
|
||||
|
||||
if (!node.selected) {
|
||||
addSelectedNodes([node])
|
||||
} else if (node.selected && multiSelectionActive) {
|
||||
removeSelectedElements({ nodes: [node] })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { Connection, CoordinateExtent, Edge, Getters, GraphEdge, GraphNode, Node } from '~/types'
|
||||
import { connectionExists, getEdgeId, isEdge, isGraphEdge, parseEdge, parseNode } from '~/utils/graph'
|
||||
|
||||
export const isDef = <T>(val: T): val is NonNullable<T> => typeof val !== 'undefined'
|
||||
|
||||
export const addEdgeToStore = (edgeParams: Edge | Connection, edges: Edge[]) => {
|
||||
if (!edgeParams.source || !edgeParams.target) {
|
||||
console.warn("[vueflow]: 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
|
||||
}
|
||||
|
||||
export const updateEdgeAction = (edge: GraphEdge, newConnection: Connection, edges: GraphEdge[]) => {
|
||||
if (!newConnection.source || !newConnection.target) {
|
||||
console.warn("[vueflow]: 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(`[vueflow]: The old edge with id=${edge.id} does not exist.`)
|
||||
return false
|
||||
}
|
||||
|
||||
const newEdge = {
|
||||
...edge,
|
||||
id: getEdgeId(newConnection),
|
||||
source: newConnection.source,
|
||||
target: newConnection.target,
|
||||
sourceHandle: newConnection.sourceHandle,
|
||||
targetHandle: newConnection.targetHandle,
|
||||
}
|
||||
|
||||
edges.splice(edges.indexOf(foundEdge), 1, newEdge)
|
||||
|
||||
return newEdge
|
||||
}
|
||||
|
||||
export const createGraphNodes = (
|
||||
nodes: Node[],
|
||||
getNode: Getters['getNode'],
|
||||
currGraphNodes: GraphNode[],
|
||||
extent: CoordinateExtent,
|
||||
) => {
|
||||
const parentNodes: Record<string, true> = {}
|
||||
|
||||
const graphNodes = nodes.map((node) => {
|
||||
const parsed = shallowReactive(
|
||||
parseNode(node, extent, {
|
||||
...getNode(node.id),
|
||||
parentNode: node.parentNode,
|
||||
}),
|
||||
)
|
||||
if (node.parentNode) {
|
||||
parentNodes[node.parentNode] = true
|
||||
}
|
||||
|
||||
return parsed
|
||||
})
|
||||
|
||||
graphNodes.forEach((node) => {
|
||||
const nextNodes = [...graphNodes, ...currGraphNodes]
|
||||
if (node.parentNode && !nextNodes.find((n) => n.id === node.parentNode)) {
|
||||
console.warn(`[vueflow]: 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
|
||||
}
|
||||
Reference in New Issue
Block a user