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-11-28 22:57:38 +01:00
parent 58f5c2568c
commit 58a8f9d4df
21 changed files with 387 additions and 354 deletions

View File

@@ -38,8 +38,7 @@
"d3": "^7.1.1",
"d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0",
"microdiff": "^1.2.0",
"pinia": "^2.0.0-rc.10"
"microdiff": "^1.2.0"
},
"devDependencies": {
"@antfu/eslint-config": "^0.9.0",

View File

@@ -54,7 +54,7 @@ export default {
@stop="onStop"
>
<div
:key="`vue-flow-nodesselection-rect-${store.$id}`"
:key="`vue-flow-nodesselection-rect-${store.id}`"
class="vue-flow__nodesselection-rect"
:style="innerStyle"
@contextmenu="onContextMenu"

View File

@@ -3,6 +3,6 @@ export { default as useHooks } from './useHooks'
export * from './useHooks'
export { default as useKeyPress } from './useKeyPress'
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 useVueFlow, initFlow } from './useVueFlow'

View File

@@ -1,6 +1,6 @@
import useStore from './useStore'
import { getHostForElement } from '~/utils'
import { Connection, ConnectionMode, ElementId, HandleType, ValidConnectionFunc } from '~/types'
import { Connection, ConnectionMode, ElementId, HandleType, ReactiveFlowStore, ValidConnectionFunc } from '~/types'
type Result = {
elementBelow: Element | null
@@ -67,7 +67,7 @@ const resetRecentHandle = (hoveredHandle: Element): void => {
hoveredHandle?.classList.remove('vue-flow__handle-connecting')
}
export default (store = useStore()) =>
export default (store: ReactiveFlowStore = useStore()) =>
(
event: MouseEvent,
handleId: ElementId,

View File

@@ -1,4 +1,4 @@
import { EmitFunc, FlowHooks, FlowEvents, FlowStore } from '~/types'
import { EmitFunc, FlowHooks, FlowEvents, FlowStore, ReactiveFlowStore } from '~/types'
// flow event hooks
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)

View File

@@ -1,38 +1,46 @@
import { FlowExportObject, FlowOptions, FlowState } from '~/types'
import { useStateStore } from '~/store'
import { FlowExportObject, FlowOptions, FlowState, ReactiveFlowStore } from '~/types'
import { useNewStore } from '~/store'
import { Store } from '~/context'
import { onLoadToObject, initialState } from '~/utils'
let id = 0
export default (options?: Partial<FlowOptions>, key?: string) => {
let store = inject(Store, undefined)
if (!store) {
const withStorage = options?.storageKey ?? key ?? false
if (import.meta.env.DEV) console.warn('store context not found; creating default store')
let storedState = ref<FlowExportObject>()
const initial = initialState()
const storageKey = key ?? `vue-flow-${id++}`
const preloadedState = {
...initial,
...(options as FlowState),
}
if (withStorage) {
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]
}
}
store = useStateStore(storageKey, preloadedState)()
if (withStorage && storageKey === store.$id) {
const toObject = onLoadToObject(store)
store.$subscribe(() => {
storedState.value = toObject()
})
}
provide(Store, store)
export const createStore = (options?: FlowOptions) => {
const withStorage = options?.storageKey ?? false
let storedState = ref<FlowExportObject>()
const initial = initialState()
const storageKey = options?.id ?? `vue-flow-${id++}`
const preloadedState = {
...initial,
...(options as FlowState),
}
if (withStorage) {
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]
}
}
const store = useNewStore(storageKey, preloadedState)
if (withStorage && storageKey === store.id?.value) {
const toObject = onLoadToObject(store.state)
watch(
store.state,
() => {
storedState.value = toObject()
},
{ deep: true },
)
}
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)
}

View File

@@ -1,10 +1,12 @@
import useStore from './useStore'
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)
return store
}
export default (options?: Partial<FlowOptions>): FlowStore => useStore(options)
export default useStore

View File

