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:
Braks
2021-10-15 20:26:36 +02:00
parent fec2ef40da
commit 9307c9066a
28 changed files with 1732 additions and 296 deletions

View File

@@ -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,
}

View File

@@ -1,8 +1,8 @@
{
"singleQuote": true,
"trailingComma": "none",
"trailingComma": "all",
"semi": false,
"quoteProps": "consistent",
"bracketSpacing": true,
"vueIndentScriptAndStyle": false,
"printWidth": 130
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

12
src/auto-imports.d.ts vendored
View File

@@ -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
View 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

View 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>

View File

@@ -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
View 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
View 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>

View 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>

View 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>

View File

@@ -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,
}
}

View 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>

View 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>

View File

@@ -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,
}
}

View File

@@ -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
})
*/
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
}
}
},
},
})
}

View File

@@ -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__ : '-',
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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
}

View File

@@ -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))

View File

@@ -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 =>

View File

@@ -2,7 +2,7 @@
"compilerOptions": {
"baseUrl": ".",
"module": "ESNext",
"target": "es2016",
"target": "es2020",
"lib": [
"DOM",
"ESNext"