refactor(pkg): Move vue-flow into separate pkg directory

This commit is contained in:
Braks
2022-05-22 22:46:47 +02:00
parent df43ff3b8b
commit b9dbb97374
94 changed files with 17 additions and 5 deletions
+8
View File
@@ -0,0 +1,8 @@
module.exports = {
rules: {
'no-use-before-define': 0,
'vue/no-setup-props-destructure': 0,
},
extends: ['../../.eslintrc.js'],
ignorePatterns: ['!**/*'],
}
+73
View File
@@ -0,0 +1,73 @@
{
"name": "@braks/vue-flow",
"version": "0.4.10",
"private": false,
"license": "MIT",
"author": "Burak Cakmakoglu<78412429+bcakmakoglu@users.noreply.github.com>",
"repository": {
"type": "git",
"url": "git+https://github.com/bcakmakoglu/vue-flow"
},
"homepage": "https://github.com/bcakmakoglu/vue-flow#readme",
"bugs": {
"url": "https://github.com/bcakmakoglu/vue-flow/issues"
},
"main": "./dist/vue-flow.cjs.js",
"module": "./dist/vue-flow.es.js",
"types": "./dist/index.d.ts",
"unpkg": "./dist/vue-flow.iife.js",
"jsdelivr": "./dist/vue-flow.iife.js",
"files": [
"dist"
],
"sideEffects": false,
"scripts": {
"prepare": "ts-patch install -s",
"build": "vite build",
"types": "yarn prepare && vue-tsc --declaration --emitDeclarationOnly && tsc -p tsconfig.build.json && shx rm -rf tmp && yarn lint:dist",
"typedoc": "typedoc --tsconfig tsconfig.docs.json dist/index.d.ts --out typedocs",
"theme": "postcss src/style.css -o dist/style.css && postcss src/theme-default.css -o dist/theme-default.css",
"lint": "eslint --ext \".js,.jsx,.ts,.tsx\" --fix --ignore-path ../../.gitignore .",
"lint:dist": "eslint --ext \".ts,.tsx\" -c .eslintrc.js --fix ./dist",
"prepublishOnly": "shx cp ../../README.md .",
"release": "yarn np",
"postpublish": "shx rm README.md"
},
"peerDependencies": {
"vue": "^3.2.25"
},
"dependencies": {
"@braks/revue-draggable": "^0.4.2",
"@types/d3": "^7.1.0",
"@vueuse/core": "^8.4.2",
"d3": "^7.4.4",
"d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0"
},
"devDependencies": {
"@rollup/plugin-replace": "^2.4.2",
"@vitejs/plugin-vue": "^2.3.3",
"autoprefixer": "^10.3.7",
"np": "^7.5.0",
"postcss": "^8.4.8",
"postcss-cli": "^9.1.0",
"postcss-nested": "^5.0.6",
"ts-patch": "^2.0.1",
"typedoc": "^0.22.15",
"typescript-transform-paths": "^3.3.1",
"unplugin-auto-import": "^0.7.1",
"vite": "^2.9.9",
"vite-plugin-vue-type-imports": "^0.1.3",
"vite-svg-loader": "^2.2.0",
"vue-tsc": "^0.34.12"
},
"publishConfig": {
"access": "public",
"registry": "http://registry.npmjs.org/"
},
"np": {
"branch": "master",
"yarn": true,
"message": "v%s"
}
}
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
plugins: [require('autoprefixer'), require('postcss-nested')],
}
@@ -0,0 +1,86 @@
<script lang="ts" setup>
import { BackgroundVariant } from '../../types'
import { useVueFlow } from '../../composables'
import type { BackgroundProps } from '../../types/components'
const {
variant = 'dots' as BackgroundVariant,
gap = 10,
size = 0.4,
height = 100,
width = 100,
x = 0,
y = 0,
bgColor,
patternColor: initialPatternColor,
} = defineProps<BackgroundProps>()
const defaultColors: Record<BackgroundVariant, string> = {
[BackgroundVariant.Dots]: '#81818a',
[BackgroundVariant.Lines]: '#eee',
}
const { viewport } = $(useVueFlow())
const background = $computed(() => {
const scaledGap = gap && gap * viewport.zoom
const xOffset = scaledGap && viewport.x % scaledGap
const yOffset = scaledGap && viewport.y % scaledGap
const bgSize = size * viewport.zoom
return {
scaledGap,
xOffset,
yOffset,
size: bgSize,
}
})
// when there are multiple flows on a page we need to make sure that every background gets its own pattern.
const patternId = `pattern-${Math.floor(Math.random() * 100000)}`
const patternColor = computed(() => initialPatternColor || defaultColors[variant || BackgroundVariant.Dots])
const d = computed(
() => `M${background.scaledGap / 2} 0 V${background.scaledGap} M0 ${background.scaledGap / 2} H${background.scaledGap}`,
)
</script>
<script lang="ts">
export default {
name: 'Background',
}
</script>
<template>
<svg
class="vue-flow__background"
:style="{
height: `${height > 100 ? 100 : height}%`,
width: `${width > 100 ? 100 : width}%`,
}"
>
<pattern
:id="patternId"
:x="background.xOffset"
:y="background.yOffset"
:width="background.scaledGap"
:height="background.scaledGap"
patternUnits="userSpaceOnUse"
>
<slot name="pattern">
<template v-if="variant === BackgroundVariant.Lines">
<path :stroke="patternColor" :stroke-width="size" :d="d" />
</template>
<template v-else-if="variant === BackgroundVariant.Dots">
<circle :cx="background.size" :cy="background.size" :r="background.size" :fill="patternColor" />
</template>
<svg v-if="bgColor" height="100" width="100">
<rect width="100%" height="100%" :fill="bgColor" />
</svg>
</slot>
</pattern>
<rect :x="x" :y="y" width="100%" height="100%" :fill="`url(#${patternId})`" />
<slot></slot>
</svg>
</template>
@@ -0,0 +1,11 @@
<script lang="ts">
export default {
name: 'ControlButton',
}
</script>
<template>
<button class="vue-flow__controls-button">
<slot></slot>
</button>
</template>
@@ -0,0 +1,88 @@
<script lang="ts" setup>
import { useVueFlow } from '../../composables'
import type { ControlProps } from '../../types/components'
import ControlButton from './ControlButton.vue'
import PlusIcon from '~/assets/icons/plus.svg'
import MinusIcon from '~/assets/icons/minus.svg'
import Fitview from '~/assets/icons/fitview.svg'
import Lock from '~/assets/icons/lock.svg'
import Unlock from '~/assets/icons/unlock.svg'
const { showZoom = true, showFitView = true, showInteractive = true, fitViewParams } = defineProps<ControlProps>()
const emit = defineEmits<{
(event: 'zoom-in'): void
(event: 'zoom-out'): void
(event: 'fit-view'): void
(event: 'interaction-change', active: boolean): void
}>()
const { instance, nodesDraggable, nodesConnectable, elementsSelectable, setInteractive } = $(useVueFlow())
const isInteractive = computed(() => nodesDraggable && nodesConnectable && elementsSelectable)
const onZoomInHandler = () => {
instance?.zoomIn()
emit('zoom-in')
}
const onZoomOutHandler = () => {
instance?.zoomOut()
emit('zoom-out')
}
const onFitViewHandler = () => {
instance?.fitView(fitViewParams)
emit('fit-view')
}
const onInteractiveChangeHandler = () => {
setInteractive(!isInteractive.value)
emit('interaction-change', !isInteractive.value)
}
</script>
<script lang="ts">
export default {
name: 'Controls',
}
</script>
<template>
<div class="vue-flow__controls">
<template v-if="showZoom">
<slot name="control-zoom-in">
<ControlButton class="vue-flow__controls-zoomin" @click="onZoomInHandler">
<slot name="icon-zoom-in">
<PlusIcon />
</slot>
</ControlButton>
</slot>
<slot name="control-zoom-out">
<ControlButton class="vue-flow__controls-zoomout" @click="onZoomOutHandler">
<slot name="icon-zoom-out">
<MinusIcon />
</slot>
</ControlButton>
</slot>
</template>
<slot name="control-fitview">
<ControlButton v-if="showFitView" class="vue-flow__controls-fitview" @click="onFitViewHandler">
<slot name="icon-fitview">
<Fitview />
</slot>
</ControlButton>
</slot>
<slot name="control-interactive">
<ControlButton v-if="showInteractive" class="vue-flow__controls-interactive" @click="onInteractiveChangeHandler">
<slot name="icon-unlock">
<Unlock v-if="isInteractive" />
</slot>
<slot name="icon-lock">
<Lock v-if="!isInteractive" />
</slot>
</ControlButton>
</slot>
<slot></slot>
</div>
</template>
@@ -0,0 +1,143 @@
<script lang="ts" setup>
import type { GraphNode, MiniMapNodeFunc, ShapeRendering } from '../../types'
import { useVueFlow, useWindow } from '../../composables'
import { getBoundsofRects, getRectOfNodes } from '../../utils'
import type { MiniMapProps } from '../../types/components'
import MiniMapNode from './MiniMapNode'
const {
nodeStrokeColor = '#555',
nodeColor = '#fff',
nodeClassName,
nodeBorderRadius = 5,
nodeStrokeWidth = 2,
maskColor = 'rgb(240, 242, 243, 0.7)',
} = defineProps<MiniMapProps>()
const attrs: Record<string, any> = useAttrs()
const window = useWindow()
const defaultWidth = 200
const defaultHeight = 150
const { viewport, dimensions, hooks, getNodes } = $(useVueFlow())
const elementWidth = attrs.style?.width ?? defaultWidth
const elementHeight = attrs.style?.height ?? defaultHeight
const nodeColorFunc: MiniMapNodeFunc = nodeColor instanceof Function ? nodeColor : () => nodeColor as string
const nodeStrokeColorFunc: MiniMapNodeFunc =
nodeStrokeColor instanceof Function ? nodeStrokeColor : () => nodeStrokeColor as string
const nodeClassNameFunc = nodeClassName instanceof Function ? nodeClassName : ((() => nodeClassName) as MiniMapNodeFunc)
const shapeRendering: ShapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision'
const bb = $computed(() => {
return getRectOfNodes(getNodes)
})
const viewBB = $computed(() => ({
x: -viewport.x / viewport.zoom,
y: -viewport.y / viewport.zoom,
width: dimensions.width / viewport.zoom,
height: dimensions.height / viewport.zoom,
}))
const viewBox = $(
controlledComputed($$(viewBB), () => {
const boundingRect = getNodes && getNodes.length ? getBoundsofRects(bb, viewBB) : viewBB
const scaledWidth = boundingRect.width / elementWidth
const scaledHeight = boundingRect.height / elementHeight
const viewScale = Math.max(scaledWidth, scaledHeight)
const viewWidth = viewScale * elementWidth
const viewHeight = viewScale * elementHeight
const offset = 5 * viewScale
return {
offset,
x: boundingRect.x - (viewWidth - boundingRect.width) / 2 - offset,
y: boundingRect.y - (viewHeight - boundingRect.height) / 2 - offset,
width: viewWidth + offset * 2,
height: viewHeight + offset * 2,
}
}),
)
const d = controlledComputed($$(viewBox), () => {
if (viewBox.x && viewBox.y) {
return `
M${viewBox.x - viewBox.offset},${viewBox.y - viewBox.offset}
h${viewBox.width + viewBox.offset * 2}
v${viewBox.height + viewBox.offset * 2}
h${-viewBox.width - viewBox.offset * 2}z
M${viewBB.x},${viewBB.y}
h${viewBB.width}
v${viewBB.height}
h${-viewBB.width}z`
} else {
return ''
}
})
const onNodeClick = (event: MouseEvent, node: GraphNode) => {
hooks.miniMapNodeClick.trigger({ event, node })
}
const onNodeDblClick = (event: MouseEvent, node: GraphNode) => {
hooks.miniMapNodeDoubleClick.trigger({ event, node })
}
</script>
<script lang="ts">
export default {
name: 'MiniMap',
}
</script>
<template>
<svg
:width="elementWidth"
:height="elementHeight"
:viewBox="[viewBox.x, viewBox.y, viewBox.width, viewBox.height].join(' ')"
class="vue-flow__minimap"
>
<MiniMapNode
v-for="node of getNodes"
:id="node.id"
:key="node.id"
v-memo="[node.computedPosition, node.dimensions]"
:position="node.computedPosition"
:dimensions="node.dimensions"
:style="node.style"
:class="nodeClassNameFunc(node)"
:color="nodeColorFunc(node)"
:border-radius="nodeBorderRadius"
:stroke-color="nodeStrokeColorFunc(node)"
:stroke-width="nodeStrokeWidth"
:shape-rendering="shapeRendering"
@click="(e: MouseEvent) => onNodeClick(e, node)"
@dblclick="(e: MouseEvent) => onNodeDblClick(e, node)"
>
<slot
:id="node.id"
:name="`node-${node.type}`"
:parent-node="node.parentNode"
:selected="node.selected"
:dragging="node.dragging"
:position="node.computedPosition"
:dimensions="node.dimensions"
:style="node.style"
:class="nodeClassNameFunc(node)"
:color="nodeColorFunc(node)"
:border-radius="nodeBorderRadius"
:stroke-color="nodeStrokeColorFunc(node)"
:stroke-width="nodeStrokeWidth"
:shape-rendering="shapeRendering"
/>
</MiniMapNode>
<path class="vue-flow__minimap-mask" :d="d" :fill="maskColor" fill-rule="evenodd" />
</svg>
</template>
@@ -0,0 +1,44 @@
import type { CSSProperties, FunctionalComponent } from 'vue'
import type { MiniMapNodeProps } from '~/types'
const MiniMapNode: FunctionalComponent<MiniMapNodeProps> = function (
{
position: { x, y },
dimensions: { height, width },
strokeWidth,
strokeColor,
borderRadius,
color,
shapeRendering = 'geometricPrecision',
},
{ attrs, emit, slots },
) {
const style = (attrs.style ?? {}) as CSSProperties
return [
h('rect', {
class: ['vue-flow__minimap-node', attrs.class].join(' '),
style,
x,
y,
rx: borderRadius,
ry: borderRadius,
width,
height,
fill: color || (style.background as string) || style.backgroundColor,
stroke: strokeColor,
strokeWidth,
shapeRendering,
onClick: (e: MouseEvent) => emit('click', e),
onDblClick: (e: MouseEvent) => emit('dbl-click', e),
}),
slots?.default?.(),
]
}
MiniMapNode.props = ['position', 'dimensions', 'strokeWidth', 'strokeColor', 'borderRadius', 'color', 'shapeRendering']
// @todo add mouseover events
MiniMapNode.emits = ['click', 'dbl-click']
export default MiniMapNode
@@ -0,0 +1,3 @@
export { default as MiniMap } from './MiniMap/MiniMap.vue'
export { default as Controls } from './Controls/Controls.vue'
export { default as Background } from './Background/Background.vue'
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 30">
<path d="M3.692 4.63c0-.53.4-.938.939-.938h5.215V0H4.708C2.13 0 0 2.054 0 4.63v5.216h3.692V4.631zM27.354 0h-5.2v3.692h5.17c.53 0 .984.4.984.939v5.215H32V4.631A4.624 4.624 0 0027.354 0zm.954 24.83c0 .532-.4.94-.939.94h-5.215v3.768h5.215c2.577 0 4.631-2.13 4.631-4.707v-5.139h-3.692v5.139zm-23.677.94c-.531 0-.939-.4-.939-.94v-5.138H0v5.139c0 2.577 2.13 4.707 4.708 4.707h5.138V25.77H4.631z" />
</svg>

After

Width:  |  Height:  |  Size: 463 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 32">
<path d="M21.333 10.667H19.81V7.619C19.81 3.429 16.38 0 12.19 0 8 0 4.571 3.429 4.571 7.619v3.048H3.048A3.056 3.056 0 000 13.714v15.238A3.056 3.056 0 003.048 32h18.285a3.056 3.056 0 003.048-3.048V13.714a3.056 3.056 0 00-3.048-3.047zM12.19 24.533a3.056 3.056 0 01-3.047-3.047 3.056 3.056 0 013.047-3.048 3.056 3.056 0 013.048 3.048 3.056 3.056 0 01-3.048 3.047zm4.724-13.866H7.467V7.619c0-2.59 2.133-4.724 4.723-4.724 2.591 0 4.724 2.133 4.724 4.724v3.048z" />
</svg>

