feat: script setup style
* Replace jsx with script setup syntax
* Simplify logic and make it more "composable"
* Move Zoom functions to useZoom composable
* Update eslint config & tsconfig
This commit is contained in:
+4
-37
@@ -1,42 +1,9 @@
|
|||||||
const baseRules = {
|
const baseRules = {
|
||||||
'@typescript-eslint/no-explicit-any': 0,
|
'prettier/prettier': ['error', {}, { usePrettierrc: true }],
|
||||||
'@typescript-eslint/ban-ts-ignore': 0,
|
|
||||||
'@typescript-eslint/ban-ts-comment': 0,
|
|
||||||
'@typescript-eslint/no-empty-function': 0,
|
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 0,
|
|
||||||
'no-use-before-define': 0,
|
|
||||||
'no-unused-vars': 'off',
|
|
||||||
'@typescript-eslint/no-unused-vars': ['warn'],
|
|
||||||
indent: ['warn', 2, { SwitchCase: 1, flatTernaryExpressions: true }],
|
|
||||||
quotes: ['error', 'single', { avoidEscape: true }],
|
|
||||||
'import/no-mutable-exports': 0,
|
|
||||||
'no-cond-assign': [2],
|
|
||||||
'no-console': 'off',
|
|
||||||
camelcase: 0,
|
|
||||||
'@typescript/camelcase': 0,
|
|
||||||
'vue/no-v-html': 'off',
|
|
||||||
'vue/attribute-hyphenation': 0,
|
|
||||||
'no-undef': 'off',
|
|
||||||
'prettier/prettier': ['error', {}, { usePrettierrc: true }]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
extends: ['@antfu', 'plugin:prettier/recommended'],
|
||||||
parser: '@typescript-eslint/parser',
|
plugins: ['prettier'],
|
||||||
parserOptions: {
|
rules: baseRules,
|
||||||
ecmaVersion: 2020,
|
|
||||||
sourceType: 'module',
|
|
||||||
ecmaFeatures: {
|
|
||||||
jsx: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
env: {
|
|
||||||
node: true,
|
|
||||||
commonjs: true,
|
|
||||||
es6: true,
|
|
||||||
browser: true
|
|
||||||
},
|
|
||||||
extends: ['eslint:recommended', 'plugin:vue/vue3-recommended', '@vue/typescript/recommended', 'plugin:prettier/recommended'],
|
|
||||||
plugins: [],
|
|
||||||
rules: baseRules
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"trailingComma": "none",
|
"trailingComma": "all",
|
||||||
"semi": false,
|
"semi": false,
|
||||||
|
"quoteProps": "consistent",
|
||||||
"bracketSpacing": true,
|
"bracketSpacing": true,
|
||||||
"vueIndentScriptAndStyle": false,
|
|
||||||
"printWidth": 130
|
"printWidth": 130
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
"pinia": "^2.0.0-rc.13"
|
"pinia": "^2.0.0-rc.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@antfu/eslint-config": "^0.9.0",
|
||||||
"@babel/core": "^7.15.8",
|
"@babel/core": "^7.15.8",
|
||||||
"@rollup/plugin-babel": "^5.3.0",
|
"@rollup/plugin-babel": "^5.3.0",
|
||||||
"@rollup/plugin-commonjs": "^19.0.2",
|
"@rollup/plugin-commonjs": "^19.0.2",
|
||||||
@@ -79,6 +80,7 @@
|
|||||||
"vite": "^2.6.7",
|
"vite": "^2.6.7",
|
||||||
"vite-svg-loader": "^2.2.0",
|
"vite-svg-loader": "^2.2.0",
|
||||||
"vue": "^3.2.20",
|
"vue": "^3.2.20",
|
||||||
|
"vue-eslint-parser": "^7.11.0",
|
||||||
"vue-router": "^4.0.12",
|
"vue-router": "^4.0.12",
|
||||||
"vue-tsc": "^0.2.3"
|
"vue-tsc": "^0.2.3"
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+774
File diff suppressed because it is too large
Load Diff
Vendored
+12
@@ -16,6 +16,7 @@ declare global {
|
|||||||
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
|
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
|
||||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||||
const defineComponent: typeof import('vue')['defineComponent']
|
const defineComponent: typeof import('vue')['defineComponent']
|
||||||
|
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
|
||||||
const extendRef: typeof import('@vueuse/core')['extendRef']
|
const extendRef: typeof import('@vueuse/core')['extendRef']
|
||||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||||
const h: typeof import('vue')['h']
|
const h: typeof import('vue')['h']
|
||||||
@@ -60,6 +61,7 @@ declare global {
|
|||||||
const toRef: typeof import('vue')['toRef']
|
const toRef: typeof import('vue')['toRef']
|
||||||
const toRefs: typeof import('vue')['toRefs']
|
const toRefs: typeof import('vue')['toRefs']
|
||||||
const triggerRef: typeof import('vue')['triggerRef']
|
const triggerRef: typeof import('vue')['triggerRef']
|
||||||
|
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
|
||||||
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
|
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
|
||||||
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
|
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
|
||||||
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
|
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
|
||||||
@@ -78,12 +80,14 @@ declare global {
|
|||||||
const useCssVar: typeof import('@vueuse/core')['useCssVar']
|
const useCssVar: typeof import('@vueuse/core')['useCssVar']
|
||||||
const useDark: typeof import('@vueuse/core')['useDark']
|
const useDark: typeof import('@vueuse/core')['useDark']
|
||||||
const useDebounce: typeof import('@vueuse/core')['useDebounce']
|
const useDebounce: typeof import('@vueuse/core')['useDebounce']
|
||||||
|
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
|
||||||
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
|
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
|
||||||
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
|
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
|
||||||
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
|
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
|
||||||
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
|
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
|
||||||
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
|
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
|
||||||
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
|
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
|
||||||
|
const useDraggable: typeof import('@vueuse/core')['useDraggable']
|
||||||
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
|
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
|
||||||
const useElementSize: typeof import('@vueuse/core')['useElementSize']
|
const useElementSize: typeof import('@vueuse/core')['useElementSize']
|
||||||
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
|
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
|
||||||
@@ -98,6 +102,7 @@ declare global {
|
|||||||
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
|
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
|
||||||
const useInterval: typeof import('@vueuse/core')['useInterval']
|
const useInterval: typeof import('@vueuse/core')['useInterval']
|
||||||
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
|
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
|
||||||
|
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
|
||||||
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
|
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
|
||||||
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
|
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
|
||||||
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
|
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
|
||||||
@@ -114,6 +119,7 @@ declare global {
|
|||||||
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
|
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
|
||||||
const useParallax: typeof import('@vueuse/core')['useParallax']
|
const useParallax: typeof import('@vueuse/core')['useParallax']
|
||||||
const usePermission: typeof import('@vueuse/core')['usePermission']
|
const usePermission: typeof import('@vueuse/core')['usePermission']
|
||||||
|
const usePointer: typeof import('@vueuse/core')['usePointer']
|
||||||
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
|
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
|
||||||
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
|
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
|
||||||
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
|
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
|
||||||
@@ -122,6 +128,7 @@ declare global {
|
|||||||
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
||||||
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
|
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
|
||||||
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
|
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
|
||||||
|
const useScroll: typeof import('@vueuse/core')['useScroll']
|
||||||
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
|
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
|
||||||
const useShare: typeof import('@vueuse/core')['useShare']
|
const useShare: typeof import('@vueuse/core')['useShare']
|
||||||
const useSlots: typeof import('vue')['useSlots']
|
const useSlots: typeof import('vue')['useSlots']
|
||||||
@@ -130,6 +137,7 @@ declare global {
|
|||||||
const useSwipe: typeof import('@vueuse/core')['useSwipe']
|
const useSwipe: typeof import('@vueuse/core')['useSwipe']
|
||||||
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
|
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
|
||||||
const useThrottle: typeof import('@vueuse/core')['useThrottle']
|
const useThrottle: typeof import('@vueuse/core')['useThrottle']
|
||||||
|
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
|
||||||
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
|
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
|
||||||
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
|
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
|
||||||
const useTimeout: typeof import('@vueuse/core')['useTimeout']
|
const useTimeout: typeof import('@vueuse/core')['useTimeout']
|
||||||
@@ -140,8 +148,10 @@ declare global {
|
|||||||
const useTransition: typeof import('@vueuse/core')['useTransition']
|
const useTransition: typeof import('@vueuse/core')['useTransition']
|
||||||
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
|
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
|
||||||
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
|
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
|
||||||
|
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
|
||||||
const useVModel: typeof import('@vueuse/core')['useVModel']
|
const useVModel: typeof import('@vueuse/core')['useVModel']
|
||||||
const useVModels: typeof import('@vueuse/core')['useVModels']
|
const useVModels: typeof import('@vueuse/core')['useVModels']
|
||||||
|
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
|
||||||
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
|
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
|
||||||
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
|
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
|
||||||
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
|
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
|
||||||
@@ -149,7 +159,9 @@ declare global {
|
|||||||
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
|
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
|
||||||
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
|
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
|
||||||
const watch: typeof import('vue')['watch']
|
const watch: typeof import('vue')['watch']
|
||||||
|
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
|
||||||
const watchEffect: typeof import('vue')['watchEffect']
|
const watchEffect: typeof import('vue')['watchEffect']
|
||||||
|
const watchOnce: typeof import('@vueuse/core')['watchOnce']
|
||||||
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
|
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
|
||||||
const whenever: typeof import('@vueuse/core')['whenever']
|
const whenever: typeof import('@vueuse/core')['whenever']
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
import { D3ZoomEvent, zoom, zoomIdentity, ZoomTransform } from 'd3-zoom'
|
||||||
|
import { pointer, select } from 'd3-selection'
|
||||||
|
import { Ref } from 'vue'
|
||||||
|
import { FlowTransform, PanOnScrollMode, RevueFlowOptions } from '~/types'
|
||||||
|
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
|
||||||
|
import { clamp } from '~/utils'
|
||||||
|
import useKeyPress from '~/hooks/useKeyPress'
|
||||||
|
import useZoomPanHelper from '~/hooks/useZoomPanHelper'
|
||||||
|
import { onLoadGetElements, onLoadProject, onLoadToObject } from '~/utils/graph'
|
||||||
|
|
||||||
|
const viewChanged = (prevTransform: FlowTransform, eventTransform: ZoomTransform): boolean =>
|
||||||
|
prevTransform.x !== eventTransform.x || prevTransform.y !== eventTransform.y || prevTransform.zoom !== eventTransform.k
|
||||||
|
|
||||||
|
const eventToFlowTransform = (eventTransform: ZoomTransform): FlowTransform => ({
|
||||||
|
x: eventTransform.x,
|
||||||
|
y: eventTransform.y,
|
||||||
|
zoom: eventTransform.k,
|
||||||
|
})
|
||||||
|
|
||||||
|
type UseZoomOptions = RevueFlowOptions
|
||||||
|
|
||||||
|
const useZoom = (zoomPane: Ref<HTMLDivElement>, options: UseZoomOptions) => {
|
||||||
|
const {
|
||||||
|
selectionKeyCode = 'Shift',
|
||||||
|
zoomActivationKeyCode = 'Meta',
|
||||||
|
minZoom = 0.5,
|
||||||
|
maxZoom = 2,
|
||||||
|
defaultZoom = 1,
|
||||||
|
defaultPosition = [0, 0],
|
||||||
|
translateExtent = [
|
||||||
|
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
||||||
|
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
||||||
|
],
|
||||||
|
zoomOnScroll = true,
|
||||||
|
zoomOnPinch = true,
|
||||||
|
zoomOnDoubleClick = true,
|
||||||
|
panOnScroll = false,
|
||||||
|
panOnScrollSpeed = 0.5,
|
||||||
|
panOnScrollMode = PanOnScrollMode.Free,
|
||||||
|
paneMoveable = true,
|
||||||
|
} = options
|
||||||
|
const hooks = inject<RevueFlowHooks>('hooks')!
|
||||||
|
const prevTransform = ref<FlowTransform>({ x: 0, y: 0, zoom: 0 })
|
||||||
|
|
||||||
|
const clampedX = clamp(defaultPosition[0], translateExtent[0][0], translateExtent[1][0])
|
||||||
|
const clampedY = clamp(defaultPosition[1], translateExtent[0][1], translateExtent[1][1])
|
||||||
|
const clampedZoom = clamp(defaultZoom, minZoom, maxZoom)
|
||||||
|
const transform = ref([clampedX, clampedY, clampedZoom])
|
||||||
|
|
||||||
|
until(zoomPane)
|
||||||
|
.toBeTruthy()
|
||||||
|
.then(() => {
|
||||||
|
const d3Zoom = zoom().scaleExtent([minZoom, maxZoom]).translateExtent(translateExtent)
|
||||||
|
const d3Selection = select(unrefElement(zoomPane) as Element).call(d3Zoom)
|
||||||
|
const d3ZoomHandler = d3Selection.on('wheel.zoom')
|
||||||
|
|
||||||
|
const updatedTransform = zoomIdentity.translate(clampedX, clampedY).scale(clampedZoom)
|
||||||
|
|
||||||
|
const { fitView, zoomIn, zoomOut, zoomTo } = useZoomPanHelper({ ...options, d3Zoom, d3Selection } as any)
|
||||||
|
hooks.load.trigger({
|
||||||
|
fitView: (params = { padding: 0.1 }) => fitView(params),
|
||||||
|
zoomIn,
|
||||||
|
zoomOut,
|
||||||
|
zoomTo,
|
||||||
|
setTransform: transform,
|
||||||
|
} as any)
|
||||||
|
d3Zoom.transform(d3Selection, updatedTransform)
|
||||||
|
|
||||||
|
const applyZoomHandlers = () => {
|
||||||
|
d3Zoom.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||||
|
if (viewChanged(prevTransform.value, event.transform)) {
|
||||||
|
const flowTransform = eventToFlowTransform(event.transform)
|
||||||
|
prevTransform.value = flowTransform
|
||||||
|
|
||||||
|
hooks.moveStart.trigger(flowTransform)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
d3Zoom.on('end', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||||
|
if (viewChanged(prevTransform.value, event.transform)) {
|
||||||
|
const flowTransform = eventToFlowTransform(event.transform)
|
||||||
|
prevTransform.value = flowTransform
|
||||||
|
|
||||||
|
hooks.moveEnd.trigger(flowTransform)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
useKeyPress(selectionKeyCode, (keyPress) => {
|
||||||
|
if (keyPress) {
|
||||||
|
d3Zoom.on('zoom', null)
|
||||||
|
} else {
|
||||||
|
d3Zoom.on('zoom', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||||
|
transform.value = [event.transform.x, event.transform.y, event.transform.k]
|
||||||
|
hooks.move.trigger(eventToFlowTransform(event.transform))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
useKeyPress(zoomActivationKeyCode, (keyPress) => {
|
||||||
|
if (panOnScroll && keyPress) {
|
||||||
|
d3Selection
|
||||||
|
?.on('wheel', (event: WheelEvent) => {
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.on('wheel.zoom', null)
|
||||||
|
} else if (typeof d3ZoomHandler !== 'undefined') {
|
||||||
|
d3Selection?.on('wheel', null).on('wheel.zoom', d3ZoomHandler)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyZoomFilter = () => {
|
||||||
|
const keyPress = useKeyPress(selectionKeyCode)
|
||||||
|
d3Zoom.filter((event: MouseEvent) => {
|
||||||
|
const zoomScroll = zoomOnScroll
|
||||||
|
const pinchZoom = zoomOnPinch && event.ctrlKey
|
||||||
|
|
||||||
|
// if all interactions are disabled, we prevent all zoom events
|
||||||
|
if (!paneMoveable && !zoomScroll && !panOnScroll && !zoomOnDoubleClick && !zoomOnPinch) return false
|
||||||
|
|
||||||
|
// during a selection we prevent all other interactions
|
||||||
|
if (keyPress.value) return false
|
||||||
|
|
||||||
|
// if zoom on double click is disabled, we prevent the double click event
|
||||||
|
if (!zoomOnDoubleClick && event.type === 'dblclick') return false
|
||||||
|
|
||||||
|
if ((event.target as Element).closest('.nowheel') && event.type === 'wheel') return false
|
||||||
|
|
||||||
|
// when the target element is a node, we still allow zooming
|
||||||
|
if (
|
||||||
|
((event.target as Element).closest('.revue-flow__node') || (event.target as Element).closest('.revue-flow__edge')) &&
|
||||||
|
event.type !== 'wheel'
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
|
||||||
|
// when the target element is a node selection, we still allow zooming
|
||||||
|
if ((event.target as Element).closest('.revue-flow__nodesselection') && 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 (!paneMoveable && (event.type === 'mousedown' || event.type === 'touchstart')) return false
|
||||||
|
|
||||||
|
// default filter for d3-zoom
|
||||||
|
return (!event.ctrlKey || event.type === 'wheel') && !event.button
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
applyZoomHandlers()
|
||||||
|
applyZoomFilter()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
transform,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useZoom
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { CSSProperties } from 'vue'
|
||||||
|
import Edge from '~/components/Edges/Edge'
|
||||||
|
import { ConnectionLineComponent, ConnectionLineType, ConnectionMode, Dimensions, Edge as TEdge, Transform } from '~/types'
|
||||||
|
import MarkerDefinitions from '~/container/MarkerDefinitions.vue'
|
||||||
|
|
||||||
|
interface EdgeRendererProps {
|
||||||
|
edges: TEdge[]
|
||||||
|
edgeTypes: any
|
||||||
|
transform: Transform
|
||||||
|
connectionLineType: ConnectionLineType
|
||||||
|
connectionLineStyle?: CSSProperties
|
||||||
|
connectionLineComponent?: ConnectionLineComponent
|
||||||
|
connectionMode?: ConnectionMode
|
||||||
|
arrowHeadColor: string
|
||||||
|
markerEndId?: string
|
||||||
|
onlyRenderVisibleElements: boolean
|
||||||
|
edgeUpdaterRadius?: number
|
||||||
|
dimensions: Dimensions
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<EdgeRendererProps>(), {
|
||||||
|
transform: () => [0, 0, 1],
|
||||||
|
edges: () => [],
|
||||||
|
arrowHeadColor: '#fff',
|
||||||
|
dimensions: () => ({ width: 0, height: 0 }),
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<svg :width="props.dimensions.width" :height="props.dimensions.height" class="revue-flow__edges">
|
||||||
|
<MarkerDefinitions :color="props.arrowHeadColor" />
|
||||||
|
<g
|
||||||
|
:transform="
|
||||||
|
props.transform &&
|
||||||
|
props.transform.length &&
|
||||||
|
`translate(${props.transform[0]},${props.transform[1]}) scale(${props.transform[2]})`
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<template v-for="(edge, i) of props.edges" :key="`edge-${i}`">
|
||||||
|
<Edge
|
||||||
|
:edge="edge"
|
||||||
|
:type="props.edgeTypes[edge.type || 'default']"
|
||||||
|
:only-render-visible-elements="props.onlyRenderVisibleElements"
|
||||||
|
:marker-end-id="props.markerEndId"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '../../components/Edges'
|
import { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '~/components/Edges'
|
||||||
import { rectToBox } from '../../utils/graph'
|
import { rectToBox } from '~/utils/graph'
|
||||||
import { Position, Node, XYPosition, ElementId, HandleElement, Transform, Edge, EdgeType } from '../../types'
|
import { Position, Node, XYPosition, ElementId, HandleElement, Transform, Edge, EdgeType } from '~/types'
|
||||||
|
|
||||||
export function createEdgeTypes(edgeTypes: Record<string, EdgeType>): Record<string, EdgeType> {
|
export function createEdgeTypes(edgeTypes: Record<string, EdgeType>): Record<string, EdgeType> {
|
||||||
const standardTypes: Record<string, EdgeType> = {
|
const standardTypes: Record<string, EdgeType> = {
|
||||||
default: edgeTypes.default || BezierEdge,
|
default: edgeTypes.default || BezierEdge,
|
||||||
straight: edgeTypes.bezier || StraightEdge,
|
straight: edgeTypes.bezier || StraightEdge,
|
||||||
step: edgeTypes.step || StepEdge,
|
step: edgeTypes.step || StepEdge,
|
||||||
smoothstep: edgeTypes.step || SmoothStepEdge
|
smoothstep: edgeTypes.step || SmoothStepEdge,
|
||||||
}
|
}
|
||||||
|
|
||||||
const wrappedTypes = {} as Record<string, EdgeType>
|
const wrappedTypes = {} as Record<string, EdgeType>
|
||||||
@@ -21,7 +21,7 @@ export function createEdgeTypes(edgeTypes: Record<string, EdgeType>): Record<str
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...standardTypes,
|
...standardTypes,
|
||||||
...specialTypes
|
...specialTypes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,22 +35,22 @@ export function getHandlePosition(position: Position, node: Node, handle: any |
|
|||||||
case Position.Top:
|
case Position.Top:
|
||||||
return {
|
return {
|
||||||
x: x + width / 2,
|
x: x + width / 2,
|
||||||
y
|
y,
|
||||||
}
|
}
|
||||||
case Position.Right:
|
case Position.Right:
|
||||||
return {
|
return {
|
||||||
x: x + width,
|
x: x + width,
|
||||||
y: y + height / 2
|
y: y + height / 2,
|
||||||
}
|
}
|
||||||
case Position.Bottom:
|
case Position.Bottom:
|
||||||
return {
|
return {
|
||||||
x: x + width / 2,
|
x: x + width / 2,
|
||||||
y: y + height
|
y: y + height,
|
||||||
}
|
}
|
||||||
case Position.Left:
|
case Position.Left:
|
||||||
return {
|
return {
|
||||||
x,
|
x,
|
||||||
y: y + height / 2
|
y: y + height / 2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,7 @@ export const getEdgePositions = (
|
|||||||
sourcePosition: Position,
|
sourcePosition: Position,
|
||||||
targetNode: Node,
|
targetNode: Node,
|
||||||
targetHandle: HandleElement | unknown,
|
targetHandle: HandleElement | unknown,
|
||||||
targetPosition: Position
|
targetPosition: Position,
|
||||||
): EdgePositions => {
|
): EdgePositions => {
|
||||||
const sourceHandlePos = getHandlePosition(sourcePosition, sourceNode, sourceHandle)
|
const sourceHandlePos = getHandlePosition(sourcePosition, sourceNode, sourceHandle)
|
||||||
const targetHandlePos = getHandlePosition(targetPosition, targetNode, targetHandle)
|
const targetHandlePos = getHandlePosition(targetPosition, targetNode, targetHandle)
|
||||||
@@ -94,7 +94,7 @@ export const getEdgePositions = (
|
|||||||
sourceX: sourceHandlePos.x,
|
sourceX: sourceHandlePos.x,
|
||||||
sourceY: sourceHandlePos.y,
|
sourceY: sourceHandlePos.y,
|
||||||
targetX: targetHandlePos.x,
|
targetX: targetHandlePos.x,
|
||||||
targetY: targetHandlePos.y
|
targetY: targetHandlePos.y,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ export function isEdgeVisible({ sourcePos, targetPos, width, height, transform }
|
|||||||
x: Math.min(sourcePos.x, targetPos.x),
|
x: Math.min(sourcePos.x, targetPos.x),
|
||||||
y: Math.min(sourcePos.y, targetPos.y),
|
y: Math.min(sourcePos.y, targetPos.y),
|
||||||
x2: Math.max(sourcePos.x, targetPos.x),
|
x2: Math.max(sourcePos.x, targetPos.x),
|
||||||
y2: Math.max(sourcePos.y, targetPos.y)
|
y2: Math.max(sourcePos.y, targetPos.y),
|
||||||
}
|
}
|
||||||
|
|
||||||
if (edgeBox.x === edgeBox.x2) {
|
if (edgeBox.x === edgeBox.x2) {
|
||||||
@@ -126,7 +126,7 @@ export function isEdgeVisible({ sourcePos, targetPos, width, height, transform }
|
|||||||
x: (0 - transform[0]) / transform[2],
|
x: (0 - transform[0]) / transform[2],
|
||||||
y: (0 - transform[1]) / transform[2],
|
y: (0 - transform[1]) / transform[2],
|
||||||
width: width / transform[2],
|
width: width / transform[2],
|
||||||
height: height / transform[2]
|
height: height / transform[2],
|
||||||
})
|
})
|
||||||
|
|
||||||
const xOverlap = Math.max(0, Math.min(viewBox.x2, edgeBox.x2) - Math.max(viewBox.x, edgeBox.x))
|
const xOverlap = Math.max(0, Math.min(viewBox.x2, edgeBox.x2) - Math.max(viewBox.x, edgeBox.x))
|
||||||
@@ -152,6 +152,6 @@ export const getSourceTargetNodes = (edge: Edge, nodes: Node[]): SourceTargetNod
|
|||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
},
|
},
|
||||||
{ sourceNode: null, targetNode: null } as SourceTargetNode
|
{ sourceNode: null, targetNode: null } as SourceTargetNode,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import '~/style.css'
|
||||||
|
import '~/theme-default.css'
|
||||||
|
import { CSSProperties } from 'vue'
|
||||||
|
import ZoomPane from './ZoomPane.vue'
|
||||||
|
import SelectionPane from './SelectionPane.vue'
|
||||||
|
import NodeRenderer from '~/container/NodeRenderer.vue'
|
||||||
|
import EdgeRenderer from '~/container/EdgeRenderer.vue'
|
||||||
|
import {
|
||||||
|
ConnectionLineType,
|
||||||
|
ConnectionMode,
|
||||||
|
Elements,
|
||||||
|
PanOnScrollMode,
|
||||||
|
RevueFlowStore,
|
||||||
|
Node,
|
||||||
|
Edge,
|
||||||
|
NodeType,
|
||||||
|
EdgeType,
|
||||||
|
ConnectionLineComponent,
|
||||||
|
KeyCode,
|
||||||
|
TranslateExtent,
|
||||||
|
NodeExtent,
|
||||||
|
} from '~/types'
|
||||||
|
import { RevueFlowHooks, useRevueFlow } from '~/hooks/RevueFlowHooks'
|
||||||
|
import configureStore from '~/store/configure-store'
|
||||||
|
import { initialState } from '~/store'
|
||||||
|
import { createNodeTypes } from '~/container/NodeRenderer/utils'
|
||||||
|
import InputNode from '~/components/Nodes/InputNode'
|
||||||
|
import DefaultNode from '~/components/Nodes/DefaultNode'
|
||||||
|
import OutputNode from '~/components/Nodes/OutputNode'
|
||||||
|
import { BezierEdge, SmoothStepEdge, StepEdge, StraightEdge } from '~/components/Edges'
|
||||||
|
import { createEdgeTypes } from '~/container/EdgeRenderer/utils'
|
||||||
|
import { isEdge, isNode, parseEdge, parseNode } from '~/utils/graph'
|
||||||
|
|
||||||
|
interface RevueFlowProps {
|
||||||
|
modelValue: Elements
|
||||||
|
nodeTypes?: Record<string, NodeType>
|
||||||
|
edgeTypes?: Record<string, EdgeType>
|
||||||
|
connectionMode?: ConnectionMode
|
||||||
|
connectionLineType?: ConnectionLineType
|
||||||
|
connectionLineStyle?: CSSProperties
|
||||||
|
connectionLineComponent?: ConnectionLineComponent
|
||||||
|
deleteKeyCode?: KeyCode
|
||||||
|
selectionKeyCode?: KeyCode
|
||||||
|
multiSelectionKeyCode?: KeyCode
|
||||||
|
zoomActivationKeyCode?: KeyCode
|
||||||
|
snapToGrid?: boolean
|
||||||
|
snapGrid?: [number, number]
|
||||||
|
onlyRenderVisibleElements?: boolean
|
||||||
|
nodesDraggable?: boolean
|
||||||
|
nodesConnectable?: boolean
|
||||||
|
elementsSelectable?: boolean
|
||||||
|
selectNodesOnDrag?: boolean
|
||||||
|
paneMoveable?: boolean
|
||||||
|
minZoom?: number
|
||||||
|
maxZoom?: number
|
||||||
|
defaultZoom?: number
|
||||||
|
defaultPosition?: [number, number]
|
||||||
|
translateExtent?: TranslateExtent
|
||||||
|
nodeExtent?: NodeExtent
|
||||||
|
arrowHeadColor?: string
|
||||||
|
markerEndId?: string
|
||||||
|
zoomOnScroll?: boolean
|
||||||
|
zoomOnPinch?: boolean
|
||||||
|
panOnScroll?: boolean
|
||||||
|
panOnScrollSpeed?: number
|
||||||
|
panOnScrollMode?: PanOnScrollMode
|
||||||
|
zoomOnDoubleClick?: boolean
|
||||||
|
edgeUpdaterRadius?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type NextElements = {
|
||||||
|
nextNodes: Node[]
|
||||||
|
nextEdges: Edge[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<RevueFlowProps>(), {
|
||||||
|
modelValue: () => [] as Elements,
|
||||||
|
connectionMode: ConnectionMode.Strict,
|
||||||
|
connectionLineType: ConnectionLineType.Bezier,
|
||||||
|
selectionKeyCode: 'Shift',
|
||||||
|
multiSelectionKeyCode: 'Meta',
|
||||||
|
zoomActivationKeyCode: 'Meta',
|
||||||
|
deleteKeyCode: 'Backspace',
|
||||||
|
snapToGrid: false,
|
||||||
|
snapGrid: () => [15, 15],
|
||||||
|
onlyRenderVisibleElements: false,
|
||||||
|
nodesConnectable: true,
|
||||||
|
elementsSelectable: true,
|
||||||
|
selectNodesOnDrag: true,
|
||||||
|
minZoom: 0.5,
|
||||||
|
maxZoom: 2,
|
||||||
|
defaultZoom: 1,
|
||||||
|
defaultPosition: () => [0, 0],
|
||||||
|
translateExtent: () => [
|
||||||
|
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
||||||
|
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
||||||
|
],
|
||||||
|
nodeExtent: () => [
|
||||||
|
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
||||||
|
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
||||||
|
],
|
||||||
|
arrowHeadColor: '#b1b1b7',
|
||||||
|
zoomOnScroll: true,
|
||||||
|
zoomOnPinch: true,
|
||||||
|
zoomOnDoubleClick: true,
|
||||||
|
panOnScroll: false,
|
||||||
|
panOnScrollSpeed: 0.5,
|
||||||
|
panOnScrollMode: PanOnScrollMode.Free,
|
||||||
|
paneMoveable: true,
|
||||||
|
edgeUpdaterRadius: 10,
|
||||||
|
})
|
||||||
|
const emit = defineEmits(Object.keys(useRevueFlow()))
|
||||||
|
|
||||||
|
const store = configureStore(initialState)()
|
||||||
|
provide<RevueFlowStore>('store', store)
|
||||||
|
|
||||||
|
const hooks = useRevueFlow().bind(emit)
|
||||||
|
provide<RevueFlowHooks>('hooks', hooks)
|
||||||
|
|
||||||
|
const elements = useVModel(props, 'modelValue', emit)
|
||||||
|
|
||||||
|
const defaultNodeTypes = {
|
||||||
|
input: InputNode,
|
||||||
|
default: DefaultNode,
|
||||||
|
output: OutputNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultEdgeTypes = {
|
||||||
|
default: BezierEdge,
|
||||||
|
straight: StraightEdge,
|
||||||
|
step: StepEdge,
|
||||||
|
smoothstep: SmoothStepEdge,
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeTypes = createNodeTypes({ ...defaultNodeTypes, ...props.nodeTypes })
|
||||||
|
const edgeTypes = createEdgeTypes({ ...defaultEdgeTypes, ...props.edgeTypes })
|
||||||
|
const nodes = ref<Node[]>([])
|
||||||
|
const edges = ref<Edge[]>([])
|
||||||
|
|
||||||
|
const setElements = (elements: Elements) => {
|
||||||
|
const propElements = elements
|
||||||
|
const nextElements: NextElements = {
|
||||||
|
nextNodes: [],
|
||||||
|
nextEdges: [],
|
||||||
|
}
|
||||||
|
const { nextNodes, nextEdges } = propElements.reduce((res, propElement): NextElements => {
|
||||||
|
if (isNode(propElement)) {
|
||||||
|
const storeNode = nodes.value.find((node) => node.id === propElement.id)
|
||||||
|
|
||||||
|
if (storeNode) {
|
||||||
|
const updatedNode: Node = {
|
||||||
|
...storeNode,
|
||||||
|
...propElement,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storeNode.position.x !== propElement.position.x || storeNode.position.y !== propElement.position.y) {
|
||||||
|
updatedNode.__rf.position = propElement.position
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof propElement.type !== 'undefined' && propElement.type !== storeNode.type) {
|
||||||
|
// we reset the elements dimensions here in order to force a re-calculation of the bounds.
|
||||||
|
// When the type of a node changes it is possible that the number or positions of handles changes too.
|
||||||
|
updatedNode.__rf.width = null
|
||||||
|
}
|
||||||
|
|
||||||
|
res.nextNodes.push(updatedNode)
|
||||||
|
} else {
|
||||||
|
res.nextNodes.push(parseNode(propElement, props.nodeExtent))
|
||||||
|
}
|
||||||
|
} else if (isEdge(propElement)) {
|
||||||
|
const storeEdge = edges.value.find((se) => se.id === propElement.id)
|
||||||
|
|
||||||
|
if (storeEdge) {
|
||||||
|
res.nextEdges.push({
|
||||||
|
...storeEdge,
|
||||||
|
...propElement,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
res.nextEdges.push(parseEdge(propElement))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}, nextElements)
|
||||||
|
|
||||||
|
nodes.value = nextNodes
|
||||||
|
edges.value = nextEdges
|
||||||
|
}
|
||||||
|
|
||||||
|
setElements(elements.value)
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="revue-flow">
|
||||||
|
<ZoomPane v-bind="props">
|
||||||
|
<template #default="{ transform, dimensions }">
|
||||||
|
<SelectionPane
|
||||||
|
:delete-key-code="props.deleteKeyCode"
|
||||||
|
:multi-selection-key-code="props.multiSelectionKeyCode"
|
||||||
|
:selection-key-code="props.selectionKeyCode"
|
||||||
|
>
|
||||||
|
<NodeRenderer v-bind="props" :node-types="nodeTypes" :nodes="nodes" :transform="transform" :dimensions="dimensions" />
|
||||||
|
<EdgeRenderer
|
||||||
|
v-if="false"
|
||||||
|
v-bind="props"
|
||||||
|
:edge-types="edgeTypes"
|
||||||
|
:edges="edges"
|
||||||
|
:transform="transform"
|
||||||
|
:dimensions="dimensions"
|
||||||
|
/>
|
||||||
|
</SelectionPane>
|
||||||
|
</template>
|
||||||
|
</ZoomPane>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
interface MarkerProps {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<MarkerProps>()
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<marker
|
||||||
|
:id="props.id"
|
||||||
|
class="revue-flow__arrowhead"
|
||||||
|
markerWidth="12.5"
|
||||||
|
markerHeight="12.5"
|
||||||
|
viewBox="-10 -10 20 20"
|
||||||
|
orient="auto"
|
||||||
|
refX="0"
|
||||||
|
refY="0"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</marker>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import Marker from './Marker.vue'
|
||||||
|
|
||||||
|
interface MarkerDefinitionsProps {
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<MarkerDefinitionsProps>(), {
|
||||||
|
color: '',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<defs>
|
||||||
|
<Marker id="revue-flow__arrowclosed">
|
||||||
|
<polyline
|
||||||
|
:stroke="props.color"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="1"
|
||||||
|
:fill="props.color"
|
||||||
|
points="-5,-4 0,0 -5,4 -5,-4"
|
||||||
|
/>
|
||||||
|
</Marker>
|
||||||
|
<Marker id="revue-flow__arrow">
|
||||||
|
<polyline
|
||||||
|
:stroke="props.color"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="1.5"
|
||||||
|
fill="none"
|
||||||
|
points="-5,-4 0,0 -5,4"
|
||||||
|
/>
|
||||||
|
</Marker>
|
||||||
|
</defs>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import Node from '~/components/Nodes/Node'
|
||||||
|
import { NodeType, Node as TNode, Transform, Dimensions } from '~/types'
|
||||||
|
import { getNodesInside } from '~/utils/graph'
|
||||||
|
|
||||||
|
interface NodeRendererProps {
|
||||||
|
nodes: TNode[]
|
||||||
|
transform: Transform
|
||||||
|
nodeTypes: Record<string, NodeType>
|
||||||
|
selectNodesOnDrag: boolean
|
||||||
|
snapToGrid: boolean
|
||||||
|
snapGrid: [number, number]
|
||||||
|
onlyRenderVisibleElements: boolean
|
||||||
|
dimensions: Dimensions
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<NodeRendererProps>(), {
|
||||||
|
selectNodesOnDrag: true,
|
||||||
|
snapGrid: () => [15, 15],
|
||||||
|
snapToGrid: false,
|
||||||
|
onlyRenderVisibleElements: false,
|
||||||
|
transform: () => [0, 0, 1],
|
||||||
|
nodes: () => [],
|
||||||
|
dimensions: () => ({ width: 0, height: 0 }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const getNodes = () =>
|
||||||
|
props.onlyRenderVisibleElements
|
||||||
|
? props.nodes &&
|
||||||
|
getNodesInside(
|
||||||
|
props.nodes,
|
||||||
|
{
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: props.dimensions.width,
|
||||||
|
height: props.dimensions.height,
|
||||||
|
},
|
||||||
|
props.transform,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
: props.nodes
|
||||||
|
|
||||||
|
const cNodes = computed(() => getNodes())
|
||||||
|
|
||||||
|
const type = (node: TNode) => {
|
||||||
|
const nodeType = node.type || 'default'
|
||||||
|
if (props.nodeTypes) {
|
||||||
|
const type = props.nodeTypes[nodeType] || props.nodeTypes.default
|
||||||
|
if (!props.nodeTypes[nodeType]) {
|
||||||
|
console.warn(`Node type "${nodeType}" not found. Using fallback type "default".`)
|
||||||
|
}
|
||||||
|
return type
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="revue-flow__nodes"
|
||||||
|
:style="{ transform: `translate(${props.transform[0]}px,${props.transform[1]}px) scale(${props.transform[2]})` }"
|
||||||
|
>
|
||||||
|
<template v-for="(node, i) of cNodes" :key="`node-${i}`">
|
||||||
|
<div>Node</div>
|
||||||
|
<Node
|
||||||
|
v-if="type(node)"
|
||||||
|
:node="node"
|
||||||
|
:snap-grid="props.snapGrid"
|
||||||
|
:snap-to-grid="props.snapToGrid"
|
||||||
|
:select-nodes-on-drag="props.selectNodesOnDrag"
|
||||||
|
:type="type(node)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import DefaultNode from '../../components/Nodes/DefaultNode'
|
import DefaultNode from '~/components/Nodes/DefaultNode'
|
||||||
import InputNode from '../../components/Nodes/InputNode'
|
import InputNode from '~/components/Nodes/InputNode'
|
||||||
import OutputNode from '../../components/Nodes/OutputNode'
|
import OutputNode from '~/components/Nodes/OutputNode'
|
||||||
import { NodeType } from '../../types'
|
import { NodeType } from '~/types'
|
||||||
|
|
||||||
export function createNodeTypes(nodeTypes: Record<string, NodeType>): Record<string, NodeType> {
|
export function createNodeTypes(nodeTypes: Record<string, NodeType>): Record<string, NodeType> {
|
||||||
const standardTypes: Record<string, NodeType> = {
|
const standardTypes: Record<string, NodeType> = {
|
||||||
input: nodeTypes.input || InputNode,
|
input: nodeTypes.input || InputNode,
|
||||||
default: nodeTypes.default || DefaultNode,
|
default: nodeTypes.default || DefaultNode,
|
||||||
output: nodeTypes.output || OutputNode
|
output: nodeTypes.output || OutputNode,
|
||||||
}
|
}
|
||||||
|
|
||||||
const wrappedTypes = {} as NodeType
|
const wrappedTypes = {} as NodeType
|
||||||
@@ -21,6 +21,6 @@ export function createNodeTypes(nodeTypes: Record<string, NodeType>): Record<str
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...standardTypes,
|
...standardTypes,
|
||||||
...specialTypes
|
...specialTypes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { inject } from 'vue'
|
||||||
|
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
|
||||||
|
import useKeyPress from '~/hooks/useKeyPress'
|
||||||
|
import { KeyCode } from '~/types'
|
||||||
|
import useGlobalKeyHandler from '~/hooks/useGlobalKeyHandler'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
selectionKeyCode: KeyCode
|
||||||
|
deleteKeyCode: KeyCode
|
||||||
|
multiSelectionKeyCode: KeyCode
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
selectionKeyCode: 'Shift',
|
||||||
|
deleteKeyCode: 'Backspace',
|
||||||
|
multiSelectionKeyCode: 'Meta',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const hooks = inject<RevueFlowHooks>('hooks')!
|
||||||
|
const keyPressed = useKeyPress(props.selectionKeyCode)
|
||||||
|
const nodeSelectionActive = ref(false)
|
||||||
|
const selectedElements = ref([])
|
||||||
|
|
||||||
|
const onClick = (event: MouseEvent) => {
|
||||||
|
hooks.paneClick.trigger(event)
|
||||||
|
nodeSelectionActive.value = false
|
||||||
|
selectedElements.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
const onContextMenu = (event: MouseEvent) => hooks.paneContextMenu.trigger(event)
|
||||||
|
|
||||||
|
const onWheel = (event: WheelEvent) => hooks.paneScroll.trigger(event)
|
||||||
|
|
||||||
|
useGlobalKeyHandler({
|
||||||
|
onElementsRemove: hooks.elementsRemove.trigger,
|
||||||
|
deleteKeyCode: props.deleteKeyCode,
|
||||||
|
multiSelectionKeyCode: props.multiSelectionKeyCode,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<slot></slot>
|
||||||
|
<div v-if="keyPressed" id="user-selection">User Selection</div>
|
||||||
|
<div v-if="nodeSelectionActive" id="nodes-selection">Nodes Selection</div>
|
||||||
|
<div class="revue-flow__pane" @click="onClick" @contextmenu="onContextMenu" @wheel="onWheel" />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { RevueFlowOptions, ConnectionLineType, ConnectionMode, Elements, PanOnScrollMode } from '~/types'
|
||||||
|
import useZoom from '~/composables/useZoom'
|
||||||
|
import useResizeHandler from '~/hooks/useResizeHandler'
|
||||||
|
|
||||||
|
interface RevueFlowProps extends RevueFlowOptions {
|
||||||
|
modelValue: Elements
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<RevueFlowProps>(), {
|
||||||
|
modelValue: () => [] as Elements,
|
||||||
|
connectionMode: ConnectionMode.Strict,
|
||||||
|
connectionLineType: ConnectionLineType.Bezier,
|
||||||
|
selectionKeyCode: 'Shift',
|
||||||
|
multiSelectionKeyCode: 'Meta',
|
||||||
|
zoomActivationKeyCode: 'Meta',
|
||||||
|
deleteKeyCode: 'Backspace',
|
||||||
|
snapToGrid: false,
|
||||||
|
snapGrid: () => [15, 15],
|
||||||
|
onlyRenderVisibleElements: false,
|
||||||
|
nodesConnectable: true,
|
||||||
|
elementsSelectable: true,
|
||||||
|
selectNodesOnDrag: true,
|
||||||
|
minZoom: 0.5,
|
||||||
|
maxZoom: 2,
|
||||||
|
defaultZoom: 1,
|
||||||
|
defaultPosition: () => [0, 0],
|
||||||
|
translateExtent: () => [
|
||||||
|
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
||||||
|
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
||||||
|
],
|
||||||
|
nodeExtent: () => [
|
||||||
|
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
||||||
|
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
||||||
|
],
|
||||||
|
arrowHeadColor: '#b1b1b7',
|
||||||
|
zoomOnScroll: true,
|
||||||
|
zoomOnPinch: true,
|
||||||
|
zoomOnDoubleClick: true,
|
||||||
|
panOnScroll: false,
|
||||||
|
panOnScrollSpeed: 0.5,
|
||||||
|
panOnScrollMode: PanOnScrollMode.Free,
|
||||||
|
paneMoveable: true,
|
||||||
|
edgeUpdaterRadius: 10,
|
||||||
|
})
|
||||||
|
const zoomPane = templateRef<HTMLDivElement>('zoom-pane', null)
|
||||||
|
const transform = useZoom(zoomPane, props)
|
||||||
|
const dimensions = useResizeHandler(zoomPane)
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div ref="zoom-pane" class="revue-flow__renderer revue-flow__zoompane">
|
||||||
|
<slot :transform="transform" :dimensions="dimensions"></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// global hooks
|
// global hooks
|
||||||
|
import { EventHook } from '@vueuse/core'
|
||||||
import { Connection, Edge, Elements, FlowTransform, Node, OnConnectStartParams, OnLoadParams } from '../types'
|
import { Connection, Edge, Elements, FlowTransform, Node, OnConnectStartParams, OnLoadParams } from '../types'
|
||||||
import { createEventHook, EventHook } from '@vueuse/core'
|
|
||||||
|
|
||||||
export interface RevueFlowHooks {
|
export interface RevueFlowHooks {
|
||||||
elementClick: EventHook<{ event: MouseEvent; element: Node | Edge }>
|
elementClick: EventHook<{ event: MouseEvent; element: Node | Edge }>
|
||||||
@@ -84,7 +84,7 @@ export const useRevueFlow = (): RevueFlowHooks & { bind: (emit: (event: string,
|
|||||||
edgeDoubleClick: createEventHook<{ event: MouseEvent; edge: Edge }>(),
|
edgeDoubleClick: createEventHook<{ event: MouseEvent; edge: Edge }>(),
|
||||||
edgeClick: createEventHook<{ event: MouseEvent; edge: Edge }>(),
|
edgeClick: createEventHook<{ event: MouseEvent; edge: Edge }>(),
|
||||||
edgeUpdateStart: createEventHook<{ event: MouseEvent; edge: Edge }>(),
|
edgeUpdateStart: createEventHook<{ event: MouseEvent; edge: Edge }>(),
|
||||||
edgeUpdateEnd: createEventHook<{ event: MouseEvent; edge: Edge }>()
|
edgeUpdateEnd: createEventHook<{ event: MouseEvent; edge: Edge }>(),
|
||||||
}
|
}
|
||||||
|
|
||||||
const bind = (emit: (event: string, args: any) => void) => {
|
const bind = (emit: (event: string, args: any) => void) => {
|
||||||
@@ -99,6 +99,6 @@ export const useRevueFlow = (): RevueFlowHooks & { bind: (emit: (event: string,
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...eventHooks,
|
...eventHooks,
|
||||||
bind
|
bind,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,29 @@
|
|||||||
import useKeyPress from './useKeyPress'
|
|
||||||
import { isNode, getConnectedEdges } from '../utils/graph'
|
|
||||||
import { Elements, KeyCode, ElementId, FlowElement, RevueFlowStore, Edge } from '../types'
|
|
||||||
import { computed, watch } from 'vue'
|
import { computed, watch } from 'vue'
|
||||||
|
import useKeyPress from './useKeyPress'
|
||||||
|
import { isNode, getConnectedEdges } from '~/utils/graph'
|
||||||
|
import { Elements, KeyCode, ElementId, FlowElement, RevueFlowStore, Edge } from '~/types'
|
||||||
|
|
||||||
interface HookParams {
|
interface HookParams {
|
||||||
store: RevueFlowStore
|
|
||||||
deleteKeyCode: KeyCode
|
deleteKeyCode: KeyCode
|
||||||
multiSelectionKeyCode: KeyCode
|
multiSelectionKeyCode: KeyCode
|
||||||
onElementsRemove?: (elements: Elements) => void
|
onElementsRemove?: (elements: Elements) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ({ store, deleteKeyCode, multiSelectionKeyCode, onElementsRemove }: HookParams): void => {
|
export default ({ deleteKeyCode, multiSelectionKeyCode, onElementsRemove }: HookParams): void => {
|
||||||
|
const selectedElements = ref([])
|
||||||
const deleteKeyPressed = useKeyPress(deleteKeyCode)
|
const deleteKeyPressed = useKeyPress(deleteKeyCode)
|
||||||
const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode)
|
const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode)
|
||||||
const selectedElements = computed(() => store.selectedElements || [])
|
/**
|
||||||
const edges = computed(() => store.edges)
|
* const selectedElements = computed(() => store.selectedElements || [])
|
||||||
|
const edges = computed(() => store.edges)
|
||||||
|
|
||||||
watch(selectedElements, () => {
|
watch(selectedElements, () => {
|
||||||
if (onElementsRemove && deleteKeyPressed.value && selectedElements.value.length > 0) {
|
if (onElementsRemove && deleteKeyPressed.value && selectedElements.value.length > 0) {
|
||||||
const selectedNodes = selectedElements.value.filter(isNode)
|
const selectedNodes = selectedElements.value.filter(isNode)
|
||||||
const connectedEdges = getConnectedEdges(selectedNodes, edges.value as Edge[])
|
const connectedEdges = getConnectedEdges(selectedNodes, edges.value as Edge[])
|
||||||
const elementsToRemove = [...selectedElements.value, ...connectedEdges].reduce(
|
const elementsToRemove = [...selectedElements.value, ...connectedEdges].reduce(
|
||||||
(res, item) => res.set(item.id, item),
|
(res, item) => res.set(item.id, item),
|
||||||
new Map<ElementId, FlowElement>()
|
new Map<ElementId, FlowElement>(),
|
||||||
)
|
)
|
||||||
|
|
||||||
onElementsRemove(Array.from(elementsToRemove.values()))
|
onElementsRemove(Array.from(elementsToRemove.values()))
|
||||||
@@ -31,7 +32,8 @@ export default ({ store, deleteKeyCode, multiSelectionKeyCode, onElementsRemove
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(multiSelectionKeyPressed, () => {
|
watch(multiSelectionKeyPressed, () => {
|
||||||
store.multiSelectionActive = multiSelectionKeyPressed.value
|
store.multiSelectionActive = multiSelectionKeyPressed.value
|
||||||
})
|
})
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { Ref } from 'vue'
|
|||||||
import { onKeyDown, onKeyPressed, onKeyUp } from '@vueuse/core'
|
import { onKeyDown, onKeyPressed, onKeyUp } from '@vueuse/core'
|
||||||
import { KeyCode } from '~/types'
|
import { KeyCode } from '~/types'
|
||||||
|
|
||||||
export default (keyCode: KeyCode, cb?: (keyPress: boolean) => void): Ref<boolean> => {
|
export default (keyCode: KeyCode, onChange?: (keyPressed: boolean) => void): Ref<boolean> => {
|
||||||
const keyPressed = controlledRef<boolean>(false, {
|
const isPressed = controlledRef<boolean>(false, {
|
||||||
onBeforeChange(val, oldVal) {
|
onBeforeChange(val, oldVal) {
|
||||||
if (val === oldVal) return false
|
if (val === oldVal) return false
|
||||||
},
|
},
|
||||||
onChanged() {
|
onChanged() {
|
||||||
if (cb && typeof cb === 'function') cb(keyPressed.value)
|
if (onChange && typeof onChange === 'function') onChange(isPressed.value)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ export default (keyCode: KeyCode, cb?: (keyPress: boolean) => void): Ref<boolean
|
|||||||
(e) => e.key === keyCode || e.keyCode === keyCode,
|
(e) => e.key === keyCode || e.keyCode === keyCode,
|
||||||
(e) => {
|
(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
keyPressed.value = true
|
isPressed.value = true
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ export default (keyCode: KeyCode, cb?: (keyPress: boolean) => void): Ref<boolean
|
|||||||
(e) => e.key === keyCode || e.keyCode === keyCode,
|
(e) => e.key === keyCode || e.keyCode === keyCode,
|
||||||
(e) => {
|
(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
keyPressed.value = true
|
isPressed.value = true
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,15 +32,15 @@ export default (keyCode: KeyCode, cb?: (keyPress: boolean) => void): Ref<boolean
|
|||||||
(event) => event.key === keyCode || event.keyCode === keyCode,
|
(event) => event.key === keyCode || event.keyCode === keyCode,
|
||||||
(e) => {
|
(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
keyPressed.value = false
|
isPressed.value = false
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
useEventListener(window, 'blur', () => {
|
useEventListener(window, 'blur', () => {
|
||||||
keyPressed.value = false
|
isPressed.value = false
|
||||||
})
|
})
|
||||||
|
|
||||||
if (cb && typeof cb === 'function') cb(keyPressed.value)
|
if (onChange && typeof onChange === 'function') onChange(isPressed.value)
|
||||||
|
|
||||||
return keyPressed
|
return isPressed
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,20 @@
|
|||||||
import { createEventHook, MaybeRef, unrefElement, until, useEventListener, useResizeObserver } from '@vueuse/core'
|
import { Ref } from 'vue'
|
||||||
import { getDimensions } from '../utils'
|
import { getDimensions } from '~/utils'
|
||||||
import { Dimensions } from '../types'
|
import { Dimensions } from '~/types'
|
||||||
|
|
||||||
export default (rendererNode: MaybeRef<HTMLDivElement>, updateSize: (size: Dimensions) => any) => {
|
export default (el: Ref<HTMLDivElement>) => {
|
||||||
const resizeHandlerHook = createEventHook()
|
const dimensions = ref<Dimensions>({ width: 0, height: 0 })
|
||||||
const updateDimensions = () => {
|
const updateDimensions = () => {
|
||||||
if (!unrefElement(rendererNode)) {
|
const unrefEl = unrefElement(el)
|
||||||
return
|
if (!unrefEl) return
|
||||||
}
|
|
||||||
|
|
||||||
const size = getDimensions(unrefElement(rendererNode))
|
const size = getDimensions(unrefEl as HTMLDivElement)
|
||||||
|
if (size.height === 0 || size.width === 0)
|
||||||
if (size.height === 0 || size.width === 0) {
|
|
||||||
console.log('The revue Flow parent container needs a width and a height to render the graph.')
|
console.log('The revue Flow parent container needs a width and a height to render the graph.')
|
||||||
}
|
|
||||||
|
|
||||||
updateSize(size)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
until(rendererNode)
|
useEventListener(window, 'resize', updateDimensions)
|
||||||
.toBeTruthy()
|
useResizeObserver(el, () => updateDimensions())
|
||||||
.then(() => {
|
|
||||||
updateDimensions()
|
|
||||||
useEventListener(window, 'resize', updateDimensions)
|
|
||||||
useResizeObserver(unrefElement(rendererNode), () => updateDimensions())
|
|
||||||
resizeHandlerHook.trigger(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return dimensions
|
||||||
onReady: resizeHandlerHook.on
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,91 +1,76 @@
|
|||||||
import { zoomIdentity } from 'd3-zoom'
|
import { zoomIdentity } from 'd3-zoom'
|
||||||
import { computed, effectScope, inject } from 'vue'
|
|
||||||
import { createEventHook, EventHookOn, until } from '@vueuse/core'
|
|
||||||
import { Selection, ZoomBehavior } from 'd3'
|
import { Selection, ZoomBehavior } from 'd3'
|
||||||
import { getRectOfNodes, pointToRendererPoint, getTransformForBounds } from '../utils/graph'
|
import { getRectOfNodes, pointToRendererPoint, getTransformForBounds } from '~/utils/graph'
|
||||||
import { FitViewParams, FlowTransform, ZoomPanHelperFunctions, Rect, XYPosition, RevueFlowStore } from '../types'
|
import { FitViewParams, FlowTransform, ZoomPanHelperFunctions, Rect, XYPosition, Node, Transform, SnapGrid } from '~/types'
|
||||||
|
|
||||||
const DEFAULT_PADDING = 0.1
|
const DEFAULT_PADDING = 0.1
|
||||||
|
|
||||||
const useZoomPanHelper = (): { onReady: EventHookOn<ZoomPanHelperFunctions> } => {
|
const useZoomPanHelper = ({
|
||||||
const zoomPanHelperEvent = createEventHook<ZoomPanHelperFunctions>()
|
d3Zoom,
|
||||||
const store = inject<RevueFlowStore>('store')!
|
d3Selection,
|
||||||
|
nodes = [],
|
||||||
|
width = 0,
|
||||||
|
height = 0,
|
||||||
|
minZoom = 0.5,
|
||||||
|
maxZoom = 2,
|
||||||
|
transform = [0, 0, 0],
|
||||||
|
snapToGrid = false,
|
||||||
|
snapGrid = [15, 15],
|
||||||
|
}: {
|
||||||
|
d3Zoom: ZoomBehavior<Element, unknown>
|
||||||
|
d3Selection: Selection<Element, any, any, any>
|
||||||
|
nodes: Node[]
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
minZoom: number
|
||||||
|
maxZoom: number
|
||||||
|
transform: Transform
|
||||||
|
snapToGrid: boolean
|
||||||
|
snapGrid: SnapGrid
|
||||||
|
}): ZoomPanHelperFunctions => ({
|
||||||
|
zoomIn: () => d3Zoom?.scaleBy(d3Selection, 1.2),
|
||||||
|
zoomOut: () => d3Zoom?.scaleBy(d3Selection, 1 / 1.2),
|
||||||
|
zoomTo: (zoomLevel: number) => d3Zoom.scaleTo(d3Selection, zoomLevel),
|
||||||
|
transform: (transform: FlowTransform) => {
|
||||||
|
const nextTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.zoom)
|
||||||
|
|
||||||
const scope = effectScope()
|
d3Zoom.transform(d3Selection, nextTransform)
|
||||||
scope.run(() => {
|
},
|
||||||
const d3Zoom = computed(() => store.d3Zoom)
|
fitView: (options: FitViewParams = { padding: DEFAULT_PADDING, includeHiddenNodes: false }) => {
|
||||||
const d3Selection = computed(() => store.d3Selection)
|
if (!nodes.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const createZoomPanHelper = (
|
const bounds = getRectOfNodes(options.includeHiddenNodes ? nodes : nodes.filter((node) => !node.isHidden))
|
||||||
d3Zoom: ZoomBehavior<Element, unknown>,
|
const [x, y, zoom] = getTransformForBounds(
|
||||||
d3Selection: Selection<Element, any, any, any>
|
bounds,
|
||||||
): ZoomPanHelperFunctions => ({
|
width,
|
||||||
zoomIn: () => d3Zoom?.scaleBy(d3Selection, 1.2),
|
height,
|
||||||
zoomOut: () => d3Zoom?.scaleBy(d3Selection, 1 / 1.2),
|
options.minZoom || minZoom,
|
||||||
zoomTo: (zoomLevel: number) => d3Zoom.scaleTo(d3Selection, zoomLevel),
|
options.maxZoom || maxZoom,
|
||||||
transform: (transform: FlowTransform) => {
|
options.padding || DEFAULT_PADDING,
|
||||||
const nextTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.zoom)
|
)
|
||||||
|
const transform = zoomIdentity.translate(x, y).scale(zoom)
|
||||||
|
|
||||||
d3Zoom.transform(d3Selection, nextTransform)
|
d3Zoom?.transform(d3Selection, transform)
|
||||||
},
|
},
|
||||||
fitView: (options: FitViewParams = { padding: DEFAULT_PADDING, includeHiddenNodes: false }) => {
|
setCenter: (x: number, y: number, zoom?: number) => {
|
||||||
const { nodes, width, height, minZoom, maxZoom } = store
|
const nextZoom = typeof zoom !== 'undefined' ? zoom : maxZoom
|
||||||
|
const centerX = width / 2 - x * nextZoom
|
||||||
|
const centerY = height / 2 - y * nextZoom
|
||||||
|
const transform = zoomIdentity.translate(centerX, centerY).scale(nextZoom)
|
||||||
|
|
||||||
if (!nodes.length) {
|
d3Zoom.transform(d3Selection, transform)
|
||||||
return
|
},
|
||||||
}
|
fitBounds: (bounds: Rect, padding = DEFAULT_PADDING) => {
|
||||||
|
const [x, y, zoom] = getTransformForBounds(bounds, width, height, minZoom, maxZoom, padding)
|
||||||
|
const transform = zoomIdentity.translate(x, y).scale(zoom)
|
||||||
|
|
||||||
const bounds = getRectOfNodes(options.includeHiddenNodes ? nodes : nodes.filter((node) => !node.isHidden))
|
d3Zoom.transform(d3Selection, transform)
|
||||||
const [x, y, zoom] = getTransformForBounds(
|
},
|
||||||
bounds,
|
project: (position: XYPosition) => {
|
||||||
width,
|
return pointToRendererPoint(position, transform, snapToGrid, snapGrid)
|
||||||
height,
|
},
|
||||||
options.minZoom || minZoom,
|
})
|
||||||
options.maxZoom || maxZoom,
|
|
||||||
options.padding || DEFAULT_PADDING
|
|
||||||
)
|
|
||||||
const transform = zoomIdentity.translate(x, y).scale(zoom)
|
|
||||||
|
|
||||||
d3Zoom?.transform(d3Selection, transform)
|
|
||||||
},
|
|
||||||
setCenter: (x: number, y: number, zoom?: number) => {
|
|
||||||
const { width, height, maxZoom } = store
|
|
||||||
|
|
||||||
const nextZoom = typeof zoom !== 'undefined' ? zoom : maxZoom
|
|
||||||
const centerX = width / 2 - x * nextZoom
|
|
||||||
const centerY = height / 2 - y * nextZoom
|
|
||||||
const transform = zoomIdentity.translate(centerX, centerY).scale(nextZoom)
|
|
||||||
|
|
||||||
d3Zoom.transform(d3Selection, transform)
|
|
||||||
},
|
|
||||||
fitBounds: (bounds: Rect, padding = DEFAULT_PADDING) => {
|
|
||||||
const { width, height, minZoom, maxZoom } = store
|
|
||||||
const [x, y, zoom] = getTransformForBounds(bounds, width, height, minZoom, maxZoom, padding)
|
|
||||||
const transform = zoomIdentity.translate(x, y).scale(zoom)
|
|
||||||
|
|
||||||
d3Zoom.transform(d3Selection, transform)
|
|
||||||
},
|
|
||||||
project: (position: XYPosition) => {
|
|
||||||
const { transform, snapToGrid, snapGrid } = store
|
|
||||||
return pointToRendererPoint(position, transform, snapToGrid, snapGrid)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
until(d3Zoom)
|
|
||||||
.toBeTruthy()
|
|
||||||
.then(() => {
|
|
||||||
if (d3Zoom.value && d3Selection.value) {
|
|
||||||
zoomPanHelperEvent.trigger(createZoomPanHelper(d3Zoom.value, d3Selection.value))
|
|
||||||
} else {
|
|
||||||
throw Error('D3 could not be initialized.')
|
|
||||||
}
|
|
||||||
scope.stop()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
onReady: zoomPanHelperEvent.on
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useZoomPanHelper
|
export default useZoomPanHelper
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { setActivePinia, createPinia, defineStore, StoreDefinition } from 'pinia'
|
import { setActivePinia, createPinia, defineStore, StoreDefinition } from 'pinia'
|
||||||
import isEqual from 'fast-deep-equal'
|
import isEqual from 'fast-deep-equal'
|
||||||
import { Edge, Node, NodeDiffUpdate, RevueFlowState, RevueFlowActions, XYPosition } from '../types'
|
import { Edge, Node, NodeDiffUpdate, RevueFlowState, RevueFlowActions, XYPosition } from '~/types'
|
||||||
import { getConnectedEdges, getNodesInside, getRectOfNodes, isEdge, isNode, parseEdge, parseNode } from '../utils/graph'
|
import { clampPosition, getDimensions } from '~/utils'
|
||||||
import { clampPosition, getDimensions } from '../utils'
|
import { getConnectedEdges, getNodesInside, getRectOfNodes, isEdge, isNode, parseEdge, parseNode } from '~/utils/graph'
|
||||||
import { getHandleBounds } from '../components/Nodes/utils'
|
import { getHandleBounds } from '~/components/Nodes/utils'
|
||||||
|
|
||||||
type NextElements = {
|
type NextElements = {
|
||||||
nextNodes: Node[]
|
nextNodes: Node[]
|
||||||
@@ -12,14 +12,14 @@ type NextElements = {
|
|||||||
const pinia = createPinia()
|
const pinia = createPinia()
|
||||||
|
|
||||||
export default function configureStore(
|
export default function configureStore(
|
||||||
preloadedState: RevueFlowState
|
preloadedState: RevueFlowState,
|
||||||
): StoreDefinition<string, RevueFlowState, any, RevueFlowActions> {
|
): StoreDefinition<string, RevueFlowState, any, RevueFlowActions> {
|
||||||
setActivePinia(pinia)
|
setActivePinia(pinia)
|
||||||
|
|
||||||
return defineStore({
|
return defineStore({
|
||||||
id: 'revue-flow-' + Math.random(),
|
id: `revue-flow-${Math.random()}`,
|
||||||
state: () => ({
|
state: () => ({
|
||||||
...preloadedState
|
...preloadedState,
|
||||||
}),
|
}),
|
||||||
getters: {},
|
getters: {},
|
||||||
actions: {
|
actions: {
|
||||||
@@ -27,7 +27,7 @@ export default function configureStore(
|
|||||||
const propElements = elements
|
const propElements = elements
|
||||||
const nextElements: NextElements = {
|
const nextElements: NextElements = {
|
||||||
nextNodes: [],
|
nextNodes: [],
|
||||||
nextEdges: []
|
nextEdges: [],
|
||||||
}
|
}
|
||||||
const { nextNodes, nextEdges } = propElements.reduce((res, propElement): NextElements => {
|
const { nextNodes, nextEdges } = propElements.reduce((res, propElement): NextElements => {
|
||||||
if (isNode(propElement)) {
|
if (isNode(propElement)) {
|
||||||
@@ -36,7 +36,7 @@ export default function configureStore(
|
|||||||
if (storeNode) {
|
if (storeNode) {
|
||||||
const updatedNode: Node = {
|
const updatedNode: Node = {
|
||||||
...storeNode,
|
...storeNode,
|
||||||
...propElement
|
...propElement,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (storeNode.position.x !== propElement.position.x || storeNode.position.y !== propElement.position.y) {
|
if (storeNode.position.x !== propElement.position.x || storeNode.position.y !== propElement.position.y) {
|
||||||
@@ -59,7 +59,7 @@ export default function configureStore(
|
|||||||
if (storeEdge) {
|
if (storeEdge) {
|
||||||
res.nextEdges.push({
|
res.nextEdges.push({
|
||||||
...storeEdge,
|
...storeEdge,
|
||||||
...propElement
|
...propElement,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
res.nextEdges.push(parseEdge(propElement))
|
res.nextEdges.push(parseEdge(propElement))
|
||||||
@@ -90,8 +90,8 @@ export default function configureStore(
|
|||||||
__rf: {
|
__rf: {
|
||||||
...node.__rf,
|
...node.__rf,
|
||||||
...dimensions,
|
...dimensions,
|
||||||
handleBounds
|
handleBounds,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,7 +107,7 @@ export default function configureStore(
|
|||||||
const [gridSizeX, gridSizeY] = this.snapGrid
|
const [gridSizeX, gridSizeY] = this.snapGrid
|
||||||
position = {
|
position = {
|
||||||
x: gridSizeX * Math.round(pos.x / gridSizeX),
|
x: gridSizeX * Math.round(pos.x / gridSizeX),
|
||||||
y: gridSizeY * Math.round(pos.y / gridSizeY)
|
y: gridSizeY * Math.round(pos.y / gridSizeY),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,8 +117,8 @@ export default function configureStore(
|
|||||||
...node,
|
...node,
|
||||||
__rf: {
|
__rf: {
|
||||||
...node.__rf,
|
...node.__rf,
|
||||||
position
|
position,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,14 +134,14 @@ export default function configureStore(
|
|||||||
...node,
|
...node,
|
||||||
__rf: {
|
__rf: {
|
||||||
...node.__rf,
|
...node.__rf,
|
||||||
isDragging
|
isDragging,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if (diff) {
|
if (diff) {
|
||||||
updatedNode.__rf.position = {
|
updatedNode.__rf.position = {
|
||||||
x: node.__rf.position.x + diff.x,
|
x: node.__rf.position.x + diff.x,
|
||||||
y: node.__rf.position.y + diff.y
|
y: node.__rf.position.y + diff.y,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ export default function configureStore(
|
|||||||
startY: mousePos.y,
|
startY: mousePos.y,
|
||||||
x: mousePos.x,
|
x: mousePos.x,
|
||||||
y: mousePos.y,
|
y: mousePos.y,
|
||||||
draw: true
|
draw: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateUserSelection(mousePos) {
|
updateUserSelection(mousePos) {
|
||||||
@@ -172,7 +172,7 @@ export default function configureStore(
|
|||||||
x: mousePos.x < startX ? mousePos.x : this.userSelectionRect.x,
|
x: mousePos.x < startX ? mousePos.x : this.userSelectionRect.x,
|
||||||
y: mousePos.y < startY ? mousePos.y : this.userSelectionRect.y,
|
y: mousePos.y < startY ? mousePos.y : this.userSelectionRect.y,
|
||||||
width: Math.abs(mousePos.x - startX),
|
width: Math.abs(mousePos.x - startX),
|
||||||
height: Math.abs(mousePos.y - startY)
|
height: Math.abs(mousePos.y - startY),
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedNodes = getNodesInside(this.nodes, nextUserSelectRect, this.transform)
|
const selectedNodes = getNodesInside(this.nodes, nextUserSelectRect, this.transform)
|
||||||
@@ -231,8 +231,8 @@ export default function configureStore(
|
|||||||
...node,
|
...node,
|
||||||
__rf: {
|
__rf: {
|
||||||
...node.__rf,
|
...node.__rf,
|
||||||
position: clampPosition(node.__rf.position, nodeExtent)
|
position: clampPosition(node.__rf.position, nodeExtent),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -255,7 +255,7 @@ export default function configureStore(
|
|||||||
this.nodesDraggable = isInteractive
|
this.nodesDraggable = isInteractive
|
||||||
this.nodesConnectable = isInteractive
|
this.nodesConnectable = isInteractive
|
||||||
this.elementsSelectable = isInteractive
|
this.elementsSelectable = isInteractive
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -16,12 +16,12 @@ export const initialState: RevueFlowState = {
|
|||||||
maxZoom: 2,
|
maxZoom: 2,
|
||||||
translateExtent: [
|
translateExtent: [
|
||||||
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
||||||
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY]
|
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
||||||
],
|
],
|
||||||
|
|
||||||
nodeExtent: [
|
nodeExtent: [
|
||||||
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
||||||
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY]
|
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
||||||
],
|
],
|
||||||
|
|
||||||
nodesSelectionActive: false,
|
nodesSelectionActive: false,
|
||||||
@@ -34,7 +34,7 @@ export const initialState: RevueFlowState = {
|
|||||||
y: 0,
|
y: 0,
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
draw: false
|
draw: false,
|
||||||
},
|
},
|
||||||
connectionNodeId: null,
|
connectionNodeId: null,
|
||||||
connectionHandleId: null,
|
connectionHandleId: null,
|
||||||
@@ -51,5 +51,5 @@ export const initialState: RevueFlowState = {
|
|||||||
|
|
||||||
multiSelectionActive: false,
|
multiSelectionActive: false,
|
||||||
|
|
||||||
revueFlowVersion: typeof __REVUE_FLOW_VERSION__ !== 'undefined' ? __REVUE_FLOW_VERSION__ : '-'
|
revueFlowVersion: typeof __REVUE_FLOW_VERSION__ !== 'undefined' ? __REVUE_FLOW_VERSION__ : '-',
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
import { ComponentPublicInstance, HTMLAttributes, VNode } from 'vue'
|
import { DefineComponent, HTMLAttributes, VNode } from 'vue'
|
||||||
import { ArrowHeadType, ElementId, Position } from './types'
|
import { ArrowHeadType, ElementId, Position } from './types'
|
||||||
import { Connection } from './connection'
|
import { Connection } from './connection'
|
||||||
|
|
||||||
@@ -64,6 +64,6 @@ export interface EdgeTextProps extends HTMLAttributes {
|
|||||||
labelBgBorderRadius?: number
|
labelBgBorderRadius?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EdgeType = ComponentPublicInstance<EdgeSmoothStepProps>
|
export type EdgeType = DefineComponent<EdgeSmoothStepProps>
|
||||||
|
|
||||||
export type OnEdgeUpdateFunc<T = any> = (oldEdge: Edge<T>, newConnection: Connection) => void
|
export type OnEdgeUpdateFunc<T = any> = (oldEdge: Edge<T>, newConnection: Connection) => void
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
import { ComponentPublicInstance } from 'vue'
|
import { DefineComponent } from 'vue'
|
||||||
import { XYPosition } from './types'
|
import { XYPosition } from './types'
|
||||||
import { ElementId, Position } from './index'
|
import { ElementId, Position } from './index'
|
||||||
|
|
||||||
@@ -57,4 +57,4 @@ export interface NodeProps<T = any> {
|
|||||||
isDragging?: boolean
|
isDragging?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NodeType = ComponentPublicInstance<NodeProps>
|
export type NodeType = DefineComponent<NodeProps>
|
||||||
|
|||||||
+43
-2
@@ -1,5 +1,8 @@
|
|||||||
import { Edge } from './edge'
|
import { CSSProperties, HTMLAttributes } from 'vue'
|
||||||
import { Node } from './node'
|
import { Edge, EdgeType } from './edge'
|
||||||
|
import { Node, NodeExtent, NodeType, TranslateExtent } from './node'
|
||||||
|
import { ConnectionLineComponent, ConnectionLineType, ConnectionMode } from '~/types/connection'
|
||||||
|
import { KeyCode, PanOnScrollMode } from '~/types/panel'
|
||||||
|
|
||||||
export type ElementId = string
|
export type ElementId = string
|
||||||
|
|
||||||
@@ -81,3 +84,41 @@ export type OnLoadParams<T = any> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type OnLoadFunc<T = any> = (params: OnLoadParams<T>) => void
|
export type OnLoadFunc<T = any> = (params: OnLoadParams<T>) => void
|
||||||
|
|
||||||
|
export interface RevueFlowOptions extends Omit<HTMLAttributes, 'onLoad'> {
|
||||||
|
nodeTypes?: Record<string, NodeType>
|
||||||
|
edgeTypes?: Record<string, EdgeType>
|
||||||
|
connectionMode?: ConnectionMode
|
||||||
|
connectionLineType?: ConnectionLineType
|
||||||
|
connectionLineStyle?: CSSProperties
|
||||||
|
connectionLineComponent?: ConnectionLineComponent
|
||||||
|
deleteKeyCode?: KeyCode
|
||||||
|
selectionKeyCode?: KeyCode
|
||||||
|
multiSelectionKeyCode?: KeyCode
|
||||||
|
zoomActivationKeyCode?: KeyCode
|
||||||
|
snapToGrid?: boolean
|
||||||
|
snapGrid?: [number, number]
|
||||||
|
onlyRenderVisibleElements?: boolean
|
||||||
|
nodesDraggable?: boolean
|
||||||
|
nodesConnectable?: boolean
|
||||||
|
elementsSelectable?: boolean
|
||||||
|
selectNodesOnDrag?: boolean
|
||||||
|
paneMoveable?: boolean
|
||||||
|
minZoom?: number
|
||||||
|
maxZoom?: number
|
||||||
|
defaultZoom?: number
|
||||||
|
defaultPosition?: [number, number]
|
||||||
|
translateExtent?: TranslateExtent
|
||||||
|
nodeExtent?: NodeExtent
|
||||||
|
arrowHeadColor?: string
|
||||||
|
markerEndId?: string
|
||||||
|
zoomOnScroll?: boolean
|
||||||
|
zoomOnPinch?: boolean
|
||||||
|
panOnScroll?: boolean
|
||||||
|
panOnScrollSpeed?: number
|
||||||
|
panOnScrollMode?: PanOnScrollMode
|
||||||
|
zoomOnDoubleClick?: boolean
|
||||||
|
edgeUpdaterRadius?: number
|
||||||
|
// nodeTypesId?: string
|
||||||
|
// edgeTypesId?: string / used by react-flow to detect a re-render of node components
|
||||||
|
}
|
||||||
|
|||||||
+58
-73
@@ -1,5 +1,3 @@
|
|||||||
import { clampPosition, clamp } from './index'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ElementId,
|
ElementId,
|
||||||
Node,
|
Node,
|
||||||
@@ -12,8 +10,9 @@ import {
|
|||||||
Connection,
|
Connection,
|
||||||
FlowExportObject,
|
FlowExportObject,
|
||||||
NodeExtent,
|
NodeExtent,
|
||||||
RevueFlowStore
|
RevueFlowStore,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
|
import { clampPosition, clamp } from './index'
|
||||||
|
|
||||||
export const isEdge = (element: Node | Connection | Edge): element is Edge =>
|
export const isEdge = (element: Node | Connection | Edge): element is Edge =>
|
||||||
'id' in element && 'source' in element && 'target' in element
|
'id' in element && 'source' in element && 'target' in element
|
||||||
@@ -62,7 +61,7 @@ const connectionExists = (edge: Edge, elements: Elements) => {
|
|||||||
el.source === edge.source &&
|
el.source === edge.source &&
|
||||||
el.target === edge.target &&
|
el.target === edge.target &&
|
||||||
(el.sourceHandle === edge.sourceHandle || (!el.sourceHandle && !edge.sourceHandle)) &&
|
(el.sourceHandle === edge.sourceHandle || (!el.sourceHandle && !edge.sourceHandle)) &&
|
||||||
(el.targetHandle === edge.targetHandle || (!el.targetHandle && !edge.targetHandle))
|
(el.targetHandle === edge.targetHandle || (!el.targetHandle && !edge.targetHandle)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +77,7 @@ export const addEdge = (edgeParams: Edge | Connection, elements: Elements): Elem
|
|||||||
} else {
|
} else {
|
||||||
edge = {
|
edge = {
|
||||||
...edgeParams,
|
...edgeParams,
|
||||||
id: getEdgeId(edgeParams)
|
id: getEdgeId(edgeParams),
|
||||||
} as Edge
|
} as Edge
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +108,7 @@ export const updateEdge = (oldEdge: Edge, newConnection: Connection, elements: E
|
|||||||
source: newConnection.source,
|
source: newConnection.source,
|
||||||
target: newConnection.target,
|
target: newConnection.target,
|
||||||
sourceHandle: newConnection.sourceHandle,
|
sourceHandle: newConnection.sourceHandle,
|
||||||
targetHandle: newConnection.targetHandle
|
targetHandle: newConnection.targetHandle,
|
||||||
} as Edge
|
} as Edge
|
||||||
|
|
||||||
return elements.filter((e) => e.id !== oldEdge.id).concat(edge)
|
return elements.filter((e) => e.id !== oldEdge.id).concat(edge)
|
||||||
@@ -119,75 +118,70 @@ export const pointToRendererPoint = (
|
|||||||
{ x, y }: XYPosition,
|
{ x, y }: XYPosition,
|
||||||
[tx, ty, tScale]: Transform,
|
[tx, ty, tScale]: Transform,
|
||||||
snapToGrid: boolean,
|
snapToGrid: boolean,
|
||||||
[snapX, snapY]: [number, number]
|
[snapX, snapY]: [number, number],
|
||||||
): XYPosition => {
|
): XYPosition => {
|
||||||
const position: XYPosition = {
|
const position: XYPosition = {
|
||||||
x: (x - tx) / tScale,
|
x: (x - tx) / tScale,
|
||||||
y: (y - ty) / tScale
|
y: (y - ty) / tScale,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (snapToGrid) {
|
if (snapToGrid) {
|
||||||
return {
|
return {
|
||||||
x: snapX * Math.round(position.x / snapX),
|
x: snapX * Math.round(position.x / snapX),
|
||||||
y: snapY * Math.round(position.y / snapY)
|
y: snapY * Math.round(position.y / snapY),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return position
|
return position
|
||||||
}
|
}
|
||||||
|
|
||||||
export const onLoadProject = (currentStore: RevueFlowStore) => {
|
export const onLoadProject =
|
||||||
return (position: XYPosition): XYPosition => {
|
(currentStore: RevueFlowStore) =>
|
||||||
return pointToRendererPoint(position, currentStore.transform, currentStore.snapToGrid, currentStore.snapGrid)
|
(position: XYPosition): XYPosition =>
|
||||||
}
|
pointToRendererPoint(position, currentStore.transform, currentStore.snapToGrid, currentStore.snapGrid)
|
||||||
}
|
|
||||||
|
|
||||||
export const parseNode = (node: Node, nodeExtent: NodeExtent): Node => {
|
export const parseNode = (node: Node, nodeExtent: NodeExtent): Node => ({
|
||||||
return {
|
...node,
|
||||||
...node,
|
id: node.id.toString(),
|
||||||
id: node.id.toString(),
|
type: node.type || 'default',
|
||||||
type: node.type || 'default',
|
__rf: {
|
||||||
__rf: {
|
position: clampPosition(node.position, nodeExtent),
|
||||||
position: clampPosition(node.position, nodeExtent),
|
width: null,
|
||||||
width: null,
|
height: null,
|
||||||
height: null,
|
handleBounds: {},
|
||||||
handleBounds: {},
|
isDragging: false,
|
||||||
isDragging: false
|
},
|
||||||
}
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const parseEdge = (edge: Edge): Edge => {
|
export const parseEdge = (edge: Edge): Edge => ({
|
||||||
return {
|
...edge,
|
||||||
...edge,
|
source: edge.source.toString(),
|
||||||
source: edge.source.toString(),
|
target: edge.target.toString(),
|
||||||
target: edge.target.toString(),
|
sourceHandle: edge.sourceHandle ? edge.sourceHandle.toString() : null,
|
||||||
sourceHandle: edge.sourceHandle ? edge.sourceHandle.toString() : null,
|
targetHandle: edge.targetHandle ? edge.targetHandle.toString() : null,
|
||||||
targetHandle: edge.targetHandle ? edge.targetHandle.toString() : null,
|
id: edge.id.toString(),
|
||||||
id: edge.id.toString(),
|
type: edge.type || 'default',
|
||||||
type: edge.type || 'default'
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getBoundsOfBoxes = (box1: Box, box2: Box): Box => ({
|
const getBoundsOfBoxes = (box1: Box, box2: Box): Box => ({
|
||||||
x: Math.min(box1.x, box2.x),
|
x: Math.min(box1.x, box2.x),
|
||||||
y: Math.min(box1.y, box2.y),
|
y: Math.min(box1.y, box2.y),
|
||||||
x2: Math.max(box1.x2, box2.x2),
|
x2: Math.max(box1.x2, box2.x2),
|
||||||
y2: Math.max(box1.y2, box2.y2)
|
y2: Math.max(box1.y2, box2.y2),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const rectToBox = ({ x, y, width, height }: Rect): Box => ({
|
export const rectToBox = ({ x, y, width, height }: Rect): Box => ({
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
x2: x + width,
|
x2: x + width,
|
||||||
y2: y + height
|
y2: y + height,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({
|
export const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
width: x2 - x,
|
width: x2 - x,
|
||||||
height: y2 - y
|
height: y2 - y,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getBoundsofRects = (rect1: Rect, rect2: Rect): Rect =>
|
export const getBoundsofRects = (rect1: Rect, rect2: Rect): Rect =>
|
||||||
@@ -201,10 +195,10 @@ export const getRectOfNodes = (nodes: Node[]): Rect => {
|
|||||||
rectToBox({
|
rectToBox({
|
||||||
...position,
|
...position,
|
||||||
width,
|
width,
|
||||||
height
|
height,
|
||||||
} as any)
|
} as any),
|
||||||
),
|
),
|
||||||
{ x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity }
|
{ x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity },
|
||||||
)
|
)
|
||||||
|
|
||||||
return boxToRect(box)
|
return boxToRect(box)
|
||||||
@@ -212,7 +206,7 @@ export const getRectOfNodes = (nodes: Node[]): Rect => {
|
|||||||
|
|
||||||
export const graphPosToZoomedPos = ({ x, y }: XYPosition, [tx, ty, tScale]: Transform): XYPosition => ({
|
export const graphPosToZoomedPos = ({ x, y }: XYPosition, [tx, ty, tScale]: Transform): XYPosition => ({
|
||||||
x: x * tScale + tx,
|
x: x * tScale + tx,
|
||||||
y: y * tScale + ty
|
y: y * tScale + ty,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getNodesInside = (nodes: Node[], rect: Rect, [tx, ty, tScale]: Transform = [0, 0, 1], partially = false): Node[] => {
|
export const getNodesInside = (nodes: Node[], rect: Rect, [tx, ty, tScale]: Transform = [0, 0, 1], partially = false): Node[] => {
|
||||||
@@ -220,7 +214,7 @@ export const getNodesInside = (nodes: Node[], rect: Rect, [tx, ty, tScale]: Tran
|
|||||||
x: (rect.x - tx) / tScale,
|
x: (rect.x - tx) / tScale,
|
||||||
y: (rect.y - ty) / tScale,
|
y: (rect.y - ty) / tScale,
|
||||||
width: rect.width / tScale,
|
width: rect.width / tScale,
|
||||||
height: rect.height / tScale
|
height: rect.height / tScale,
|
||||||
})
|
})
|
||||||
|
|
||||||
return nodes.filter(({ __rf: { position, width, height, isDragging } }) => {
|
return nodes.filter(({ __rf: { position, width, height, isDragging } }) => {
|
||||||
@@ -250,35 +244,26 @@ export const getConnectedEdges = (nodes: Node[], edges: Edge[]): Edge[] => {
|
|||||||
return edges.filter((edge) => nodeIds.includes(edge.source) || nodeIds.includes(edge.target))
|
return edges.filter((edge) => nodeIds.includes(edge.source) || nodeIds.includes(edge.target))
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseElements = (nodes: Node[], edges: Edge[]): Elements => {
|
const parseElements = (nodes: Node[], edges: Edge[]): Elements => [
|
||||||
return [
|
...nodes.map((node) => {
|
||||||
...nodes.map((node) => {
|
const n = { ...node }
|
||||||
const n = { ...node }
|
|
||||||
|
|
||||||
n.position = n.__rf.position
|
n.position = n.__rf.position
|
||||||
|
|
||||||
n.__rf = {} as any
|
n.__rf = {} as any
|
||||||
return n
|
return n
|
||||||
}),
|
}),
|
||||||
...edges.map((e) => ({ ...e }))
|
...edges.map((e) => ({ ...e })),
|
||||||
]
|
]
|
||||||
}
|
|
||||||
|
|
||||||
export const onLoadGetElements = (currentStore: RevueFlowStore) => {
|
export const onLoadGetElements = (currentStore: RevueFlowStore) => (): Elements =>
|
||||||
return (): Elements => {
|
parseElements(currentStore.nodes || [], currentStore.edges || [])
|
||||||
return parseElements(currentStore.nodes || [], currentStore.edges || [])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const onLoadToObject = (currentStore: RevueFlowStore) => {
|
export const onLoadToObject = (currentStore: RevueFlowStore) => (): FlowExportObject => ({
|
||||||
return (): FlowExportObject => {
|
elements: parseElements(currentStore.nodes || [], currentStore.edges || []),
|
||||||
return {
|
position: [currentStore.transform[0], currentStore.transform[1]],
|
||||||
elements: parseElements(currentStore.nodes || [], currentStore.edges || []),
|
zoom: currentStore.transform[2],
|
||||||
position: [currentStore.transform[0], currentStore.transform[1]],
|
})
|
||||||
zoom: currentStore.transform[2]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getTransformForBounds = (
|
export const getTransformForBounds = (
|
||||||
bounds: Rect,
|
bounds: Rect,
|
||||||
@@ -286,7 +271,7 @@ export const getTransformForBounds = (
|
|||||||
height: number,
|
height: number,
|
||||||
minZoom: number,
|
minZoom: number,
|
||||||
maxZoom: number,
|
maxZoom: number,
|
||||||
padding = 0.1
|
padding = 0.1,
|
||||||
): Transform => {
|
): Transform => {
|
||||||
const xZoom = width / (bounds.width * (1 + padding))
|
const xZoom = width / (bounds.width * (1 + padding))
|
||||||
const yZoom = height / (bounds.height * (1 + padding))
|
const yZoom = height / (bounds.height * (1 + padding))
|
||||||
|
|||||||
+3
-3
@@ -5,16 +5,16 @@ export const isInputDOMNode = (e: KeyboardEvent | MouseEvent): boolean => {
|
|||||||
return ['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].includes(target.nodeName) || target.hasAttribute('contentEditable')
|
return ['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].includes(target.nodeName) || target.hasAttribute('contentEditable')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getDimensions = (node: HTMLDivElement): Dimensions => ({
|
export const getDimensions = (node: HTMLElement): Dimensions => ({
|
||||||
width: node.offsetWidth,
|
width: node.offsetWidth,
|
||||||
height: node.offsetHeight
|
height: node.offsetHeight,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const clamp = (val: number, min = 0, max = 1): number => Math.min(Math.max(val, min), max)
|
export const clamp = (val: number, min = 0, max = 1): number => Math.min(Math.max(val, min), max)
|
||||||
|
|
||||||
export const clampPosition = (position: XYPosition, extent: NodeExtent): XYPosition => ({
|
export const clampPosition = (position: XYPosition, extent: NodeExtent): XYPosition => ({
|
||||||
x: clamp(position.x, extent[0][0], extent[1][0]),
|
x: clamp(position.x, extent[0][0], extent[1][0]),
|
||||||
y: clamp(position.y, extent[0][1], extent[1][1])
|
y: clamp(position.y, extent[0][1], extent[1][1]),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getHostForElement = (element: HTMLElement): Document | ShadowRoot =>
|
export const getHostForElement = (element: HTMLElement): Document | ShadowRoot =>
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"target": "es2016",
|
"target": "es2020",
|
||||||
"lib": [
|
"lib": [
|
||||||
"DOM",
|
"DOM",
|
||||||
"ESNext"
|
"ESNext"
|
||||||
|
|||||||
Reference in New Issue
Block a user