@@ -1,11 +1,11 @@
import { zoomIdentity } from 'd3-zoom'
import useStore from './useStore'
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
export default (store = useStore()): UseZoomPanHelper => {
export default (store: ReactiveFlowStore = useStore()): UseZoomPanHelper => {
return {
zoomIn: () => store.d3Selection && store.d3Zoom?.scaleBy(store.d3Selection, 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)
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
let nodes: GraphNode[] = []

View File

@@ -70,12 +70,12 @@ export default {
</script>
<template>
<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
v-if="props.nodesSelectionActive"
id="nodes-selection"
:key="`nodes-selction-${store.$id}`"
:key="`nodes-selction-${store.id}`"
: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>

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { CSSProperties, onBeforeUnmount } from 'vue'
import { CSSProperties } from 'vue'
import {
ConnectionLineType,
ConnectionMode,
@@ -64,6 +64,8 @@ interface FlowProps extends FlowOptions {
const props = withDefaults(defineProps<FlowProps>(), {
modelValue: () => [],
edgeTypes: () => [],
nodeTypes: () => [],
connectionMode: ConnectionMode.Loose,
connectionLineType: ConnectionLineType.Bezier,
selectionKeyCode: 'Shift',
@@ -103,10 +105,9 @@ const props = withDefaults(defineProps<FlowProps>(), {
loading: false,
})
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 options = Object.assign({}, store.$state, props)
store.setState(options)
store.setState(props)
watch(
() => props,
() => store.setState(props),
@@ -144,22 +145,16 @@ const transitionName = computed(() => {
}
return name
})
</script>
onBeforeUnmount(() => store?.$dispose())
</script>
<script lang="ts">
export default {
name: 'VueFlow'
}
</script>
<template>
<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">
<template #default>
<Renderer :key="`renderer-${store.$id}`" :elements="elements">
<Renderer :key="`renderer-${store.id}`" :elements="elements">
<ZoomPane
:key="`zoom-pane-${store.$id}`"
:key="`zoom-pane-${store.id}`"
:prevent-scrolling="store.preventScrolling"
:selection-key-code="store.selectionKeyCode"
:zoom-activation-key-code="store.zoomActivationKeyCode"
@@ -175,7 +170,7 @@ export default {
>
<template #default="zoomPaneProps">
<SelectionPane
:key="`selection-pane-${store.$id}`"
:key="`selection-pane-${store.id}`"
:edges="store.getEdges"
:selected-elements="store.selectedElements"
:selection-active="store.selectionActive"
@@ -186,7 +181,7 @@ export default {
:selection-key-code="store.selectionKeyCode"
>
<NodeRenderer
:key="`node-renderer-${store.$id}`"
:key="`node-renderer-${store.id}`"
:nodes="store.getNodes"
:node-types="store.getNodeTypes"
:snap-to-grid="store.snapToGrid"
@@ -200,13 +195,13 @@ export default {
<template
v-for="nodeName of Object.keys(store.getNodeTypes)"
#[`node-${nodeName}`]="nodeProps"
:key="`node-${nodeName}-${store.$id}`"
:key="`node-${nodeName}-${store.id}`"
>
<slot :name="`node-${nodeName}`" v-bind="nodeProps" />
</template>
</NodeRenderer>
<EdgeRenderer
:key="`edge-renderer-${store.$id}`"
:key="`edge-renderer-${store.id}`"
:transform="store.transform"
:dimensions="store.dimensions"
:edges="store.getEdges"
@@ -230,13 +225,13 @@ export default {
<template
v-for="edgeName of Object.keys(store.getEdgeTypes)"
#[`edge-${edgeName}`]="edgeProps"
:key="`edge-${edgeName}-${store.$id}`"
:key="`edge-${edgeName}-${store.id}`"
>
<slot :name="`edge-${edgeName}`" v-bind="edgeProps" />
</template>
<template #custom-connection-line="customConnectionLineProps">
<slot
:key="`connection-line-${store.$id}`"
:key="`connection-line-${store.id}`"
name="custom-connection-line"
v-bind="customConnectionLineProps"
/>
@@ -249,8 +244,8 @@ export default {
</Renderer>
</template>
<template v-if="store.loading" #fallback>
<slot :key="`loading-indicator-${store.$id}`" name="loading-indicator">
<LoadingIndicator :key="`default-loading-indicator-${store.$id}`" v-bind="store.loading">
<slot :key="`loading-indicator-${store.id}`" name="loading-indicator">
<LoadingIndicator :key="`default-loading-indicator-${store.id}`" v-bind="store.loading">
<slot name="loading-label" />
</LoadingIndicator>
</slot>

169
src/store/actions.ts Normal file
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
src/store/getters.ts Normal file
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,
}
}

View File

@@ -1 +1 @@
export { default as useStateStore } from './stateStore'
export { default as useNewStore } from './store'

5
src/store/state.ts Normal file
View File

@@ -0,0 +1,5 @@
import { FlowState } from '~/types'
export default (preloadedState: FlowState) => ({
...preloadedState,
})

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
src/store/store.ts Normal file
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,
}
}

View File

@@ -32,9 +32,6 @@ export type OnConnectStartParams = {
handleId?: ElementId
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 {
Strict = 'strict',

View File

@@ -1,4 +1,5 @@
import { Store } from 'pinia'
import { ComputedRef, ToRefs } from 'vue'
import { UnwrapNestedRefs } from '@vue/reactivity'
import {
Dimensions,
ElementId,
@@ -13,14 +14,7 @@ import {
XYPosition,
} from './flow'
import { HandleType, EdgeComponent, NodeComponent } from './components'
import {
ConnectionMode,
OnConnectEndFunc,
OnConnectFunc,
OnConnectStartFunc,
OnConnectStopFunc,
SetConnectionId,
} from './connection'
import { ConnectionMode, SetConnectionId } from './connection'
import { GraphEdge } from './edge'
import { NodeExtent, GraphNode, TranslateExtent } from './node'
import { D3Selection, D3Zoom, D3ZoomHandler, InitD3ZoomPayload } from './zoom'
@@ -28,13 +22,13 @@ import { FlowHooks } from './hooks'
export interface FlowState extends Omit<FlowOptions, 'elements'> {
hooks: FlowHooks
instance?: FlowInstance
instance: FlowInstance | undefined
elements: FlowElements
d3Zoom?: D3Zoom
d3Selection?: D3Selection
d3ZoomHandler?: D3ZoomHandler
d3Zoom: D3Zoom | undefined
d3Selection: D3Selection | undefined
d3ZoomHandler: D3ZoomHandler | undefined
minZoom: number
maxZoom: number
translateExtent: TranslateExtent
@@ -42,16 +36,16 @@ export interface FlowState extends Omit<FlowOptions, 'elements'> {
dimensions: Dimensions
transform: Transform
selectedElements?: FlowElements
selectedNodesBbox?: Rect
selectedElements: FlowElements | undefined
selectedNodesBbox: Rect | undefined
nodesSelectionActive: boolean
selectionActive: boolean
userSelectionRect: SelectionRect
multiSelectionActive: boolean
connectionNodeId?: ElementId
connectionHandleId?: ElementId
connectionHandleType?: HandleType
connectionNodeId: ElementId | undefined
connectionHandleId: ElementId | undefined
connectionHandleType: HandleType | undefined
connectionPosition: XYPosition
connectionMode: ConnectionMode
@@ -63,11 +57,6 @@ export interface FlowState extends Omit<FlowOptions, 'elements'> {
nodesConnectable: boolean
elementsSelectable: boolean
onConnect?: OnConnectFunc
onConnectStart?: OnConnectStartFunc
onConnectStop?: OnConnectStopFunc
onConnectEnd?: OnConnectEndFunc
isReady: boolean
vueFlowVersion: string
@@ -83,22 +72,22 @@ export interface FlowActions {
setMinZoom: (zoom: number) => void
setMaxZoom: (zoom: number) => void
setTranslateExtent: (translateExtent: TranslateExtent) => void
setNodeExtent: (nodeExtent: NodeExtent) => void
resetSelectedElements: () => void
unsetNodesSelection: () => void
updateSize: (size: Dimensions) => void
setConnectionNodeId: (payload: SetConnectionId) => void
setInteractive: (isInteractive: boolean) => void
addElements: (elements: Elements) => void
setState: (state: FlowOptions) => void
setState: (state: Partial<FlowOptions>) => void
}
export interface FlowGetters {
getEdgeTypes: () => Record<string, EdgeComponent>
getNodeTypes: () => Record<string, NodeComponent>
getNodes: () => GraphNode[]
getEdges: () => GraphEdge[]
getSelectedNodes: () => GraphNode[]
getEdgeTypes: ComputedRef<Record<string, EdgeComponent>>
getNodeTypes: ComputedRef<Record<string, NodeComponent>>
getNodes: ComputedRef<GraphNode[]>
getEdges: ComputedRef<GraphEdge[]>
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>

View File

@@ -15,8 +15,8 @@ import {
export function getHandlePosition(position: Position, node: GraphNode, handle?: HandleElement): XYPosition {
const x = (handle?.x ?? 0) + node.position.x
const y = (handle?.y ?? 0) + node.position.y
const width = handle?.width || node.__vf.width
const height = handle?.height || node.__vf.height
const width = handle?.width ?? node.__vf.width
const height = handle?.height ?? node.__vf.height
switch (position) {
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
// so we just pick the first one
let handle
if (bounds.length === 1 || !handleId) {
if (bounds.length === 1 ?? !handleId) {
handle = bounds[0]
} else if (handleId) {
handle = bounds.find((d) => d.id === handleId)

View File

@@ -16,6 +16,8 @@ import {
FlowElements,
GraphEdge,
NextElements,
ReactiveFlowStore,
FlowState,
} from '~/types'
import { useWindow } from '~/composables'
import { getSourceTargetNodes } from '~/utils/edge'
@@ -161,7 +163,7 @@ export const pointToRendererPoint = (
return position
}
export const onLoadProject = (currentStore: FlowStore) => (position: XYPosition) =>
export const onLoadProject = (currentStore: ReactiveFlowStore) => (position: XYPosition) =>
pointToRendererPoint(position, currentStore.transform, currentStore.snapToGrid, currentStore.snapGrid)
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))
}
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
return JSON.parse(
JSON.stringify({

View File

@@ -16,6 +16,8 @@ export const defaultEdgeTypes: DefaultEdgeTypes = {
}
export const initialState = (): FlowState => ({
isReady: false,
instance: undefined,
dimensions: {
width: 0,
height: 0,
@@ -50,6 +52,9 @@ export const initialState = (): FlowState => ({
panOnScrollMode: PanOnScrollMode.Free,
paneMoveable: true,
edgeUpdaterRadius: 10,
onlyRenderVisibleElements: undefined,
defaultZoom: undefined,
defaultPosition: undefined,
nodesSelectionActive: false,
selectionActive: false,
@@ -65,6 +70,8 @@ export const initialState = (): FlowState => ({
},
arrowHeadColor: '#b1b1b7',
connectionLineStyle: {},
connectionLineType: undefined,
connectionNodeId: undefined,
connectionHandleId: undefined,
connectionHandleType: 'source',
@@ -78,12 +85,18 @@ export const initialState = (): FlowState => ({
nodesDraggable: true,
nodesConnectable: true,
elementsSelectable: true,
selectNodesOnDrag: undefined,
multiSelectionActive: false,
deleteKeyCode: undefined,
selectionKeyCode: undefined,
multiSelectionKeyCode: undefined,
zoomActivationKeyCode: undefined,
isReady: false,
hooks: createHooks(),
loading: false,
markerEndId: undefined,
storageKey: undefined,
vueFlowVersion: typeof __VUE_FLOW_VERSION__ !== 'undefined' ? __VUE_FLOW_VERSION__ : '-',
})