refactor(store)!: remove pinia

* replace pinia with a reactive state object and inject/provide

Signed-off-by: Braks <78412429+bcakmakoglu@users.noreply.github.com>

update(store)!: inject store if possible, otherwise create a new one by id

Signed-off-by: Braks <78412429+bcakmakoglu@users.noreply.github.com>
This commit is contained in:
Braks
2021-12-04 15:42:18 +01:00
parent 58f5c2568c
commit 58a8f9d4df
21 changed files with 385 additions and 352 deletions
+1 -2
View File
@@ -38,8 +38,7 @@
"d3": "^7.1.1", "d3": "^7.1.1",
"d3-selection": "^3.0.0", "d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0", "d3-zoom": "^3.0.0",
"microdiff": "^1.2.0", "microdiff": "^1.2.0"
"pinia": "^2.0.0-rc.10"
}, },
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^0.9.0", "@antfu/eslint-config": "^0.9.0",
@@ -54,7 +54,7 @@ export default {
@stop="onStop" @stop="onStop"
> >
<div <div
:key="`vue-flow-nodesselection-rect-${store.$id}`" :key="`vue-flow-nodesselection-rect-${store.id}`"
class="vue-flow__nodesselection-rect" class="vue-flow__nodesselection-rect"
:style="innerStyle" :style="innerStyle"
@contextmenu="onContextMenu" @contextmenu="onContextMenu"
+1 -1
View File
@@ -3,6 +3,6 @@ export { default as useHooks } from './useHooks'
export * from './useHooks' export * from './useHooks'
export { default as useKeyPress } from './useKeyPress' export { default as useKeyPress } from './useKeyPress'
export { default as useZoomPanHelper } from './useZoomPanHelper' export { default as useZoomPanHelper } from './useZoomPanHelper'
export { default as useStore } from './useStore' export { default as useStore, createStore } from './useStore'
export { default as useWindow } from './useWindow' export { default as useWindow } from './useWindow'
export { default as useVueFlow, initFlow } from './useVueFlow' export { default as useVueFlow, initFlow } from './useVueFlow'
+2 -2
View File
@@ -1,6 +1,6 @@
import useStore from './useStore' import useStore from './useStore'
import { getHostForElement } from '~/utils' import { getHostForElement } from '~/utils'
import { Connection, ConnectionMode, ElementId, HandleType, ValidConnectionFunc } from '~/types' import { Connection, ConnectionMode, ElementId, HandleType, ReactiveFlowStore, ValidConnectionFunc } from '~/types'
type Result = { type Result = {
elementBelow: Element | null elementBelow: Element | null
@@ -67,7 +67,7 @@ const resetRecentHandle = (hoveredHandle: Element): void => {
hoveredHandle?.classList.remove('vue-flow__handle-connecting') hoveredHandle?.classList.remove('vue-flow__handle-connecting')
} }
export default (store = useStore()) => export default (store: ReactiveFlowStore = useStore()) =>
( (
event: MouseEvent, event: MouseEvent,
handleId: ElementId, handleId: ElementId,
+2 -2
View File
@@ -1,4 +1,4 @@
import { EmitFunc, FlowHooks, FlowEvents, FlowStore } from '~/types' import { EmitFunc, FlowHooks, FlowEvents, FlowStore, ReactiveFlowStore } from '~/types'
// flow event hooks // flow event hooks
export const createHooks = (): FlowHooks => ({ export const createHooks = (): FlowHooks => ({
@@ -49,4 +49,4 @@ const bind = (emit: EmitFunc, hooks: FlowHooks) => {
} }
} }
export default (store: FlowStore, emit: EmitFunc) => bind(emit, store.hooks) export default (store: ReactiveFlowStore, emit: EmitFunc) => bind(emit, store.hooks)
+37 -29
View File
@@ -1,38 +1,46 @@
import { FlowExportObject, FlowOptions, FlowState } from '~/types' import { FlowExportObject, FlowOptions, FlowState, ReactiveFlowStore } from '~/types'
import { useStateStore } from '~/store' import { useNewStore } from '~/store'
import { Store } from '~/context' import { Store } from '~/context'
import { onLoadToObject, initialState } from '~/utils' import { onLoadToObject, initialState } from '~/utils'
let id = 0 let id = 0
export default (options?: Partial<FlowOptions>, key?: string) => { export const createStore = (options?: FlowOptions) => {
let store = inject(Store, undefined) const withStorage = options?.storageKey ?? false
if (!store) { let storedState = ref<FlowExportObject>()
const withStorage = options?.storageKey ?? key ?? false const initial = initialState()
if (import.meta.env.DEV) console.warn('store context not found; creating default store') const storageKey = options?.id ?? `vue-flow-${id++}`
let storedState = ref<FlowExportObject>() const preloadedState = {
const initial = initialState() ...initial,
const storageKey = key ?? `vue-flow-${id++}` ...(options as FlowState),
const preloadedState = { }
...initial, if (withStorage) {
...(options as FlowState), storedState = useStorage<FlowExportObject>(storageKey, { elements: [], position: [0, 0], zoom: 0 })
if (storedState.value) {
preloadedState.elements = storedState.value.elements ?? options?.elements ?? []
if (storedState.value.position && storedState.value.zoom)
preloadedState.transform = [storedState.value.position[0], storedState.value.position[1], storedState.value.zoom]
} }
if (withStorage) { }
storedState = useStorage<FlowExportObject>(storageKey, { elements: [], position: [0, 0], zoom: 0 }) const store = useNewStore(storageKey, preloadedState)
if (storedState.value) { if (withStorage && storageKey === store.id?.value) {
preloadedState.elements = storedState.value.elements ?? options?.elements ?? [] const toObject = onLoadToObject(store.state)
if (storedState.value.position && storedState.value.zoom) watch(
preloadedState.transform = [storedState.value.position[0], storedState.value.position[1], storedState.value.zoom] store.state,
} () => {
}
store = useStateStore(storageKey, preloadedState)()
if (withStorage && storageKey === store.$id) {
const toObject = onLoadToObject(store)
store.$subscribe(() => {
storedState.value = toObject() storedState.value = toObject()
}) },
} { deep: true },
provide(Store, store) )
} }
return store return store
} }
export default (options?: FlowOptions): ReactiveFlowStore => {
let store = options?.id ? createStore(options) : inject(Store, undefined)
if (!store) {
store = createStore(options)
}
provide(Store, store)
return reactive(store)
}
+5 -3
View File
@@ -1,10 +1,12 @@
import useStore from './useStore' import useStore from './useStore'
import useHooks from './useHooks' import useHooks from './useHooks'
import { EmitFunc, FlowOptions, FlowStore } from '~/types' import { EmitFunc, ReactiveFlowStore } from '~/types'
export const initFlow = (emit: EmitFunc, key?: string, store = useStore(undefined, key)) => { let key = 0
export const initFlow = (emit: EmitFunc, id = `vue-flow-${key++}`): ReactiveFlowStore => {
const store = useStore({ id })
useHooks(store, emit) useHooks(store, emit)
return store return store
} }
export default (options?: Partial<FlowOptions>): FlowStore => useStore(options) export default useStore
+9 -3
View File
@@ -1,11 +1,11 @@
import { zoomIdentity } from 'd3-zoom' import { zoomIdentity } from 'd3-zoom'
import useStore from './useStore' import useStore from './useStore'
import { getRectOfNodes, pointToRendererPoint, getTransformForBounds, isGraphNode } from '~/utils' import { getRectOfNodes, pointToRendererPoint, getTransformForBounds, isGraphNode } from '~/utils'
import { FitViewParams, FlowTransform, GraphNode, Rect, UseZoomPanHelper, XYPosition } from '~/types' import { FitViewParams, FlowTransform, GraphNode, ReactiveFlowStore, Rect, UseZoomPanHelper, XYPosition } from '~/types'
const DEFAULT_PADDING = 0.1 const DEFAULT_PADDING = 0.1
export default (store = useStore()): UseZoomPanHelper => { export default (store: ReactiveFlowStore = useStore()): UseZoomPanHelper => {
return { return {
zoomIn: () => store.d3Selection && store.d3Zoom?.scaleBy(store.d3Selection, 1.2), zoomIn: () => store.d3Selection && store.d3Zoom?.scaleBy(store.d3Selection, 1.2),
zoomOut: () => store.d3Selection && store.d3Zoom?.scaleBy(store.d3Selection, 1 / 1.2), zoomOut: () => store.d3Selection && store.d3Zoom?.scaleBy(store.d3Selection, 1 / 1.2),
@@ -14,7 +14,13 @@ export default (store = useStore()): UseZoomPanHelper => {
const nextTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.zoom) const nextTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.zoom)
store.d3Selection && store.d3Zoom?.transform(store.d3Selection, nextTransform) store.d3Selection && store.d3Zoom?.transform(store.d3Selection, nextTransform)
}, },
fitView: (options: FitViewParams = { padding: DEFAULT_PADDING, includeHiddenNodes: false, transitionDuration: 0 }) => { fitView: (
options: FitViewParams = {
padding: DEFAULT_PADDING,
includeHiddenNodes: false,
transitionDuration: 0,
},
) => {
if (!store.getNodes.length) return if (!store.getNodes.length) return
let nodes: GraphNode[] = [] let nodes: GraphNode[] = []
@@ -70,12 +70,12 @@ export default {
</script> </script>
<template> <template>
<slot></slot> <slot></slot>
<UserSelection v-if="userSelection" id="user-selection" :key="`user-selection-${store.$id}`" /> <UserSelection v-if="userSelection" id="user-selection" :key="`user-selection-${store.id}`" />
<NodesSelection <NodesSelection
v-if="props.nodesSelectionActive" v-if="props.nodesSelectionActive"
id="nodes-selection" id="nodes-selection"
:key="`nodes-selction-${store.$id}`" :key="`nodes-selction-${store.id}`"
:selected-elements="props.selectedElements" :selected-elements="props.selectedElements"
/> />
<div :key="`flow-pane-${store.$id}`" class="vue-flow__pane" @click="onClick" @contextmenu="onContextMenu" @wheel="onWheel" /> <div :key="`flow-pane-${store.id}`" class="vue-flow__pane" @click="onClick" @contextmenu="onContextMenu" @wheel="onWheel" />
</template> </template>
+17 -22
View File
@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { CSSProperties, onBeforeUnmount } from 'vue' import { CSSProperties } from 'vue'
import { import {
ConnectionLineType, ConnectionLineType,
ConnectionMode, ConnectionMode,
@@ -64,6 +64,8 @@ interface FlowProps extends FlowOptions {
const props = withDefaults(defineProps<FlowProps>(), { const props = withDefaults(defineProps<FlowProps>(), {
modelValue: () => [], modelValue: () => [],
edgeTypes: () => [],
nodeTypes: () => [],
connectionMode: ConnectionMode.Loose, connectionMode: ConnectionMode.Loose,
connectionLineType: ConnectionLineType.Bezier, connectionLineType: ConnectionLineType.Bezier,
selectionKeyCode: 'Shift', selectionKeyCode: 'Shift',
@@ -103,10 +105,9 @@ const props = withDefaults(defineProps<FlowProps>(), {
loading: false, loading: false,
}) })
const emit = defineEmits([...Object.keys(createHooks()), 'update:modelValue']) const emit = defineEmits([...Object.keys(createHooks()), 'update:modelValue'])
const store = initFlow(emit, typeof props.storageKey === 'string' ? props.storageKey : props.id) const store = initFlow(emit, props.id)
const elements = useVModel(props, 'modelValue', emit) const elements = useVModel(props, 'modelValue', emit)
const options = Object.assign({}, store.$state, props) store.setState(props)
store.setState(options)
watch( watch(
() => props, () => props,
() => store.setState(props), () => store.setState(props),
@@ -144,22 +145,16 @@ const transitionName = computed(() => {
} }
return name return name
}) })
</script>
onBeforeUnmount(() => store?.$dispose())
</script>
<script lang="ts">
export default {
name: 'VueFlow'
}
</script>
<template> <template>
<div class="vue-flow"> <div class="vue-flow">
<Transition :key="`vue-flow-transition-${store.$id}`" :name="transitionName"> <Transition :key="`vue-flow-transition-${store.id}`" :name="transitionName">
<Suspense timeout="0"> <Suspense timeout="0">
<template #default> <template #default>
<Renderer :key="`renderer-${store.$id}`" :elements="elements"> <Renderer :key="`renderer-${store.id}`" :elements="elements">
<ZoomPane <ZoomPane
:key="`zoom-pane-${store.$id}`" :key="`zoom-pane-${store.id}`"
:prevent-scrolling="store.preventScrolling" :prevent-scrolling="store.preventScrolling"
:selection-key-code="store.selectionKeyCode" :selection-key-code="store.selectionKeyCode"
:zoom-activation-key-code="store.zoomActivationKeyCode" :zoom-activation-key-code="store.zoomActivationKeyCode"
@@ -175,7 +170,7 @@ export default {
> >
<template #default="zoomPaneProps"> <template #default="zoomPaneProps">
<SelectionPane <SelectionPane
:key="`selection-pane-${store.$id}`" :key="`selection-pane-${store.id}`"
:edges="store.getEdges" :edges="store.getEdges"
:selected-elements="store.selectedElements" :selected-elements="store.selectedElements"
:selection-active="store.selectionActive" :selection-active="store.selectionActive"
@@ -186,7 +181,7 @@ export default {
:selection-key-code="store.selectionKeyCode" :selection-key-code="store.selectionKeyCode"
> >
<NodeRenderer <NodeRenderer
:key="`node-renderer-${store.$id}`" :key="`node-renderer-${store.id}`"
:nodes="store.getNodes" :nodes="store.getNodes"
:node-types="store.getNodeTypes" :node-types="store.getNodeTypes"
:snap-to-grid="store.snapToGrid" :snap-to-grid="store.snapToGrid"
@@ -200,13 +195,13 @@ export default {
<template <template
v-for="nodeName of Object.keys(store.getNodeTypes)" v-for="nodeName of Object.keys(store.getNodeTypes)"
#[`node-${nodeName}`]="nodeProps" #[`node-${nodeName}`]="nodeProps"
:key="`node-${nodeName}-${store.$id}`" :key="`node-${nodeName}-${store.id}`"
> >
<slot :name="`node-${nodeName}`" v-bind="nodeProps" /> <slot :name="`node-${nodeName}`" v-bind="nodeProps" />
</template> </template>
</NodeRenderer> </NodeRenderer>
<EdgeRenderer <EdgeRenderer
:key="`edge-renderer-${store.$id}`" :key="`edge-renderer-${store.id}`"
:transform="store.transform" :transform="store.transform"
:dimensions="store.dimensions" :dimensions="store.dimensions"
:edges="store.getEdges" :edges="store.getEdges"
@@ -230,13 +225,13 @@ export default {
<template <template
v-for="edgeName of Object.keys(store.getEdgeTypes)" v-for="edgeName of Object.keys(store.getEdgeTypes)"
#[`edge-${edgeName}`]="edgeProps" #[`edge-${edgeName}`]="edgeProps"
:key="`edge-${edgeName}-${store.$id}`" :key="`edge-${edgeName}-${store.id}`"
> >
<slot :name="`edge-${edgeName}`" v-bind="edgeProps" /> <slot :name="`edge-${edgeName}`" v-bind="edgeProps" />
</template> </template>
<template #custom-connection-line="customConnectionLineProps"> <template #custom-connection-line="customConnectionLineProps">
<slot <slot
:key="`connection-line-${store.$id}`" :key="`connection-line-${store.id}`"
name="custom-connection-line" name="custom-connection-line"
v-bind="customConnectionLineProps" v-bind="customConnectionLineProps"
/> />
@@ -249,8 +244,8 @@ export default {
</Renderer> </Renderer>
</template> </template>
<template v-if="store.loading" #fallback> <template v-if="store.loading" #fallback>
<slot :key="`loading-indicator-${store.$id}`" name="loading-indicator"> <slot :key="`loading-indicator-${store.id}`" name="loading-indicator">
<LoadingIndicator :key="`default-loading-indicator-${store.$id}`" v-bind="store.loading"> <LoadingIndicator :key="`default-loading-indicator-${store.id}`" v-bind="store.loading">
<slot name="loading-label" /> <slot name="loading-label" />
</LoadingIndicator> </LoadingIndicator>
</slot> </slot>
+169
View File
@@ -0,0 +1,169 @@
import microDiff from 'microdiff'
import { Elements, FlowActions, FlowGetters, FlowState } from '~/types'
import { getConnectedEdges, getNodesInside, getRectOfNodes, parseElements, processElements } from '~/utils'
export default (state: FlowState, getters: FlowGetters): FlowActions => {
const setElements: FlowActions['setElements'] = async (elements, force = true) => {
const { nodes, edges } = parseElements(elements, state.elements, state.nodeExtent)
if (force) state.elements = []
await processElements([...nodes, ...edges], (processed) => {
state.elements = [...state.elements, ...processed]
})
state.hooks.elementsProcessed.trigger(state.elements)
}
const setUserSelection: FlowActions['setUserSelection'] = (mousePos) => {
state.selectionActive = true
state.userSelectionRect = {
width: 0,
height: 0,
startX: mousePos.x,
startY: mousePos.y,
x: mousePos.x,
y: mousePos.y,
draw: true,
}
}
const updateUserSelection: FlowActions['updateUserSelection'] = (mousePos) => {
const startX = state.userSelectionRect.startX
const startY = state.userSelectionRect.startY
const nextUserSelectRect: FlowState['userSelectionRect'] = {
...state.userSelectionRect,
x: mousePos.x < startX ? mousePos.x : state.userSelectionRect.x,
y: mousePos.y < startY ? mousePos.y : state.userSelectionRect.y,
width: Math.abs(mousePos.x - startX),
height: Math.abs(mousePos.y - startY),
}
const selectedNodes = getNodesInside(getters.getNodes.value, state.userSelectionRect, state.transform)
const selectedEdges = getConnectedEdges(selectedNodes, getters.getEdges.value)
const nextSelectedElements = [...selectedNodes, ...selectedEdges]
state.userSelectionRect = nextUserSelectRect
state.selectedElements = nextSelectedElements
}
const unsetUserSelection: FlowActions['unsetUserSelection'] = () => {
state.selectionActive = false
state.userSelectionRect.draw = false
if (!getters.getSelectedNodes || getters.getSelectedNodes.value.length === 0) {
state.selectedElements = undefined
state.hooks.selectionChange.trigger(undefined)
state.nodesSelectionActive = false
} else {
state.selectedNodesBbox = getRectOfNodes(getters.getSelectedNodes.value)
state.nodesSelectionActive = true
}
}
const addSelectedElements: FlowActions['addSelectedElements'] = (elements: any) => {
const selectedElementsArr = Array.isArray(elements) ? elements : [elements]
const selectedElementsUpdated = microDiff(selectedElementsArr, state.selectedElements ?? []).length
state.selectedElements = selectedElementsUpdated ? selectedElementsArr : state.selectedElements
if (selectedElementsUpdated) state.hooks.selectionChange.trigger(selectedElementsArr)
}
const initD3Zoom: FlowActions['initD3Zoom'] = ({ d3ZoomHandler, d3Zoom, d3Selection }) => {
state.d3Zoom = d3Zoom
state.d3Selection = d3Selection
state.d3ZoomHandler = d3ZoomHandler
}
const setMinZoom: FlowActions['setMinZoom'] = (minZoom: any) => {
state.d3Zoom?.scaleExtent([minZoom, state.maxZoom])
state.minZoom = minZoom
}
const setMaxZoom: FlowActions['setMaxZoom'] = (maxZoom: any) => {
state.d3Zoom?.scaleExtent([state.minZoom, maxZoom])
state.maxZoom = maxZoom
}
const setTranslateExtent: FlowActions['setTranslateExtent'] = (translateExtent: any) => {
state.d3Zoom?.translateExtent(translateExtent)
state.translateExtent = translateExtent
}
const resetSelectedElements: FlowActions['resetSelectedElements'] = () => {
state.selectedElements = undefined
}
const unsetNodesSelection: FlowActions['unsetNodesSelection'] = () => {
state.nodesSelectionActive = false
}
const updateSize: FlowActions['updateSize'] = (size) => {
state.dimensions = size
}
const setConnectionNodeId: FlowActions['setConnectionNodeId'] = ({
connectionHandleId,
connectionHandleType,
connectionNodeId,
}) => {
state.connectionNodeId = connectionNodeId
state.connectionHandleId = connectionHandleId
state.connectionHandleType = connectionHandleType
}
const setInteractive: FlowActions['setInteractive'] = (isInteractive) => {
state.nodesDraggable = isInteractive
state.nodesConnectable = isInteractive
state.elementsSelectable = isInteractive
}
const addElements: FlowActions['addElements'] = (elements: Elements) => {
setElements(elements, false)
}
const setState: FlowActions['setState'] = (newState) => {
if (typeof newState.loading !== 'undefined') state.loading = newState.loading
if (typeof newState.panOnScroll !== 'undefined') state.panOnScroll = newState.panOnScroll
if (typeof newState.panOnScrollMode !== 'undefined') state.panOnScrollMode = newState.panOnScrollMode
if (typeof newState.panOnScrollSpeed !== 'undefined') state.panOnScrollSpeed = newState.panOnScrollSpeed
if (typeof newState.paneMoveable !== 'undefined') state.paneMoveable = newState.paneMoveable
if (typeof newState.zoomOnScroll !== 'undefined') state.zoomOnScroll = newState.zoomOnScroll
if (typeof newState.preventScrolling !== 'undefined') state.preventScrolling = newState.preventScrolling
if (typeof newState.zoomOnDoubleClick !== 'undefined') state.zoomOnDoubleClick = newState.zoomOnDoubleClick
if (typeof newState.zoomOnPinch !== 'undefined') state.zoomOnPinch = newState.zoomOnPinch
if (typeof newState.defaultZoom !== 'undefined') state.defaultZoom = newState.defaultZoom
if (typeof newState.defaultPosition !== 'undefined') state.defaultPosition = newState.defaultPosition
if (typeof newState.edgeTypes !== 'undefined') state.edgeTypes = newState.edgeTypes
if (typeof newState.nodeTypes !== 'undefined') state.nodeTypes = newState.nodeTypes
if (typeof newState.storageKey !== 'undefined') state.storageKey = newState.storageKey
if (typeof newState.edgeUpdaterRadius !== 'undefined') state.edgeUpdaterRadius = newState.edgeUpdaterRadius
if (typeof newState.elementsSelectable !== 'undefined') state.elementsSelectable = newState.elementsSelectable
if (typeof newState.onlyRenderVisibleElements !== 'undefined')
state.onlyRenderVisibleElements = newState.onlyRenderVisibleElements
if (typeof newState.edgesUpdatable !== 'undefined') state.edgesUpdatable = newState.edgesUpdatable
if (typeof newState.nodesConnectable !== 'undefined') state.nodesConnectable = newState.nodesConnectable
if (typeof newState.nodesDraggable !== 'undefined') state.nodesDraggable = newState.nodesDraggable
if (typeof newState.arrowHeadColor !== 'undefined') state.arrowHeadColor = newState.arrowHeadColor
if (typeof newState.markerEndId !== 'undefined') state.markerEndId = newState.markerEndId
if (typeof newState.deleteKeyCode !== 'undefined') state.deleteKeyCode = newState.deleteKeyCode
if (typeof newState.selectionKeyCode !== 'undefined') state.selectionKeyCode = newState.selectionKeyCode
if (typeof newState.zoomActivationKeyCode !== 'undefined') state.zoomActivationKeyCode = newState.zoomActivationKeyCode
if (typeof newState.multiSelectionKeyCode !== 'undefined') state.multiSelectionKeyCode = newState.multiSelectionKeyCode
if (typeof newState.snapToGrid !== 'undefined') state.snapToGrid = newState.snapToGrid
if (typeof newState.snapGrid !== 'undefined') state.snapGrid = newState.snapGrid
if (typeof newState.nodeExtent !== 'undefined') state.nodeExtent = newState.nodeExtent
if (!state.isReady)
until(() => state.d3Zoom)
.not.toBeUndefined()
.then(() => {
if (typeof newState.maxZoom !== 'undefined') setMaxZoom(newState.maxZoom)
if (typeof newState.minZoom !== 'undefined') setMinZoom(newState.minZoom)
if (typeof newState.translateExtent !== 'undefined') setTranslateExtent(newState.translateExtent)
})
else {
if (typeof newState.maxZoom !== 'undefined') setMaxZoom(newState.maxZoom)
if (typeof newState.minZoom !== 'undefined') setMinZoom(newState.minZoom)
if (typeof newState.translateExtent !== 'undefined') setTranslateExtent(newState.translateExtent)
}
}
return {
setElements,
setUserSelection,
updateUserSelection,
unsetUserSelection,
addSelectedElements,
initD3Zoom,
setMinZoom,
setMaxZoom,
setTranslateExtent,
resetSelectedElements,
unsetNodesSelection,
updateSize,
setConnectionNodeId,
setInteractive,
addElements,
setState,
}
}
+73
View File
@@ -0,0 +1,73 @@
import { FlowGetters, FlowState, GraphEdge, GraphNode } from '~/types'
import { defaultEdgeTypes, defaultNodeTypes, getNodesInside, getSourceTargetNodes, isEdge, isGraphNode } from '~/utils'
export default (state: FlowState): FlowGetters => {
const getEdgeTypes = computed(() => {
const edgeTypes: Record<string, any> = {
...defaultEdgeTypes,
}
state.edgeTypes?.forEach((n) => (edgeTypes[n] = n))
return edgeTypes
})
const getNodeTypes = computed(() => {
const nodeTypes: Record<string, any> = {
...defaultNodeTypes,
}
state.nodeTypes?.forEach((n) => (nodeTypes[n] = n))
return nodeTypes
})
const getNodes = computed<GraphNode[]>(() => {
if (state.isReady) {
const nodes = state.elements.filter((n) => isGraphNode(n) && !n.isHidden) as GraphNode[]
return state.onlyRenderVisibleElements
? nodes &&
getNodesInside(
nodes,
{
x: 0,
y: 0,
width: state.dimensions.width,
height: state.dimensions.height,
},
state.transform,
true,
)
: nodes ?? []
}
return []
})
const getEdges = computed<GraphEdge[]>(() => {
const edges = state.elements.filter((e) => isEdge(e) && !e.isHidden) as GraphEdge[]
if (state.isReady) {
return (
edges
.map((edge) => {
const { sourceNode, targetNode } = getSourceTargetNodes(edge, getNodes.value)
if (!sourceNode) console.warn(`couldn't create edge for source id: ${edge.source}; edge id: ${edge.id}`)
if (!targetNode) console.warn(`couldn't create edge for target id: ${edge.target}; edge id: ${edge.id}`)
return {
...edge,
sourceNode,
targetNode,
}
})
.filter(({ sourceNode, targetNode }) => !!(sourceNode && targetNode)) ?? []
)
}
return []
})
const getSelectedNodes = computed<GraphNode[]>(() => state.selectedElements?.filter(isGraphNode) ?? [])
return {
getEdgeTypes,
getNodeTypes,
getEdges,
getNodes,
getSelectedNodes,
}
}
+1 -1
View File
@@ -1 +1 @@
export { default as useStateStore } from './stateStore' export { default as useNewStore } from './store'
+5
View File
@@ -0,0 +1,5 @@
import { FlowState } from '~/types'
export default (preloadedState: FlowState) => ({
...preloadedState,
})
-241
View File
@@ -1,241 +0,0 @@
import microDiff from 'microdiff'
import { setActivePinia, createPinia, defineStore, StoreDefinition, acceptHMRUpdate, getActivePinia } from 'pinia'
import { FlowState, FlowActions, Elements, FlowGetters, GraphNode, GraphEdge } from '~/types'
import {
getConnectedEdges,
getNodesInside,
getRectOfNodes,
defaultNodeTypes,
defaultEdgeTypes,
isGraphNode,
getSourceTargetNodes,
isEdge,
parseElements,
processElements,
} from '~/utils'
const pinia = createPinia()
export default (id: string, preloadedState: FlowState) => {
setActivePinia(getActivePinia() ?? pinia)
const store: StoreDefinition<string, FlowState, FlowGetters, FlowActions> = defineStore({
id: id ?? 'vue-flow',
state: () => ({
...preloadedState,
}),
getters: {
getEdgeTypes() {
const edgeTypes: Record<string, any> = {
...defaultEdgeTypes,
}
this.edgeTypes?.forEach((n) => (edgeTypes[n] = n))
return edgeTypes
},
getNodeTypes() {
const nodeTypes: Record<string, any> = {
...defaultNodeTypes,
}
this.nodeTypes?.forEach((n) => (nodeTypes[n] = n))
return nodeTypes
},
getNodes(): GraphNode[] {
if (this.isReady) {
const nodes = this.elements.filter((n) => isGraphNode(n) && !n.isHidden) as GraphNode[]
return this.onlyRenderVisibleElements
? nodes &&
getNodesInside(
nodes,
{
x: 0,
y: 0,
width: this.dimensions.width,
height: this.dimensions.height,
},
this.transform,
true,
)
: nodes ?? []
}
return []
},
getEdges(): GraphEdge[] {
const edges = this.elements.filter((e) => isEdge(e) && !e.isHidden) as GraphEdge[]
if (this.isReady) {
return (
edges
.map((edge) => {
const { sourceNode, targetNode } = getSourceTargetNodes(edge, this.getNodes)
if (!sourceNode) console.warn(`couldn't create edge for source id: ${edge.source}; edge id: ${edge.id}`)
if (!targetNode) console.warn(`couldn't create edge for target id: ${edge.target}; edge id: ${edge.id}`)
return {
...edge,
sourceNode,
targetNode,
}
})
.filter(({ sourceNode, targetNode }) => !!(sourceNode && targetNode)) ?? []
)
}
return []
},
getSelectedNodes(): GraphNode[] {
return this.selectedElements?.filter(isGraphNode) ?? []
},
},
actions: {
async setElements(elements, force = true) {
const { nodes, edges } = parseElements(elements, this.elements, this.nodeExtent)
if (force) this.elements = []
await processElements([...nodes, ...edges], (processed) => {
this.elements = [...this.elements, ...processed]
})
this.hooks.elementsProcessed.trigger(this.elements)
},
setUserSelection(mousePos) {
this.selectionActive = true
this.userSelectionRect = {
width: 0,
height: 0,
startX: mousePos.x,
startY: mousePos.y,
x: mousePos.x,
y: mousePos.y,
draw: true,
}
},
updateUserSelection(mousePos) {
const startX = this.userSelectionRect.startX
const startY = this.userSelectionRect.startY
const nextUserSelectRect: FlowState['userSelectionRect'] = {
...this.userSelectionRect,
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),
}
const selectedNodes = getNodesInside(this.getNodes, this.userSelectionRect, this.transform)
const selectedEdges = getConnectedEdges(selectedNodes, this.getEdges)
const nextSelectedElements = [...selectedNodes, ...selectedEdges]
this.userSelectionRect = nextUserSelectRect
this.selectedElements = nextSelectedElements
},
unsetUserSelection() {
this.selectionActive = false
this.userSelectionRect.draw = false
if (!this.getSelectedNodes || this.getSelectedNodes.length === 0) {
this.selectedElements = undefined
this.hooks.selectionChange.trigger(undefined)
this.nodesSelectionActive = false
} else {
this.selectedNodesBbox = getRectOfNodes(this.getSelectedNodes)
this.nodesSelectionActive = true
}
},
addSelectedElements(elements) {
const selectedElementsArr = Array.isArray(elements) ? elements : [elements]
const selectedElementsUpdated = microDiff(selectedElementsArr, this.selectedElements ?? []).length
this.selectedElements = selectedElementsUpdated ? selectedElementsArr : this.selectedElements
if (selectedElementsUpdated) this.hooks.selectionChange.trigger(selectedElementsArr)
},
initD3Zoom({ d3ZoomHandler, d3Zoom, d3Selection }) {
this.d3Zoom = d3Zoom
this.d3Selection = d3Selection
this.d3ZoomHandler = d3ZoomHandler
},
setMinZoom(minZoom) {
this.d3Zoom?.scaleExtent([minZoom, this.maxZoom])
this.minZoom = minZoom
},
setMaxZoom(maxZoom) {
this.d3Zoom?.scaleExtent([this.minZoom, maxZoom])
this.maxZoom = maxZoom
},
setTranslateExtent(translateExtent) {
this.d3Zoom?.translateExtent(translateExtent)
this.translateExtent = translateExtent
},
setNodeExtent(nodeExtent) {
this.nodeExtent = nodeExtent
},
resetSelectedElements() {
this.selectedElements = undefined
},
unsetNodesSelection() {
this.nodesSelectionActive = false
},
updateSize(size) {
this.dimensions = size
},
setConnectionNodeId({ connectionHandleId, connectionHandleType, connectionNodeId }) {
this.connectionNodeId = connectionNodeId
this.connectionHandleId = connectionHandleId
this.connectionHandleType = connectionHandleType
},
setInteractive(isInteractive) {
this.nodesDraggable = isInteractive
this.nodesConnectable = isInteractive
this.elementsSelectable = isInteractive
},
addElements(elements: Elements) {
this.setElements(elements, false)
},
setState(state) {
if (typeof state.loading !== 'undefined') this.loading = state.loading
if (typeof state.panOnScroll !== 'undefined') this.panOnScroll = state.panOnScroll
if (typeof state.panOnScrollMode !== 'undefined') this.panOnScrollMode = state.panOnScrollMode
if (typeof state.panOnScrollSpeed !== 'undefined') this.panOnScrollSpeed = state.panOnScrollSpeed
if (typeof state.paneMoveable !== 'undefined') this.paneMoveable = state.paneMoveable
if (typeof state.zoomOnScroll !== 'undefined') this.zoomOnScroll = state.zoomOnScroll
if (typeof state.preventScrolling !== 'undefined') this.preventScrolling = state.preventScrolling
if (typeof state.zoomOnDoubleClick !== 'undefined') this.zoomOnDoubleClick = state.zoomOnDoubleClick
if (typeof state.zoomOnPinch !== 'undefined') this.zoomOnPinch = state.zoomOnPinch
if (typeof state.defaultZoom !== 'undefined') this.defaultZoom = state.defaultZoom
if (typeof state.defaultPosition !== 'undefined') this.defaultPosition = state.defaultPosition
if (typeof state.edgeTypes !== 'undefined') this.edgeTypes = state.edgeTypes
if (typeof state.nodeTypes !== 'undefined') this.nodeTypes = state.nodeTypes
if (typeof state.storageKey !== 'undefined') this.storageKey = state.storageKey
if (typeof state.edgeUpdaterRadius !== 'undefined') this.edgeUpdaterRadius = state.edgeUpdaterRadius
if (typeof state.elementsSelectable !== 'undefined') this.elementsSelectable = state.elementsSelectable
if (typeof state.onlyRenderVisibleElements !== 'undefined')
this.onlyRenderVisibleElements = state.onlyRenderVisibleElements
if (typeof state.edgesUpdatable !== 'undefined') this.edgesUpdatable = state.edgesUpdatable
if (typeof state.nodesConnectable !== 'undefined') this.nodesConnectable = state.nodesConnectable
if (typeof state.nodesDraggable !== 'undefined') this.nodesDraggable = state.nodesDraggable
if (typeof state.arrowHeadColor !== 'undefined') this.arrowHeadColor = state.arrowHeadColor
if (typeof state.markerEndId !== 'undefined') this.markerEndId = state.markerEndId
if (typeof state.deleteKeyCode !== 'undefined') this.deleteKeyCode = state.deleteKeyCode
if (typeof state.selectionKeyCode !== 'undefined') this.selectionKeyCode = state.selectionKeyCode
if (typeof state.zoomActivationKeyCode !== 'undefined') this.zoomActivationKeyCode = state.zoomActivationKeyCode
if (typeof state.multiSelectionKeyCode !== 'undefined') this.multiSelectionKeyCode = state.multiSelectionKeyCode
if (typeof state.snapToGrid !== 'undefined') this.snapToGrid = state.snapToGrid
if (typeof state.snapGrid !== 'undefined') this.snapGrid = state.snapGrid
if (!this.isReady)
until(() => this.d3Zoom)
.not.toBeUndefined()
.then(() => {
if (typeof state.maxZoom !== 'undefined') this.setMaxZoom(state.maxZoom)
if (typeof state.minZoom !== 'undefined') this.setMinZoom(state.minZoom)
if (typeof state.translateExtent !== 'undefined') this.setTranslateExtent(state.translateExtent)
if (typeof state.nodeExtent !== 'undefined') this.setNodeExtent(state.nodeExtent)
})
else {
if (typeof state.maxZoom !== 'undefined') this.setMaxZoom(state.maxZoom)
if (typeof state.minZoom !== 'undefined') this.setMinZoom(state.minZoom)
if (typeof state.translateExtent !== 'undefined') this.setTranslateExtent(state.translateExtent)
if (typeof state.nodeExtent !== 'undefined') this.setNodeExtent(state.nodeExtent)
}
},
},
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(store, import.meta.hot))
}
return store
}
+16
View File
@@ -0,0 +1,16 @@
import useState from './state'
import useActions from './actions'
import useGetters from './getters'
import { FlowState, FlowStore } from '~/types'
export default (id: string, preloadedState: FlowState): FlowStore => {
const state = reactive(useState(preloadedState))
const getters = useGetters(state)
const actions = useActions(state, getters)
return {
state,
...toRefs(state),
...getters,
...actions,
}
}
-3
View File
@@ -32,9 +32,6 @@ export type OnConnectStartParams = {
handleId?: ElementId handleId?: ElementId
handleType?: HandleType handleType?: HandleType
} }
export type OnConnectStartFunc = (event: MouseEvent, params: OnConnectStartParams) => void
export type OnConnectStopFunc = (event: MouseEvent) => void
export type OnConnectEndFunc = (event: MouseEvent) => void
export enum ConnectionMode { export enum ConnectionMode {
Strict = 'strict', Strict = 'strict',
+20 -31
View File
@@ -1,4 +1,5 @@
import { Store } from 'pinia' import { ComputedRef, ToRefs } from 'vue'
import { UnwrapNestedRefs } from '@vue/reactivity'
import { import {
Dimensions, Dimensions,
ElementId, ElementId,
@@ -13,14 +14,7 @@ import {
XYPosition, XYPosition,
} from './flow' } from './flow'
import { HandleType, EdgeComponent, NodeComponent } from './components' import { HandleType, EdgeComponent, NodeComponent } from './components'
import { import { ConnectionMode, SetConnectionId } from './connection'
ConnectionMode,
OnConnectEndFunc,
OnConnectFunc,
OnConnectStartFunc,
OnConnectStopFunc,
SetConnectionId,
} from './connection'
import { GraphEdge } from './edge' import { GraphEdge } from './edge'
import { NodeExtent, GraphNode, TranslateExtent } from './node' import { NodeExtent, GraphNode, TranslateExtent } from './node'
import { D3Selection, D3Zoom, D3ZoomHandler, InitD3ZoomPayload } from './zoom' import { D3Selection, D3Zoom, D3ZoomHandler, InitD3ZoomPayload } from './zoom'
@@ -28,13 +22,13 @@ import { FlowHooks } from './hooks'
export interface FlowState extends Omit<FlowOptions, 'elements'> { export interface FlowState extends Omit<FlowOptions, 'elements'> {
hooks: FlowHooks hooks: FlowHooks
instance?: FlowInstance instance: FlowInstance | undefined
elements: FlowElements elements: FlowElements
d3Zoom?: D3Zoom d3Zoom: D3Zoom | undefined
d3Selection?: D3Selection d3Selection: D3Selection | undefined
d3ZoomHandler?: D3ZoomHandler d3ZoomHandler: D3ZoomHandler | undefined
minZoom: number minZoom: number
maxZoom: number maxZoom: number
translateExtent: TranslateExtent translateExtent: TranslateExtent
@@ -42,16 +36,16 @@ export interface FlowState extends Omit<FlowOptions, 'elements'> {
dimensions: Dimensions dimensions: Dimensions
transform: Transform transform: Transform
selectedElements?: FlowElements selectedElements: FlowElements | undefined
selectedNodesBbox?: Rect selectedNodesBbox: Rect | undefined
nodesSelectionActive: boolean nodesSelectionActive: boolean
selectionActive: boolean selectionActive: boolean
userSelectionRect: SelectionRect userSelectionRect: SelectionRect
multiSelectionActive: boolean multiSelectionActive: boolean
connectionNodeId?: ElementId connectionNodeId: ElementId | undefined
connectionHandleId?: ElementId connectionHandleId: ElementId | undefined
connectionHandleType?: HandleType connectionHandleType: HandleType | undefined
connectionPosition: XYPosition connectionPosition: XYPosition
connectionMode: ConnectionMode connectionMode: ConnectionMode
@@ -63,11 +57,6 @@ export interface FlowState extends Omit<FlowOptions, 'elements'> {
nodesConnectable: boolean nodesConnectable: boolean
elementsSelectable: boolean elementsSelectable: boolean
onConnect?: OnConnectFunc
onConnectStart?: OnConnectStartFunc
onConnectStop?: OnConnectStopFunc
onConnectEnd?: OnConnectEndFunc
isReady: boolean isReady: boolean
vueFlowVersion: string vueFlowVersion: string
@@ -83,22 +72,22 @@ export interface FlowActions {
setMinZoom: (zoom: number) => void setMinZoom: (zoom: number) => void
setMaxZoom: (zoom: number) => void setMaxZoom: (zoom: number) => void
setTranslateExtent: (translateExtent: TranslateExtent) => void setTranslateExtent: (translateExtent: TranslateExtent) => void
setNodeExtent: (nodeExtent: NodeExtent) => void
resetSelectedElements: () => void resetSelectedElements: () => void
unsetNodesSelection: () => void unsetNodesSelection: () => void
updateSize: (size: Dimensions) => void updateSize: (size: Dimensions) => void
setConnectionNodeId: (payload: SetConnectionId) => void setConnectionNodeId: (payload: SetConnectionId) => void
setInteractive: (isInteractive: boolean) => void setInteractive: (isInteractive: boolean) => void
addElements: (elements: Elements) => void addElements: (elements: Elements) => void
setState: (state: FlowOptions) => void setState: (state: Partial<FlowOptions>) => void
} }
export interface FlowGetters { export interface FlowGetters {
getEdgeTypes: () => Record<string, EdgeComponent> getEdgeTypes: ComputedRef<Record<string, EdgeComponent>>
getNodeTypes: () => Record<string, NodeComponent> getNodeTypes: ComputedRef<Record<string, NodeComponent>>
getNodes: () => GraphNode[] getNodes: ComputedRef<GraphNode[]>
getEdges: () => GraphEdge[] getEdges: ComputedRef<GraphEdge[]>
getSelectedNodes: () => GraphNode[] getSelectedNodes: ComputedRef<GraphNode[]>
} }
export type FlowStore = Store<string, FlowState, FlowGetters, FlowActions> export type FlowStore = { state: FlowState } & ToRefs<FlowState> & FlowActions & FlowGetters
export type ReactiveFlowStore = UnwrapNestedRefs<FlowStore>
+3 -3
View File
@@ -15,8 +15,8 @@ import {
export function getHandlePosition(position: Position, node: GraphNode, handle?: HandleElement): XYPosition { export function getHandlePosition(position: Position, node: GraphNode, handle?: HandleElement): XYPosition {
const x = (handle?.x ?? 0) + node.position.x const x = (handle?.x ?? 0) + node.position.x
const y = (handle?.y ?? 0) + node.position.y const y = (handle?.y ?? 0) + node.position.y
const width = handle?.width || node.__vf.width const width = handle?.width ?? node.__vf.width
const height = handle?.height || node.__vf.height const height = handle?.height ?? node.__vf.height
switch (position) { switch (position) {
case Position.Top: case Position.Top:
@@ -48,7 +48,7 @@ export function getHandle(bounds: HandleElement[], handleId?: ElementId): Handle
// there is no handleId when there are no multiple handles/ handles with ids // there is no handleId when there are no multiple handles/ handles with ids
// so we just pick the first one // so we just pick the first one
let handle let handle
if (bounds.length === 1 || !handleId) { if (bounds.length === 1 ?? !handleId) {
handle = bounds[0] handle = bounds[0]
} else if (handleId) { } else if (handleId) {
handle = bounds.find((d) => d.id === handleId) handle = bounds.find((d) => d.id === handleId)
+5 -3
View File
@@ -16,6 +16,8 @@ import {
FlowElements, FlowElements,
GraphEdge, GraphEdge,
NextElements, NextElements,
ReactiveFlowStore,
FlowState,
} from '~/types' } from '~/types'
import { useWindow } from '~/composables' import { useWindow } from '~/composables'
import { getSourceTargetNodes } from '~/utils/edge' import { getSourceTargetNodes } from '~/utils/edge'
@@ -161,7 +163,7 @@ export const pointToRendererPoint = (
return position return position
} }
export const onLoadProject = (currentStore: FlowStore) => (position: XYPosition) => export const onLoadProject = (currentStore: ReactiveFlowStore) => (position: XYPosition) =>
pointToRendererPoint(position, currentStore.transform, currentStore.snapToGrid, currentStore.snapGrid) pointToRendererPoint(position, currentStore.transform, currentStore.snapToGrid, currentStore.snapGrid)
export const parseNode = (node: Node, nodeExtent: NodeExtent): GraphNode => ({ export const parseNode = (node: Node, nodeExtent: NodeExtent): GraphNode => ({
@@ -276,9 +278,9 @@ export const getConnectedEdges = (nodes: GraphNode[], edges: GraphEdge[]) => {
return edges.filter((edge) => nodeIds.includes(edge.source) || nodeIds.includes(edge.target)) return edges.filter((edge) => nodeIds.includes(edge.source) || nodeIds.includes(edge.target))
} }
export const onLoadGetElements = (currentStore: FlowStore) => (): FlowElements => currentStore.elements export const onLoadGetElements = (currentStore: ReactiveFlowStore) => (): FlowElements => currentStore.elements
export const onLoadToObject = (currentStore: FlowStore) => (): FlowExportObject => { export const onLoadToObject = (currentStore: FlowState) => (): FlowExportObject => {
// we have to stringify/parse so objects containing refs (like nodes and edges) can potentially be saved in a storage // we have to stringify/parse so objects containing refs (like nodes and edges) can potentially be saved in a storage
return JSON.parse( return JSON.parse(
JSON.stringify({ JSON.stringify({
+15 -2
View File
@@ -16,6 +16,8 @@ export const defaultEdgeTypes: DefaultEdgeTypes = {
} }
export const initialState = (): FlowState => ({ export const initialState = (): FlowState => ({
isReady: false,
instance: undefined,
dimensions: { dimensions: {
width: 0, width: 0,
height: 0, height: 0,
@@ -50,6 +52,9 @@ export const initialState = (): FlowState => ({
panOnScrollMode: PanOnScrollMode.Free, panOnScrollMode: PanOnScrollMode.Free,
paneMoveable: true, paneMoveable: true,
edgeUpdaterRadius: 10, edgeUpdaterRadius: 10,
onlyRenderVisibleElements: undefined,
defaultZoom: undefined,
defaultPosition: undefined,
nodesSelectionActive: false, nodesSelectionActive: false,
selectionActive: false, selectionActive: false,
@@ -65,6 +70,8 @@ export const initialState = (): FlowState => ({
}, },
arrowHeadColor: '#b1b1b7', arrowHeadColor: '#b1b1b7',
connectionLineStyle: {},
connectionLineType: undefined,
connectionNodeId: undefined, connectionNodeId: undefined,
connectionHandleId: undefined, connectionHandleId: undefined,
connectionHandleType: 'source', connectionHandleType: 'source',
@@ -78,12 +85,18 @@ export const initialState = (): FlowState => ({
nodesDraggable: true, nodesDraggable: true,
nodesConnectable: true, nodesConnectable: true,
elementsSelectable: true, elementsSelectable: true,
selectNodesOnDrag: undefined,
multiSelectionActive: false, multiSelectionActive: false,
deleteKeyCode: undefined,
selectionKeyCode: undefined,
multiSelectionKeyCode: undefined,
zoomActivationKeyCode: undefined,
isReady: false,
hooks: createHooks(), hooks: createHooks(),
loading: false, loading: false,
markerEndId: undefined,
storageKey: undefined,
vueFlowVersion: typeof __VUE_FLOW_VERSION__ !== 'undefined' ? __VUE_FLOW_VERSION__ : '-', vueFlowVersion: typeof __VUE_FLOW_VERSION__ !== 'undefined' ? __VUE_FLOW_VERSION__ : '-',
}) })