feat(core): add onError hook

Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com>
This commit is contained in:
braks
2023-03-23 15:58:30 +01:00
committed by Braks
parent 87249e5dde
commit 8c3f54ec28
22 changed files with 250 additions and 180 deletions
+3 -1
View File
@@ -14,12 +14,13 @@ declare global {
const EdgeId: typeof import('./context/index')['EdgeId'] const EdgeId: typeof import('./context/index')['EdgeId']
const EdgeRef: typeof import('./context/index')['EdgeRef'] const EdgeRef: typeof import('./context/index')['EdgeRef']
const EffectScope: typeof import('vue')['EffectScope'] const EffectScope: typeof import('vue')['EffectScope']
const ErrorCode: typeof import('./utils/errors')['ErrorCode']
const NodeId: typeof import('./context/index')['NodeId'] const NodeId: typeof import('./context/index')['NodeId']
const NodeRef: typeof import('./context/index')['NodeRef'] const NodeRef: typeof import('./context/index')['NodeRef']
const Slots: typeof import('./context/index')['Slots'] const Slots: typeof import('./context/index')['Slots']
const Storage: typeof import('./composables/useVueFlow')['Storage'] const Storage: typeof import('./composables/useVueFlow')['Storage']
const VueFlow: typeof import('./context/index')['VueFlow'] const VueFlow: typeof import('./context/index')['VueFlow']
const VueFlowError: typeof import('./utils/log')['VueFlowError'] const VueFlowError: typeof import('./utils/errors')['VueFlowError']
const addEdge: typeof import('./utils/graph')['addEdge'] const addEdge: typeof import('./utils/graph')['addEdge']
const addEdgeToStore: typeof import('./utils/store')['addEdgeToStore'] const addEdgeToStore: typeof import('./utils/store')['addEdgeToStore']
const applyChanges: typeof import('./utils/changes')['applyChanges'] const applyChanges: typeof import('./utils/changes')['applyChanges']
@@ -44,6 +45,7 @@ declare global {
const createAdditionChange: typeof import('./utils/changes')['createAdditionChange'] const createAdditionChange: typeof import('./utils/changes')['createAdditionChange']
const createApp: typeof import('vue')['createApp'] const createApp: typeof import('vue')['createApp']
const createEventHook: typeof import('@vueuse/core')['createEventHook'] const createEventHook: typeof import('@vueuse/core')['createEventHook']
const createExtendedEventHook: typeof import('./utils/createExtendedEventHook')['createExtendedEventHook']
const createGlobalState: typeof import('@vueuse/core')['createGlobalState'] const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createGraphNodes: typeof import('./utils/store')['createGraphNodes'] const createGraphNodes: typeof import('./utils/store')['createGraphNodes']
const createHooks: typeof import('./store/hooks')['createHooks'] const createHooks: typeof import('./store/hooks')['createHooks']
@@ -235,7 +235,7 @@ const NodeWrapper = defineComponent({
nextPos.y = snapGrid[1] * Math.round(nextPos.y / snapGrid[1]) nextPos.y = snapGrid[1] * Math.round(nextPos.y / snapGrid[1])
} }
const { computedPosition, position } = calcNextPosition(node, nextPos, nodeExtent, parentNode) const { computedPosition, position } = calcNextPosition(node, nextPos, emits.error, nodeExtent, parentNode)
// only overwrite positions if there are changes when clamping // only overwrite positions if there are changes when clamping
if (node.computedPosition.x !== computedPosition.x || node.computedPosition.y !== computedPosition.y) { if (node.computedPosition.x !== computedPosition.x || node.computedPosition.y !== computedPosition.y) {
+2
View File
@@ -40,6 +40,7 @@ function useDrag(params: UseDragParams) {
removeSelectedElements, removeSelectedElements,
addSelectedNodes, addSelectedNodes,
updateNodePositions, updateNodePositions,
emits,
} = $(useVueFlow()) } = $(useVueFlow())
const { onStart, onDrag, onStop, el, disabled, id, selectable } = $(params) const { onStart, onDrag, onStop, el, disabled, id, selectable } = $(params)
@@ -83,6 +84,7 @@ function useDrag(params: UseDragParams) {
const { computedPosition } = calcNextPosition( const { computedPosition } = calcNextPosition(
n, n,
nextPosition, nextPosition,
emits.error,
nodeExtent, nodeExtent,
n.parentNode ? findNode(n.parentNode) : undefined, n.parentNode ? findNode(n.parentNode) : undefined,
) )
+3 -2
View File
@@ -1,4 +1,5 @@
import type { CustomEvent, ElementData } from '~/types' import type { CustomEvent, ElementData } from '~/types'
import { VueFlowError } from '~/utils/errors'
/** /**
* Access an edge * Access an edge
@@ -11,12 +12,12 @@ export default function useEdge<Data = ElementData, CustomEvents extends Record<
const edgeId = id ?? inject(EdgeId, '') const edgeId = id ?? inject(EdgeId, '')
const edgeEl = inject(EdgeRef, null) const edgeEl = inject(EdgeRef, null)
const { findEdge } = useVueFlow() const { findEdge, emits } = useVueFlow()
const edge = findEdge<Data, CustomEvents>(edgeId) const edge = findEdge<Data, CustomEvents>(edgeId)
if (!edge) { if (!edge) {
throw new VueFlowError(`Edge with id ${edgeId} not found!`, 'useEdge') emits.error(new VueFlowError(ErrorCode.EDGE_NOT_FOUND, edgeId))
} }
return { return {
@@ -1,15 +1,15 @@
import type { EdgeEventsEmit, EdgeEventsOn, GraphEdge, VueFlowStore } from '~/types' import type { EdgeEventsEmit, EdgeEventsOn, GraphEdge, VueFlowStore } from '~/types'
const createEdgeHooks = () => ({ const createEdgeHooks = () => ({
doubleClick: createEventHook(), doubleClick: createExtendedEventHook(),
click: createEventHook(), click: createExtendedEventHook(),
mouseEnter: createEventHook(), mouseEnter: createExtendedEventHook(),
mouseMove: createEventHook(), mouseMove: createExtendedEventHook(),
mouseLeave: createEventHook(), mouseLeave: createExtendedEventHook(),
contextMenu: createEventHook(), contextMenu: createExtendedEventHook(),
updateStart: createEventHook(), updateStart: createExtendedEventHook(),
update: createEventHook(), update: createExtendedEventHook(),
updateEnd: createEventHook(), updateEnd: createExtendedEventHook(),
}) })
export default function useEdgeHooks(edge: GraphEdge, emits: VueFlowStore['emits']): { emit: EdgeEventsEmit; on: EdgeEventsOn } { export default function useEdgeHooks(edge: GraphEdge, emits: VueFlowStore['emits']): { emit: EdgeEventsEmit; on: EdgeEventsOn } {
+4 -4
View File
@@ -11,19 +11,19 @@ export default function useNode<Data = ElementData, CustomEvents extends Record<
const nodeId = id ?? inject(NodeId, '') const nodeId = id ?? inject(NodeId, '')
const nodeEl = inject(NodeRef, null) const nodeEl = inject(NodeRef, null)
const { findNode, getEdges } = useVueFlow() const { findNode, getEdges, emits } = useVueFlow()
const node = findNode<Data, CustomEvents>(nodeId) const node = findNode<Data, CustomEvents>(nodeId)
if (!node) { if (!node) {
throw new VueFlowError(`Node with id ${nodeId} not found!`, 'useNode') emits.error(new VueFlowError(ErrorCode.NODE_NOT_FOUND, nodeId))
} }
return { return {
id: nodeId, id: nodeId,
nodeEl, nodeEl,
node, node,
parentNode: computed(() => (node.parentNode ? findNode(node.parentNode) : undefined)), parentNode: computed(() => (node!.parentNode ? findNode(node!.parentNode) : undefined)),
connectedEdges: computed(() => getConnectedEdges([node], getEdges.value)), connectedEdges: computed(() => getConnectedEdges([node!], getEdges.value)),
} }
} }
@@ -1,15 +1,15 @@
import type { GraphNode, NodeEventsEmit, NodeEventsOn, VueFlowStore } from '~/types' import type { GraphNode, NodeEventsEmit, NodeEventsOn, VueFlowStore } from '~/types'
const createNodeHooks = () => ({ const createNodeHooks = () => ({
doubleClick: createEventHook(), doubleClick: createExtendedEventHook(),
click: createEventHook(), click: createExtendedEventHook(),
mouseEnter: createEventHook(), mouseEnter: createExtendedEventHook(),
mouseMove: createEventHook(), mouseMove: createExtendedEventHook(),
mouseLeave: createEventHook(), mouseLeave: createExtendedEventHook(),
contextMenu: createEventHook(), contextMenu: createExtendedEventHook(),
dragStart: createEventHook(), dragStart: createExtendedEventHook(),
drag: createEventHook(), drag: createExtendedEventHook(),
dragStop: createEventHook(), dragStop: createExtendedEventHook(),
}) })
export default function useNodeHooks(node: GraphNode, emits: VueFlowStore['emits']): { emit: NodeEventsEmit; on: NodeEventsOn } { export default function useNodeHooks(node: GraphNode, emits: VueFlowStore['emits']): { emit: NodeEventsEmit; on: NodeEventsOn } {
@@ -1,7 +1,8 @@
import type { NodeDragItem, XYPosition } from '~/types' import type { NodeDragItem, XYPosition } from '~/types'
function useUpdateNodePositions() { function useUpdateNodePositions() {
const { getSelectedNodes, nodeExtent, updateNodePositions, findNode, snapGrid, snapToGrid, nodesDraggable } = useVueFlow() const { getSelectedNodes, nodeExtent, updateNodePositions, findNode, snapGrid, snapToGrid, nodesDraggable, emits } =
useVueFlow()
return (positionDiff: XYPosition, isShiftPressed = false) => { return (positionDiff: XYPosition, isShiftPressed = false) => {
// by default a node moves 5px on each key press, or 20px if shift is pressed // by default a node moves 5px on each key press, or 20px if shift is pressed
@@ -21,6 +22,7 @@ function useUpdateNodePositions() {
const { computedPosition } = calcNextPosition( const { computedPosition } = calcNextPosition(
n, n,
nextPosition, nextPosition,
emits.error,
nodeExtent.value, nodeExtent.value,
n.parentNode ? findNode(n.parentNode) : undefined, n.parentNode ? findNode(n.parentNode) : undefined,
) )
@@ -123,11 +123,6 @@ export default (options?: FlowProps): VueFlowStore => {
if (options) vueFlow.setState(options) if (options) vueFlow.setState(options)
} }
/**
* Vue flow wasn't able to find any store instance - we can't proceed
*/
if (!vueFlow) throw new VueFlowError('Store instance not found.', 'useVueFlow')
// always provide a fresh instance into context on call // always provide a fresh instance into context on call
if (scope) { if (scope) {
provide(VueFlow, vueFlow) provide(VueFlow, vueFlow)
@@ -19,6 +19,7 @@ const {
getEdgeTypes, getEdgeTypes,
elevateEdgesOnSelect, elevateEdgesOnSelect,
dimensions, dimensions,
emits,
} = $(useVueFlow()) } = $(useVueFlow())
const sourceNode = controlledComputed( const sourceNode = controlledComputed(
@@ -75,7 +76,7 @@ function getType(type?: string, template?: GraphEdge['template']) {
const slot = slots?.[`edge-${name}`] const slot = slots?.[`edge-${name}`]
if (!slot) { if (!slot) {
warn(`Edge type "${type}" not found and no edge-slot detected. Using fallback type "default".`) emits.error(new VueFlowError(ErrorCode.EDGE_TYPE_MISSING, edgeType))
return false return false
} }
@@ -68,7 +68,7 @@ function getType(type?: string, template?: GraphNode['template']) {
const slot = slots?.[`node-${name}`] const slot = slots?.[`node-${name}`]
if (!slot) { if (!slot) {
warn(`Node type "${type}" not found and no node-slot detected. Using fallback type "default".`) emits.error(new VueFlowError(ErrorCode.NODE_TYPE_MISSING, nodeType))
return false return false
} }
@@ -287,7 +287,9 @@ function setDimensions() {
const { width, height } = getDimensions(viewportEl.value) const { width, height } = getDimensions(viewportEl.value)
if (width === 0 || height === 0) warn('The Vue Flow parent container needs a width and a height to render the graph.') if (width === 0 || height === 0) {
emits.error(new VueFlowError(ErrorCode.MISSING_VIEWPORT_DIMENSIONS))
}
dimensions.width = width || 500 dimensions.width = width || 500
dimensions.height = height || 500 dimensions.height = height || 500
+1 -1
View File
@@ -71,6 +71,6 @@ export { default as useEdge } from './composables/useEdge'
export { useGetPointerPosition } from './composables/useGetPointerPosition' export { useGetPointerPosition } from './composables/useGetPointerPosition'
export { VueFlowError } from './utils/log' export { VueFlowError, ErrorCode } from './utils/errors'
export * from './types' export * from './types'
+34 -11
View File
@@ -290,7 +290,7 @@ export function useActions(state: State, getters: ComputedGetters): Actions {
if (!state.initialized && !nextNodes.length) return if (!state.initialized && !nextNodes.length) return
state.nodes = createGraphNodes(nextNodes, findNode, state.nodes) state.nodes = createGraphNodes(nextNodes, state.nodes, findNode, state.hooks.error.trigger)
} }
const setEdges: Actions['setEdges'] = (edges) => { const setEdges: Actions['setEdges'] = (edges) => {
@@ -315,9 +315,21 @@ export function useActions(state: State, getters: ComputedGetters): Actions {
const missingSource = !sourceNode || typeof sourceNode === 'undefined' const missingSource = !sourceNode || typeof sourceNode === 'undefined'
const missingTarget = !targetNode || typeof targetNode === 'undefined' const missingTarget = !targetNode || typeof targetNode === 'undefined'
if (missingSource) warn(`Couldn't create edge for source id: ${edge.source}; edge id: ${edge.id}`) if (missingSource && missingTarget) {
if (missingTarget) warn(`Couldn't create edge for target id: ${edge.target}; edge id: ${edge.id}`) state.hooks.error.trigger(new VueFlowError(ErrorCode.EDGE_SOURCE_TARGET_MISSING, edge.id, edge.source, edge.target))
if (missingSource || missingTarget) return res } else {
if (missingSource) {
state.hooks.error.trigger(new VueFlowError(ErrorCode.EDGE_SOURCE_MISSING, edge.id, edge.source))
}
if (missingTarget) {
state.hooks.error.trigger(new VueFlowError(ErrorCode.EDGE_TARGET_MISSING, edge.id, edge.target))
}
}
if (missingSource || missingTarget) {
return res
}
const storedEdge = getters.getEdge.value(edge.id) const storedEdge = getters.getEdge.value(edge.id)
@@ -343,7 +355,7 @@ export function useActions(state: State, getters: ComputedGetters): Actions {
const addNodes: Actions['addNodes'] = (nodes) => { const addNodes: Actions['addNodes'] = (nodes) => {
const nextNodes = nodes instanceof Function ? nodes(state.nodes) : nodes const nextNodes = nodes instanceof Function ? nodes(state.nodes) : nodes
const graphNodes = createGraphNodes(nextNodes, findNode, state.nodes) const graphNodes = createGraphNodes(nextNodes, state.nodes, findNode, state.hooks.error.trigger)
const changes = graphNodes.map(createAdditionChange) const changes = graphNodes.map(createAdditionChange)
@@ -370,6 +382,7 @@ export function useActions(state: State, getters: ComputedGetters): Actions {
...state.defaultEdgeOptions, ...state.defaultEdgeOptions,
}, },
state.edges, state.edges,
state.hooks.error.trigger,
) )
if (edge) { if (edge) {
@@ -379,9 +392,21 @@ export function useActions(state: State, getters: ComputedGetters): Actions {
const missingSource = !sourceNode || typeof sourceNode === 'undefined' const missingSource = !sourceNode || typeof sourceNode === 'undefined'
const missingTarget = !targetNode || typeof targetNode === 'undefined' const missingTarget = !targetNode || typeof targetNode === 'undefined'
if (missingSource) warn(`Couldn't create edge for source id: ${edge.source}; edge id: ${edge.id}`) if (missingSource && missingTarget) {
if (missingTarget) warn(`Couldn't create edge for target id: ${edge.target}; edge id: ${edge.id}`) state.hooks.error.trigger(new VueFlowError(ErrorCode.EDGE_SOURCE_TARGET_MISSING, edge.id, edge.source, edge.target))
if (missingTarget || missingSource) return acc } else {
if (missingSource) {
state.hooks.error.trigger(new VueFlowError(ErrorCode.EDGE_SOURCE_MISSING, edge.id, edge.source))
}
if (missingTarget) {
state.hooks.error.trigger(new VueFlowError(ErrorCode.EDGE_TARGET_MISSING, edge.id, edge.target))
}
}
if (missingSource || missingTarget) {
return acc
}
acc.push( acc.push(
createAdditionChange<GraphEdge>({ createAdditionChange<GraphEdge>({
@@ -445,7 +470,7 @@ export function useActions(state: State, getters: ComputedGetters): Actions {
} }
const updateEdge: Actions['updateEdge'] = (oldEdge, newConnection, shouldReplaceId = true) => const updateEdge: Actions['updateEdge'] = (oldEdge, newConnection, shouldReplaceId = true) =>
updateEdgeAction(oldEdge, newConnection, state.edges, findEdge, shouldReplaceId) updateEdgeAction(oldEdge, newConnection, state.edges, findEdge, shouldReplaceId, state.hooks.error.trigger)
const applyNodeChanges: Actions['applyNodeChanges'] = (changes) => applyChanges(changes, state.nodes) const applyNodeChanges: Actions['applyNodeChanges'] = (changes) => applyChanges(changes, state.nodes)
@@ -468,8 +493,6 @@ export function useActions(state: State, getters: ComputedGetters): Actions {
state.connectionPosition = position state.connectionPosition = position
state.connectionStartHandle.result = result state.connectionStartHandle.result = result
state.connectionStatus = status state.connectionStatus = status
} else {
warn('Cannot update connection as it has not been started')
} }
} }
+1 -1
View File
@@ -60,7 +60,7 @@ export function useGetters(state: State): ComputedGetters {
if (!source || !target) { if (!source || !target) {
state.edges = state.edges.filter((edge) => edge.id !== e.id) state.edges = state.edges.filter((edge) => edge.id !== e.id)
warn(`Orphaned edge ${e.id} removed.`) state.hooks.error.trigger(new VueFlowError(ErrorCode.EDGE_ORPHANED, e.id))
return return
} }
+53 -52
View File
@@ -1,64 +1,65 @@
import type { Ref } from 'vue' import type { Ref } from 'vue'
import type { Emits, FlowHooks } from '~/types' import type { FlowHooks } from '~/types'
// flow event hooks // flow event hooks
export const createHooks = (): FlowHooks => ({ export const createHooks = (): FlowHooks => ({
edgesChange: createEventHook(), edgesChange: createExtendedEventHook(),
nodesChange: createEventHook(), nodesChange: createExtendedEventHook(),
nodeDoubleClick: createEventHook(), nodeDoubleClick: createExtendedEventHook(),
nodeClick: createEventHook(), nodeClick: createExtendedEventHook(),
nodeMouseEnter: createEventHook(), nodeMouseEnter: createExtendedEventHook(),
nodeMouseMove: createEventHook(), nodeMouseMove: createExtendedEventHook(),
nodeMouseLeave: createEventHook(), nodeMouseLeave: createExtendedEventHook(),
nodeContextMenu: createEventHook(), nodeContextMenu: createExtendedEventHook(),
nodeDragStart: createEventHook(), nodeDragStart: createExtendedEventHook(),
nodeDrag: createEventHook(), nodeDrag: createExtendedEventHook(),
nodeDragStop: createEventHook(), nodeDragStop: createExtendedEventHook(),
nodesInitialized: createEventHook(), nodesInitialized: createExtendedEventHook(),
miniMapNodeClick: createEventHook(), miniMapNodeClick: createExtendedEventHook(),
miniMapNodeDoubleClick: createEventHook(), miniMapNodeDoubleClick: createExtendedEventHook(),
miniMapNodeMouseEnter: createEventHook(), miniMapNodeMouseEnter: createExtendedEventHook(),
miniMapNodeMouseMove: createEventHook(), miniMapNodeMouseMove: createExtendedEventHook(),
miniMapNodeMouseLeave: createEventHook(), miniMapNodeMouseLeave: createExtendedEventHook(),
connect: createEventHook(), connect: createExtendedEventHook(),
connectStart: createEventHook(), connectStart: createExtendedEventHook(),
connectEnd: createEventHook(), connectEnd: createExtendedEventHook(),
paneReady: createEventHook(), paneReady: createExtendedEventHook(),
move: createEventHook(), move: createExtendedEventHook(),
moveStart: createEventHook(), moveStart: createExtendedEventHook(),
moveEnd: createEventHook(), moveEnd: createExtendedEventHook(),
selectionDragStart: createEventHook(), selectionDragStart: createExtendedEventHook(),
selectionDrag: createEventHook(), selectionDrag: createExtendedEventHook(),
selectionDragStop: createEventHook(), selectionDragStop: createExtendedEventHook(),
selectionContextMenu: createEventHook(), selectionContextMenu: createExtendedEventHook(),
selectionStart: createEventHook(), selectionStart: createExtendedEventHook(),
selectionEnd: createEventHook(), selectionEnd: createExtendedEventHook(),
viewportChangeStart: createEventHook(), viewportChangeStart: createExtendedEventHook(),
viewportChange: createEventHook(), viewportChange: createExtendedEventHook(),
viewportChangeEnd: createEventHook(), viewportChangeEnd: createExtendedEventHook(),
paneScroll: createEventHook(), paneScroll: createExtendedEventHook(),
paneClick: createEventHook(), paneClick: createExtendedEventHook(),
paneContextMenu: createEventHook(), paneContextMenu: createExtendedEventHook(),
paneMouseEnter: createEventHook(), paneMouseEnter: createExtendedEventHook(),
paneMouseMove: createEventHook(), paneMouseMove: createExtendedEventHook(),
paneMouseLeave: createEventHook(), paneMouseLeave: createExtendedEventHook(),
edgeContextMenu: createEventHook(), edgeContextMenu: createExtendedEventHook(),
edgeMouseEnter: createEventHook(), edgeMouseEnter: createExtendedEventHook(),
edgeMouseMove: createEventHook(), edgeMouseMove: createExtendedEventHook(),
edgeMouseLeave: createEventHook(), edgeMouseLeave: createExtendedEventHook(),
edgeDoubleClick: createEventHook(), edgeDoubleClick: createExtendedEventHook(),
edgeClick: createEventHook(), edgeClick: createExtendedEventHook(),
edgeUpdateStart: createEventHook(), edgeUpdateStart: createExtendedEventHook(),
edgeUpdate: createEventHook(), edgeUpdate: createExtendedEventHook(),
edgeUpdateEnd: createEventHook(), edgeUpdateEnd: createExtendedEventHook(),
updateNodeInternals: createEventHook(), updateNodeInternals: createExtendedEventHook(),
error: createExtendedEventHook((err) => warn(err.message)),
}) })
export function useHooks(emit: Emits, hooks: Ref<FlowHooks>) { export function useHooks(emit: (...args: any[]) => void, hooks: Ref<FlowHooks>) {
onBeforeMount(() => { onBeforeMount(() => {
for (const [key, value] of Object.entries(hooks.value)) { for (const [key, value] of Object.entries(hooks.value)) {
const listener = (data: any) => { const listener = (data: any) => {
emit(key as Emits extends (event: infer Event) => void ? Event : never, data) emit(key, data)
} }
value.on(listener) value.on(listener)
+2 -63
View File
@@ -6,7 +6,7 @@ import type { Connection, OnConnectStartParams } from './connection'
import type { ViewportTransform } from './zoom' import type { ViewportTransform } from './zoom'
import type { EdgeChange, NodeChange } from './changes' import type { EdgeChange, NodeChange } from './changes'
import type { VueFlowStore } from './store' import type { VueFlowStore } from './store'
import type { FlowElements } from './flow' import type { VueFlowError } from '~/utils/errors'
export type MouseTouchEvent = MouseEvent | TouchEvent export type MouseTouchEvent = MouseEvent | TouchEvent
@@ -86,68 +86,7 @@ export interface FlowEvents {
edgeUpdateStart: EdgeMouseEvent edgeUpdateStart: EdgeMouseEvent
edgeUpdate: EdgeUpdateEvent edgeUpdate: EdgeUpdateEvent
edgeUpdateEnd: EdgeMouseEvent edgeUpdateEnd: EdgeMouseEvent
} error: VueFlowError
export interface Emits {
(event: 'nodesChange', changes: NodeChange[]): void
(event: 'edgesChange', changes: EdgeChange[]): void
(event: 'nodeDoubleClick', nodeMouseEvent: NodeMouseEvent): void
(event: 'nodeClick', nodeMouseEvent: NodeMouseEvent): void
(event: 'nodeMouseEnter', nodeMouseEvent: NodeMouseEvent): void
(event: 'nodeMouseMove', nodeMouseEvent: NodeMouseEvent): void
(event: 'nodeMouseLeave', nodeMouseEvent: NodeMouseEvent): void
(event: 'nodeContextMenu', nodeMouseEvent: NodeMouseEvent): void
(event: 'nodeDragStart', nodeDragEvent: NodeDragEvent): void
(event: 'nodeDrag', nodeDragEvent: NodeDragEvent): void
(event: 'nodeDragStop', nodeDragEvent: NodeDragEvent): void
(event: 'nodesInitialized'): void
(event: 'miniMapNodeClick', nodeMouseEvent: NodeMouseEvent): void
(event: 'miniMapNodeDoubleClick', nodeMouseEvent: NodeMouseEvent): void
(event: 'miniMapNodeMouseEnter', nodeMouseEvent: NodeMouseEvent): void
(event: 'miniMapNodeMouseMove', nodeMouseEvent: NodeMouseEvent): void
(event: 'miniMapNodeMouseLeave', nodeMouseEvent: NodeMouseEvent): void
(event: 'connect', connectionEvent: Connection): void
(
event: 'connectStart',
connectionEvent: {
event?: MouseEvent
} & OnConnectStartParams,
): void
(event: 'connectEnd', connectionEvent?: MouseEvent): void
(event: 'moveStart', moveEvent: { event: D3ZoomEvent<HTMLDivElement, any>; flowTransform: ViewportTransform }): void
(event: 'move', moveEvent: { event: D3ZoomEvent<HTMLDivElement, any>; flowTransform: ViewportTransform }): void
(event: 'moveEnd', moveEvent: { event: D3ZoomEvent<HTMLDivElement, any>; flowTransform: ViewportTransform }): void
(event: 'selectionDragStart', selectionEvent: NodeDragEvent): void
(event: 'selectionDrag', selectionEvent: NodeDragEvent): void
(event: 'selectionDragStop', selectionEvent: NodeDragEvent): void
(event: 'selectionContextMenu', selectionEvent: { event: MouseEvent; nodes: GraphNode[] }): void
(event: 'selectionStart', selectionEvent: MouseEvent): void
(event: 'selectionEnd', selectionEvent: MouseEvent): void
(event: 'viewportChangeStart', viewport: ViewportTransform): void
(event: 'viewportChange', viewport: ViewportTransform): void
(event: 'viewportChangeEnd', viewport: ViewportTransform): void
(event: 'paneReady', paneEvent: VueFlowStore): void
(event: 'paneScroll', paneEvent: WheelEvent | undefined): void
(event: 'paneClick', paneEvent: MouseEvent): void
(event: 'paneContextMenu', paneEvent: MouseEvent): void
(event: 'paneMouseEnter', paneEvent: MouseEvent): void
(event: 'paneMouseMove', paneEvent: MouseEvent): void
(event: 'paneMouseLeave', paneEvent: MouseEvent): void
(event: 'edgeContextMenu', edgeMouseEvent: EdgeMouseEvent): void
(event: 'edgeMouseEnter', edgeMouseEvent: EdgeMouseEvent): void
(event: 'edgeMouseMove', edgeMouseEvent: EdgeMouseEvent): void
(event: 'edgeMouseLeave', edgeMouseEvent: EdgeMouseEvent): void
(event: 'edgeDoubleClick', edgeMouseEvent: EdgeMouseEvent): void
(event: 'edgeClick', edgeMouseEvent: EdgeMouseEvent): void
(event: 'edgeUpdateStart', edgeMouseEvent: EdgeMouseEvent): void
(event: 'edgeUpdate', edgeUpdateEvent: EdgeUpdateEvent): void
(event: 'edgeUpdateEnd', edgeMouseEvent: EdgeMouseEvent): void
(event: 'updateNodeInternals'): void
/** v-model event definitions */
(event: 'update:modelValue', value: FlowElements): void
(event: 'update:nodes', value: GraphNode[]): void
(event: 'update:edges', value: GraphEdge[]): void
} }
export type FlowHooks = Readonly<{ export type FlowHooks = Readonly<{
@@ -0,0 +1,49 @@
import type { Ref } from 'vue'
import type { EventHook } from '@vueuse/core'
/**
* Source code taken from https://github.com/vueuse/vueuse/blob/main/packages/shared/createEventHook/index.ts
*
* Modified to be able to check if there are any event listeners
*/
export interface EventHookExtended<T> extends EventHook<T> {
fns: Ref<Set<(param: T) => void>>
}
export function createExtendedEventHook<T = any>(defaultHandler: (param: T) => void = () => {}): EventHookExtended<T> {
const fns = ref(new Set<(param: T) => void>())
if (defaultHandler) {
fns.value.add(defaultHandler)
}
const off = (fn: (param: T) => void) => {
fns.value.delete(fn)
}
const on = (fn: (param: T) => void) => {
if (fns.value.has(defaultHandler)) {
fns.value.delete(defaultHandler)
}
fns.value.add(fn)
const offFn = () => off(fn)
tryOnScopeDispose(offFn)
return {
off: offFn,
}
}
const trigger = (param: T) => {
return Promise.all(Array.from(fns.value).map((fn) => fn(param)))
}
return {
on,
off,
trigger,
fns,
}
}
+10 -4
View File
@@ -1,5 +1,5 @@
import { isNumber } from '@vueuse/shared' import { isNumber } from '@vueuse/shared'
import type { Actions, CoordinateExtent, ExtendedParentExtent, GraphNode, NodeDragItem, XYPosition } from '~/types' import type { Actions, CoordinateExtent, ExtendedParentExtent, GraphNode, NodeDragItem, State, XYPosition } from '~/types'
export function hasSelector(target: Element, selector: string, node: Element): boolean { export function hasSelector(target: Element, selector: string, node: Element): boolean {
let current = target let current = target
@@ -97,7 +97,12 @@ function getParentExtent(
return false return false
} }
export function getExtent<T extends NodeDragItem | GraphNode>(item: T, extent?: CoordinateExtent, parent?: GraphNode) { export function getExtent<T extends NodeDragItem | GraphNode>(
item: T,
onError: State['hooks']['error']['trigger'],
extent?: CoordinateExtent,
parent?: GraphNode,
) {
let currentExtent = item.extent || extent let currentExtent = item.extent || extent
if (item.extent === 'parent' || (!Array.isArray(item.extent) && item.extent?.range === 'parent')) { if (item.extent === 'parent' || (!Array.isArray(item.extent) && item.extent?.range === 'parent')) {
@@ -108,7 +113,7 @@ export function getExtent<T extends NodeDragItem | GraphNode>(item: T, extent?:
currentExtent = parentExtent currentExtent = parentExtent
} }
} else { } else {
warn('Only child nodes can use a parent extent.') onError(new VueFlowError(ErrorCode.NODE_EXTENT_INVALID, item.id))
currentExtent = extent currentExtent = extent
} }
@@ -127,10 +132,11 @@ export function getExtent<T extends NodeDragItem | GraphNode>(item: T, extent?:
export function calcNextPosition( export function calcNextPosition(
node: GraphNode | NodeDragItem, node: GraphNode | NodeDragItem,
nextPosition: XYPosition, nextPosition: XYPosition,
onError: State['hooks']['error']['trigger'],
nodeExtent?: CoordinateExtent, nodeExtent?: CoordinateExtent,
parentNode?: GraphNode, parentNode?: GraphNode,
) { ) {
const extent = getExtent(node, nodeExtent, parentNode) const extent = getExtent(node, onError, nodeExtent, parentNode)
const clampedPos = clampPosition(nextPosition, extent) const clampedPos = clampPosition(nextPosition, extent)
+47
View File
@@ -0,0 +1,47 @@
export enum ErrorCode {
MISSING_VIEWPORT_DIMENSIONS = 'MISSING_VIEWPORT_DIMENSIONS',
NODE_NOT_FOUND = 'NODE_NOT_FOUND',
NODE_MISSING_PARENT = 'NODE_MISSING_PARENT',
NODE_TYPE_MISSING = 'NODE_TYPE_MISSING',
NODE_EXTENT_INVALID = 'NODE_EXTENT_INVALID',
EDGE_INVALID = 'EDGE_INVALID',
EDGE_NOT_FOUND = 'EDGE_NOT_FOUND',
EDGE_SOURCE_MISSING = 'EDGE_SOURCE_MISSING',
EDGE_TARGET_MISSING = 'EDGE_TARGET_MISSING',
EDGE_TYPE_MISSING = 'EDGE_TYPE_MISSING',
EDGE_SOURCE_TARGET_SAME = 'EDGE_SOURCE_TARGET_SAME',
EDGE_SOURCE_TARGET_MISSING = 'EDGE_SOURCE_TARGET_MISSING',
EDGE_ORPHANED = 'EDGE_ORPHANED',
}
const messages = {
[ErrorCode.MISSING_VIEWPORT_DIMENSIONS]: () => 'The Vue Flow parent container needs a width and a height to render the graph',
[ErrorCode.NODE_NOT_FOUND]: (id: string) => `Node not found\nNode: ${id}`,
[ErrorCode.NODE_MISSING_PARENT]: (id: string, parentId: string) => `Node is missing a parent\nNode: ${id}\nParent: ${parentId}`,
[ErrorCode.NODE_TYPE_MISSING]: (type: string) => `Node type is missing\nType: ${type}`,
[ErrorCode.NODE_EXTENT_INVALID]: (id: string) => `Only child nodes can use a parent extent\nNode: ${id}`,
[ErrorCode.EDGE_INVALID]: (id: string) => `An edge needs a source and a target\nEdge: ${id}`,
[ErrorCode.EDGE_SOURCE_MISSING]: (id: string, source: string) => `Edge source is missing\nEdge: ${id} \nSource: ${source}`,
[ErrorCode.EDGE_TARGET_MISSING]: (id: string, target: string) => `Edge target is missing\nEdge: ${id} \nTarget: ${target}`,
[ErrorCode.EDGE_TYPE_MISSING]: (type: string) => `Edge type is missing\nType: ${type}`,
[ErrorCode.EDGE_SOURCE_TARGET_SAME]: (id: string, source: string, target: string) =>
`Edge source and target are the same\nEdge: ${id} \nSource: ${source} \nTarget: ${target}`,
[ErrorCode.EDGE_SOURCE_TARGET_MISSING]: (id: string, source: string, target: string) =>
`Edge source or target is missing\nEdge: ${id} \nSource: ${source} \nTarget: ${target}`,
[ErrorCode.EDGE_ORPHANED]: (id: string) =>
`Edge was orphaned (suddenly missing source or target) and has been removed\nEdge: ${id}`,
[ErrorCode.EDGE_NOT_FOUND]: (id: string) => `Edge not found\nEdge: ${id}`,
} as const
export class VueFlowError<T extends ErrorCode = ErrorCode> extends Error {
code: T
constructor(
code: T,
...args: (typeof messages)[T] extends (...args: any[]) => string ? Parameters<(typeof messages)[T]> : never
) {
// @ts-expect-error - TS doesn't know that the message is a key of messages
super(messages[code]?.(...args))
this.code = code
}
}
-6
View File
@@ -1,11 +1,5 @@
const productionEnvs = ['production', 'prod'] const productionEnvs = ['production', 'prod']
export class VueFlowError extends Error {
constructor(message: string, scope?: string) {
super(`[Vue Flow]: ${scope ? `${scope} - ` : ''}${message}`)
}
}
export function warn(message: string, ...args: any[]) { export function warn(message: string, ...args: any[]) {
if (!productionEnvs.includes(__ENV__ || '')) { if (!productionEnvs.includes(__ENV__ || '')) {
console.warn(`[Vue Flow]: ${message}`, ...args) console.warn(`[Vue Flow]: ${message}`, ...args)
+13 -7
View File
@@ -1,10 +1,10 @@
import type { Actions, Connection, Edge, GraphEdge, GraphNode, Node } from '~/types' import type { Actions, Connection, Edge, GraphEdge, GraphNode, Node, State } from '~/types'
export const isDef = <T>(val: T): val is NonNullable<T> => typeof unref(val) !== 'undefined' export const isDef = <T>(val: T): val is NonNullable<T> => typeof unref(val) !== 'undefined'
export function addEdgeToStore(edgeParams: Edge | Connection, edges: Edge[]) { export function addEdgeToStore(edgeParams: Edge | Connection, edges: Edge[], onError: State['hooks']['error']['trigger']) {
if (!edgeParams.source || !edgeParams.target) { if (!edgeParams.source || !edgeParams.target) {
warn("Can't create edge. An edge needs a source and a target.") onError(new VueFlowError(ErrorCode.EDGE_INVALID, (edgeParams as Edge).id))
return false return false
} }
@@ -31,16 +31,17 @@ export function updateEdgeAction(
edges: GraphEdge[], edges: GraphEdge[],
findEdge: Actions['findEdge'], findEdge: Actions['findEdge'],
shouldReplaceId: boolean, shouldReplaceId: boolean,
onError: State['hooks']['error']['trigger'],
) { ) {
if (!newConnection.source || !newConnection.target) { if (!newConnection.source || !newConnection.target) {
warn("Can't create new edge. An edge needs a source and a target.") onError(new VueFlowError(ErrorCode.EDGE_INVALID, edge.id))
return false return false
} }
const foundEdge = findEdge(edge.id) const foundEdge = findEdge(edge.id)
if (!foundEdge) { if (!foundEdge) {
warn(`The old edge with id=${edge.id} does not exist.`) onError(new VueFlowError(ErrorCode.EDGE_NOT_FOUND, edge.id))
return false return false
} }
@@ -60,7 +61,12 @@ export function updateEdgeAction(
return newEdge return newEdge
} }
export function createGraphNodes(nodes: Node[], findNode: Actions['findNode'], currGraphNodes: GraphNode[]) { export function createGraphNodes(
nodes: Node[],
currGraphNodes: GraphNode[],
findNode: Actions['findNode'],
onError: State['hooks']['error']['trigger'],
) {
const parentNodes: Record<string, true> = {} const parentNodes: Record<string, true> = {}
const graphNodes = nodes.map((node) => { const graphNodes = nodes.map((node) => {
@@ -82,7 +88,7 @@ export function createGraphNodes(nodes: Node[], findNode: Actions['findNode'], c
const parentNode = nextNodes.find((n) => n.id === node.parentNode) const parentNode = nextNodes.find((n) => n.id === node.parentNode)
if (node.parentNode && !parentNode) { if (node.parentNode && !parentNode) {
warn(`Parent node ${node.parentNode} not found`) onError(new VueFlowError(ErrorCode.NODE_MISSING_PARENT, node.id, node.parentNode))
} }
if (node.parentNode || parentNodes[node.id]) { if (node.parentNode || parentNodes[node.id]) {