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:
41
.eslintrc.js
41
.eslintrc.js
@@ -1,42 +1,9 @@
|
||||
const baseRules = {
|
||||
'@typescript-eslint/no-explicit-any': 0,
|
||||
'@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 }]
|
||||
'prettier/prettier': ['error', {}, { usePrettierrc: true }],
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
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
|
||||
extends: ['@antfu', 'plugin:prettier/recommended'],
|
||||
plugins: ['prettier'],
|
||||
rules: baseRules,
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"trailingComma": "all",
|
||||
"semi": false,
|
||||
"quoteProps": "consistent",
|
||||
"bracketSpacing": true,
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"printWidth": 130
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"pinia": "^2.0.0-rc.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^0.9.0",
|
||||
"@babel/core": "^7.15.8",
|
||||
"@rollup/plugin-babel": "^5.3.0",
|
||||
"@rollup/plugin-commonjs": "^19.0.2",
|
||||
@@ -79,6 +80,7 @@
|
||||
"vite": "^2.6.7",
|
||||
"vite-svg-loader": "^2.2.0",
|
||||
"vue": "^3.2.20",
|
||||
"vue-eslint-parser": "^7.11.0",
|
||||
"vue-router": "^4.0.12",
|
||||
"vue-tsc": "^0.2.3"
|
||||
},
|
||||
|
||||
774
pnpm-lock.yaml
generated
774
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
12
src/auto-imports.d.ts
vendored
12
src/auto-imports.d.ts
vendored
@@ -16,6 +16,7 @@ declare global {
|
||||
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 extendRef: typeof import('@vueuse/core')['extendRef']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const h: typeof import('vue')['h']
|
||||
@@ -60,6 +61,7 @@ declare global {
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
|
||||
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
|
||||
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
|
||||
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
|
||||
@@ -78,12 +80,14 @@ declare global {
|
||||
const useCssVar: typeof import('@vueuse/core')['useCssVar']
|
||||
const useDark: typeof import('@vueuse/core')['useDark']
|
||||
const useDebounce: typeof import('@vueuse/core')['useDebounce']
|
||||
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
|
||||
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
|
||||
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
|
||||
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
|
||||
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
|
||||
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
|
||||
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
|
||||
const useDraggable: typeof import('@vueuse/core')['useDraggable']
|
||||
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
|
||||
const useElementSize: typeof import('@vueuse/core')['useElementSize']
|
||||
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
|
||||
@@ -98,6 +102,7 @@ declare global {
|
||||
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']
|
||||
@@ -114,6 +119,7 @@ declare global {
|
||||
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']
|
||||
@@ -122,6 +128,7 @@ declare global {
|
||||
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
||||
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
|
||||
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
|
||||
const useScroll: typeof import('@vueuse/core')['useScroll']
|
||||
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
|
||||
const useShare: typeof import('@vueuse/core')['useShare']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
@@ -130,6 +137,7 @@ declare global {
|
||||
const useSwipe: typeof import('@vueuse/core')['useSwipe']
|
||||
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
|
||||
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']
|
||||
@@ -140,8 +148,10 @@ declare global {
|
||||
const useTransition: typeof import('@vueuse/core')['useTransition']
|
||||
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
|
||||
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
|
||||
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 useWebSocket: typeof import('@vueuse/core')['useWebSocket']
|
||||
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
|
||||
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
|
||||
@@ -149,7 +159,9 @@ declare global {
|
||||
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
|
||||
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchOnce: typeof import('@vueuse/core')['watchOnce']
|
||||
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
|
||||
const whenever: typeof import('@vueuse/core')['whenever']
|
||||
}
|
||||
|
||||
187
src/composables/useZoom.ts
Normal file
187
src/composables/useZoom.ts
Normal file
@@ -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
|
||||
49
src/container/EdgeRenderer.vue
Normal file
49
src/container/EdgeRenderer.vue
Normal file
@@ -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 { rectToBox } from '../../utils/graph'
|
||||
import { Position, Node, XYPosition, ElementId, HandleElement, Transform, Edge, EdgeType } from '../../types'
|
||||
import { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '~/components/Edges'
|
||||
import { rectToBox } from '~/utils/graph'
|
||||
import { Position, Node, XYPosition, ElementId, HandleElement, Transform, Edge, EdgeType } from '~/types'
|
||||
|
||||
export function createEdgeTypes(edgeTypes: Record<string, EdgeType>): Record<string, EdgeType> {
|
||||
const standardTypes: Record<string, EdgeType> = {
|
||||
default: edgeTypes.default || BezierEdge,
|
||||
straight: edgeTypes.bezier || StraightEdge,
|
||||
step: edgeTypes.step || StepEdge,
|
||||
smoothstep: edgeTypes.step || SmoothStepEdge
|
||||
smoothstep: edgeTypes.step || SmoothStepEdge,
|
||||
}
|
||||
|
||||
const wrappedTypes = {} as Record<string, EdgeType>
|
||||
@@ -21,7 +21,7 @@ export function createEdgeTypes(edgeTypes: Record<string, EdgeType>): Record<str
|
||||
|
||||
return {
|
||||
...standardTypes,
|
||||
...specialTypes
|
||||
...specialTypes,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,22 +35,22 @@ export function getHandlePosition(position: Position, node: Node, handle: any |
|
||||
case Position.Top:
|
||||
return {
|
||||
x: x + width / 2,
|
||||
y
|
||||
y,
|
||||
}
|
||||
case Position.Right:
|
||||
return {
|
||||
x: x + width,
|
||||
y: y + height / 2
|
||||
y: y + height / 2,
|
||||
}
|
||||
case Position.Bottom:
|
||||
return {
|
||||
x: x + width / 2,
|
||||
y: y + height
|
||||
y: y + height,
|
||||
}
|
||||
case Position.Left:
|
||||
return {
|
||||
x,
|
||||
y: y + height / 2
|
||||
y: y + height / 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ export const getEdgePositions = (
|
||||
sourcePosition: Position,
|
||||
targetNode: Node,
|
||||
targetHandle: HandleElement | unknown,
|
||||
targetPosition: Position
|
||||
targetPosition: Position,
|
||||
): EdgePositions => {
|
||||
const sourceHandlePos = getHandlePosition(sourcePosition, sourceNode, sourceHandle)
|
||||
const targetHandlePos = getHandlePosition(targetPosition, targetNode, targetHandle)
|
||||
@@ -94,7 +94,7 @@ export const getEdgePositions = (
|
||||
sourceX: sourceHandlePos.x,
|
||||
sourceY: sourceHandlePos.y,
|
||||
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),
|
||||
y: Math.min(sourcePos.y, targetPos.y),
|
||||
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) {
|
||||
@@ -126,7 +126,7 @@ export function isEdgeVisible({ sourcePos, targetPos, width, height, transform }
|
||||
x: (0 - transform[0]) / transform[2],
|
||||
y: (0 - transform[1]) / 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))
|
||||
@@ -152,6 +152,6 @@ export const getSourceTargetNodes = (edge: Edge, nodes: Node[]): SourceTargetNod
|
||||
}
|
||||
return res
|
||||
},
|
||||
{ sourceNode: null, targetNode: null } as SourceTargetNode
|
||||
{ sourceNode: null, targetNode: null } as SourceTargetNode,
|
||||
)
|
||||
}
|
||||
|
||||
215
src/container/Flow.vue
Normal file
215
src/container/Flow.vue
Normal file
@@ -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>
|
||||
21
src/container/Marker.vue
Normal file
21
src/container/Marker.vue
Normal file
@@ -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>
|
||||
35
src/container/MarkerDefinitions.vue
Normal file
35
src/container/MarkerDefinitions.vue
Normal file
@@ -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>
|
||||
74
src/container/NodeRenderer.vue
Normal file
74
src/container/NodeRenderer.vue
Normal file
@@ -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 InputNode from '../../components/Nodes/InputNode'
|
||||
import OutputNode from '../../components/Nodes/OutputNode'
|
||||
import { NodeType } from '../../types'
|
||||
import DefaultNode from '~/components/Nodes/DefaultNode'
|
||||
import InputNode from '~/components/Nodes/InputNode'
|
||||
import OutputNode from '~/components/Nodes/OutputNode'
|
||||
import { NodeType } from '~/types'
|
||||
|
||||
export function createNodeTypes(nodeTypes: Record<string, NodeType>): Record<string, NodeType> {
|
||||
const standardTypes: Record<string, NodeType> = {
|
||||
input: nodeTypes.input || InputNode,
|
||||
default: nodeTypes.default || DefaultNode,
|
||||
output: nodeTypes.output || OutputNode
|
||||
output: nodeTypes.output || OutputNode,
|
||||
}
|
||||
|
||||
const wrappedTypes = {} as NodeType
|
||||
@@ -21,6 +21,6 @@ export function createNodeTypes(nodeTypes: Record<string, NodeType>): Record<str
|
||||
|
||||
return {
|
||||
...standardTypes,
|
||||
...specialTypes
|
||||
...specialTypes,
|
||||
}
|
||||
}
|
||||
|
||||
46
src/container/SelectionPane.vue
Normal file
46
src/container/SelectionPane.vue
Normal file
@@ -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>
|
||||
54
src/container/ZoomPane.vue
Normal file
54
src/container/ZoomPane.vue
Normal file
@@ -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
|
||||
import { EventHook } from '@vueuse/core'
|
||||
import { Connection, Edge, Elements, FlowTransform, Node, OnConnectStartParams, OnLoadParams } from '../types'
|
||||
import { createEventHook, EventHook } from '@vueuse/core'
|
||||
|
||||
export interface RevueFlowHooks {
|
||||
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 }>(),
|
||||
edgeClick: 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) => {
|
||||
@@ -99,6 +99,6 @@ export const useRevueFlow = (): RevueFlowHooks & { bind: (emit: (event: string,
|
||||
|
||||
return {
|
||||
...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 useKeyPress from './useKeyPress'
|
||||
import { isNode, getConnectedEdges } from '~/utils/graph'
|
||||
import { Elements, KeyCode, ElementId, FlowElement, RevueFlowStore, Edge } from '~/types'
|
||||
|
||||
interface HookParams {
|
||||
store: RevueFlowStore
|
||||
deleteKeyCode: KeyCode
|
||||
multiSelectionKeyCode: KeyCode
|
||||
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 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) {
|
||||
const selectedNodes = selectedElements.value.filter(isNode)
|
||||
const connectedEdges = getConnectedEdges(selectedNodes, edges.value as Edge[])
|
||||
const elementsToRemove = [...selectedElements.value, ...connectedEdges].reduce(
|
||||
(res, item) => res.set(item.id, item),
|
||||
new Map<ElementId, FlowElement>()
|
||||
new Map<ElementId, FlowElement>(),
|
||||
)
|
||||
|
||||
onElementsRemove(Array.from(elementsToRemove.values()))
|
||||
@@ -31,7 +32,8 @@ export default ({ store, deleteKeyCode, multiSelectionKeyCode, onElementsRemove
|
||||
}
|
||||
})
|
||||
|
||||
watch(multiSelectionKeyPressed, () => {
|
||||
watch(multiSelectionKeyPressed, () => {
|
||||
store.multiSelectionActive = multiSelectionKeyPressed.value
|
||||
})
|
||||
*/
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ import { Ref } from 'vue'
|
||||
import { onKeyDown, onKeyPressed, onKeyUp } from '@vueuse/core'
|
||||
import { KeyCode } from '~/types'
|
||||
|
||||
export default (keyCode: KeyCode, cb?: (keyPress: boolean) => void): Ref<boolean> => {
|
||||
const keyPressed = controlledRef<boolean>(false, {
|
||||
export default (keyCode: KeyCode, onChange?: (keyPressed: boolean) => void): Ref<boolean> => {
|
||||
const isPressed = controlledRef<boolean>(false, {
|
||||
onBeforeChange(val, oldVal) {
|
||||
if (val === oldVal) return false
|
||||
},
|
||||
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.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.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,
|
||||
(e) => {
|
||||
e.preventDefault()
|
||||
keyPressed.value = false
|
||||
isPressed.value = false
|
||||
},
|
||||
)
|
||||
|
||||
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 { getDimensions } from '../utils'
|
||||
import { Dimensions } from '../types'
|
||||
import { Ref } from 'vue'
|
||||
import { getDimensions } from '~/utils'
|
||||
import { Dimensions } from '~/types'
|
||||
|
||||
export default (rendererNode: MaybeRef<HTMLDivElement>, updateSize: (size: Dimensions) => any) => {
|
||||
const resizeHandlerHook = createEventHook()
|
||||
export default (el: Ref<HTMLDivElement>) => {
|
||||
const dimensions = ref<Dimensions>({ width: 0, height: 0 })
|
||||
const updateDimensions = () => {
|
||||
if (!unrefElement(rendererNode)) {
|
||||
return
|
||||
}
|
||||
const unrefEl = unrefElement(el)
|
||||
if (!unrefEl) return
|
||||
|
||||
const size = getDimensions(unrefElement(rendererNode))
|
||||
|
||||
if (size.height === 0 || size.width === 0) {
|
||||
const size = getDimensions(unrefEl as HTMLDivElement)
|
||||
if (size.height === 0 || size.width === 0)
|
||||
console.log('The revue Flow parent container needs a width and a height to render the graph.')
|
||||
}
|
||||
|
||||
updateSize(size)
|
||||
}
|
||||
|
||||
until(rendererNode)
|
||||
.toBeTruthy()
|
||||
.then(() => {
|
||||
updateDimensions()
|
||||
useEventListener(window, 'resize', updateDimensions)
|
||||
useResizeObserver(unrefElement(rendererNode), () => updateDimensions())
|
||||
resizeHandlerHook.trigger(true)
|
||||
})
|
||||
useEventListener(window, 'resize', updateDimensions)
|
||||
useResizeObserver(el, () => updateDimensions())
|
||||
|
||||
return {
|
||||
onReady: resizeHandlerHook.on
|
||||
}
|
||||
return dimensions
|
||||
}
|
||||
|
||||
@@ -1,91 +1,76 @@
|
||||
import { zoomIdentity } from 'd3-zoom'
|
||||
import { computed, effectScope, inject } from 'vue'
|
||||
import { createEventHook, EventHookOn, until } from '@vueuse/core'
|
||||
import { Selection, ZoomBehavior } from 'd3'
|
||||
import { getRectOfNodes, pointToRendererPoint, getTransformForBounds } from '../utils/graph'
|
||||
import { FitViewParams, FlowTransform, ZoomPanHelperFunctions, Rect, XYPosition, RevueFlowStore } from '../types'
|
||||
import { getRectOfNodes, pointToRendererPoint, getTransformForBounds } from '~/utils/graph'
|
||||
import { FitViewParams, FlowTransform, ZoomPanHelperFunctions, Rect, XYPosition, Node, Transform, SnapGrid } from '~/types'
|
||||
|
||||
const DEFAULT_PADDING = 0.1
|
||||
|
||||
const useZoomPanHelper = (): { onReady: EventHookOn<ZoomPanHelperFunctions> } => {
|
||||
const zoomPanHelperEvent = createEventHook<ZoomPanHelperFunctions>()
|
||||
const store = inject<RevueFlowStore>('store')!
|
||||
const useZoomPanHelper = ({
|
||||
d3Zoom,
|
||||
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()
|
||||
scope.run(() => {
|
||||
const d3Zoom = computed(() => store.d3Zoom)
|
||||
const d3Selection = computed(() => store.d3Selection)
|
||||
d3Zoom.transform(d3Selection, nextTransform)
|
||||
},
|
||||
fitView: (options: FitViewParams = { padding: DEFAULT_PADDING, includeHiddenNodes: false }) => {
|
||||
if (!nodes.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const createZoomPanHelper = (
|
||||
d3Zoom: ZoomBehavior<Element, unknown>,
|
||||
d3Selection: Selection<Element, any, any, any>
|
||||
): 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 bounds = getRectOfNodes(options.includeHiddenNodes ? nodes : nodes.filter((node) => !node.isHidden))
|
||||
const [x, y, zoom] = getTransformForBounds(
|
||||
bounds,
|
||||
width,
|
||||
height,
|
||||
options.minZoom || minZoom,
|
||||
options.maxZoom || maxZoom,
|
||||
options.padding || DEFAULT_PADDING,
|
||||
)
|
||||
const transform = zoomIdentity.translate(x, y).scale(zoom)
|
||||
|
||||
d3Zoom.transform(d3Selection, nextTransform)
|
||||
},
|
||||
fitView: (options: FitViewParams = { padding: DEFAULT_PADDING, includeHiddenNodes: false }) => {
|
||||
const { nodes, width, height, minZoom, maxZoom } = store
|
||||
d3Zoom?.transform(d3Selection, transform)
|
||||
},
|
||||
setCenter: (x: number, y: number, zoom?: number) => {
|
||||
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) {
|
||||
return
|
||||
}
|
||||
d3Zoom.transform(d3Selection, transform)
|
||||
},
|
||||
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))
|
||||
const [x, y, zoom] = getTransformForBounds(
|
||||
bounds,
|
||||
width,
|
||||
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
|
||||
}
|
||||
}
|
||||
d3Zoom.transform(d3Selection, transform)
|
||||
},
|
||||
project: (position: XYPosition) => {
|
||||
return pointToRendererPoint(position, transform, snapToGrid, snapGrid)
|
||||
},
|
||||
})
|
||||
|
||||
export default useZoomPanHelper
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { setActivePinia, createPinia, defineStore, StoreDefinition } from 'pinia'
|
||||
import isEqual from 'fast-deep-equal'
|
||||
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 { getHandleBounds } from '../components/Nodes/utils'
|
||||
import { Edge, Node, NodeDiffUpdate, RevueFlowState, RevueFlowActions, XYPosition } from '~/types'
|
||||
import { clampPosition, getDimensions } from '~/utils'
|
||||
import { getConnectedEdges, getNodesInside, getRectOfNodes, isEdge, isNode, parseEdge, parseNode } from '~/utils/graph'
|
||||
import { getHandleBounds } from '~/components/Nodes/utils'
|
||||
|
||||
type NextElements = {
|
||||
nextNodes: Node[]
|
||||
@@ -12,14 +12,14 @@ type NextElements = {
|
||||
const pinia = createPinia()
|
||||
|
||||
export default function configureStore(
|
||||
preloadedState: RevueFlowState
|
||||
preloadedState: RevueFlowState,
|
||||
): StoreDefinition<string, RevueFlowState, any, RevueFlowActions> {
|
||||
setActivePinia(pinia)
|
||||
|
||||
return defineStore({
|
||||
id: 'revue-flow-' + Math.random(),
|
||||
id: `revue-flow-${Math.random()}`,
|
||||
state: () => ({
|
||||
...preloadedState
|
||||
...preloadedState,
|
||||
}),
|
||||
getters: {},
|
||||
actions: {
|
||||
@@ -27,7 +27,7 @@ export default function configureStore(
|
||||
const propElements = elements
|
||||
const nextElements: NextElements = {
|
||||
nextNodes: [],
|
||||
nextEdges: []
|
||||
nextEdges: [],
|
||||
}
|
||||
const { nextNodes, nextEdges } = propElements.reduce((res, propElement): NextElements => {
|
||||
if (isNode(propElement)) {
|
||||
@@ -36,7 +36,7 @@ export default function configureStore(
|
||||
if (storeNode) {
|
||||
const updatedNode: Node = {
|
||||
...storeNode,
|
||||
...propElement
|
||||
...propElement,
|
||||
}
|
||||
|
||||
if (storeNode.position.x !== propElement.position.x || storeNode.position.y !== propElement.position.y) {
|
||||
@@ -59,7 +59,7 @@ export default function configureStore(
|
||||
if (storeEdge) {
|
||||
res.nextEdges.push({
|
||||
...storeEdge,
|
||||
...propElement
|
||||
...propElement,
|
||||
})
|
||||
} else {
|
||||
res.nextEdges.push(parseEdge(propElement))
|
||||
@@ -90,8 +90,8 @@ export default function configureStore(
|
||||
__rf: {
|
||||
...node.__rf,
|
||||
...dimensions,
|
||||
handleBounds
|
||||
}
|
||||
handleBounds,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,7 +107,7 @@ export default function configureStore(
|
||||
const [gridSizeX, gridSizeY] = this.snapGrid
|
||||
position = {
|
||||
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,
|
||||
__rf: {
|
||||
...node.__rf,
|
||||
position
|
||||
}
|
||||
position,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,14 +134,14 @@ export default function configureStore(
|
||||
...node,
|
||||
__rf: {
|
||||
...node.__rf,
|
||||
isDragging
|
||||
}
|
||||
isDragging,
|
||||
},
|
||||
}
|
||||
|
||||
if (diff) {
|
||||
updatedNode.__rf.position = {
|
||||
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,
|
||||
x: mousePos.x,
|
||||
y: mousePos.y,
|
||||
draw: true
|
||||
draw: true,
|
||||
}
|
||||
},
|
||||
updateUserSelection(mousePos) {
|
||||
@@ -172,7 +172,7 @@ export default function configureStore(
|
||||
x: mousePos.x < startX ? mousePos.x : this.userSelectionRect.x,
|
||||
y: mousePos.y < startY ? mousePos.y : this.userSelectionRect.y,
|
||||
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)
|
||||
@@ -231,8 +231,8 @@ export default function configureStore(
|
||||
...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.nodesConnectable = isInteractive
|
||||
this.elementsSelectable = isInteractive
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,12 +16,12 @@ export const initialState: RevueFlowState = {
|
||||
maxZoom: 2,
|
||||
translateExtent: [
|
||||
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
||||
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY]
|
||||
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
||||
],
|
||||
|
||||
nodeExtent: [
|
||||
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
||||
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY]
|
||||
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
||||
],
|
||||
|
||||
nodesSelectionActive: false,
|
||||
@@ -34,7 +34,7 @@ export const initialState: RevueFlowState = {
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
draw: false
|
||||
draw: false,
|
||||
},
|
||||
connectionNodeId: null,
|
||||
connectionHandleId: null,
|
||||
@@ -51,5 +51,5 @@ export const initialState: RevueFlowState = {
|
||||
|
||||
multiSelectionActive: false,
|
||||
|
||||
revueFlowVersion: typeof __REVUE_FLOW_VERSION__ !== 'undefined' ? __REVUE_FLOW_VERSION__ : '-'
|
||||
revueFlowVersion: typeof __REVUE_FLOW_VERSION__ !== 'undefined' ? __REVUE_FLOW_VERSION__ : '-',
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentPublicInstance, HTMLAttributes, VNode } from 'vue'
|
||||
import { DefineComponent, HTMLAttributes, VNode } from 'vue'
|
||||
import { ArrowHeadType, ElementId, Position } from './types'
|
||||
import { Connection } from './connection'
|
||||
|
||||
@@ -64,6 +64,6 @@ export interface EdgeTextProps extends HTMLAttributes {
|
||||
labelBgBorderRadius?: number
|
||||
}
|
||||
|
||||
export type EdgeType = ComponentPublicInstance<EdgeSmoothStepProps>
|
||||
export type EdgeType = DefineComponent<EdgeSmoothStepProps>
|
||||
|
||||
export type OnEdgeUpdateFunc<T = any> = (oldEdge: Edge<T>, newConnection: Connection) => void
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentPublicInstance } from 'vue'
|
||||
import { DefineComponent } from 'vue'
|
||||
import { XYPosition } from './types'
|
||||
import { ElementId, Position } from './index'
|
||||
|
||||
@@ -57,4 +57,4 @@ export interface NodeProps<T = any> {
|
||||
isDragging?: boolean
|
||||
}
|
||||
|
||||
export type NodeType = ComponentPublicInstance<NodeProps>
|
||||
export type NodeType = DefineComponent<NodeProps>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { Edge } from './edge'
|
||||
import { Node } from './node'
|
||||
import { CSSProperties, HTMLAttributes } from 'vue'
|
||||
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
|
||||
|
||||
@@ -81,3 +84,41 @@ export type OnLoadParams<T = any> = {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { clampPosition, clamp } from './index'
|
||||
|
||||
import {
|
||||
ElementId,
|
||||
Node,
|
||||
@@ -12,8 +10,9 @@ import {
|
||||
Connection,
|
||||
FlowExportObject,
|
||||
NodeExtent,
|
||||
RevueFlowStore
|
||||
RevueFlowStore,
|
||||
} from '../types'
|
||||
import { clampPosition, clamp } from './index'
|
||||
|
||||
export const isEdge = (element: Node | Connection | Edge): element is Edge =>
|
||||
'id' in element && 'source' in element && 'target' in element
|
||||
@@ -62,7 +61,7 @@ const connectionExists = (edge: Edge, elements: Elements) => {
|
||||
el.source === edge.source &&
|
||||
el.target === edge.target &&
|
||||
(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 {
|
||||
edge = {
|
||||
...edgeParams,
|
||||
id: getEdgeId(edgeParams)
|
||||
id: getEdgeId(edgeParams),
|
||||
} as Edge
|
||||
}
|
||||
|
||||
@@ -109,7 +108,7 @@ export const updateEdge = (oldEdge: Edge, newConnection: Connection, elements: E
|
||||
source: newConnection.source,
|
||||
target: newConnection.target,
|
||||
sourceHandle: newConnection.sourceHandle,
|
||||
targetHandle: newConnection.targetHandle
|
||||
targetHandle: newConnection.targetHandle,
|
||||
} as Edge
|
||||
|
||||
return elements.filter((e) => e.id !== oldEdge.id).concat(edge)
|
||||
@@ -119,75 +118,70 @@ export const pointToRendererPoint = (
|
||||
{ x, y }: XYPosition,
|
||||
[tx, ty, tScale]: Transform,
|
||||
snapToGrid: boolean,
|
||||
[snapX, snapY]: [number, number]
|
||||
[snapX, snapY]: [number, number],
|
||||
): XYPosition => {
|
||||
const position: XYPosition = {
|
||||
x: (x - tx) / tScale,
|
||||
y: (y - ty) / tScale
|
||||
y: (y - ty) / tScale,
|
||||
}
|
||||
|
||||
if (snapToGrid) {
|
||||
return {
|
||||
x: snapX * Math.round(position.x / snapX),
|
||||
y: snapY * Math.round(position.y / snapY)
|
||||
y: snapY * Math.round(position.y / snapY),
|
||||
}
|
||||
}
|
||||
|
||||
return position
|
||||
}
|
||||
|
||||
export const onLoadProject = (currentStore: RevueFlowStore) => {
|
||||
return (position: XYPosition): XYPosition => {
|
||||
return pointToRendererPoint(position, currentStore.transform, currentStore.snapToGrid, currentStore.snapGrid)
|
||||
}
|
||||
}
|
||||
export const onLoadProject =
|
||||
(currentStore: RevueFlowStore) =>
|
||||
(position: XYPosition): XYPosition =>
|
||||
pointToRendererPoint(position, currentStore.transform, currentStore.snapToGrid, currentStore.snapGrid)
|
||||
|
||||
export const parseNode = (node: Node, nodeExtent: NodeExtent): Node => {
|
||||
return {
|
||||
...node,
|
||||
id: node.id.toString(),
|
||||
type: node.type || 'default',
|
||||
__rf: {
|
||||
position: clampPosition(node.position, nodeExtent),
|
||||
width: null,
|
||||
height: null,
|
||||
handleBounds: {},
|
||||
isDragging: false
|
||||
}
|
||||
}
|
||||
}
|
||||
export const parseNode = (node: Node, nodeExtent: NodeExtent): Node => ({
|
||||
...node,
|
||||
id: node.id.toString(),
|
||||
type: node.type || 'default',
|
||||
__rf: {
|
||||
position: clampPosition(node.position, nodeExtent),
|
||||
width: null,
|
||||
height: null,
|
||||
handleBounds: {},
|
||||
isDragging: false,
|
||||
},
|
||||
})
|
||||
|
||||
export const parseEdge = (edge: Edge): Edge => {
|
||||
return {
|
||||
...edge,
|
||||
source: edge.source.toString(),
|
||||
target: edge.target.toString(),
|
||||
sourceHandle: edge.sourceHandle ? edge.sourceHandle.toString() : null,
|
||||
targetHandle: edge.targetHandle ? edge.targetHandle.toString() : null,
|
||||
id: edge.id.toString(),
|
||||
type: edge.type || 'default'
|
||||
}
|
||||
}
|
||||
export const parseEdge = (edge: Edge): Edge => ({
|
||||
...edge,
|
||||
source: edge.source.toString(),
|
||||
target: edge.target.toString(),
|
||||
sourceHandle: edge.sourceHandle ? edge.sourceHandle.toString() : null,
|
||||
targetHandle: edge.targetHandle ? edge.targetHandle.toString() : null,
|
||||
id: edge.id.toString(),
|
||||
type: edge.type || 'default',
|
||||
})
|
||||
|
||||
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)
|
||||
y2: Math.max(box1.y2, box2.y2),
|
||||
})
|
||||
|
||||
export const rectToBox = ({ x, y, width, height }: Rect): Box => ({
|
||||
x,
|
||||
y,
|
||||
x2: x + width,
|
||||
y2: y + height
|
||||
y2: y + height,
|
||||
})
|
||||
|
||||
export const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({
|
||||
x,
|
||||
y,
|
||||
width: x2 - x,
|
||||
height: y2 - y
|
||||
height: y2 - y,
|
||||
})
|
||||
|
||||
export const getBoundsofRects = (rect1: Rect, rect2: Rect): Rect =>
|
||||
@@ -201,10 +195,10 @@ export const getRectOfNodes = (nodes: Node[]): Rect => {
|
||||
rectToBox({
|
||||
...position,
|
||||
width,
|
||||
height
|
||||
} as any)
|
||||
height,
|
||||
} as any),
|
||||
),
|
||||
{ x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity }
|
||||
{ x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity },
|
||||
)
|
||||
|
||||
return boxToRect(box)
|
||||
@@ -212,7 +206,7 @@ export const getRectOfNodes = (nodes: Node[]): Rect => {
|
||||
|
||||
export const graphPosToZoomedPos = ({ x, y }: XYPosition, [tx, ty, tScale]: Transform): XYPosition => ({
|
||||
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[] => {
|
||||
@@ -220,7 +214,7 @@ export const getNodesInside = (nodes: Node[], rect: Rect, [tx, ty, tScale]: Tran
|
||||
x: (rect.x - tx) / tScale,
|
||||
y: (rect.y - ty) / tScale,
|
||||
width: rect.width / tScale,
|
||||
height: rect.height / tScale
|
||||
height: rect.height / tScale,
|
||||
})
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
const parseElements = (nodes: Node[], edges: Edge[]): Elements => {
|
||||
return [
|
||||
...nodes.map((node) => {
|
||||
const n = { ...node }
|
||||
const parseElements = (nodes: Node[], edges: Edge[]): Elements => [
|
||||
...nodes.map((node) => {
|
||||
const n = { ...node }
|
||||
|
||||
n.position = n.__rf.position
|
||||
n.position = n.__rf.position
|
||||
|
||||
n.__rf = {} as any
|
||||
return n
|
||||
}),
|
||||
...edges.map((e) => ({ ...e }))
|
||||
]
|
||||
}
|
||||
n.__rf = {} as any
|
||||
return n
|
||||
}),
|
||||
...edges.map((e) => ({ ...e })),
|
||||
]
|
||||
|
||||
export const onLoadGetElements = (currentStore: RevueFlowStore) => {
|
||||
return (): Elements => {
|
||||
return parseElements(currentStore.nodes || [], currentStore.edges || [])
|
||||
}
|
||||
}
|
||||
export const onLoadGetElements = (currentStore: RevueFlowStore) => (): Elements =>
|
||||
parseElements(currentStore.nodes || [], currentStore.edges || [])
|
||||
|
||||
export const onLoadToObject = (currentStore: RevueFlowStore) => {
|
||||
return (): FlowExportObject => {
|
||||
return {
|
||||
elements: parseElements(currentStore.nodes || [], currentStore.edges || []),
|
||||
position: [currentStore.transform[0], currentStore.transform[1]],
|
||||
zoom: currentStore.transform[2]
|
||||
}
|
||||
}
|
||||
}
|
||||
export const onLoadToObject = (currentStore: RevueFlowStore) => (): FlowExportObject => ({
|
||||
elements: parseElements(currentStore.nodes || [], currentStore.edges || []),
|
||||
position: [currentStore.transform[0], currentStore.transform[1]],
|
||||
zoom: currentStore.transform[2],
|
||||
})
|
||||
|
||||
export const getTransformForBounds = (
|
||||
bounds: Rect,
|
||||
@@ -286,7 +271,7 @@ export const getTransformForBounds = (
|
||||
height: number,
|
||||
minZoom: number,
|
||||
maxZoom: number,
|
||||
padding = 0.1
|
||||
padding = 0.1,
|
||||
): Transform => {
|
||||
const xZoom = width / (bounds.width * (1 + padding))
|
||||
const yZoom = height / (bounds.height * (1 + padding))
|
||||
|
||||
@@ -5,16 +5,16 @@ export const isInputDOMNode = (e: KeyboardEvent | MouseEvent): boolean => {
|
||||
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,
|
||||
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 clampPosition = (position: XYPosition, extent: NodeExtent): XYPosition => ({
|
||||
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 =>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"target": "es2016",
|
||||
"target": "es2020",
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ESNext"
|
||||
|
||||
Reference in New Issue
Block a user