refactor(core): rename core pkg dir to core
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
export { default as useHandle } from './useHandle'
|
||||
export { default as useKeyPress } from './useKeyPress'
|
||||
export { default as useZoomPanHelper } from './useZoomPanHelper'
|
||||
export { default as useWindow } from './useWindow'
|
||||
export { default as useVueFlow } from './useVueFlow'
|
||||
export { default as useDrag } from './useDrag'
|
||||
export { default as useNodeHooks } from './useNodeHooks'
|
||||
export { default as useEdgeHooks } from './useEdgeHooks'
|
||||
export { default as useEdge } from './useEdge'
|
||||
export { default as useNode } from './useNode'
|
||||
@@ -0,0 +1,167 @@
|
||||
import type { D3DragEvent, SubjectPosition } from 'd3-drag'
|
||||
import { drag } from 'd3-drag'
|
||||
import { select } from 'd3-selection'
|
||||
import type { Ref } from 'vue'
|
||||
import type { MaybeRef } from '@vueuse/core'
|
||||
import useVueFlow from './useVueFlow'
|
||||
import { handleNodeClick, pointToRendererPoint } from '~/utils'
|
||||
import type { NodeDragEvent, NodeDragItem, SnapGrid, XYPosition } from '~/types'
|
||||
import { getDragItems, getEventHandlerParams, hasSelector, updatePosition } from '~/utils/drag'
|
||||
|
||||
export type UseDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>
|
||||
|
||||
interface UseDragParams {
|
||||
onStart: (event: NodeDragEvent['event'], currentNode: NodeDragEvent['node'], nodes: NodeDragEvent['nodes']) => void
|
||||
onDrag: (event: NodeDragEvent['event'], currentNode: NodeDragEvent['node'], nodes: NodeDragEvent['nodes']) => void
|
||||
onStop: (event: NodeDragEvent['event'], currentNode: NodeDragEvent['node'], nodes: NodeDragEvent['nodes']) => void
|
||||
el: Ref<Element>
|
||||
disabled?: MaybeRef<boolean>
|
||||
id?: string
|
||||
}
|
||||
|
||||
function useDrag(params: UseDragParams) {
|
||||
const scope = effectScope()
|
||||
|
||||
const dragging = scope.run(() => {
|
||||
const {
|
||||
viewport,
|
||||
snapToGrid,
|
||||
snapGrid: globalSnapGrid,
|
||||
noDragClassName,
|
||||
nodes,
|
||||
nodeExtent,
|
||||
getNode,
|
||||
multiSelectionActive,
|
||||
nodesSelectionActive,
|
||||
selectNodesOnDrag,
|
||||
removeSelectedElements,
|
||||
addSelectedNodes,
|
||||
updateNodePositions,
|
||||
} = $(useVueFlow())
|
||||
|
||||
const { onStart, onDrag, onStop, el, disabled = false, id } = $(params)
|
||||
|
||||
const dragging = ref(false)
|
||||
let dragItems = $ref<NodeDragItem[]>()
|
||||
let lastPos = $ref<Partial<XYPosition>>({ x: undefined, y: undefined })
|
||||
let dragHandler = $ref<any>()
|
||||
|
||||
const hasSnapGrid = (sg?: SnapGrid) => (sg ?? snapToGrid ? globalSnapGrid : undefined)
|
||||
|
||||
const getMousePosition = (event: UseDragEvent, snapGrid: SnapGrid) => {
|
||||
const x = event.sourceEvent.touches ? event.sourceEvent.touches[0].clientX : event.sourceEvent.clientX
|
||||
const y = event.sourceEvent.touches ? event.sourceEvent.touches[0].clientY : event.sourceEvent.clientY
|
||||
|
||||
return pointToRendererPoint(
|
||||
{
|
||||
x,
|
||||
y,
|
||||
},
|
||||
viewport,
|
||||
!!snapGrid ?? snapToGrid,
|
||||
snapGrid ?? globalSnapGrid,
|
||||
)
|
||||
}
|
||||
|
||||
watch([() => disabled, () => el], () => {
|
||||
if (el) {
|
||||
const selection = select(el)
|
||||
const node = id ? getNode(id) : undefined
|
||||
|
||||
if (disabled) {
|
||||
selection.on('.drag', null)
|
||||
} else {
|
||||
dragHandler = drag()
|
||||
.on('start', (event: UseDragEvent) => {
|
||||
if (!selectNodesOnDrag && !multiSelectionActive && id) {
|
||||
if (!node?.selected) {
|
||||
removeSelectedElements()
|
||||
}
|
||||
}
|
||||
|
||||
if (node && !disabled && selectNodesOnDrag) {
|
||||
handleNodeClick(node, multiSelectionActive, addSelectedNodes, removeSelectedElements, $$(nodesSelectionActive))
|
||||
}
|
||||
|
||||
const mousePos = getMousePosition(event, hasSnapGrid(node?.snapGrid) as SnapGrid)
|
||||
dragItems = getDragItems(nodes, mousePos, getNode, id)
|
||||
|
||||
if (onStart && dragItems) {
|
||||
const [currentNode, nodes] = getEventHandlerParams({
|
||||
id,
|
||||
dragItems,
|
||||
getNode: $$(getNode),
|
||||
})
|
||||
onStart(event.sourceEvent, currentNode, nodes)
|
||||
}
|
||||
})
|
||||
.on('drag', (event: UseDragEvent) => {
|
||||
const snapGrid = hasSnapGrid(node?.snapGrid) as SnapGrid
|
||||
|
||||
const mousePos = getMousePosition(event, snapGrid)
|
||||
|
||||
// skip events without movement
|
||||
if ((lastPos.x !== mousePos.x || lastPos.y !== mousePos.y) && dragItems) {
|
||||
lastPos = mousePos
|
||||
dragItems = dragItems.map((n) =>
|
||||
updatePosition(
|
||||
n,
|
||||
mousePos,
|
||||
!!snapGrid ?? snapToGrid,
|
||||
snapGrid ?? globalSnapGrid,
|
||||
n.parentNode ? getNode(n.parentNode) : undefined,
|
||||
nodeExtent,
|
||||
),
|
||||
)
|
||||
|
||||
updateNodePositions(dragItems, true, true)
|
||||
dragging.value = true
|
||||
|
||||
if (onDrag) {
|
||||
const [currentNode, nodes] = getEventHandlerParams({
|
||||
id,
|
||||
dragItems,
|
||||
getNode: $$(getNode),
|
||||
})
|
||||
onDrag(event.sourceEvent, currentNode, nodes)
|
||||
}
|
||||
}
|
||||
|
||||
event.on('end', (event) => {
|
||||
dragging.value = false
|
||||
if (onStop && dragItems) {
|
||||
updateNodePositions(dragItems, false, false)
|
||||
|
||||
const [currentNode, nodes] = getEventHandlerParams({
|
||||
id,
|
||||
dragItems,
|
||||
getNode: $$(getNode),
|
||||
})
|
||||
onStop(event.sourceEvent, currentNode, nodes)
|
||||
}
|
||||
})
|
||||
})
|
||||
.filter((event: D3DragEvent<HTMLDivElement, null, SubjectPosition>['sourceEvent']) => {
|
||||
const target = event.target as HTMLDivElement
|
||||
return (
|
||||
!event.button &&
|
||||
(!noDragClassName ||
|
||||
(!hasSelector(target, `.${noDragClassName}`, $$(el)) &&
|
||||
(!node?.dragHandle || hasSelector(target, node.dragHandle, $$(el)))))
|
||||
)
|
||||
})
|
||||
|
||||
selection.call(dragHandler)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return dragging
|
||||
})
|
||||
|
||||
tryOnScopeDispose(() => scope.stop())
|
||||
|
||||
return dragging
|
||||
}
|
||||
|
||||
export default useDrag
|
||||
@@ -0,0 +1,29 @@
|
||||
import useVueFlow from './useVueFlow'
|
||||
import { EdgeId, EdgeRef } from '~/context'
|
||||
import type { CustomEvent, ElementData } from '~/types'
|
||||
|
||||
/**
|
||||
* Access an edge
|
||||
*
|
||||
* If no edge id is provided, the edge id is injected from context
|
||||
*
|
||||
* Meaning if you do not provide an id, this composable has to be called in a child of your custom edge component, or it will throw
|
||||
*/
|
||||
export default function useEdge<Data = ElementData, CustomEvents extends Record<string, CustomEvent> = any>(id?: string) {
|
||||
const edgeId = id ?? inject(EdgeId, '')
|
||||
const edgeEl = inject(EdgeRef, null)
|
||||
|
||||
const { findEdge } = useVueFlow()
|
||||
|
||||
const edge = findEdge<Data, CustomEvents>(edgeId)
|
||||
|
||||
if (!edge) {
|
||||
throw new Error(`[vue-flow]: useEdge - Edge with id ${edgeId} not found!`)
|
||||
}
|
||||
|
||||
return {
|
||||
id: edgeId,
|
||||
edge,
|
||||
edgeEl,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { EdgeEventsEmit, EdgeEventsOn, GraphEdge, VueFlowStore } from '~/types'
|
||||
|
||||
const createEdgeHooks = () => ({
|
||||
doubleClick: createEventHook(),
|
||||
click: createEventHook(),
|
||||
mouseEnter: createEventHook(),
|
||||
mouseMove: createEventHook(),
|
||||
mouseLeave: createEventHook(),
|
||||
contextMenu: createEventHook(),
|
||||
updateStart: createEventHook(),
|
||||
update: createEventHook(),
|
||||
updateEnd: createEventHook(),
|
||||
})
|
||||
|
||||
export default function useEdgeHooks(edge: GraphEdge, emits: VueFlowStore['emits']): { emit: EdgeEventsEmit; on: EdgeEventsOn } {
|
||||
const edgeHooks = createEdgeHooks()
|
||||
|
||||
edgeHooks.doubleClick.on((event) => {
|
||||
emits.edgeDoubleClick(event)
|
||||
edge.events?.doubleClick?.(event)
|
||||
})
|
||||
|
||||
edgeHooks.click.on((event) => {
|
||||
emits.edgeClick(event)
|
||||
edge.events?.click?.(event)
|
||||
})
|
||||
|
||||
edgeHooks.mouseEnter.on((event) => {
|
||||
emits.edgeMouseEnter(event)
|
||||
edge.events?.mouseEnter?.(event)
|
||||
})
|
||||
|
||||
edgeHooks.mouseMove.on((event) => {
|
||||
emits.edgeMouseMove(event)
|
||||
edge.events?.mouseMove?.(event)
|
||||
})
|
||||
|
||||
edgeHooks.mouseLeave.on((event) => {
|
||||
emits.edgeMouseLeave(event)
|
||||
edge.events?.mouseLeave?.(event)
|
||||
})
|
||||
|
||||
edgeHooks.contextMenu.on((event) => {
|
||||
emits.edgeContextMenu(event)
|
||||
edge.events?.contextMenu?.(event)
|
||||
})
|
||||
|
||||
edgeHooks.updateStart.on((event) => {
|
||||
emits.edgeUpdateStart(event)
|
||||
edge.events?.updateStart?.(event)
|
||||
})
|
||||
|
||||
edgeHooks.update.on((event) => {
|
||||
emits.edgeUpdate(event)
|
||||
edge.events?.update?.(event)
|
||||
})
|
||||
|
||||
edgeHooks.updateEnd.on((event) => {
|
||||
emits.edgeUpdateEnd(event)
|
||||
edge.events?.updateEnd?.(event)
|
||||
})
|
||||
|
||||
return Object.entries(edgeHooks).reduce(
|
||||
(hooks, [key, value]) => {
|
||||
hooks.emit[key as keyof EdgeEventsEmit] = value.trigger
|
||||
hooks.on[key as keyof EdgeEventsOn] = value.on
|
||||
|
||||
return hooks
|
||||
},
|
||||
{ emit: {} as EdgeEventsEmit, on: {} as EdgeEventsOn },
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
import { isFunction } from '@vueuse/core'
|
||||
import useVueFlow from './useVueFlow'
|
||||
import { getHostForElement } from '~/utils'
|
||||
import type { Connection, Getters, GraphEdge, HandleType, ValidConnectionFunc } from '~/types'
|
||||
import { ConnectionMode } from '~/types'
|
||||
|
||||
interface Result {
|
||||
elementBelow: Element | null
|
||||
isValid: boolean
|
||||
connection: Connection
|
||||
isHoveringHandle: boolean
|
||||
}
|
||||
|
||||
// checks if element below mouse is a handle and returns connection in form of an object { source: 123, target: 312 }
|
||||
export const checkElementBelowIsValid = (
|
||||
event: MouseEvent,
|
||||
connectionMode: ConnectionMode,
|
||||
isTarget: boolean,
|
||||
nodeId: string,
|
||||
handleId: string | null,
|
||||
isValidConnection: ValidConnectionFunc | undefined,
|
||||
doc: Document,
|
||||
edges: GraphEdge[],
|
||||
getNode: Getters['getNode'],
|
||||
) => {
|
||||
const elementBelow = doc.elementFromPoint(event.clientX, event.clientY)
|
||||
const elementBelowIsTarget = elementBelow?.classList.contains('target') || false
|
||||
const elementBelowIsSource = elementBelow?.classList.contains('source') || false
|
||||
|
||||
const result: Result = {
|
||||
elementBelow,
|
||||
isValid: false,
|
||||
connection: { source: '', target: '', sourceHandle: null, targetHandle: null },
|
||||
isHoveringHandle: false,
|
||||
}
|
||||
|
||||
if (elementBelow && (elementBelowIsTarget || elementBelowIsSource)) {
|
||||
result.isHoveringHandle = true
|
||||
|
||||
// in strict mode we don't allow target to target or source to source connections
|
||||
const isValid =
|
||||
connectionMode === ConnectionMode.Strict ? (isTarget && elementBelowIsSource) || (!isTarget && elementBelowIsTarget) : true
|
||||
|
||||
if (isValid) {
|
||||
const elementBelowNodeId = elementBelow.getAttribute('data-nodeid') ?? ''
|
||||
const elementBelowHandleId = elementBelow.getAttribute('data-handleid') ?? ''
|
||||
|
||||
const sourceId = isTarget ? elementBelowNodeId : nodeId
|
||||
const sourceHandleId = isTarget ? elementBelowHandleId : handleId
|
||||
const targetId = isTarget ? nodeId : elementBelowNodeId
|
||||
const targetHandleId = isTarget ? handleId : elementBelowHandleId
|
||||
|
||||
const connection: Connection = {
|
||||
source: sourceId,
|
||||
sourceHandle: sourceHandleId,
|
||||
target: targetId,
|
||||
targetHandle: targetHandleId,
|
||||
}
|
||||
|
||||
result.connection = connection
|
||||
|
||||
result.isValid =
|
||||
(isFunction(isValidConnection)
|
||||
? isValidConnection(connection, { edges, sourceNode: getNode(sourceId)!, targetNode: getNode(targetId)! })
|
||||
: true) ||
|
||||
!result.connection.target ||
|
||||
!result.connection.source
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const resetRecentHandle = (hoveredHandle: Element): void => {
|
||||
hoveredHandle?.classList.remove('vue-flow__handle-valid')
|
||||
hoveredHandle?.classList.remove('vue-flow__handle-connecting')
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const {
|
||||
edges,
|
||||
connectOnClick,
|
||||
nodesConnectable,
|
||||
connectionStartHandle,
|
||||
connectionMode,
|
||||
emits,
|
||||
startConnection,
|
||||
updateConnection,
|
||||
endConnection,
|
||||
getNode,
|
||||
vueFlowRef,
|
||||
} = $(useVueFlow())
|
||||
|
||||
let recentHoveredHandle: Element
|
||||
|
||||
const onMouseDown = (
|
||||
event: MouseEvent,
|
||||
handleId: string | null,
|
||||
nodeId: string,
|
||||
isTarget: boolean,
|
||||
isValidConnection?: ValidConnectionFunc,
|
||||
elementEdgeUpdaterType?: HandleType,
|
||||
onEdgeUpdate?: (connection: Connection) => void,
|
||||
onEdgeUpdateEnd?: () => void,
|
||||
) => {
|
||||
const doc = getHostForElement(event.target as HTMLElement)
|
||||
if (!doc) return
|
||||
|
||||
let validConnectFunc = isValidConnection
|
||||
|
||||
const node = getNode(nodeId)
|
||||
|
||||
if (node && (typeof node.connectable === 'undefined' ? nodesConnectable : node.connectable) === false) return
|
||||
|
||||
if (!isValidConnection) {
|
||||
if (node) validConnectFunc = !isTarget ? node.isValidTargetPos : node.isValidSourcePos
|
||||
}
|
||||
|
||||
const elementBelow = doc.elementFromPoint(event.clientX, event.clientY)
|
||||
const elementBelowIsTarget = elementBelow?.classList.contains('target')
|
||||
const elementBelowIsSource = elementBelow?.classList.contains('source')
|
||||
|
||||
if (!vueFlowRef || (!elementBelowIsTarget && !elementBelowIsSource && !elementEdgeUpdaterType)) return
|
||||
|
||||
const handleType = elementEdgeUpdaterType ?? (elementBelowIsTarget ? 'target' : 'source')
|
||||
|
||||
const containerBounds = vueFlowRef.getBoundingClientRect()
|
||||
|
||||
startConnection(
|
||||
{
|
||||
nodeId,
|
||||
handleId,
|
||||
type: handleType,
|
||||
},
|
||||
{
|
||||
x: event.clientX - containerBounds.left,
|
||||
y: event.clientY - containerBounds.top,
|
||||
},
|
||||
event,
|
||||
)
|
||||
|
||||
function onMouseMove(event: MouseEvent) {
|
||||
updateConnection({
|
||||
x: event.clientX - containerBounds.left,
|
||||
y: event.clientY - containerBounds.top,
|
||||
})
|
||||
|
||||
const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid(
|
||||
event,
|
||||
connectionMode,
|
||||
isTarget,
|
||||
nodeId,
|
||||
handleId,
|
||||
validConnectFunc,
|
||||
doc,
|
||||
edges,
|
||||
getNode,
|
||||
)
|
||||
|
||||
if (!isHoveringHandle) return resetRecentHandle(recentHoveredHandle)
|
||||
|
||||
const isOwnHandle = connection.source === connection.target
|
||||
|
||||
if (!isOwnHandle && elementBelow) {
|
||||
recentHoveredHandle = elementBelow
|
||||
elementBelow.classList.add('vue-flow__handle-connecting')
|
||||
elementBelow.classList.toggle('vue-flow__handle-valid', isValid)
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseUp(event: MouseEvent) {
|
||||
const { connection, isValid } = checkElementBelowIsValid(
|
||||
event,
|
||||
connectionMode,
|
||||
isTarget,
|
||||
nodeId,
|
||||
handleId,
|
||||
validConnectFunc,
|
||||
doc,
|
||||
edges,
|
||||
getNode,
|
||||
)
|
||||
|
||||
const isOwnHandle = connection.source === connection.target
|
||||
|
||||
if (isValid && !isOwnHandle) {
|
||||
if (!onEdgeUpdate) emits.connect(connection)
|
||||
else onEdgeUpdate(connection)
|
||||
}
|
||||
|
||||
if (elementEdgeUpdaterType) onEdgeUpdateEnd?.()
|
||||
|
||||
resetRecentHandle(recentHoveredHandle)
|
||||
|
||||
endConnection(event)
|
||||
|
||||
doc.removeEventListener('mousemove', onMouseMove as EventListenerOrEventListenerObject)
|
||||
doc.removeEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject)
|
||||
}
|
||||
|
||||
doc.addEventListener('mousemove', onMouseMove as EventListenerOrEventListenerObject)
|
||||
doc.addEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject)
|
||||
}
|
||||
|
||||
const onClick = (
|
||||
event: MouseEvent,
|
||||
handleId: string | null,
|
||||
nodeId: string,
|
||||
handleType: HandleType,
|
||||
isValidConnection?: ValidConnectionFunc,
|
||||
) => {
|
||||
if (!connectOnClick) return
|
||||
if (!connectionStartHandle) {
|
||||
startConnection({ nodeId, type: handleType, handleId }, undefined, event)
|
||||
} else {
|
||||
let validConnectFunc: ValidConnectionFunc = isValidConnection ?? (() => true)
|
||||
|
||||
const node = getNode(nodeId)
|
||||
|
||||
if (node && (typeof node.connectable === 'undefined' ? nodesConnectable : node.connectable) === false) return
|
||||
|
||||
if (!isValidConnection) {
|
||||
if (node) validConnectFunc = (handleType !== 'target' ? node.isValidTargetPos : node.isValidSourcePos) ?? (() => true)
|
||||
}
|
||||
|
||||
const doc = getHostForElement(event.target as HTMLElement)
|
||||
|
||||
const { connection, isValid } = checkElementBelowIsValid(
|
||||
event as MouseEvent,
|
||||
connectionMode,
|
||||
connectionStartHandle.type === 'target',
|
||||
connectionStartHandle.nodeId,
|
||||
connectionStartHandle.handleId || null,
|
||||
validConnectFunc,
|
||||
doc,
|
||||
edges,
|
||||
getNode,
|
||||
)
|
||||
|
||||
const isOwnHandle = connection.source === connection.target
|
||||
|
||||
if (isValid && !isOwnHandle) emits.connect(connection)
|
||||
|
||||
endConnection(event)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
onMouseDown,
|
||||
onClick,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { Ref } from 'vue'
|
||||
import type { KeyFilter, MaybeRef } from '@vueuse/core'
|
||||
import { isBoolean, isFunction } from '@vueuse/core'
|
||||
import useWindow from './useWindow'
|
||||
|
||||
function isInputDOMNode(event: KeyboardEvent): boolean {
|
||||
const target = event.target as HTMLElement
|
||||
const hasAttribute = isFunction(target.hasAttribute) ? target.hasAttribute('contenteditable') : false
|
||||
const closest = isFunction(target.closest) ? target.closest('.nokey') : null
|
||||
|
||||
return ['INPUT', 'SELECT', 'TEXTAREA'].includes(target?.nodeName) || hasAttribute || !!closest
|
||||
}
|
||||
|
||||
export default (keyFilter: MaybeRef<KeyFilter>, onChange?: (keyPressed: boolean) => void): Ref<boolean> => {
|
||||
const window = useWindow()
|
||||
|
||||
let isPressed = $ref(unref(keyFilter) === true)
|
||||
|
||||
watch($$(isPressed), () => {
|
||||
if (onChange && typeof onChange === 'function') onChange(isPressed)
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
const unrefKeyFilter = unref(keyFilter)
|
||||
|
||||
if (isBoolean(unrefKeyFilter)) {
|
||||
isPressed = unrefKeyFilter
|
||||
return
|
||||
}
|
||||
|
||||
onKeyStroke(
|
||||
unrefKeyFilter,
|
||||
(e) => {
|
||||
if (isInputDOMNode(e)) return
|
||||
|
||||
e.preventDefault()
|
||||
isPressed = true
|
||||
},
|
||||
{ eventName: 'keydown' },
|
||||
)
|
||||
|
||||
onKeyStroke(
|
||||
unrefKeyFilter,
|
||||
(e) => {
|
||||
if (isInputDOMNode(e)) return
|
||||
|
||||
e.preventDefault()
|
||||
isPressed = false
|
||||
},
|
||||
{ eventName: 'keyup' },
|
||||
)
|
||||
})
|
||||
|
||||
if (typeof window.addEventListener !== 'undefined') {
|
||||
useEventListener(window, 'blur', () => {
|
||||
isPressed = false
|
||||
})
|
||||
}
|
||||
|
||||
return $$(isPressed)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import useVueFlow from './useVueFlow'
|
||||
import { NodeId, NodeRef } from '~/context'
|
||||
import type { CustomEvent, ElementData } from '~/types'
|
||||
import { getConnectedEdges } from '~/utils'
|
||||
|
||||
/**
|
||||
* Access a node, it's parent (if one exists) and connected edges
|
||||
*
|
||||
* If no node id is provided, the node id is injected from context
|
||||
*
|
||||
* Meaning if you do not provide an id, this composable has to be called in a child of your custom node component, or it will throw
|
||||
*/
|
||||
export default function useNode<Data = ElementData, CustomEvents extends Record<string, CustomEvent> = any>(id?: string) {
|
||||
const nodeId = id ?? inject(NodeId, '')
|
||||
const nodeEl = inject(NodeRef, null)
|
||||
|
||||
const { findNode, getEdges } = useVueFlow()
|
||||
|
||||
const node = findNode<Data, CustomEvents>(nodeId)
|
||||
|
||||
if (!node) {
|
||||
throw new Error(`[vue-flow]: useNode - Node with id ${nodeId} not found!`)
|
||||
}
|
||||
|
||||
return {
|
||||
id: nodeId,
|
||||
node,
|
||||
nodeEl,
|
||||
parentNode: node.parentNode ? findNode(node.parentNode) : undefined,
|
||||
connectedEdges: computed(() => getConnectedEdges([node], getEdges.value)),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { GraphNode, NodeEventsEmit, NodeEventsOn, VueFlowStore } from '~/types'
|
||||
|
||||
const createNodeHooks = () => ({
|
||||
doubleClick: createEventHook(),
|
||||
click: createEventHook(),
|
||||
mouseEnter: createEventHook(),
|
||||
mouseMove: createEventHook(),
|
||||
mouseLeave: createEventHook(),
|
||||
contextMenu: createEventHook(),
|
||||
dragStart: createEventHook(),
|
||||
drag: createEventHook(),
|
||||
dragStop: createEventHook(),
|
||||
})
|
||||
|
||||
export default function useNodeHooks(node: GraphNode, emits: VueFlowStore['emits']): { emit: NodeEventsEmit; on: NodeEventsOn } {
|
||||
const nodeHooks = createNodeHooks()
|
||||
|
||||
nodeHooks.doubleClick.on((event) => {
|
||||
emits.nodeDoubleClick(event)
|
||||
node.events?.doubleClick?.(event)
|
||||
})
|
||||
|
||||
nodeHooks.click.on((event) => {
|
||||
emits.nodeClick(event)
|
||||
node.events?.click?.(event)
|
||||
})
|
||||
|
||||
nodeHooks.mouseEnter.on((event) => {
|
||||
emits.nodeMouseEnter(event)
|
||||
node.events?.mouseEnter?.(event)
|
||||
})
|
||||
|
||||
nodeHooks.mouseMove.on((event) => {
|
||||
emits.nodeMouseMove(event)
|
||||
node.events?.mouseMove?.(event)
|
||||
})
|
||||
|
||||
nodeHooks.mouseLeave.on((event) => {
|
||||
emits.nodeMouseLeave(event)
|
||||
node.events?.mouseLeave?.(event)
|
||||
})
|
||||
|
||||
nodeHooks.contextMenu.on((event) => {
|
||||
emits.nodeContextMenu(event)
|
||||
node.events?.contextMenu?.(event)
|
||||
})
|
||||
|
||||
nodeHooks.dragStart.on((event) => {
|
||||
emits.nodeDragStart(event)
|
||||
node.events?.dragStart?.(event)
|
||||
})
|
||||
|
||||
nodeHooks.drag.on((event) => {
|
||||
emits.nodeDrag(event)
|
||||
node.events?.drag?.(event)
|
||||
})
|
||||
|
||||
nodeHooks.dragStop.on((event) => {
|
||||
emits.nodeDragStop(event)
|
||||
node.events?.dragStop?.(event)
|
||||
})
|
||||
|
||||
return Object.entries(nodeHooks).reduce(
|
||||
(hooks, [key, value]) => {
|
||||
hooks.emit[key as keyof NodeEventsEmit] = value.trigger
|
||||
hooks.on[key as keyof NodeEventsOn] = value.on
|
||||
return hooks
|
||||
},
|
||||
{ emit: {} as NodeEventsEmit, on: {} as NodeEventsOn },
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import type { EffectScope } from 'vue'
|
||||
import type { FlowOptions, FlowProps, State, VueFlowStore } from '~/types'
|
||||
import { VueFlow } from '~/context'
|
||||
import { useActions, useGetters, useState } from '~/store'
|
||||
|
||||
/**
|
||||
* Stores all currently created store instances
|
||||
*/
|
||||
export class Storage {
|
||||
public currentId = 0
|
||||
public flows = new Map<string, VueFlowStore>()
|
||||
static instance: Storage
|
||||
|
||||
public static getInstance(): Storage {
|
||||
if (!Storage.instance) {
|
||||
Storage.instance = new Storage()
|
||||
}
|
||||
|
||||
return Storage.instance
|
||||
}
|
||||
|
||||
public set(id: string, flow: VueFlowStore) {
|
||||
return this.flows.set(id, flow)
|
||||
}
|
||||
|
||||
public get(id: string) {
|
||||
return this.flows.get(id)
|
||||
}
|
||||
|
||||
public remove(id: string) {
|
||||
return this.flows.delete(id)
|
||||
}
|
||||
|
||||
public create(id: string, preloadedState?: FlowOptions): VueFlowStore {
|
||||
const state: State = useState(preloadedState)
|
||||
|
||||
const reactiveState = reactive(state)
|
||||
|
||||
const getters = useGetters(reactiveState)
|
||||
|
||||
const actions = useActions(reactiveState, getters)
|
||||
|
||||
const hooksOn = <any>{}
|
||||
Object.entries(reactiveState.hooks).forEach(([n, h]) => {
|
||||
const name = `on${n.charAt(0).toUpperCase() + n.slice(1)}`
|
||||
hooksOn[name] = h.on
|
||||
})
|
||||
|
||||
const emits = <any>{}
|
||||
Object.entries(reactiveState.hooks).forEach(([n, h]) => {
|
||||
emits[n] = h.trigger
|
||||
})
|
||||
|
||||
actions.setState(reactiveState)
|
||||
|
||||
if (preloadedState) {
|
||||
if (preloadedState.modelValue) actions.setElements(preloadedState.modelValue)
|
||||
if (preloadedState.nodes) actions.setNodes(preloadedState.nodes)
|
||||
if (preloadedState.edges) actions.setEdges(preloadedState.edges)
|
||||
}
|
||||
|
||||
const flow: VueFlowStore = {
|
||||
...hooksOn,
|
||||
...getters,
|
||||
...actions,
|
||||
...toRefs(reactiveState),
|
||||
emits,
|
||||
id,
|
||||
$destroy: () => {
|
||||
this.remove(id)
|
||||
},
|
||||
}
|
||||
|
||||
this.set(id, flow)
|
||||
|
||||
return flow
|
||||
}
|
||||
|
||||
public getId() {
|
||||
return `vue-flow-${this.currentId++}`
|
||||
}
|
||||
}
|
||||
|
||||
type Injection = VueFlowStore | null | undefined
|
||||
type Scope = (EffectScope & { vueFlowId: string }) | undefined
|
||||
|
||||
export default (options?: FlowProps): VueFlowStore => {
|
||||
const storage = Storage.getInstance()
|
||||
|
||||
const scope = getCurrentScope() as Scope
|
||||
|
||||
const id = options?.id
|
||||
const vueFlowId = scope?.vueFlowId || id
|
||||
|
||||
let vueFlow: Injection
|
||||
|
||||
let isParentScope = false
|
||||
|
||||
/**
|
||||
* check if we can get a store instance through injections
|
||||
* this should be the regular way after initialization
|
||||
*/
|
||||
if (scope) {
|
||||
const injection = inject(VueFlow, null)
|
||||
if (typeof injection !== 'undefined' && injection !== null) vueFlow = injection
|
||||
}
|
||||
|
||||
/**
|
||||
* check if we can get a store instance through storage
|
||||
* this requires options id or an id on the current scope
|
||||
*/
|
||||
if (!vueFlow) {
|
||||
if (vueFlowId) vueFlow = storage.get(vueFlowId)
|
||||
}
|
||||
|
||||
/**
|
||||
* If we cannot find any store instance in the previous steps
|
||||
* _or_ if the store instance we found does not match up with provided ids
|
||||
* create a new store instance and register it in storage
|
||||
*/
|
||||
if (!vueFlow || (vueFlow && id && id !== vueFlow.id)) {
|
||||
const name = id ?? storage.getId()
|
||||
|
||||
vueFlow = storage.create(name, options)
|
||||
|
||||
if (scope) {
|
||||
isParentScope = true
|
||||
}
|
||||
} else {
|
||||
// if composable was called with additional options after initialization, overwrite state with the options values
|
||||
if (options) vueFlow.setState(options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Vue flow wasn't able to find any store instance - we can't proceed
|
||||
*/
|
||||
if (!vueFlow) throw new Error('[vueflow]: store instance not found.')
|
||||
|
||||
// always provide a fresh instance into context on call
|
||||
if (scope) {
|
||||
provide(VueFlow, vueFlow)
|
||||
|
||||
scope.vueFlowId = vueFlow.id
|
||||
|
||||
if (isParentScope) {
|
||||
// dispose of state values and storage entry
|
||||
tryOnScopeDispose(() => {
|
||||
if (storage.get(vueFlow!.id)) {
|
||||
vueFlow!.$destroy()
|
||||
}
|
||||
|
||||
vueFlow = null
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return vueFlow
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
type UseWindow = Window & typeof globalThis & { chrome?: any }
|
||||
|
||||
export default (): UseWindow => {
|
||||
if (typeof window !== 'undefined') return window as UseWindow
|
||||
else return { chrome: false } as UseWindow
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { zoomIdentity } from 'd3-zoom'
|
||||
import useVueFlow from './useVueFlow'
|
||||
import useWindow from './useWindow'
|
||||
import { clampPosition, getRectOfNodes, getTransformForBounds, pointToRendererPoint } from '~/utils'
|
||||
import type { D3Selection, Dimensions, Getters, GraphNode, ViewportFunctions } from '~/types'
|
||||
|
||||
const DEFAULT_PADDING = 0.1
|
||||
|
||||
const transition = (selection: D3Selection, ms = 0) => selection.transition().duration(ms)
|
||||
|
||||
const untilDimensions = async (dimensions: Dimensions, getNodes: Getters['getNodes']) => {
|
||||
// if ssr we can't wait for dimensions, they'll never really exist
|
||||
const window = useWindow()
|
||||
if ('screen' in window) {
|
||||
// wait until viewport dimensions has been established
|
||||
await until(dimensions).toMatch(({ height, width }) => !isNaN(width) && width > 0 && !isNaN(height) && height > 0)
|
||||
|
||||
// if initial nodes are present, wait until the node dimensions have been established
|
||||
if (getNodes.length > 0) {
|
||||
await until(getNodes).toMatch(
|
||||
(nodes) =>
|
||||
!!nodes.filter(({ dimensions: { width, height } }) => !isNaN(width) && width > 0 && !isNaN(height) && height > 0)
|
||||
.length,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use {@link useVueFlow} instead (all viewport functions are also available in {@link useVueFlow})
|
||||
*/
|
||||
export default (vueFlowId?: string): ViewportFunctions => {
|
||||
const {
|
||||
onPaneReady,
|
||||
nodes,
|
||||
d3Zoom,
|
||||
d3Selection,
|
||||
dimensions,
|
||||
translateExtent,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
viewport,
|
||||
snapToGrid,
|
||||
snapGrid,
|
||||
getNodes,
|
||||
} = $(useVueFlow({ id: vueFlowId }))
|
||||
|
||||
let hasDimensions = $ref(false)
|
||||
|
||||
onPaneReady(() => (hasDimensions = true))
|
||||
|
||||
const zoomTo: ViewportFunctions['zoomTo'] = async (zoomLevel, options) => {
|
||||
if (!hasDimensions) await untilDimensions(dimensions, getNodes)
|
||||
|
||||
if (d3Selection && d3Zoom) {
|
||||
d3Zoom.scaleTo(transition(d3Selection, options?.duration), zoomLevel)
|
||||
}
|
||||
}
|
||||
|
||||
const zoom = async (scale: number, duration?: number) => {
|
||||
if (!hasDimensions) await untilDimensions(dimensions, getNodes)
|
||||
|
||||
if (d3Selection && d3Zoom) {
|
||||
d3Zoom.scaleBy(transition(d3Selection, duration), scale)
|
||||
}
|
||||
}
|
||||
|
||||
const zoomIn: ViewportFunctions['zoomIn'] = async (options) => {
|
||||
await zoom(1.2, options?.duration)
|
||||
}
|
||||
|
||||
const zoomOut: ViewportFunctions['zoomOut'] = async (options) => {
|
||||
await zoom(1 / 1.2, options?.duration)
|
||||
}
|
||||
|
||||
const transformViewport = (x: number, y: number, zoom: number, duration?: number) => {
|
||||
// enforce translate extent
|
||||
const { x: clampedX, y: clampedY } = clampPosition({ x: -x, y: -y }, translateExtent)
|
||||
|
||||
const nextTransform = zoomIdentity.translate(-clampedX, -clampedY).scale(zoom)
|
||||
|
||||
if (d3Selection && d3Zoom) {
|
||||
d3Zoom.transform(transition(d3Selection, duration), nextTransform)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
zoomTo,
|
||||
setTransform: async (transform, options) => {
|
||||
if (!hasDimensions) await untilDimensions(dimensions, getNodes)
|
||||
transformViewport(transform.x, transform.y, transform.zoom, options?.duration)
|
||||
},
|
||||
getTransform: () => ({
|
||||
x: viewport.x,
|
||||
y: viewport.y,
|
||||
zoom: viewport.zoom,
|
||||
}),
|
||||
fitView: async (
|
||||
options = {
|
||||
padding: DEFAULT_PADDING,
|
||||
includeHiddenNodes: false,
|
||||
duration: 0,
|
||||
},
|
||||
) => {
|
||||
if (!hasDimensions) await untilDimensions(dimensions, getNodes)
|
||||
|
||||
if (!getNodes.length) return
|
||||
|
||||
let nodeBounds: GraphNode[] = []
|
||||
if (options.nodes) {
|
||||
nodeBounds = nodes.filter((n) => options.nodes?.includes(n.id))
|
||||
}
|
||||
|
||||
if (!nodeBounds || !nodeBounds.length) {
|
||||
nodeBounds = options.includeHiddenNodes ? nodeBounds : getNodes
|
||||
}
|
||||
|
||||
const bounds = getRectOfNodes(nodeBounds)
|
||||
|
||||
const { x, y, zoom } = getTransformForBounds(
|
||||
bounds,
|
||||
dimensions.width,
|
||||
dimensions.height,
|
||||
options.minZoom ?? minZoom,
|
||||
options.maxZoom ?? maxZoom,
|
||||
options.padding ?? DEFAULT_PADDING,
|
||||
options.offset,
|
||||
)
|
||||
|
||||
transformViewport(x, y, zoom, options?.duration)
|
||||
},
|
||||
setCenter: async (x, y, options) => {
|
||||
if (!hasDimensions) await untilDimensions(dimensions, getNodes)
|
||||
|
||||
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : maxZoom
|
||||
const centerX = dimensions.width / 2 - x * nextZoom
|
||||
const centerY = dimensions.height / 2 - y * nextZoom
|
||||
|
||||
transformViewport(centerX, centerY, nextZoom, options?.duration)
|
||||
},
|
||||
fitBounds: async (bounds, options = { padding: DEFAULT_PADDING }) => {
|
||||
if (!hasDimensions) await untilDimensions(dimensions, getNodes)
|
||||
|
||||
const { x, y, zoom } = getTransformForBounds(bounds, dimensions.width, dimensions.height, minZoom, maxZoom, options.padding)
|
||||
|
||||
transformViewport(x, y, zoom, options?.duration)
|
||||
},
|
||||
project: (position) => pointToRendererPoint(position, viewport, snapToGrid, snapGrid),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user