After

Width:  |  Height:  |  Size: 530 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 5">
<path d="M0 0h32v4.2H0z"/>
</svg>

After

Width:  |  Height:  |  Size: 96 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<path d="M32 18.133H18.133V32h-4.266V18.133H0v-4.266h13.867V0h4.266v13.867H32z"/>
</svg>

After

Width:  |  Height:  |  Size: 152 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 32">
<path d="M21.333 10.667H19.81V7.619C19.81 3.429 16.38 0 12.19 0c-4.114 1.828-1.37 2.133.305 2.438 1.676.305 4.42 2.59 4.42 5.181v3.048H3.047A3.056 3.056 0 000 13.714v15.238A3.056 3.056 0 003.048 32h18.285a3.056 3.056 0 003.048-3.048V13.714a3.056 3.056 0 00-3.048-3.047zM12.19 24.533a3.056 3.056 0 01-3.047-3.047 3.056 3.056 0 013.047-3.048 3.056 3.056 0 013.048 3.048 3.056 3.056 0 01-3.048 3.047z" />
</svg>

After

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

+241
View File
@@ -0,0 +1,241 @@
// Generated by 'unplugin-auto-import'
// We suggest you to commit this file into source control
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 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 EffectScope: typeof import('vue')['EffectScope']
const extendRef: typeof import('@vueuse/core')['extendRef']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject']
const isDefined: typeof import('@vueuse/core')['isDefined']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const 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 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 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 useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
const useDraggable: typeof import('@vueuse/core')['useDraggable']
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
const useElementHover: typeof import('@vueuse/core')['useElementHover']
const useElementSize: typeof import('@vueuse/core')['useElementSize']
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
const useEventBus: typeof import('@vueuse/core')['useEventBus']
const useEventListener: typeof import('@vueuse/core')['useEventListener']
const useEventSource: typeof import('@vueuse/core')['useEventSource']
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
const useFavicon: typeof import('@vueuse/core')['useFavicon']
const useFetch: typeof import('@vueuse/core')['useFetch']
const 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 useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
const useInterval: typeof import('@vueuse/core')['useInterval']
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
const useMemoize: typeof import('@vueuse/core')['useMemoize']
const useMemory: typeof import('@vueuse/core')['useMemory']
const useMounted: typeof import('@vueuse/core')['useMounted']
const useMouse: typeof import('@vueuse/core')['useMouse']
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
const useNetwork: typeof import('@vueuse/core')['useNetwork']
const useNow: typeof import('@vueuse/core')['useNow']
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
const useOnline: typeof import('@vueuse/core')['useOnline']
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
const useParallax: typeof import('@vueuse/core')['useParallax']
const usePermission: typeof import('@vueuse/core')['usePermission']
const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const 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 useStorage: typeof import('@vueuse/core')['useStorage']
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
const useThrottle: typeof import('@vueuse/core')['useThrottle']
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
const useTimeout: typeof import('@vueuse/core')['useTimeout']
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
const 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 useVibrate: typeof import('@vueuse/core')['useVibrate']
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
const useVModel: typeof import('@vueuse/core')['useVModel']
const useVModels: typeof import('@vueuse/core')['useVModels']
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
const watch: typeof import('vue')['watch']
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
const 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 watchThrottled: typeof import('@vueuse/core')['watchThrottled']
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
const whenever: typeof import('@vueuse/core')['whenever']
}
export {}
@@ -0,0 +1,111 @@
<script lang="ts" setup>
import { getBezierPath, getSmoothStepPath } from '../Edges/utils'
import type { GraphNode, HandleElement, HandleType } from '../../types'
import { ConnectionLineType, Position } from '../../types'
import { useVueFlow } from '../../composables'
import { Slots } from '../../context'
const { sourceNode } = defineProps<{
sourceNode: GraphNode
}>()
const {
getNodes,
connectionHandleId,
connectionHandleType,
connectionPosition,
connectionLineType,
connectionLineStyle,
connectionNodeId,
viewport,
} = $(useVueFlow())
const slots = inject(Slots)?.['connection-line']
const hasSlot = slots?.({})
const sourceHandle =
connectionHandleId && connectionHandleType
? sourceNode.handleBounds[connectionHandleType as HandleType]?.find((d: HandleElement) => d.id === connectionHandleId)
: connectionHandleType && sourceNode.handleBounds[(connectionHandleType as HandleType) ?? '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}`
switch (connectionLineType) {
case ConnectionLineType.Bezier:
path = getBezierPath({
sourceX,
sourceY,
sourcePosition: sourceHandle?.position,
targetX,
targetY,
targetPosition,
})
break
case ConnectionLineType.Step:
path = getSmoothStepPath({
sourceX,
sourceY,
sourcePosition: sourceHandle?.position,
targetX,
targetY,
targetPosition,
borderRadius: 0,
})
break
case ConnectionLineType.SmoothStep:
path = getSmoothStepPath({
sourceX,
sourceY,
sourcePosition: sourceHandle?.position,
targetX,
targetY,
targetPosition,
})
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,
connectionLineType,
connectionLineStyle,
nodes: getNodes,
sourceNode,
sourceHandle,
}"
/>
<path v-else :d="dAttr" class="vue-flow__connection-path" :style="connectionLineStyle || {}" />
</g>
</template>
@@ -0,0 +1,76 @@
import type { CSSProperties, FunctionalComponent } from 'vue'
import EdgeText from './EdgeText.vue'
interface Props {
centerX: number
centerY: number
path: string
label?: any
style?: CSSProperties
labelStyle?: any
labelShowBg?: boolean
labelBgStyle?: any
labelBgPadding?: [number, number]
labelBgBorderRadius?: number
markerStart?: string
markerEnd?: string
}
/**
* 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<Props> = function ({
path,
centerX,
centerY,
label,
labelBgBorderRadius,
labelBgPadding,
labelBgStyle,
labelShowBg,
labelStyle,
markerStart,
markerEnd,
style,
}) {
return [
h('path', {
style: { ...style },
d: path,
class: 'vue-flow__edge-path',
markerEnd,
markerStart,
}),
label
? h(EdgeText, {
x: centerX,
y: centerY,
label,
labelStyle,
labelShowBg,
labelBgStyle,
labelBgPadding,
labelBgBorderRadius,
})
: null,
]
}
BaseEdge.props = [
'path',
'centerX',
'centerY',
'label',
'labelBgBorderRadius',
'labelBgPadding',
'labelBgStyle',
'labelShowBg',
'labelStyle',
'markerStart',
'markerEnd',
'style',
]
BaseEdge.inheritAttrs = false
export default BaseEdge
@@ -0,0 +1,81 @@
import type { FunctionalComponent } from 'vue'
import { getBezierCenter, 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 [centerX, centerY] = getBezierCenter({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
curvature,
})
const path = getBezierPath({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
curvature,
})
return h(BaseEdge, {
path,
centerX,
centerY,
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,51 @@
<script lang="ts" setup>
import type { EdgeTextProps } from '../../types/components'
import type { Rect } from '../../types'
const props = withDefaults(defineProps<EdgeTextProps>(), {
labelStyle: () => ({}),
labelShowBg: true,
labelBgStyle: () => ({}),
labelBgPadding: () => [2, 4],
labelBgBorderRadius: 2,
})
const edgeRef = templateRef<SVGTextElement>('edge-text', null)
let edgeRefBbox = $ref<Rect>({ x: 0, y: 0, width: 0, height: 0 })
onMounted(() => {
edgeRefBbox = edgeRef.value.getBBox()
})
const transform = computed(() => `translate(${props.x - edgeRefBbox.width / 2} ${props.y - edgeRefBbox.height / 2})`)
</script>
<script lang="ts">
export default {
name: 'EdgeText',
}
</script>
<template>
<g :transform="transform" :class="props.class" class="vue-flow__edge-textwrapper">
<rect
v-if="props.labelShowBg"
class="vue-flow__edge-textbg"
:width="`${edgeRefBbox.width + 2 * props.labelBgPadding[0]}px`"
:height="`${edgeRefBbox.height + 2 * props.labelBgPadding[1]}px`"
:x="-props.labelBgPadding[0]"
:y="-props.labelBgPadding[1]"
:style="props.labelBgStyle"
:rx="props.labelBgBorderRadius"
:ry="props.labelBgBorderRadius"
/>
<text ref="edge-text" class="vue-flow__edge-text" :y="edgeRefBbox.height / 2" dy="0.3em" :style="props.labelStyle">
<slot v-bind="props">
<component :is="props.label" v-if="typeof props.label !== 'string' && typeof props.label" />
<template v-else v-html="props.label">
{{ props.label }}
</template>
</slot>
</text>
</g>
</template>
@@ -0,0 +1,219 @@
<script lang="ts" setup>
import type { CSSProperties } from 'vue'
import { useHandle, useVueFlow } from '../../composables'
import type { EdgeComponent, GraphEdge } from '../../types'
import { ConnectionMode, Position } from '../../types'
import { connectionExists, getEdgePositions, getHandle, getMarkerId } from '../../utils'
import EdgeAnchor from './EdgeAnchor'
const { id, edge, name, selectable, updatable, type } = defineProps<{
id: string
edge: GraphEdge
selectable?: boolean
updatable?: boolean
type: EdgeComponent | Function | Object | false
name: string
}>()
const { emits, connectionMode, edgeUpdaterRadius, noPanClassName, setState, getEdges, getEdge, getNode, addSelectedEdges } = $(
useVueFlow(),
)
let updating = $ref(false)
const sourceNode = $computed(() => getNode(edge.source))
const targetNode = $computed(() => getNode(edge.target))
const { onMouseDown } = useHandle()
const onEdgeClick = (event: MouseEvent) => {
const data = { event, edge }
if (selectable) {
setState({
nodesSelectionActive: false,
})
addSelectedEdges([edge])
}
emits.edgeClick(data)
}
const onEdgeContextMenu = (event: MouseEvent) => emits.edgeContextMenu({ event, edge })
const onDoubleClick = (event: MouseEvent) => emits.edgeDoubleClick({ event, edge })
const onEdgeMouseEnter = (event: MouseEvent) => emits.edgeMouseEnter({ event, edge })
const onEdgeMouseMove = (event: MouseEvent) => emits.edgeMouseMove({ event, edge })
const onEdgeMouseLeave = (event: MouseEvent) => emits.edgeMouseLeave({ event, edge })
const onEdgeUpdaterMouseEnter = () => (updating = true)
const onEdgeUpdaterMouseOut = () => (updating = false)
const onEdgeUpdaterSourceMouseDown = (event: MouseEvent) => handleEdgeUpdater(event, true)
const onEdgeUpdaterTargetMouseDown = (event: MouseEvent) => handleEdgeUpdater(event, false)
const handleEdgeUpdater = (event: MouseEvent, isSourceHandle: boolean) => {
const nodeId = isSourceHandle ? edge.target : edge.source
const handleId = (isSourceHandle ? edge.targetHandle : edge.sourceHandle) ?? ''
emits.edgeUpdateStart({ event, edge })
onMouseDown(
event,
handleId,
nodeId,
isSourceHandle,
undefined,
isSourceHandle ? 'target' : 'source',
(connection) => {
if (!connectionExists(connection, getEdges)) emits.edgeUpdate({ edge, connection })
},
() => emits.edgeUpdateEnd({ event, edge }),
)
}
const sourceHandle = $computed(() => {
if (!sourceNode) return
let sourceNodeHandles
if (connectionMode === ConnectionMode.Strict) {
sourceNodeHandles = sourceNode.handleBounds.source
} else {
sourceNodeHandles = sourceNode.handleBounds.source ?? sourceNode.handleBounds.target
}
return getHandle(sourceNodeHandles, edge.sourceHandle)
})
const targetHandle = $computed(() => {
if (!targetNode) return
let targetNodeHandles
if (connectionMode === ConnectionMode.Strict) {
targetNodeHandles = targetNode.handleBounds.target
} else {
targetNodeHandles = targetNode.handleBounds.target ?? targetNode.handleBounds.source
}
return getHandle(targetNodeHandles, edge.targetHandle)
})
const sourcePosition = $(controlledComputed($$(sourceHandle), () => (sourceHandle ? sourceHandle.position : Position.Bottom)))
const targetPosition = $(controlledComputed($$(targetHandle), () => (targetHandle ? targetHandle.position : Position.Top)))
onMounted(() => {
const stop = watch(
[
$$(sourcePosition),
$$(targetPosition),
() => sourceNode?.computedPosition,
() => targetNode?.computedPosition,
() => sourceNode?.dimensions,
() => targetNode?.dimensions,
],
() => {
if (sourceNode && targetNode) {
const { sourceX, sourceY, targetY, targetX } = getEdgePositions(
sourceNode,
sourceHandle,
sourcePosition,
targetNode,
targetHandle,
targetPosition,
)
const storedEdge = getEdge(id)!
if (storedEdge) {
if (edge.sourceX !== sourceX) storedEdge.sourceX = sourceX
if (edge.sourceY !== sourceY) storedEdge.sourceY = sourceY
if (edge.targetX !== targetX) storedEdge.targetX = targetX
if (edge.targetY !== targetY) storedEdge.targetY = targetY
}
}
},
{ immediate: true, flush: 'pre' },
)
onBeforeUnmount(() => stop())
})
const getClass = () => {
const extraClass = edge.class instanceof Function ? edge.class(edge) : edge.class
return [
'vue-flow__edge',
`vue-flow__edge-${name}`,
noPanClassName,
{
selected: edge.selected,
animated: edge.animated,
inactive: !selectable,
updating,
},
extraClass,
]
}
const getStyle = () => (edge.style instanceof Function ? edge.style(edge) : edge.style) as CSSProperties
</script>
<script lang="ts">
export default {
name: 'Edge',
inheritAttrs: false,
}
</script>
<template>
<g
:class="getClass()"
@click="onEdgeClick"
@dbl-click="onDoubleClick"
@contextmenu="onEdgeContextMenu"
@mouseenter="onEdgeMouseEnter"
@mousemove="onEdgeMouseMove"
@mouseleave="onEdgeMouseLeave"
>
<component
:is="type"
:id="edge.id"
:source-node="sourceNode"
:target-node="targetNode"
:source="edge.source"
:target="edge.target"
:updatable="updatable"
:selected="edge.selected"
:animated="edge.animated"
:label="edge.label"
: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"
:data="edge.data"
:style="getStyle()"
:marker-start="`url(#${getMarkerId(edge.markerStart)})`"
:marker-end="`url(#${getMarkerId(edge.markerEnd)})`"
:source-position="sourcePosition"
:target-position="targetPosition"
:source-x="edge.sourceX"
:source-y="edge.sourceY"
:target-x="edge.targetX"
:target-y="edge.targetY"
:source-handle-id="edge.sourceHandle"
:target-handle-id="edge.targetHandle"
/>
<template v-if="updatable">
<g @mousedown="onEdgeUpdaterSourceMouseDown" @mouseenter="onEdgeUpdaterMouseEnter" @mouseout="onEdgeUpdaterMouseOut">
<EdgeAnchor :position="sourcePosition" :center-x="edge.sourceX" :center-y="edge.sourceY" :radius="edgeUpdaterRadius" />
</g>
<g @mousedown="onEdgeUpdaterTargetMouseDown" @mouseenter="onEdgeUpdaterMouseEnter" @mouseout="onEdgeUpdaterMouseOut">
<EdgeAnchor :position="targetPosition" :center-x="edge.targetX" :center-y="edge.targetY" :radius="edgeUpdaterRadius" />
</g>
</template>
</g>
</template>
@@ -0,0 +1,77 @@
import type { FunctionalComponent } from 'vue'
import { getSimpleBezierCenter, 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 [centerX, centerY] = getSimpleBezierCenter({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
})
const path = getSimpleBezierPath({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
})
return h(BaseEdge, {
path,
centerX,
centerY,
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,80 @@
import type { FunctionalComponent } from 'vue'
import { getCenter, 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,
style,
}) {
const [centerX, centerY] = getCenter({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
})
const path = getSmoothStepPath({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
borderRadius,
})
return h(BaseEdge, {
path,
centerX,
centerY,
label,
labelStyle,
labelShowBg,
labelBgStyle,
labelBgPadding,
labelBgBorderRadius,
style,
markerEnd,
markerStart,
})
}
SmoothStepEdge.props = [
'sourcePosition',
'targetPosition',
'label',
'labelStyle',
'labelShowBg',
'labelBgStyle',
'labelBgPadding',
'labelBgBorderRadius',
'sourceY',
'sourceX',
'targetX',
'targetY',
'borderRadius',
'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,62 @@
import type { FunctionalComponent } from 'vue'
import BaseEdge from './BaseEdge'
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 centerY = computed(() => {
const yOffset = Math.abs(targetY - sourceY) / 2
return targetY < sourceY ? targetY + yOffset : targetY - yOffset
})
const centerX = computed(() => {
const xOffset = Math.abs(targetX - sourceX) / 2
return targetX < sourceX ? targetX + xOffset : targetX - xOffset
})
return h(BaseEdge, {
path: `M ${sourceX},${sourceY}L ${targetX},${targetY}`,
centerX: centerX.value,
centerY: centerY.value,
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,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 './EdgeWrapper.vue'
@@ -0,0 +1,115 @@
import { Position } from '~/types'
interface GetControlWithCurvatureParams {
pos: Position
x1: number
y1: number
x2: number
y2: number
c: number
}
export interface GetBezierPathParams {
sourceX: number
sourceY: number
sourcePosition?: Position
targetX: number
targetY: number
targetPosition?: Position
curvature?: number
centerX?: number
centerY?: number
}
function calculateControlOffset(distance: number, curvature: number): number {
if (distance >= 0) {
return 0.5 * distance
} else {
return curvature * 25 * Math.sqrt(-distance)
}
}
function getControlWithCurvature({ pos, x1, y1, x2, y2, c }: GetControlWithCurvatureParams): [number, number] {
let ctX: number, ctY: number
switch (pos) {
case Position.Left:
ctX = x1 - calculateControlOffset(x1 - x2, c)
ctY = y1
break
case Position.Right:
ctX = x1 + calculateControlOffset(x2 - x1, c)
ctY = y1
break
case Position.Top:
ctX = x1
ctY = y1 - calculateControlOffset(y1 - y2, c)
break
case Position.Bottom:
ctX = x1
ctY = y1 + calculateControlOffset(y2 - y1, c)
break
}
return [ctX, ctY]
}
export function getBezierPath({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
curvature = 0.25,
}: GetBezierPathParams): string {
const [sourceControlX, sourceControlY] = getControlWithCurvature({
pos: sourcePosition,
x1: sourceX,
y1: sourceY,
x2: targetX,
y2: targetY,
c: curvature,
})
const [targetControlX, targetControlY] = getControlWithCurvature({
pos: targetPosition,
x1: targetX,
y1: targetY,
x2: sourceX,
y2: sourceY,
c: curvature,
})
return `M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`
}
export function getBezierCenter({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
curvature = 0.25,
}: GetBezierPathParams): [number, number, number, number] {
const [sourceControlX, sourceControlY] = getControlWithCurvature({
pos: sourcePosition,
x1: sourceX,
y1: sourceY,
x2: targetX,
y2: targetY,
c: curvature,
})
const [targetControlX, targetControlY] = getControlWithCurvature({
pos: targetPosition,
x1: targetX,
y1: targetY,
x2: sourceX,
y2: sourceY,
c: curvature,
})
// cubic bezier t=0.5 mid point, not the actual mid point, but easy to calculate
// https://stackoverflow.com/questions/67516101/how-to-find-distance-mid-point-of-bezier-curve
const centerX = sourceX * 0.125 + sourceControlX * 0.375 + targetControlX * 0.375 + targetX * 0.125
const centerY = sourceY * 0.125 + sourceControlY * 0.375 + targetControlY * 0.375 + targetY * 0.125
const xOffset = Math.abs(centerX - sourceX)
const yOffset = Math.abs(centerY - sourceY)
return [centerX, centerY, xOffset, yOffset]
}
@@ -0,0 +1,46 @@
import { Position } from '~/types'
export interface GetCenterParams {
sourceX: number
sourceY: number
targetX: number
targetY: number
sourcePosition?: Position
targetPosition?: Position
}
const LeftOrRight = [Position.Left, Position.Right]
export const getCenter = ({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition = Position.Bottom,
targetPosition = Position.Top,
}: GetCenterParams): [number, number, number, number] => {
const sourceIsLeftOrRight = LeftOrRight.includes(sourcePosition)
const targetIsLeftOrRight = LeftOrRight.includes(targetPosition)
// we expect flows to be horizontal or vertical (all handles left or right respectively top or bottom)
// a mixed edge is when one the source is on the left and the target is on the top for example.
const mixedEdge = (sourceIsLeftOrRight && !targetIsLeftOrRight) || (targetIsLeftOrRight && !sourceIsLeftOrRight)
if (mixedEdge) {
const xOffset = sourceIsLeftOrRight ? Math.abs(targetX - sourceX) : 0
const centerX = sourceX > targetX ? sourceX - xOffset : sourceX + xOffset
const yOffset = sourceIsLeftOrRight ? 0 : Math.abs(targetY - sourceY)
const centerY = sourceY < targetY ? sourceY + yOffset : sourceY - yOffset
return [centerX, centerY, xOffset, yOffset]
}
const xOffset = Math.abs(targetX - sourceX) / 2
const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset
const yOffset = Math.abs(targetY - sourceY) / 2
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset
return [centerX, centerY, xOffset, yOffset]
}
@@ -0,0 +1,4 @@
export * from './bezier'
export * from './simple-bezier'
export * from './smoothstep'
export * from './general'
@@ -0,0 +1,91 @@
import { Position } from '~/types'
export interface GetSimpleBezierPathParams {
sourceX: number
sourceY: number
sourcePosition?: Position
targetX: number
targetY: number
targetPosition?: Position
}
interface GetControlParams {
pos: Position
x1: number
y1: number
x2: number
y2: number
}
function getControl({ pos, x1, y1, x2, y2 }: GetControlParams): [number, number] {
let ctX: number, ctY: number
switch (pos) {
case Position.Left:
case Position.Right:
ctX = 0.5 * (x1 + x2)
ctY = y1
break
case Position.Top:
case Position.Bottom:
ctX = x1
ctY = 0.5 * (y1 + y2)
break
}
return [ctX, ctY]
}
export function getSimpleBezierPath({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
}: GetSimpleBezierPathParams): string {
const [sourceControlX, sourceControlY] = getControl({
pos: sourcePosition,
x1: sourceX,
y1: sourceY,
x2: targetX,
y2: targetY,
})
const [targetControlX, targetControlY] = getControl({
pos: targetPosition,
x1: targetX,
y1: targetY,
x2: sourceX,
y2: sourceY,
})
return `M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`
}
export function getSimpleBezierCenter({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
}: GetSimpleBezierPathParams): [number, number, number, number] {
const [sourceControlX, sourceControlY] = getControl({
pos: sourcePosition,
x1: sourceX,
y1: sourceY,
x2: targetX,
y2: targetY,
})
const [targetControlX, targetControlY] = getControl({
pos: targetPosition,
x1: targetX,
y1: targetY,
x2: sourceX,
y2: sourceY,
})
// cubic bezier t=0.5 mid point, not the actual mid point, but easy to calculate
// https://stackoverflow.com/questions/67516101/how-to-find-distance-mid-point-of-bezier-curve
const centerX = sourceX * 0.125 + sourceControlX * 0.375 + targetControlX * 0.375 + targetX * 0.125
const centerY = sourceY * 0.125 + sourceControlY * 0.375 + targetControlY * 0.375 + targetY * 0.125
const xOffset = Math.abs(centerX - sourceX)
const yOffset = Math.abs(centerY - sourceY)
return [centerX, centerY, xOffset, yOffset]
}
@@ -0,0 +1,93 @@
import { getCenter } from './general'
import { Position } from '~/types'
export interface GetSmoothStepPathParams {
sourceX: number
sourceY: number
sourcePosition?: Position
targetX: number
targetY: number
targetPosition?: Position
borderRadius?: number
centerX?: number
centerY?: number
}
// These are some helper methods for drawing the round corners
// The name indicates the direction of the path. "bottomLeftCorner" goes
// from bottom to the left and "leftBottomCorner" goes from left to the bottom.
// We have to consider the direction of the paths because of the animated lines.
const bottomLeftCorner = (x: number, y: number, size: number): string => `L ${x},${y - size}Q ${x},${y} ${x + size},${y}`
const leftBottomCorner = (x: number, y: number, size: number): string => `L ${x + size},${y}Q ${x},${y} ${x},${y - size}`
const bottomRightCorner = (x: number, y: number, size: number): string => `L ${x},${y - size}Q ${x},${y} ${x - size},${y}`
const rightBottomCorner = (x: number, y: number, size: number): string => `L ${x - size},${y}Q ${x},${y} ${x},${y - size}`
const leftTopCorner = (x: number, y: number, size: number): string => `L ${x + size},${y}Q ${x},${y} ${x},${y + size}`
const topLeftCorner = (x: number, y: number, size: number): string => `L ${x},${y + size}Q ${x},${y} ${x + size},${y}`
const topRightCorner = (x: number, y: number, size: number): string => `L ${x},${y + size}Q ${x},${y} ${x - size},${y}`
const rightTopCorner = (x: number, y: number, size: number): string => `L ${x - size},${y}Q ${x},${y} ${x},${y + size}`
export function getSmoothStepPath({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
borderRadius = 5,
centerX,
centerY,
}: GetSmoothStepPathParams): string {
const [_centerX, _centerY, offsetX, offsetY] = getCenter({ sourceX, sourceY, targetX, targetY })
const cornerWidth = Math.min(borderRadius, Math.abs(targetX - sourceX))
const cornerHeight = Math.min(borderRadius, Math.abs(targetY - sourceY))
const cornerSize = Math.min(cornerWidth, cornerHeight, offsetX, offsetY)
const leftAndRight = [Position.Left, Position.Right]
const cX = typeof centerX !== 'undefined' ? centerX : _centerX
const cY = typeof centerY !== 'undefined' ? centerY : _centerY
let firstCornerPath
let secondCornerPath
if (sourceX <= targetX) {
firstCornerPath = sourceY <= targetY ? bottomLeftCorner(sourceX, cY, cornerSize) : topLeftCorner(sourceX, cY, cornerSize)
secondCornerPath = sourceY <= targetY ? rightTopCorner(targetX, cY, cornerSize) : rightBottomCorner(targetX, cY, cornerSize)
} else {
firstCornerPath = sourceY < targetY ? bottomRightCorner(sourceX, cY, cornerSize) : topRightCorner(sourceX, cY, cornerSize)
secondCornerPath = sourceY < targetY ? leftTopCorner(targetX, cY, cornerSize) : leftBottomCorner(targetX, cY, cornerSize)
}
if (leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
if (sourceX <= targetX) {
firstCornerPath = sourceY <= targetY ? rightTopCorner(cX, sourceY, cornerSize) : rightBottomCorner(cX, sourceY, cornerSize)
secondCornerPath = sourceY <= targetY ? bottomLeftCorner(cX, targetY, cornerSize) : topLeftCorner(cX, targetY, cornerSize)
} else if (
(sourcePosition === Position.Right && targetPosition === Position.Left) ||
(sourcePosition === Position.Left && targetPosition === Position.Right) ||
(sourcePosition === Position.Left && targetPosition === Position.Left)
) {
// and sourceX > targetX
firstCornerPath = sourceY <= targetY ? leftTopCorner(cX, sourceY, cornerSize) : leftBottomCorner(cX, sourceY, cornerSize)
secondCornerPath = sourceY <= targetY ? bottomRightCorner(cX, targetY, cornerSize) : topRightCorner(cX, targetY, cornerSize)
}
} else if (leftAndRight.includes(sourcePosition) && !leftAndRight.includes(targetPosition)) {
if (sourceX <= targetX) {
firstCornerPath =
sourceY <= targetY ? rightTopCorner(targetX, sourceY, cornerSize) : rightBottomCorner(targetX, sourceY, cornerSize)
} else {
firstCornerPath =
sourceY <= targetY ? leftTopCorner(targetX, sourceY, cornerSize) : leftBottomCorner(targetX, sourceY, cornerSize)
}
secondCornerPath = ''
} else if (!leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
if (sourceX <= targetX) {
firstCornerPath =
sourceY <= targetY ? bottomLeftCorner(sourceX, targetY, cornerSize) : topLeftCorner(sourceX, targetY, cornerSize)
} else {
firstCornerPath =
sourceY <= targetY ? bottomRightCorner(sourceX, targetY, cornerSize) : topRightCorner(sourceX, targetY, cornerSize)
}
secondCornerPath = ''
}
return `M ${sourceX},${sourceY}${firstCornerPath}${secondCornerPath}L ${targetX},${targetY}`
}
@@ -0,0 +1,71 @@
<script lang="ts" setup>
import { useHandle, useVueFlow } from '../../composables'
import type { Position } from '../../types'
import { ConnectionMode } from '../../types'
import { NodeId } from '../../context'
import type { HandleProps } from '../../types/handle'
const {
type = 'source',
position = 'top' as Position,
connectable = true,
id,
isValidConnection = function () {
return true
},
} = defineProps<HandleProps>()
const { connectionStartHandle, connectionMode } = $(useVueFlow())
const nodeId = inject(NodeId, '')
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,
},
]
})
</script>
<script lang="ts">
export default {
name: 'Handle',
}
</script>
<template>
<div
: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,282 @@
<script lang="ts" setup>
import { useDraggableCore } from '@braks/revue-draggable'
import type { CSSProperties } from 'vue'
import { useVueFlow } from '../../composables'
import type { GraphNode, NodeComponent, SnapGrid } from '../../types'
import { NodeId } from '../../context'
import { getHandleBounds, getXYZPos } from '../../utils'
const { id, type, name, node, parentNode, draggable, selectable, connectable, snapGrid } = defineProps<{
id: string
node: GraphNode
parentNode?: GraphNode
draggable: boolean
selectable: boolean
connectable: boolean
snapGrid?: SnapGrid
type: NodeComponent | Function | Object | false
name: string
}>()
provide(NodeId, id)
const {
viewport,
noDragClassName,
noPanClassName,
emits,
selectNodesOnDrag,
setState,
updateNodePosition,
updateNodeDimensions,
getNode,
getNodeTypes,
addSelectedNodes,
} = $(useVueFlow())
const nodeElement = ref()
const { scale, disabled, handle, cancel, grid, onDrag, onDragStart, onDragStop } = useDraggableCore(nodeElement, {
handle: node.dragHandle,
disabled: !draggable,
grid: snapGrid,
cancel: `.${noDragClassName}`,
enableUserSelectHack: false,
scale: viewport.zoom,
})
onBeforeMount(() => {
updateNodePosition({ id: node.id, diff: { x: 0, y: 0 } })
})
onMounted(() => {
debouncedWatch(
() => viewport.zoom,
() => {
scale.value = viewport.zoom
},
{ debounce: 5, flush: 'post' },
)
watch(
() => draggable,
() => {
disabled.value = !draggable
},
)
watch(
() => node.dragHandle,
() => {
if (node.dragHandle) handle.value = node.dragHandle
},
)
watch($$(noDragClassName), () => {
if (noDragClassName) cancel.value = noDragClassName as any
})
watch(
() => snapGrid,
() => {
if (grid) grid.value = snapGrid
},
)
})
const parent = $computed(() => (node.parentNode ? getNode(node.parentNode) : undefined))
onMounted(() => {
const observer = useResizeObserver(nodeElement, () =>
updateNodeDimensions([{ id: node.id, nodeElement: nodeElement.value, forceUpdate: true }]),
)
watch(
[() => node.type, () => node.sourcePosition, () => node.targetPosition],
() => {
updateNodeDimensions([{ id: node.id, nodeElement: nodeElement.value }])
},
{ flush: 'post' },
)
onBeforeUnmount(() => observer.stop())
updateNodeDimensions([{ id: node.id, nodeElement: nodeElement.value, forceUpdate: true }])
watch(
[() => node.position, () => parent?.computedPosition, () => node.selected, () => parent?.selected],
([pos, parent]) => {
const xyzPos = {
...pos,
z: node.dragging || node.selected ? 1000 : 0,
}
const graphNode = getNode(id)!
if (parent) {
graphNode.computedPosition = getXYZPos(parent, xyzPos)
} else {
graphNode.computedPosition = xyzPos
}
graphNode.handleBounds = getHandleBounds(nodeElement.value, scale.value)
},
{ deep: true, flush: 'post' },
)
})
onUnmounted(() => {
nodeElement.value = undefined
})
const onMouseEnter = (event: MouseEvent) => {
if (!node.dragging) {
emits.nodeMouseEnter({ event, node })
}
}
const onMouseMove = (event: MouseEvent) => {
if (!node.dragging) {
emits.nodeMouseMove({ event, node })
}
}
const onMouseLeave = (event: MouseEvent) => {
if (!node.dragging) {
emits.nodeMouseLeave({ event, node })
}
}
const onContextMenu = (event: MouseEvent) => {
emits.nodeContextMenu({
event,
node,
})
}
const onDoubleClick = (event: MouseEvent) => emits.nodeDoubleClick({ event, node })
const onSelectNode = (event: MouseEvent) => {
if (!draggable) {
if (selectable) {
setState({
nodesSelectionActive: false,
})
if (!node.selected) addSelectedNodes([node])
}
emits.nodeClick({ event, node })
}
}
onDragStart(({ event }) => {
addSelectedNodes([])
emits.nodeDragStart({ event, node })
if (selectNodesOnDrag && selectable) {
setState({
nodesSelectionActive: false,
})
if (!node.selected) addSelectedNodes([node])
} else if (!selectNodesOnDrag && !node.selected && selectable) {
setState({
nodesSelectionActive: false,
})
addSelectedNodes([])
}
})
onDrag(({ event, data: { deltaX, deltaY } }) => {
updateNodePosition({ id: node.id, diff: { x: deltaX, y: deltaY }, dragging: true })
emits.nodeDrag({ event, node })
})
onDragStop(({ event, data: { deltaX, deltaY } }) => {
// onDragStop also gets called when user just clicks on a node.
// Because of that we set dragging to true inside the onDrag handler and handle the click here
if (!node.dragging) {
if (selectable && !selectNodesOnDrag && !node.selected) {
addSelectedNodes([node])
}
emits.nodeClick({ event, node })
return
}
updateNodePosition({ id: node.id, diff: { x: deltaX, y: deltaY }, dragging: false })
emits.nodeDragStop({ event, node })
})
const getClass = computed(() => {
const extraClass = node.class instanceof Function ? node.class(node) : node.class
return [
'vue-flow__node',
`vue-flow__node-${name}`,
noPanClassName,
{
dragging: node.dragging,
selected: node.selected,
selectable,
},
extraClass,
]
})
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 {
zIndex: node.computedPosition.z,
transform: `translate(${node.computedPosition.x}px,${node.computedPosition.y}px)`,
pointerEvents: selectable || draggable ? 'all' : 'none',
...styles,
} as CSSProperties
})
</script>
<script lang="ts">
export default {
name: 'Node',
inheritAttrs: false,
}
</script>
<template>
<div
ref="nodeElement"
:class="getClass"
:style="getStyle"
:data-id="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"
:selected="!!node.selected"
:connectable="connectable"
:position="node.position"
:computed-position="node.computedPosition"
:dimensions="node.dimensions"
:is-valid-target-pos="node.isValidTargetPos"
:is-valid-source-pos="node.isValidSourcePos"
:parent-node="node.parentNode"
:dragging="!!node.dragging"
:z-index="node.computedPosition.z"
:target-position="node.targetPosition"
:source-position="node.sourcePosition"
:label="node.label"
:drag-handle="node.dragHandle"
:node-element="nodeElement"
/>
</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,68 @@
<script lang="ts" setup>
import { useDraggableCore } from '@braks/revue-draggable'
import { useVueFlow } from '../../composables'
import { getRectOfNodes } from '../../utils'
const { emits, setState, viewport, getSelectedNodes, snapToGrid, snapGrid, updateNodePosition, noPanClassName } = $(useVueFlow())
const el = templateRef<HTMLDivElement>('el', null)
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 transform = computed(() => `translate(${viewport.x}px,${viewport.y}px) scale(${viewport.zoom})`)
watch($$(selectedNodesBBox), (v) => {
setState({
selectedNodesBbox: v,
})
})
const onContextMenu = (event: MouseEvent) => emits.selectionContextMenu({ event, nodes: getSelectedNodes })
const { onDragStart, onDrag, onDragStop, scale } = useDraggableCore(el, {
grid: snapToGrid ? snapGrid : undefined,
enableUserSelectHack: false,
scale: viewport.zoom,
})
onMounted(() => {
debouncedWatch(
() => viewport.zoom,
() => {
scale.value = viewport.zoom
},
{ debounce: 5 },
)
})
onDragStart(({ event }) => emits.selectionDragStart({ event, nodes: getSelectedNodes }))
onDrag(({ event, data: { deltaX, deltaY } }) => {
emits.selectionDrag({ event, nodes: getSelectedNodes })
updateNodePosition({ diff: { x: deltaX, y: deltaY }, dragging: true })
})
onDragStop(({ event }) => {
emits.selectionDragStop({ event, nodes: getSelectedNodes })
getSelectedNodes.forEach((node) => (node.dragging = false))
})
</script>
<script lang="ts">
export default {
name: 'NodesSelection',
}
</script>
<template>
<div class="vue-flow__nodesselection vue-flow__container" :class="noPanClassName" :style="{ transform }">
<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,124 @@
<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, setState, getNodes, getEdges, viewport, addSelectedEdges, addSelectedNodes } = $(useVueFlow())
const el = templateRef('user-selection', null)
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
setState({
userSelectionActive: 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,
}
setState({
userSelectionActive: true,
nodesSelectionActive: false,
})
}
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, rect, viewport)
const selectedEdges = getConnectedEdges(selectedNodes, getEdges)
rect = nextUserSelectRect
addSelectedNodes(selectedNodes)
addSelectedEdges(selectedEdges)
prevNodes = selectedNodes.length
prevEdges = selectedEdges.length
}
const onMouseUp = () => {
rect = initialRect()
setState({
nodesSelectionActive: prevNodes > 0,
userSelectionActive: false,
})
}
const onMouseLeave = () => {
setState({
nodesSelectionActive: prevNodes > 0,
})
reset()
}
onMounted(() => {
useEventListener(el, 'mousedown', onMouseDown)
useEventListener(el, 'mousemove', onMouseMove)
useEventListener(el, 'click', onMouseUp)
useEventListener(el, 'mouseup', onMouseUp)
useEventListener(el, 'mouseleave', onMouseLeave)
})
onBeforeUnmount(() => {
reset()
})
</script>
<script lang="ts">
export default {
name: 'UserSelection',
}
</script>
<template>
<div ref="user-selection" class="vue-flow__selectionpane vue-flow__container">
<SelectionRect v-if="rect.draw" :width="rect.width" :height="rect.height" :x="rect.x" :y="rect.y" />
</div>
</template>
@@ -0,0 +1,13 @@
import 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,5 @@
export { default as useHandle } from './useHandle'
export { default as useKeyPress } from './useKeyPress'
export { default as useZoomPanHelper } from './useZoomPanHelper'
export { default as useWindow } from './useWindow'
export { default as useVueFlow } from './useVueFlow'
@@ -0,0 +1,259 @@
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,
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 =
isValidConnection(connection, { edges, sourceNode: getNode(sourceId)!, targetNode: getNode(targetId)! }) ||
!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,
connectionPosition,
connectionMode,
emits,
setState,
getNode,
} = $(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 flowNode = (event.target as Element).closest('.vue-flow')
const doc = getHostForElement(event.target as HTMLElement)
if (!doc) return
let validConnectFunc: ValidConnectionFunc = isValidConnection ?? (() => true)
const node = getNode(nodeId)
if (node && (typeof node.connectable === 'undefined' ? nodesConnectable : node.connectable) === false) return
if (!isValidConnection) {
if (node) validConnectFunc = (!isTarget ? node.isValidTargetPos : node.isValidSourcePos) ?? (() => true)
}
const elementBelow = doc.elementFromPoint(event.clientX, event.clientY)
const elementBelowIsTarget = elementBelow?.classList.contains('target')
const elementBelowIsSource = elementBelow?.classList.contains('source')
if (!flowNode || (!elementBelowIsTarget && !elementBelowIsSource && !elementEdgeUpdaterType)) return
const handleType = elementEdgeUpdaterType ?? (elementBelowIsTarget ? 'target' : 'source')
const containerBounds = flowNode.getBoundingClientRect()
setState({
connectionPosition: {
x: event.clientX - containerBounds.left,
y: event.clientY - containerBounds.top,
},
connectionNodeId: nodeId,
connectionHandleId: handleId,
connectionHandleType: handleType,
})
emits.connectStart({ event, nodeId, handleId, handleType })
function onMouseMove(event: MouseEvent) {
connectionPosition.x = event.clientX - containerBounds.left
connectionPosition.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,
)
emits.connectStop(event)
const isOwnHandle = connection.source === connection.target
if (isValid && !isOwnHandle) {
if (!onEdgeUpdate) emits.connect(connection)
else onEdgeUpdate(connection)
}
emits.connectEnd(event)
if (elementEdgeUpdaterType) onEdgeUpdateEnd?.()
resetRecentHandle(recentHoveredHandle)
setState({
connectionNodeId: null,
connectionHandleId: null,
connectionHandleType: null,
connectionPosition: { x: NaN, y: NaN },
})
doc.removeEventListener('mousemove', onMouseMove as EventListenerOrEventListenerObject)
doc.removeEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject)
}
doc.addEventListener('mousemove', onMouseMove as EventListenerOrEventListenerObject)
doc.addEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject)
}
const onClick = (
event: MouseEvent,
handleId: string | null,
nodeId: string,
handleType: HandleType,
isValidConnection?: ValidConnectionFunc,
) => {
if (!connectOnClick) return
if (!connectionStartHandle) {
emits.connectStart({ event, nodeId, handleId, handleType })
setState({ connectionStartHandle: { nodeId, type: handleType, handleId } })
} 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
emits.connectStop(event)
if (isValid && !isOwnHandle) emits.connect(connection)
emits.connectEnd(event)
setState({ connectionStartHandle: null })
}
}
return {
onMouseDown,
onClick,
}
}
@@ -0,0 +1,52 @@
import type { Ref } from 'vue'
import { onKeyDown, onKeyPressed, onKeyUp } from '@vueuse/core'
import useWindow from './useWindow'
import type { KeyCode } from '~/types'
import { isInputDOMNode } from '~/utils'
export default (keyCode: Ref<KeyCode>, onChange?: (keyPressed: boolean) => void): Ref<boolean> => {
const window = useWindow()
let isPressed = $ref(false)
watchEffect(
() => {
if (onChange && typeof onChange === 'function') onChange(isPressed)
},
{ flush: 'post' },
)
onKeyPressed(
(e) => !isInputDOMNode(e) && (e.key === keyCode.value || e.keyCode === keyCode.value),
(e) => {
e.preventDefault()
isPressed = true
},
)
onKeyDown(
(e) => !isInputDOMNode(e) && (e.key === keyCode.value || e.keyCode === keyCode.value),
(e) => {
e.preventDefault()
isPressed = true
},
)
onKeyUp(
(e) => !isInputDOMNode(e) && (e.key === keyCode.value || e.keyCode === keyCode.value),
(e) => {
e.preventDefault()
isPressed = false
},
)
if (typeof window.addEventListener !== 'undefined') {
useEventListener(window, 'blur', () => {
isPressed = false
})
}
if (onChange && typeof onChange === 'function') onChange(isPressed)
return $$(isPressed)
}
@@ -0,0 +1,161 @@
import type { EffectScope } from 'vue'
import type { MaybeRef } from '@vueuse/core'
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,
}
this.set(id, flow)
return flow
}
public getId() {
return `vue-flow-${this.currentId++}`
}
}
type Injection = VueFlowStore | null | undefined
type Scope = (EffectScope & { vueFlowId: string }) | undefined
type Options = { [key in keyof FlowProps]: MaybeRef<FlowProps[key]> }
export default (options?: Options): VueFlowStore => {
const reactiveOptions = options ? reactive(options) : undefined
const storage = Storage.getInstance()
const scope = getCurrentScope() as Scope
const id = reactiveOptions?.id
const vueFlowId = scope?.vueFlowId || id
let vueFlow: Injection
/**
* 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, reactiveOptions)
if (scope) {
scope.vueFlowId = name
onBeforeMount(() => {
if (reactiveOptions) {
scope.run(() => {
watch(reactiveOptions, (opts) => {
vueFlow?.setState(opts)
})
})
}
})
}
} else {
// if composable was called with additional options after initialization, overwrite state with the options values
if (reactiveOptions) vueFlow.setState(reactiveOptions)
}
/**
* 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)
// dispose of state values and storage entry
onScopeDispose(() => {
if (storage.get(vueFlow!.id)) {
storage.remove(vueFlow!.id)
}
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,151 @@
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, ViewportFuncs } 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
}
export default (): ViewportFuncs => {
const {
onPaneReady,
nodes,
d3Zoom,
d3Selection,
dimensions,
translateExtent,
minZoom,
maxZoom,
viewport,
snapToGrid,
snapGrid,
getNodes,
} = $(useVueFlow())
let hasDimensions = $ref(false)
onPaneReady(() => (hasDimensions = true))
const zoomTo: ViewportFuncs['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: ViewportFuncs['zoomIn'] = async (options) => {
await zoom(1.2, options?.duration)
}
const zoomOut: ViewportFuncs['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,112 @@
<script lang="ts" setup>
import { ConnectionLine, EdgeWrapper } from '../../components'
import { useVueFlow } from '../../composables'
import { groupEdgesByZLevel } from '../../utils'
import type { EdgeComponent, GraphEdge } from '../../types'
import { Slots } from '../../context'
import MarkerDefinitions from './MarkerDefinitions.vue'
const slots = inject(Slots)
const {
onPaneReady,
connectionNodeId,
nodesConnectable,
connectionHandleType,
defaultMarkerColor,
edgesUpdatable,
elementsSelectable,
getSelectedNodes,
getNode,
getNodes,
getEdges,
getEdgeTypes,
} = $(useVueFlow())
const sourceNode = $(
controlledComputed(
() => connectionNodeId,
() => {
if (connectionNodeId) return getNode(connectionNodeId)
return false
},
),
)
const connectionLineVisible = $(
controlledComputed(
() => connectionNodeId,
() =>
!!(
sourceNode &&
(typeof sourceNode.connectable === 'undefined' ? nodesConnectable : sourceNode.connectable) &&
connectionNodeId &&
connectionHandleType
),
),
)
let groups = $ref<ReturnType<typeof groupEdgesByZLevel>>([])
onPaneReady(() => {
watch(
[$$(getSelectedNodes), $$(getEdges), $$(getNodes)],
() => {
nextTick(() => (groups = groupEdgesByZLevel(getEdges, getNode)))
},
{
flush: 'post',
immediate: true,
},
)
})
const getType = (edge: GraphEdge) => {
const name = edge.type || 'default'
let edgeType = edge.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 "${edge.type}" not found and no edge-slot detected. Using fallback type "default".`)
return false
}
return slot
}
</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" :default-color="defaultMarkerColor" />
<g>
<EdgeWrapper
v-for="edge of group.edges"
:id="edge.id"
:key="edge.id"
:edge="edge"
:name="getType(edge) ? edge.type ?? 'default' : 'default'"
:type="getType(edge)"
:selectable="typeof edge.selectable === 'undefined' ? elementsSelectable : edge.selectable"
:updatable="typeof edge.updatable === 'undefined' ? edgesUpdatable : edge.updatable"
/>
<ConnectionLine v-if="connectionLineVisible && !!sourceNode" :source-node="sourceNode" />
</g>
</svg>
</template>
@@ -0,0 +1,53 @@
<script lang="ts" setup>
import type { MarkerProps } from '../../types/edge'
const {
id,
type,
width = 12.5,
height = 12.5,
markerUnits = 'strokeWidth',
orient = 'auto',
strokeWidth = 1,
color = 'none',
} = defineProps<MarkerProps>()
</script>
<script lang="ts">
export default {
name: 'Marker',
}
</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 === '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 === '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,53 @@
<script lang="ts" setup>
import { useVueFlow } from '../../composables'
import type { MarkerProps, MarkerType } from '../../types/edge'
import { getMarkerId } from '../../utils'
import Marker from './Marker.vue'
const { defaultColor = '' } = defineProps<{
defaultColor: string
}>()
const { edges } = $(useVueFlow())
const markers = computed(() => {
const ids: string[] = []
return edges.reduce<MarkerProps[]>((markers, edge) => {
;[edge.markerStart, edge.markerEnd].forEach((marker) => {
if (marker) {
const markerId = getMarkerId(marker)
if (!ids.includes(markerId)) {
if (typeof marker === 'object') markers.push({ ...marker, id: markerId, color: marker.color || defaultColor })
else markers.push({ id: markerId, color: defaultColor, type: marker as MarkerType })
ids.push(markerId)
}
}
})
return markers.sort((a, b) => a.id.localeCompare(b.id))
}, [])
})
</script>
<script lang="ts">
export default {
name: 'MarkerDefinitions',
}
</script>
<template>
<defs>
<Marker
v-for="marker of markers"
:id="marker.id"
:key="marker.id"
:type="marker.type"
:color="marker.color"
:width="marker.width"
:height="marker.height"
:markerUnits="marker.markerUnits"
:stroke-width="marker.strokeWidth"
:orient="marker.orient"
/>
</defs>
</template>
@@ -0,0 +1,73 @@
<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,
} = $(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 hasSnapGrid = (sg?: SnapGrid) => (sg ?? snapToGrid ? snapGrid : undefined)
const getType = (node: GraphNode) => {
const name = node.type || 'default'
let nodeType = node.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 "${node.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">
<NodeWrapper
v-for="node of getNodes"
:id="node.id"
:key="node.id"
:type="getType(node)"
:name="getType(node) ? node.type ?? 'default' : 'default'"
:node="node"
:parent-node="node.parentNode ? getNode(node.parentNode) : undefined"
:draggable="draggable(node.draggable)"
:selectable="selectable(node.selectable)"
:connectable="connectable(node.connectable)"
:snap-grid="hasSnapGrid(node.snapGrid)"
/>
</div>
</template>
@@ -0,0 +1,89 @@
<script lang="ts" setup>
import type { EdgeChange, NodeChange } from '../../types'
import { useKeyPress, useVueFlow } from '../../composables'
import { getConnectedEdges } from '../../utils'
import { NodesSelection, UserSelection } from '../../components'
const {
id,
edges,
deleteKeyCode,
selectionKeyCode,
multiSelectionKeyCode,
emits,
nodesSelectionActive,
userSelectionActive,
elementsSelectable,
resetSelectedElements,
setState,
getSelectedEdges,
getSelectedNodes,
} = $(useVueFlow())
const onClick = (event: MouseEvent) => {
emits.paneClick(event)
setState({
nodesSelectionActive: false,
})
resetSelectedElements()
}
const onContextMenu = (event: MouseEvent) => emits.paneContextMenu(event)
const onWheel = (event: WheelEvent) => emits.paneScroll(event)
useKeyPress($$(deleteKeyCode), (keyPressed) => {
const selectedNodes = getSelectedNodes
const selectedEdges = getSelectedEdges
if (keyPressed && (selectedNodes || selectedEdges)) {
const connectedEdges = (selectedNodes && getConnectedEdges(selectedNodes, edges)) ?? []
const nodeChanges: NodeChange[] = selectedNodes.map((n) => ({ id: n.id, type: 'remove' }))
const edgeChanges: EdgeChange[] = [...selectedEdges, ...connectedEdges].map((e) => ({
id: e.id,
type: 'remove',
}))
emits.nodesChange(nodeChanges)
emits.edgesChange(edgeChanges)
setState({
nodesSelectionActive: false,
})
resetSelectedElements()
}
})
useKeyPress($$(multiSelectionKeyCode), (keyPressed) => {
setState({
multiSelectionActive: keyPressed,
})
})
const selectionKeyPressed = useKeyPress($$(selectionKeyCode), (keyPressed) => {
if (userSelectionActive && keyPressed) return
setState({
userSelectionActive: keyPressed && elementsSelectable,
})
})
</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"
/>
</template>
@@ -0,0 +1,88 @@
<script lang="ts" setup>
import NodeRenderer from '../NodeRenderer/NodeRenderer.vue'
import EdgeRenderer from '../EdgeRenderer/EdgeRenderer.vue'
import { useVueFlow, useWindow, useZoomPanHelper } from '../../composables'
import type { Dimensions, FlowExportObject, FlowInstance, XYPosition } from '../../types'
import { pointToRendererPoint } from '../../utils'
const { id, nodes, edges, viewport, snapToGrid, snapGrid, dimensions, setState, fitViewOnInit, emits } = $(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 () => {
// create new instance and set to state
const { fitView, ...rest } = useZoomPanHelper()
let instance: FlowInstance | null = {
fitView: (params = { padding: 0.1 }) => fitView(params),
...rest,
project(position: XYPosition) {
return pointToRendererPoint(position, viewport, snapToGrid, snapGrid)
},
getElements() {
return [...nodes, ...edges]
},
getNodes() {
return nodes
},
getEdges() {
return edges
},
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,
edges,
position: [viewport.x, viewport.y],
zoom: viewport.zoom,
} as FlowExportObject),
)
},
}
onScopeDispose(() => (instance = null))
// wait until proper dimensions have been established, otherwise fitView will have wrong bounds when called at paneReady
await untilDimensions(dimensions)
// hide graph until dimensions are ready, so we don't have jumping graphs (ssr for example)
ready = true
setState({
instance,
})
fitViewOnInit && instance.fitView()
emits.paneReady(instance)
})
</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 }"
>
<NodeRenderer />
<EdgeRenderer />
<slot />
</div>
</template>
@@ -0,0 +1,196 @@
<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 { FlowTransform } from '../../types'
import { PanOnScrollMode } from '../../types'
import { useKeyPress, useVueFlow } from '../../composables'
import { clamp, clampPosition } 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)
const viewChanged = (prevTransform: FlowTransform, eventTransform: ZoomTransform): boolean =>
(prevTransform.x !== eventTransform.x && !isNaN(eventTransform.x)) ||
(prevTransform.y !== eventTransform.y && !isNaN(eventTransform.y)) ||
(prevTransform.zoom !== eventTransform.k && !isNaN(eventTransform.k))
const eventToFlowTransform = (eventTransform: ZoomTransform): FlowTransform => ({
x: eventTransform.x,
y: eventTransform.y,
zoom: eventTransform.k,
})
const isWrappedWithClass = (event: Event, className: string | undefined) => (event.target as Element).closest(`.${className}`)
const clampedZoom = clamp(defaultZoom, minZoom, maxZoom)
let transform = $ref({
...clampPosition({ x: defaultPosition[0], y: defaultPosition[1] }, translateExtent),
zoom: clampedZoom,
})
const { width, height } = useElementBounding(viewportEl)
watch(
[width, height],
([newWidth, newHeight]) => {
dimensions.width = newWidth
dimensions.height = newHeight
},
{ immediate: true },
)
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 },
})
const selectionKeyPressed = useKeyPress($$(selectionKeyCode), (keyPress) => {
if (keyPress) {
d3Zoom.on('zoom', null)
} else {
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 zoomKeyPressed = useKeyPress($$(zoomActivationKeyCode))
d3Zoom.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => {
const flowTransform = eventToFlowTransform(event.transform)
transform = flowTransform
emits.moveStart({ event, flowTransform })
})
d3Zoom.on('end', (event: D3ZoomEvent<HTMLDivElement, any>) => {
if (viewChanged(transform, event.transform)) {
const flowTransform = eventToFlowTransform(event.transform)
transform = flowTransform
emits.moveEnd({ event, flowTransform })
}
})
d3Selection
.on('wheel', (event: WheelEvent) => {
if (panOnScroll && !zoomKeyPressed.value) {
if (isWrappedWithClass(event, noWheelClassName as any)) return
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 * Math.pow(2, pinchDelta)
if (d3Selection) d3Zoom.scaleTo(d3Selection, zoom, point)
return
}
// increase scroll speed in firefox
// firefox: deltaMode === 1; chrome: deltaMode === 0
const deltaNormalize = event.deltaMode === 1 ? 20 : 1
const deltaX = panOnScrollMode === PanOnScrollMode.Vertical ? 0 : event.deltaX * deltaNormalize
const deltaY = panOnScrollMode === PanOnScrollMode.Horizontal ? 0 : event.deltaY * deltaNormalize
if (d3Selection && panOnScrollSpeed) {
d3Zoom.translateBy(d3Selection, -(deltaX / currentZoom) * panOnScrollSpeed, -(deltaY / currentZoom) * panOnScrollSpeed)
}
} else {
if ((!zoomOnScroll && preventScrolling) || !preventScrolling || isWrappedWithClass(event, noWheelClassName as any)) {
return null
}
event.preventDefault()
}
})
.on('wheel.zoom', panOnScroll || typeof d3ZoomHandler === 'undefined' ? null : (d3ZoomHandler as any))
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) 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,82 @@
<script lang="ts" setup>
import Viewport from '../Viewport/Viewport.vue'
import { createHooks, useHooks } from '../../store'
import { useVueFlow } from '../../composables'
import type { FlowProps } from '../../types/flow'
import { Slots } from '../../context'
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,
})
const emit = defineEmits([...Object.keys(createHooks()), 'update:modelValue', 'update:nodes', 'update:edges'])
const modelValue = useVModel(props, 'modelValue', emit)
const modelNodes = useVModel(props, 'nodes', emit)
const modelEdges = useVModel(props, 'edges', emit)
const { id, hooks, getNodeTypes, getEdgeTypes, $reset, ...rest } = useVueFlow({ id: props.id })
const dispose = useWatch({ modelValue, nodes: modelNodes, edges: modelEdges }, props, {
id,
hooks,
getNodeTypes,
getEdgeTypes,
$reset,
...rest,
})
onUnmounted(() => {
dispose()
$reset()
})
useHooks(emit, hooks.value)
provide(Slots, useSlots())
</script>
<script lang="ts">
export default {
name: 'VueFlow',
}
</script>
<template>
<div 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,194 @@
import type { Ref, ToRefs } from 'vue'
import type { WatchPausableReturn } from '@vueuse/core'
import type { FlowProps, GraphEdge, GraphNode, VueFlowStore } from '~/types'
const isDef = <T>(val: T): val is NonNullable<T> => typeof 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]) => {
const val = [...(n as GraphNode[]), ...(e as GraphEdge[])]
if (val.length) models.modelValue!.value = val
},
{ immediate: true, flush: 'post' },
)
nextTick(() => {
if (pauseStore) pauseStore.resume()
if (pauseModel) pauseModel.resume()
})
}
},
{ immediate: !!(models.modelValue && models.modelValue.value), flush: 'post' },
)
})
}
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), flush: 'post' },
)
})
}
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), flush: 'post' },
)
})
}
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 watchApplyDefault = () => {
scope.run(() => {
watch(
() => props.applyDefault,
() => {
if (props.applyDefault && isDef(props.applyDefault)) {
store.applyDefault.value = props.applyDefault
}
},
{ immediate: isDef(props.applyDefault) },
)
watch(
store.applyDefault,
() => {
if (store.applyDefault.value) {
store.onNodesChange(store.applyNodeChanges)
store.onEdgesChange(store.applyEdgeChanges)
}
},
{ immediate: true },
)
})
}
const watchRest = () => {
const skip = ['id', 'modelValue', 'edges', 'nodes', 'maxZoom', 'minZoom', 'applyDefault']
Object.keys(props).forEach((prop) => {
if (!skip.includes(prop)) {
const model = props[prop as keyof typeof props]
const storedValue = (<any>store)[prop] as Ref
scope.run(() => {
watch(
() => model,
() => {
if (model && isDef(model)) {
storedValue.value = model
}
},
{ immediate: isDef(model) },
)
})
}
})
}
;[watchModelValue, watchNodesValue, watchEdgesValue, watchMinZoom, watchMaxZoom, watchApplyDefault, watchRest].forEach(
(watch) => watch(),
)
})
return () => scope.stop()
}
+7
View File
@@ -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'
+6
View File
@@ -0,0 +1,6 @@
import type { InjectionKey, 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 Slots: InjectionKey<TSlots> = Symbol('slots')
+5
View File
@@ -0,0 +1,5 @@
declare const __VUE_FLOW_VERSION__: string
declare const __ENV__: string
declare interface Window {
chrome?: any
}
+10
View File
@@ -0,0 +1,10 @@
/// <reference types="vite/client" />
/// <reference types="vite-svg-loader" />
/// <reference types="vue/macros-global" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}
+49
View File
@@ -0,0 +1,49 @@
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,
getBezierCenter,
getSimpleBezierPath,
getSimpleBezierCenter,
getSmoothStepPath,
getCenter as getEdgeCenter,
} from './components/Edges/utils'
export {
isNode,
isEdge,
addEdge,
updateEdge,
getOutgoers,
getIncomers,
getConnectedEdges,
getTransformForBounds,
getRectOfNodes,
graphPosToZoomedPos,
getNodesInside,
getMarkerId,
} 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 * from './additional-components'
export * from './types'
+365
View File
@@ -0,0 +1,365 @@
import useState from './state'
import type {
Actions,
ComputedGetters,
Connection,
CoordinateExtent,
Edge,
EdgeChange,
Getters,
GraphEdge,
GraphNode,
Node,
NodeChange,
NodeDimensionChange,
NodePositionChange,
State,
} from '~/types'
import {
applyChanges,
connectionExists,
createPositionChange,
createSelectionChange,
getDimensions,
getEdgeId,
getHandleBounds,
getSelectionChanges,
isEdge,
isGraphEdge,
isGraphNode,
isNode,
isParentSelected,
parseEdge,
parseNode,
} from '~/utils'
const isDef = <T>(val: T): val is NonNullable<T> => typeof val !== 'undefined'
const addEdge = (edgeParams: Edge | Connection, edges: Edge[]) => {
if (!edgeParams.source || !edgeParams.target) {
console.warn("[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
}
const updateEdgeAction = (edge: GraphEdge, newConnection: Connection, edges: GraphEdge[], add: Actions['addEdges']) => {
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
}
edges.splice(edges.indexOf(edge), 1)
const newEdge = {
...edge,
id: getEdgeId(newConnection),
source: newConnection.source,
target: newConnection.target,
sourceHandle: newConnection.sourceHandle,
targetHandle: newConnection.targetHandle,
}
add([newEdge])
return newEdge
}
const createGraphNodes = (nodes: Node[], getNode: Getters['getNode'], currGraphNodes: GraphNode[], extent: CoordinateExtent) => {
const parentNodes: Record<string, true> = {}
const graphNodes = nodes.map((node) => {
const parsed = parseNode(node, extent, {
...getNode(node.id),
parentNode: node.parentNode,
})
if (node.parentNode) {
parentNodes[node.parentNode] = true
}
return parsed
})
graphNodes.forEach((node) => {
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
}
export default (state: State, getters: ComputedGetters): Actions => {
const updateNodePosition: Actions['updateNodePosition'] = ({ id, diff = { x: 0, y: 0 }, dragging }) => {
const nodePosPromise = new Promise<NodePositionChange[]>((resolve) => {
const changes: NodePositionChange[] = []
const curr = id ? getters.getNode.value(id)! : undefined
if (curr) {
changes.push(createPositionChange({ node: curr, diff, nodeExtent: state.nodeExtent, dragging }, getters.getNode.value))
} else {
getters.getSelectedNodes.value.forEach((node) => {
if (!node.parentNode) {
changes.push(createPositionChange({ node, diff, nodeExtent: state.nodeExtent, dragging }, getters.getNode.value))
} else if (!isParentSelected(node, getters.getNode.value)) {
changes.push(createPositionChange({ node, diff, nodeExtent: state.nodeExtent, dragging }, getters.getNode.value))
}
})
}
if (changes.length) resolve(changes)
})
nodePosPromise.then((changes) => {
state.hooks.nodesChange.trigger(changes)
})
}
const updateNodeDimensions: Actions['updateNodeDimensions'] = (updates) => {
const changes: NodeDimensionChange[] = updates.reduce<NodeDimensionChange[]>((res, update) => {
const node = getters.getNode.value(update.id)
if (node) {
const dimensions = getDimensions(update.nodeElement)
const doUpdate = !!(
dimensions.width &&
dimensions.height &&
(node.dimensions.width !== dimensions.width || node.dimensions.height !== dimensions.height || update.forceUpdate)
)
node.handleBounds = getHandleBounds(update.nodeElement, state.viewport.zoom)
if (doUpdate) {
node.dimensions = dimensions
res.push({
id: node.id,
type: 'dimensions',
dimensions,
})
}
}
return res
}, [])
if (changes.length) state.hooks.nodesChange.trigger(changes)
}
const addSelectedNodes: Actions['addSelectedNodes'] = (nodes) => {
const selectedNodesIds = nodes.map((n) => n.id)
let changedNodes: NodeChange[]
if (state.multiSelectionActive) changedNodes = selectedNodesIds.map((nodeId) => createSelectionChange(nodeId, true))
else changedNodes = getSelectionChanges(state.nodes, selectedNodesIds, getters.getNode.value)
if (changedNodes.length) state.hooks.nodesChange.trigger(changedNodes)
}
const addSelectedEdges: Actions['addSelectedEdges'] = (edges) => {
const selectedEdgesIds = edges.map((e) => e.id)
let changedEdges: EdgeChange[]
if (state.multiSelectionActive) changedEdges = selectedEdgesIds.map((nodeId) => createSelectionChange(nodeId, true))
else changedEdges = getSelectionChanges(state.edges, selectedEdgesIds, getters.getNode.value)
if (changedEdges.length) state.hooks.edgesChange.trigger(changedEdges)
}
const addSelectedElements: Actions['addSelectedElements'] = (elements) => {
addSelectedNodes(elements.filter(isGraphNode))
addSelectedEdges(elements.filter(isGraphEdge))
}
const setMinZoom: Actions['setMinZoom'] = (minZoom: any) => {
state.d3Zoom?.scaleExtent([minZoom, state.maxZoom])
state.minZoom = minZoom
}
const setMaxZoom: Actions['setMaxZoom'] = (maxZoom: any) => {
state.d3Zoom?.scaleExtent([state.minZoom, maxZoom])
state.maxZoom = maxZoom
}
const setTranslateExtent: Actions['setTranslateExtent'] = (translateExtent: any) => {
state.d3Zoom?.translateExtent(translateExtent)
state.translateExtent = translateExtent
}
const resetSelectedElements: Actions['resetSelectedElements'] = () => {
addSelectedNodes([])
addSelectedEdges([])
}
const setInteractive: Actions['setInteractive'] = (isInteractive) => {
state.nodesDraggable = isInteractive
state.nodesConnectable = isInteractive
state.elementsSelectable = isInteractive
}
const setNodes: Actions['setNodes'] = (nodes, extent?: CoordinateExtent) => {
if (!state.initialized && !nodes.length) return
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(`[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 (missingSource || missingTarget) return res
const storedEdge = getters.getEdge.value(edge.id)
res.push({
...parseEdge(edge, {
...state.defaultEdgeOptions,
...storedEdge,
}),
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
state.nodes = [...state.nodes, ...createGraphNodes(curr, getters.getNode.value, state.nodes, extent ?? state.nodeExtent)]
}
const addEdges: Actions['addEdges'] = (params) => {
const curr = params instanceof Function ? params(state.edges) : params
curr.reduce<GraphEdge[]>((acc, param) => {
const edge = addEdge(
{
...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({
...edge,
sourceNode,
targetNode,
})
}
return acc
}, state.edges)
}
const updateEdge: Actions['updateEdge'] = (oldEdge, newConnection) =>
updateEdgeAction(oldEdge, newConnection, state.edges, addEdges)
const applyNodeChanges: Actions['applyNodeChanges'] = (changes) => applyChanges(changes, state.nodes, addNodes)
const applyEdgeChanges: Actions['applyEdgeChanges'] = (changes) => applyChanges(changes, state.edges, addEdges)
const setState: Actions['setState'] = (options) => {
const skip = ['modelValue', 'nodes', 'edges', 'maxZoom', 'minZoom', 'translateExtent']
const opts = options instanceof Function ? options(state) : options
if (typeof opts.modelValue !== 'undefined') setElements(opts.modelValue, opts.nodeExtent ?? state.nodeExtent)
if (typeof opts.nodes !== 'undefined') setNodes(opts.nodes, opts.nodeExtent ?? state.nodeExtent)
if (typeof opts.edges !== 'undefined') setEdges(opts.edges)
Object.keys(opts).forEach((o) => {
const option = opts[o as keyof typeof opts]
if (!skip.includes(o) && isDef(option)) (<any>state)[o] = option
})
if (!state.d3Zoom)
until(() => state.d3Zoom)
.not.toBeUndefined()
.then(() => {
if (typeof opts.maxZoom !== 'undefined') setMaxZoom(opts.maxZoom)
if (typeof opts.minZoom !== 'undefined') setMinZoom(opts.minZoom)
if (typeof opts.translateExtent !== 'undefined') setTranslateExtent(opts.translateExtent)
})
else {
if (typeof opts.maxZoom !== 'undefined') setMaxZoom(opts.maxZoom)
if (typeof opts.minZoom !== 'undefined') setMinZoom(opts.minZoom)
if (typeof opts.translateExtent !== 'undefined') setTranslateExtent(opts.translateExtent)
}
if (!state.initialized) state.initialized = true
}
return {
updateNodePosition,
updateNodeDimensions,
setElements,
setNodes,
setEdges,
addNodes,
addEdges,
updateEdge,
applyEdgeChanges,
applyNodeChanges,
addSelectedElements,
addSelectedNodes,
addSelectedEdges,
setMinZoom,
setMaxZoom,
setTranslateExtent,
resetSelectedElements,
setInteractive,
setState,
$reset: () => {
setState(useState())
},
}
}
+113
View File
@@ -0,0 +1,113 @@
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) => state.nodes[nodeIds.value.indexOf(id)])
const getEdge: ComputedGetters['getEdge'] = computed(() => (id: string) => 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) => {
const source = getNode.value(e.source)
const target = getNode.value(e.target)
if (!source || !target) {
console.warn(`[vue-flow]: Orphaned edge ${e.id} will be removed.`)
state.edges.splice(state.edges.indexOf(e), 1)
return true
}
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(edgeHidden)
return state.edges.filter((e) => {
const source = getNode.value(e.source)!
const target = getNode.value(e.target)!
return (
edgeHidden(e) &&
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 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,
getEdgeTypes,
getNodeTypes,
getEdges,
getNodes,
getSelectedElements,
getSelectedNodes,
getSelectedEdges,
}
}
+58
View File
@@ -0,0 +1,58 @@
import type { EmitFunc, 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(),
connect: createEventHook(),
connectStart: createEventHook(),
connectStop: createEventHook(),
connectEnd: createEventHook(),
paneReady: createEventHook(),
move: createEventHook(),
moveStart: createEventHook(),
moveEnd: createEventHook(),
selectionDragStart: createEventHook(),
selectionDrag: createEventHook(),
selectionDragStop: createEventHook(),
selectionContextMenu: createEventHook(),
paneScroll: createEventHook(),
paneClick: createEventHook(),
paneContextMenu: createEventHook(),
edgeContextMenu: createEventHook(),
edgeMouseEnter: createEventHook(),
edgeMouseMove: createEventHook(),
edgeMouseLeave: createEventHook(),
edgeDoubleClick: createEventHook(),
edgeClick: createEventHook(),
edgeUpdateStart: createEventHook(),
edgeUpdate: createEventHook(),
edgeUpdateEnd: createEventHook(),
})
const bind = (emit: EmitFunc, hooks: FlowHooks) => {
for (const [key, value] of Object.entries(hooks)) {
const listener = (data: any) => {
emit(key as keyof FlowHooks, data)
}
value.on(listener)
onScopeDispose(() => {
value.off(listener)
})
}
}
export default (emit: EmitFunc, hooks: FlowHooks) => bind(emit, hooks)
+4
View File
@@ -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'
+126
View File
@@ -0,0 +1,126 @@
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 => ({
nodes: [],
edges: [],
nodeTypes: {},
edgeTypes: {},
initialized: false,
instance: null,
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,
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
defaultMarkerColor: '#b1b1b7',
connectionLineStyle: {},
connectionLineType: ConnectionLineType.Bezier,
connectionNodeId: null,
connectionHandleId: null,
connectionHandleType: null,
connectionPosition: { x: NaN, y: NaN },
connectionMode: ConnectionMode.Loose,
connectionStartHandle: null,
connectOnClick: true,
snapGrid: [15, 15],
snapToGrid: false,
edgesUpdatable: false,
nodesConnectable: true,
nodesDraggable: true,
elementsSelectable: true,
selectNodesOnDrag: true,
multiSelectionActive: false,
selectionKeyCode: 'Shift',
multiSelectionKeyCode: 'Meta',
zoomActivationKeyCode: 'Meta',
deleteKeyCode: 'Backspace',
hooks: createHooks(),
applyDefault: true,
fitViewOnInit: false,
noDragClassName: 'nodrag',
noWheelClassName: 'nowheel',
noPanClassName: 'nopan',
defaultEdgeOptions: undefined,
vueFlowVersion: typeof __VUE_FLOW_VERSION__ !== 'undefined' ? __VUE_FLOW_VERSION__ : '-',
})
export default (opts?: FlowOptions): State => {
const state = defaultState()
if (opts) {
Object.keys(opts).forEach((o) => {
const option = opts[o as keyof typeof opts]
if (isDef(option)) (state as any)[o] = option
})
}
return state
}
+171
View File
@@ -0,0 +1,171 @@
.vue-flow {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
.vue-flow__container {
position: absolute;
height: 100%;
width: 100%;
left: 0;
top: 0;
}
.vue-flow__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__connection {
pointer-events: none;
.animated {
stroke-dasharray: 5;
animation: dashdraw 0.5s linear infinite;
}
}
.vue-flow__connection-path {
fill: none;
}
.vue-flow__nodes {
pointer-events: none;
transform-origin: 0 0;
}
.vue-flow__node {
position: absolute;
user-select: none;
pointer-events: all;
transform-origin: 0 0;
box-sizing: border-box;
}
.vue-flow__nodesselection {
z-index: 3;
transform-origin: left top;
pointer-events: none;
&-rect {
position: absolute;
pointer-events: all;
cursor: grab;
}
}
.vue-flow__handle {
pointer-events: none;
&.connectable {
pointer-events: all;
}
}
.vue-flow__handle-bottom {
top: auto;
left: 50%;
bottom: -4px;
transform: translate(-50%, 0);
}
.vue-flow__handle-top {
left: 50%;
top: -4px;
transform: translate(-50%, 0);
}
.vue-flow__handle-left {
top: 50%;
left: -4px;
transform: translate(0, -50%);
}
.vue-flow__handle-right {
right: -4px;
top: 50%;
transform: translate(0, -50%);
}
.vue-flow__edgeupdater {
cursor: move;
pointer-events: all;
}
/* additional components */
.vue-flow__controls {
position: absolute;
z-index: 5;
bottom: 10px;
left: 10px;
&-button {
width: 24px;
height: 24px;
border: none;
svg {
width: 100%;
}
}
}
.vue-flow__minimap {
position: absolute;
z-index: 5;
bottom: 10px;
right: 10px;
}
+158
View File
@@ -0,0 +1,158 @@
: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 {
position: absolute;
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;
}
}
}
+59
View File
@@ -0,0 +1,59 @@
import type { Dimensions, ElementData, XYPosition } from './flow'
import type { Node, NodeHandleBounds } from './node'
import type { Edge } from './edge'
export interface NodeDimensionChange {
id: string
type: 'dimensions'
dimensions: Dimensions
handleBounds?: NodeHandleBounds
}
export interface NodePositionChange {
id: string
type: 'position'
position?: 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: Node<Data>
type: 'add'
}
export interface NodeResetChange<Data = ElementData> {
item: Node<Data>
type: 'reset'
}
export type NodeChange =
| NodeDimensionChange
| NodePositionChange
| NodeSelectionChange
| NodeRemoveChange
| NodeAddChange
| NodeResetChange
export type EdgeSelectionChange = NodeSelectionChange
export type EdgeRemoveChange = NodeRemoveChange
export interface EdgeAddChange<Data = ElementData> {
item: Edge<Data>
type: 'add'
}
export interface EdgeResetChange<Data = ElementData> {
item: Edge<Data>
type: 'reset'
}
export type EdgeChange = EdgeSelectionChange | EdgeRemoveChange | EdgeAddChange | EdgeResetChange
export type ElementChange = NodeChange | EdgeChange
+105
View File
@@ -0,0 +1,105 @@
import type { CSSProperties, Component, DefineComponent, HTMLAttributes, VNode } from 'vue'
import type { BackgroundVariant, Dimensions, ElementData, XYPosition } from './flow'
import type { GraphNode, NodeProps } from './node'
import type { EdgeProps } from './edge'
import type { FitViewParams } from './zoom'
/** Global component names are components registered to the vue instance and are "autoloaded" by their string name */
type GlobalComponentName = string
/** Node Components can either be a component definition or a string name */
export type NodeComponent<Data = ElementData> =
| Component<NodeProps<Data>>
| DefineComponent<NodeProps<Data>, any, any, any, any>
| GlobalComponentName
/** Edge Components can either be a component definition or a string name */
export type EdgeComponent<Data = ElementData> =
| Component<EdgeProps<Data>>
| DefineComponent<EdgeProps<Data>, any, any, any, any, any>
| GlobalComponentName
export type DefaultEdgeTypes = { [key in 'default' | 'straight' | 'smoothstep' | 'step' | 'simplebezier']: EdgeComponent }
export type DefaultNodeTypes = { [key in 'input' | 'output' | 'default']: NodeComponent }
export interface BackgroundProps {
/** The background pattern variant, {@link BackgroundVariant} */
variant?: BackgroundVariant
/** Background pattern gap */
gap?: number
/** Background pattern size */
size?: number
/** Background pattern color */
patternColor?: string
/** Background color */
bgColor?: string
/** Background height */
height?: number
/** Background width */
width?: number
/** Background x-coordinate (offset x) */
x?: number
/** Background y-coordinate (offset y) */
y?: number
}
export interface ControlProps {
/** Show the zoom icon */
showZoom?: boolean
/** Show the fit-view icon */
showFitView?: boolean
/** Show the interactive icon */
showInteractive?: boolean
/** Params to use on fitView */
fitViewParams?: FitViewParams
}
/** expects a node and returns a color value */
export type MiniMapNodeFunc<Data = ElementData> = (node: GraphNode<Data>) => string
// hack for vue-type imports
type MiniMapNodeFunc2<Data = ElementData> = (node: GraphNode<Data>) => string
type MiniMapNodeFunc3<Data = ElementData> = (node: GraphNode<Data>) => string
export type ShapeRendering = CSSProperties['shapeRendering']
export interface MiniMapProps<Data = ElementData> {
/** Node color, can be either a string or a string func that receives the current node */
nodeColor?: string | MiniMapNodeFunc<Data>
/** Node stroke color, can be either a string or a string func that receives the current node */
nodeStrokeColor?: string | MiniMapNodeFunc2<Data>
/** Additional node class name, can be either a string or a string func that receives the current node */
nodeClassName?: string | MiniMapNodeFunc3<Data>
/** Node border radius */
nodeBorderRadius?: number
/** Node stroke width */
nodeStrokeWidth?: number
/** Background color of minimap */
maskColor?: string
}
/** these props are passed to mini map node slots */
export interface MiniMapNodeProps {
id: string
parentNode?: string
selected?: boolean
dragging?: boolean
position: XYPosition
dimensions: Dimensions
borderRadius?: number
color?: string
shapeRendering?: ShapeRendering
strokeColor?: string
strokeWidth?: number
}
/** these props are passed to edge texts */
export interface EdgeTextProps extends HTMLAttributes {
x: number
y: number
label?: string | VNode | Object
labelStyle?: CSSProperties
labelShowBg?: boolean
labelBgStyle?: CSSProperties
labelBgPadding?: [number, number]
labelBgBorderRadius?: number
}
+65
View File
@@ -0,0 +1,65 @@
import type { CSSProperties } from 'vue'
import type { Position } from './flow'
import type { GraphNode } from './node'
import type { HandleElement, HandleType } from './handle'
/** Connection line types (same as default edge types */
export enum ConnectionLineType {
Bezier = 'default',
Straight = 'straight',
Step = 'step',
SmoothStep = 'smoothstep',
}
/** Connection params that are passed when onConnect is called */
export interface Connection {
/** Source node id */
source: string
/** Target node id */
target: string
/** Source handle id */
sourceHandle: string | null
/** Target handle id */
targetHandle: string | null
}
/** 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
}
+168
View File
@@ -0,0 +1,168 @@
import type { CSSProperties, Component, VNode } from 'vue'
import type { BaseElement, ElementData, Position } from './flow'
import type { GraphNode } from './node'
import type { DefaultEdgeTypes, EdgeComponent, EdgeTextProps } from './components'
/** Edge markers */
export enum MarkerType {
Arrow = 'arrow',
ArrowClosed = 'arrowclosed',
}
/** Edge marker definition */
export interface EdgeMarker {
/** Unique marker id */
id?: string
/** Marker type */
type: MarkerType
/** Marker color */
color?: string
/** Marker width */
width?: number
/** Marker height */
height?: number
/** Marker units */
markerUnits?: string
/** Marker orientation */
orient?: string
/** Marker stroke width */
strokeWidth?: number
}
export interface MarkerProps {
id: string
type: MarkerType | string
color?: string
width?: number
height?: number
markerUnits?: string
orient?: string
strokeWidth?: number
}
export type EdgeMarkerType = string | MarkerType | EdgeMarker
export interface Edge<Data = ElementData> extends BaseElement<Data> {
label?: string | VNode | Component<EdgeTextProps>
/** node 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
/** Source position */
sourcePosition?: Position
/** Target position */
targetPosition?: Position
/** Label styles (CSSProperties) */
labelStyle?: CSSProperties
/** Show label bg */
labelShowBg?: boolean
/** Label Bg styles (CSSProperties) */
labelBgStyle?: CSSProperties
/** Label Bg padding */
labelBgPadding?: [number, number]
/** Label Bg border radius */
labelBgBorderRadius?: number
/** Animated edge */
animated?: boolean
/** EdgeMarker */
markerStart?: EdgeMarkerType
/** EdgeMarker */
markerEnd?: EdgeMarkerType
/** Disable/enable updating edge */
updatable?: boolean
/** Disable/enable selecting edge */
selectable?: boolean
/** overwrites current edge type */
template?: EdgeComponent
}
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> = Edge<Data> & {
selected?: boolean
z?: number
sourceNode: GraphNode
targetNode: GraphNode
} & EdgePositions
/** these props are passed to edge components */
export interface EdgeProps<Data = ElementData> {
id: string
sourceNode: GraphNode
targetNode: GraphNode
label?: string | VNode | Component<EdgeTextProps> | Object
type?: string
data?: Data
style?: CSSProperties
sourceX: number
sourceY: number
targetX: number
targetY: number
selected?: boolean
sourcePosition: Position
targetPosition: Position
sourceHandleId?: string
targetHandleId?: string
source: string
target: string
labelStyle?: CSSProperties
labelShowBg?: boolean
labelBgStyle?: any
labelBgPadding?: [number, number]
labelBgBorderRadius?: number
animated?: boolean
updatable?: boolean
markerStart?: string
markerEnd?: string
curvature?: number
}
/** these props are passed to smooth step edges */
export interface SmoothStepEdgeProps<Data = ElementData> extends EdgeProps<Data> {
id: string
sourceNode: GraphNode
targetNode: GraphNode
label?: string | VNode | Component<EdgeTextProps> | Object
type?: string
data?: Data
style?: CSSProperties
sourceX: number
sourceY: number
targetX: number
targetY: number
selected?: boolean
sourcePosition: Position
targetPosition: Position
sourceHandleId?: string
targetHandleId?: string
source: string
target: string
labelStyle?: any
labelShowBg?: boolean
labelBgStyle?: any
labelBgPadding?: [number, number]
labelBgBorderRadius?: number
animated?: boolean
updatable?: boolean
markerStart?: string
markerEnd?: string
borderRadius?: number
}
+150
View File
@@ -0,0 +1,150 @@
import type { CSSProperties, Component, VNode } from 'vue'
import type { DefaultEdgeOptions, Edge, GraphEdge } from './edge'
import type { CoordinateExtent, GraphNode, Node } from './node'
import type { ConnectionLineType, ConnectionMode } from './connection'
import type { KeyCode, PanOnScrollMode, ViewportFuncs } from './zoom'
import type { DefaultEdgeTypes, DefaultNodeTypes, EdgeComponent, NodeComponent } from './components'
export type ElementData = any
/** an internal element */
export type FlowElement<Data = ElementData> = GraphNode<Data> | GraphEdge<Data>
export type FlowElements<Data = ElementData> = FlowElement<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<Data = ElementData> = (element: FlowElement<Data>) => string | void
export type StyleFunc<Data = ElementData> = (element: FlowElement<Data>) => Styles | void
/** base element props */
export interface BaseElement<Data extends ElementData = ElementData> {
id: string
label?: string | VNode | Component
type?: string
data?: Data
class?: string | ClassFunc<Data>
style?: Styles | StyleFunc<Data>
hidden?: boolean
}
export type Element<Data = ElementData> = Node<Data> | Edge<Data>
export type Elements<Data = ElementData> = Element<Data>[]
/** Handle Positions */
export enum Position {
Left = 'left',
Top = 'top',
Right = 'right',
Bottom = 'bottom',
}
export interface XYPosition {
x: number
y: number
}
export type XYZPosition = XYPosition & { z: number }
export interface Dimensions {
width: number
height: number
}
export interface Box extends XYPosition {
x2: number
y2: number
}
export interface Rect extends Dimensions, XYPosition {}
export type SnapGrid = [number, number]
export enum BackgroundVariant {
Lines = 'lines',
Dots = 'dots',
}
export interface SelectionRect extends Rect {
startX: number
startY: number
draw: boolean
}
export interface FlowExportObject {
nodes: GraphNode[]
edges: GraphEdge[]
position: [number, number]
zoom: number
}
interface Exports {
getElements: () => FlowElements
getNodes: () => GraphNode[]
getEdges: () => GraphEdge[]
toObject: () => FlowExportObject
}
export type FlowInstance = Exports & ViewportFuncs
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?: { [key in keyof DefaultEdgeTypes]?: EdgeComponent } & Record<string, EdgeComponent>
/** either use the nodeTypes prop to define your node-types or use slots (<template #node-mySpecialType="props">) */
nodeTypes?: { [key in keyof DefaultNodeTypes]?: NodeComponent } & Record<string, NodeComponent>
connectionMode?: ConnectionMode
connectionLineType?: ConnectionLineType
connectionLineStyle?: CSSProperties | null
deleteKeyCode?: KeyCode
selectionKeyCode?: KeyCode
multiSelectionKeyCode?: KeyCode
zoomActivationKeyCode?: KeyCode
snapToGrid?: boolean
snapGrid?: SnapGrid
onlyRenderVisibleElements?: boolean
edgesUpdatable?: boolean
nodesDraggable?: boolean
nodesConnectable?: boolean
elementsSelectable?: boolean
selectNodesOnDrag?: boolean
/** move pane on drag, replaced prop `paneMovable` */
panOnDrag?: boolean
minZoom?: number
maxZoom?: number
defaultZoom?: number
defaultPosition?: [number, number]
translateExtent?: CoordinateExtent
nodeExtent?: CoordinateExtent
defaultMarkerColor?: string
zoomOnScroll?: boolean
zoomOnPinch?: boolean
panOnScroll?: boolean
panOnScrollSpeed?: number
panOnScrollMode?: PanOnScrollMode
zoomOnDoubleClick?: boolean
/** enable this to prevent vue flow from scrolling inside the container, i.e. allow for the page to scroll */
preventScrolling?: boolean
edgeUpdaterRadius?: number
fitViewOnInit?: boolean
/** allow connection with click handlers, i.e. support touch devices */
connectOnClick?: boolean
/** apply default change handlers for position, dimensions, adding/removing nodes. set this to false if you want to apply the changes manually */
applyDefault?: boolean
noDragClassName?: string
noWheelClassName?: string
noPanClassName?: string
/** does not work for the `addEdge` utility! */
defaultEdgeOptions?: DefaultEdgeOptions
}
export type FlowOptions = FlowProps
+36
View File
@@ -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
}
+65
View File
@@ -0,0 +1,65 @@
import type { EventHook, EventHookOn, EventHookTrigger } from '@vueuse/core'
import type { MouseTouchEvent } from '@braks/revue-draggable'
import type { D3ZoomEvent } from 'd3-zoom'
import type { FlowInstance } from './flow'
import type { GraphEdge } from './edge'
import type { GraphNode } from './node'
import type { Connection, OnConnectStartParams } from './connection'
import type { FlowTransform } from './zoom'
import type { EdgeChange, NodeChange } from './changes'
export interface FlowEvents {
nodesChange: NodeChange[]
edgesChange: EdgeChange[]
nodeDoubleClick: { event: MouseTouchEvent; node: GraphNode }
nodeClick: { event: MouseTouchEvent; node: GraphNode }
nodeMouseEnter: { event: MouseEvent; node: GraphNode }
nodeMouseMove: { event: MouseEvent; node: GraphNode }
nodeMouseLeave: { event: MouseEvent; node: GraphNode }
nodeContextMenu: { event: MouseEvent; node: GraphNode }
nodeDragStart: { event: MouseTouchEvent; node: GraphNode }
nodeDrag: { event: MouseTouchEvent; node: GraphNode }
nodeDragStop: { event: MouseTouchEvent; node: GraphNode }
miniMapNodeClick: { event: MouseTouchEvent; node: GraphNode }
miniMapNodeDoubleClick: { event: MouseTouchEvent; node: GraphNode }
connect: Connection
connectStart: {
event: MouseEvent
} & OnConnectStartParams
connectStop: MouseEvent
connectEnd: MouseEvent
paneReady: FlowInstance
move: { event: D3ZoomEvent<HTMLDivElement, any>; flowTransform: FlowTransform }
moveStart: { event: D3ZoomEvent<HTMLDivElement, any>; flowTransform: FlowTransform }
moveEnd: { event: D3ZoomEvent<HTMLDivElement, any>; flowTransform: FlowTransform }
selectionDragStart: { event: MouseTouchEvent; nodes: GraphNode[] }
selectionDrag: { event: MouseTouchEvent; nodes: GraphNode[] }
selectionDragStop: { event: MouseTouchEvent; nodes: GraphNode[] }
selectionContextMenu: { event: MouseEvent; nodes: GraphNode[] }
paneScroll: WheelEvent | undefined
paneClick: MouseEvent
paneContextMenu: MouseEvent
edgeContextMenu: { event: MouseEvent; edge: GraphEdge }
edgeMouseEnter: { event: MouseEvent; edge: GraphEdge }
edgeMouseMove: { event: MouseEvent; edge: GraphEdge }
edgeMouseLeave: { event: MouseEvent; edge: GraphEdge }
edgeDoubleClick: { event: MouseEvent; edge: GraphEdge }
edgeClick: { event: MouseEvent; edge: GraphEdge }
edgeUpdateStart: { event: MouseEvent; edge: GraphEdge }
edgeUpdate: { edge: GraphEdge; connection: Connection }
edgeUpdateEnd: { event: MouseEvent; edge: GraphEdge }
}
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]>
}>
export type EmitFunc = (name: keyof FlowHooks, ...args: FlowEvents[keyof FlowEvents][]) => void
+10
View File
@@ -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'
+117
View File
@@ -0,0 +1,117 @@
import type { Component, VNode } from 'vue'
import type { BaseElement, Dimensions, ElementData, Position, SnapGrid, XYPosition, XYZPosition } from './flow'
import type { DefaultNodeTypes, NodeComponent } from './components'
import type { HandleElement, ValidConnectionFunc } from './handle'
/** Defined as [[x-from, y-from], [x-to, y-to]] **/
export type CoordinateExtent = [[number, number], [number, number]]
export interface NodeHandleBounds {
source?: HandleElement[]
target?: HandleElement[]
}
// eslint-disable-next-line no-use-before-define
type WidthFunc = <Data = ElementData>(node: GraphNode<Data>) => number | string | void
// eslint-disable-next-line no-use-before-define
type HeightFunc = <Data = ElementData>(node: GraphNode<Data>) => number | string | void
export interface Node<Data = ElementData> extends BaseElement<Data> {
/** 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
/** overwrites current node type */
template?: NodeComponent
}
export interface GraphNode<Data = ElementData> extends Node<Data> {
/** absolute position in relation to parent elements + z-index */
computedPosition: XYZPosition
handleBounds: NodeHandleBounds
/** node width, height */
dimensions: Dimensions
isParent: boolean
selected: boolean
dragging: boolean
}
/** these props are passed to node components */
export interface NodeProps<Data = ElementData> {
/** unique node id */
id: string
/** node type */
type: keyof DefaultNodeTypes | string
/** additional data of node */
data?: Data
/** is node selected */
selected: boolean
/** can node be connected */
connectable: boolean
/** absolute position in relation to parent elements + z-index */
computedPosition: XYZPosition
/** 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
/** node DOM-element */
nodeElement: HTMLDivElement
}
+212
View File
@@ -0,0 +1,212 @@
import type { CSSProperties, ComputedRef, ToRefs } from 'vue'
import type {
Dimensions,
ElementData,
Elements,
FlowElements,
FlowInstance,
FlowOptions,
Rect,
SnapGrid,
XYPosition,
} from './flow'
import type { DefaultEdgeTypes, DefaultNodeTypes, EdgeComponent, NodeComponent } from './components'
import type { Connection, ConnectionLineType, ConnectionMode } from './connection'
import type { DefaultEdgeOptions, Edge, GraphEdge } from './edge'
import type { CoordinateExtent, GraphNode, Node } from './node'
import type { D3Selection, D3Zoom, D3ZoomHandler, KeyCode, PanOnScrollMode, Viewport } from './zoom'
import type { FlowHooks, FlowHooksEmit, FlowHooksOn } from './hooks'
import type { EdgeChange, NodeChange } from './changes'
import type { HandleType, StartHandle } from './handle'
export interface UpdateNodeDimensionsParams {
id: string
nodeElement: HTMLDivElement
forceUpdate?: boolean
}
export interface UpdateNodePositionsParams {
id?: string
diff?: XYPosition
dragging?: boolean
}
export interface State extends Omit<FlowOptions, 'id' | 'modelValue'> {
/** Event hooks, you can manipulate the triggers at your own peril */
readonly hooks: FlowHooks
readonly instance: FlowInstance | null
/** 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]
selectedNodesBbox: Rect
nodesSelectionActive: boolean
userSelectionActive: boolean
multiSelectionActive: boolean
deleteKeyCode: KeyCode
selectionKeyCode: KeyCode
multiSelectionKeyCode: KeyCode
zoomActivationKeyCode: KeyCode
connectionNodeId: string | null
connectionHandleId: string | null
connectionHandleType: HandleType | null
connectionPosition: XYPosition
connectionMode: ConnectionMode
connectionLineType: ConnectionLineType
connectionLineStyle: CSSProperties | null
connectionStartHandle: StartHandle | null
connectOnClick: boolean
edgeUpdaterRadius: number
snapToGrid: boolean
snapGrid: SnapGrid
defaultMarkerColor: string
edgesUpdatable: boolean
nodesDraggable: boolean
nodesConnectable: boolean
elementsSelectable: boolean
selectNodesOnDrag: boolean
panOnDrag: boolean
zoomOnScroll: boolean
zoomOnPinch: boolean
panOnScroll: boolean
panOnScrollSpeed: number
panOnScrollMode: PanOnScrollMode
zoomOnDoubleClick: boolean
preventScrolling: boolean
initialized: boolean
applyDefault: boolean
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 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 = ({ id, diff, dragging }: UpdateNodePositionsParams) => void
export type UpdateNodeDimensions = (updates: UpdateNodeDimensionsParams[]) => void
export interface Actions {
/** 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
/** 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
/** unselect all selected elements */
resetSelectedElements: () => 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
/** enable/disable node interaction (dragging, selecting etc) */
setInteractive: (isInteractive: boolean) => void
/** set new state */
setState: SetState
/** internal position updater, you probably don't want to use this */
updateNodePosition: UpdateNodePosition
/** internal dimensions' updater, you probably don't want to use this */
updateNodeDimensions: UpdateNodeDimensions
/** reset state to defaults */
$reset: () => 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>
/** filters hidden nodes */
getNodes: GraphNode[]
/** filters hidden edges */
getEdges: GraphEdge[]
/** returns a node by id */
getNode: <Data = ElementData>(id: string) => GraphNode<Data> | undefined
/** returns an edge by id */
getEdge: <Data = ElementData>(id: string) => GraphEdge<Data> | 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>
+72
View File
@@ -0,0 +1,72 @@
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 type KeyCode = number | string
export enum PanOnScrollMode {
Free = 'free',
Vertical = 'vertical',
Horizontal = 'horizontal',
}
export interface ViewportFuncsOptions {
duration?: number
}
export type FitViewParams = {
padding?: number
includeHiddenNodes?: boolean
minZoom?: number
maxZoom?: number
offset?: {
x?: number
y?: number
}
nodes?: string[]
} & ViewportFuncsOptions
export interface FlowTransform {
x: number
y: number
zoom: number
}
export type SetCenterOptions = ViewportFuncsOptions & {
zoom?: number
}
export type FitBoundsOptions = ViewportFuncsOptions & {
padding?: number
}
export type FitView = (fitViewOptions?: FitViewParams) => void
export type Project = (position: XYPosition) => XYPosition
export type SetCenter = (x: number, y: number, options?: SetCenterOptions) => void
export type FitBounds = (bounds: Rect, options?: FitBoundsOptions) => void
export type ZoomInOut = (options?: ViewportFuncsOptions) => void
export type ZoomTo = (zoomLevel: number, options?: ViewportFuncsOptions) => void
export type GetTransform = () => FlowTransform
export type SetTransform = (transform: FlowTransform, options?: ViewportFuncsOptions) => void
export interface ViewportFuncs {
zoomIn: ZoomInOut
zoomOut: ZoomInOut
zoomTo: ZoomTo
setTransform: SetTransform
getTransform: GetTransform
fitView: FitView
setCenter: SetCenter
fitBounds: FitBounds
project: Project
}
+200
View File
@@ -0,0 +1,200 @@
import { clampPosition, isGraphEdge, isGraphNode } from './graph'
import type {
CoordinateExtent,
Edge,
EdgeChange,
EdgeSelectionChange,
ElementChange,
FlowElement,
FlowElements,
Getters,
GraphEdge,
GraphNode,
Node,
NodeChange,
NodePositionChange,
NodeSelectionChange,
XYPosition,
} from '~/types'
interface CreatePositionChangeParams {
node: GraphNode
nodeExtent: CoordinateExtent
diff?: XYPosition
dragging?: boolean
}
function handleParentExpand(updateItem: GraphNode, curr: GraphNode[]) {
const parent = updateItem.parentNode ? curr.find((el) => el.id === updateItem.parentNode) : undefined
if (parent) {
const extendWidth = updateItem.position.x + updateItem.dimensions.width - parent.dimensions.width
const extendHeight = updateItem.position.y + updateItem.dimensions.height - parent.dimensions.height
if (extendWidth > 0 || extendHeight > 0 || updateItem.position.x < 0 || updateItem.position.y < 0) {
parent.style = { ...parent.style } || {}
if (extendWidth > 0) {
if (!parent.style.width) {
parent.style.width = parent.dimensions.width
}
if (typeof parent.style.width === 'string') {
const currWidth = parseInt(parent.style.width, 10)
parent.style.width = `${currWidth + extendWidth}px`
} else {
parent.style.width += extendWidth
}
}
if (extendHeight > 0) {
if (!parent.style.height) {
parent.style.height = parent.dimensions.height
}
if (typeof parent.style.height === 'string') {
const currWidth = parseInt(parent.style.height, 10)
parent.style.height = `${currWidth + extendHeight}px`
} else {
parent.style.height += extendHeight
}
}
if (updateItem.position.x < 0) {
const xDiff = Math.abs(updateItem.position.x)
parent.position.x = parent.position.x - xDiff
if (typeof parent.style.width === 'string') {
const currWidth = parseInt(parent.style.width, 10)
parent.style.width = `${currWidth + xDiff}px`
} else {
;(parent.style as any).width += xDiff
}
updateItem.position.x = 0
}
if (updateItem.position.y < 0) {
const yDiff = Math.abs(updateItem.position.y)
parent.position.y = parent.position.y - yDiff
if (typeof parent.style.height === 'string') {
const currWidth = parseInt(parent.style.height, 10)
parent.style.height = `${currWidth + yDiff}px`
} else {
;(parent.style as any).height += yDiff
}
updateItem.position.y = 0
}
parent.dimensions.width = (
typeof parent.style.width === 'string' ? parseInt((<string>parent.style.width)!, 10) : parent.style.width
)!
parent.dimensions.height = (
typeof parent.style.height === 'string' ? parseInt((<string>parent.style.height)!, 10) : parent.style.height
)!
}
}
}
export const applyChanges = <
T extends Node | Edge | FlowElement = Node,
C extends ElementChange = T extends GraphNode ? NodeChange : EdgeChange,
>(
changes: C[],
elements: T[],
addElement?: (els: T[]) => void,
): T[] => {
let elementIds = elements.map((el) => el.id)
changes.forEach((change) => {
if (change.type === 'add') {
if (addElement) return addElement([change.item as any])
else return elements.push(change.item as any)
}
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.dragging !== 'undefined') el.dragging = change.dragging
if (el.expandParent && el.parentNode) handleParentExpand(el, elements as GraphNode[])
}
break
case 'dimensions':
if (isGraphNode(el)) {
if (typeof change.dimensions !== 'undefined') el.dimensions = change.dimensions
if (el.expandParent && el.parentNode) handleParentExpand(el, elements as GraphNode[])
}
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 createPositionChange = (
{ node, diff, dragging, nodeExtent }: CreatePositionChangeParams,
getNode: Getters['getNode'],
): NodePositionChange => {
const parent = node.parentNode ? getNode(node.parentNode) : undefined
const change: NodePositionChange = {
id: node.id,
type: 'position',
dragging: !!dragging,
}
if (diff) {
const nextPosition = { x: node.position.x + diff.x, y: node.position.y + diff.y }
let currentExtent = node.extent === 'parent' || typeof node.extent === 'undefined' ? nodeExtent : node.extent
if (node.extent === 'parent' && parent && node.dimensions.width && node.dimensions.height) {
currentExtent =
parent.dimensions.width && parent.dimensions.height
? [
[0, 0],
[parent.dimensions.width - node.dimensions.width, parent.dimensions.height - node.dimensions.height],
]
: currentExtent
}
change.position = currentExtent ? clampPosition(nextPosition, currentExtent) : nextPosition
}
return change
}
const isParentSelected = (node: GraphNode, selectedIds: string[], getNode: Getters['getNode']): boolean => {
const parent = node.parentNode ? getNode(node.parentNode) : undefined
if (!node.parentNode || !parent) return false
if (selectedIds.includes(node.parentNode)) return true
return isParentSelected(parent, selectedIds, getNode)
}
export const getSelectionChanges = (items: FlowElements, selectedIds: string[], getNode: Getters['getNode']) => {
return items.reduce((res, item) => {
const willBeSelected =
selectedIds.includes(item.id) || !!(isGraphNode(item) && item.parentNode && isParentSelected(item, selectedIds, getNode))
if (!item.selected && willBeSelected) {
res.push(createSelectionChange(item.id, true))
} else if (item.selected && !willBeSelected) {
res.push(createSelectionChange(item.id, false))
}
return res
}, [] as (NodeSelectionChange | EdgeSelectionChange)[])
}
+160
View File
@@ -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,
}
})
}
+348
View File
@@ -0,0 +1,348 @@
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'
const isHTMLElement = (el: EventTarget): el is HTMLElement => ('nodeName' || 'hasAttribute') in el
export const isInputDOMNode = (e: KeyboardEvent | MouseEvent): boolean => {
const target = e.target
if (target && isHTMLElement(target)) {
return ['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].includes(target.nodeName) || target.hasAttribute('contentEditable')
}
return false
}
export const getDimensions = (node: HTMLElement): Dimensions => ({
width: node.offsetWidth,
height: node.offsetHeight,
})
export const clamp = (val: number, min = 0, max = 1): number => Math.min(Math.max(val, min), max)
export const clampPosition = (position: XYPosition, extent: CoordinateExtent): XYPosition => ({
x: clamp(position.x, extent[0][0], extent[1][0]),
y: clamp(position.y, extent[0][1], extent[1][1]),
})
export const getHostForElement = (element: HTMLElement): Document => {
const doc = element.getRootNode() as Document
const window = useWindow()
if ('getElementFromPoint' in doc) return doc
else return window.document
}
type MaybeElement = Node | Edge | Connection | FlowElement
export const isEdge = (element: MaybeElement): element is Edge => '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 => '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: {
width: 0,
height: 0,
},
handleBounds: {
source: [],
target: [],
},
computedPosition: {
z: 0,
x: 0,
y: 0,
},
dragging: false,
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 ?? 'default',
source: edge.source.toString(),
target: edge.target.toString(),
z: 0,
sourceX: 0,
sourceY: 0,
targetX: 0,
targetY: 0,
updatable: undefined,
selectable: undefined,
data: undefined,
...defaults,
} as GraphEdge)
: defaults
return {
...(defaults as GraphEdge),
...edge,
id: edge.id.toString(),
}
}
const getConnectedElements = (node: GraphNode, elements: Elements, dir: 'source' | 'target') => {
if (!isNode(node)) return []
const ids = elements.filter((e) => isEdge(e) && e.source === node.id).map((e) => isEdge(e) && e[dir])
return elements.filter((e) => ids.includes(e.id))
}
export const getOutgoers = (node: GraphNode, elements: Elements) => getConnectedElements(node, elements, 'target')
export const getIncomers = (node: GraphNode, elements: Elements) => getConnectedElements(node, elements, 'source')
export const getEdgeId = ({ source, sourceHandle, target, targetHandle }: Connection) =>
`vueflow__edge-${source}${sourceHandle ?? ''}-${target}${targetHandle ?? ''}`
export const connectionExists = (edge: Edge | 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, edge]
}
/**
* 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 }, dragging = false } = node
const nBox = rectToBox({ ...computedPosition, ...dimensions })
const xOverlap = Math.max(0, Math.min(rBox.x2, nBox.x2) - Math.max(rBox.x, nBox.x))
const yOverlap = Math.max(0, Math.min(rBox.y2, nBox.y2) - Math.max(rBox.y, nBox.y))
const overlappingArea = Math.ceil(xOverlap * yOverlap)
const notInitialized =
typeof dimensions.width === 'undefined' ||
typeof dimensions.height === 'undefined' ||
dimensions.width === 0 ||
dimensions.height === 0 ||
dragging
const partiallyVisible = partially && overlappingArea > 0
const area = dimensions.width * dimensions.height
return notInitialized || partiallyVisible || overlappingArea >= area
})
}
export const getConnectedEdges = (nodes: GraphNode[], edges: GraphEdge[]) => {
const nodeIds = nodes.map((node) => node.id)
return edges.filter((edge) => nodeIds.includes(edge.source) || nodeIds.includes(edge.target))
}
export const 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('&')
}
+4
View File
@@ -0,0 +1,4 @@
export * from './edge'
export * from './graph'
export * from './node'
export * from './changes'
+39
View File
@@ -0,0 +1,39 @@
import { getDimensions } from './graph'
import type { HandleElement, Position } from '~/types'
export const getHandleBoundsByHandleType = (
selector: string,
nodeElement: HTMLDivElement,
parentBounds: ClientRect | DOMRect,
k: number,
): HandleElement[] | null => {
const handles = nodeElement.querySelectorAll(selector)
if (!handles || !handles.length) return null
const handlesArray = Array.from(handles) as HTMLDivElement[]
return handlesArray.map((handle): HandleElement => {
const bounds = handle.getBoundingClientRect()
const dimensions = getDimensions(handle)
const handleId = handle.getAttribute('data-handleid') ?? undefined
const handlePosition = handle.getAttribute('data-handlepos') as Position
return {
id: handleId,
position: handlePosition,
x: (bounds.left - parentBounds.left) / k,
y: (bounds.top - parentBounds.top) / k,
...dimensions,
}
})
}
export const getHandleBounds = (nodeElement: HTMLDivElement, scale: number, id?: string) => {
const bounds = nodeElement.getBoundingClientRect()
return {
source: getHandleBoundsByHandleType(`.source${id ? `.vue-flow__handle-${id}` : ''}`, nodeElement, bounds, scale) ?? undefined,
target: getHandleBoundsByHandleType(`.target${id ? `.vue-flow__handle-${id}` : ''}`, nodeElement, bounds, scale) ?? undefined,
}
}
+30
View File
@@ -0,0 +1,30 @@
{
"compilerOptions": {
"outDir": "./tmp",
"baseUrl": ".",
"module": "ESNext",
"target": "es2017",
"lib": ["DOM", "ESNext"],
"declaration": true,
"declarationDir": "./dist",
"strict": true,
"esModuleInterop": true,
"incremental": false,
"skipLibCheck": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"noUnusedLocals": false,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"types": ["vite/client", "vue/macros"],
"paths": {
"~/*": ["src/*"]
},
"plugins": [
// Transform paths in output .d.ts files (Include this line if you output declarations files)
{ "transform": "typescript-transform-paths", "afterDeclarations": true }
]
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
+27
View File
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"outDir": "./tmp",
"baseUrl": ".",
"module": "ESNext",
"target": "es2017",
"lib": ["DOM", "ESNext"],
"strict": true,
"esModuleInterop": true,
"incremental": false,
"skipLibCheck": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"noUnusedLocals": false,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"types": ["vite/client"],
"paths": {
"~/*": ["src/*"]
},
"plugins": [
// Transform paths in output .d.ts files (Include this line if you output declarations files)
{ "transform": "typescript-transform-paths", "afterDeclarations": true }
]
},
"exclude": ["node_modules"]
}
+36
View File
@@ -0,0 +1,36 @@
{
"compilerOptions": {
"baseUrl": ".",
"module": "ESNext",
"target": "es2017",
"lib": ["DOM", "ESNext"],
"strict": true,
"esModuleInterop": true,
"incremental": false,
"skipLibCheck": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"noUnusedLocals": false,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationDir": "./dist",
"types": ["vite/client", "vue/macros"],
"paths": {
"~/*": ["src/*"]
},
"plugins": [
// Transform paths in output .d.ts files (Include this line if you output declarations files)
{ "transform": "typescript-transform-paths", "afterDeclarations": true }
]
},
"vueCompilerOptions": {
"plugins": [
// Transform paths in output .d.ts files (Include this line if you output declarations files)
{ "transform": "typescript-transform-paths", "afterDeclarations": true }
]
},
"include": ["./src"],
"exclude": ["node_modules", "dist"],
"references": [{ "path": "./tsconfig.node.json" }]
}
+11
View File
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"],
"files": ["package.json"]
}
+58
View File
@@ -0,0 +1,58 @@
import { resolve } from 'path'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueTypes from 'vite-plugin-vue-type-imports'
import svgLoader from 'vite-svg-loader'
import AutoImport from 'unplugin-auto-import/vite'
import replace from '@rollup/plugin-replace'
import pkg from './package.json'
// https://vitejs.dev/config/
export default defineConfig({
resolve: {
alias: {
'~': resolve('src'),
},
extensions: ['.ts', '.vue'],
},
build: {
emptyOutDir: false,
lib: {
formats: ['es', 'cjs', 'iife'],
entry: resolve(__dirname, 'src/index.ts'),
name: 'vueFlow',
},
rollupOptions: {
// make sure to externalize deps that shouldn't be bundled
// into your library
external: ['vue'],
output: {
format: 'esm',
dir: './dist',
// Provide global variables to use in the UMD build
// for externalized deps
globals: {
vue: 'Vue',
},
},
},
},
plugins: [
vue({
reactivityTransform: true,
}),
vueTypes(),
svgLoader(),
AutoImport({
imports: ['vue', '@vueuse/core', 'vue/macros'],
dts: 'src/auto-imports.d.ts',
}),
replace({
__VUE_FLOW_VERSION__: JSON.stringify(pkg.version),
preventAssignment: true,
}),
],
optimizeDeps: {
include: ['vue', '@vueuse/core', '@braks/revue-draggable', 'd3', 'd3-zoom', 'd3-selection'],
},
})
File diff suppressed because it is too large Load Diff