refactor(core): rename core pkg dir to core

This commit is contained in:
braks
2022-10-10 21:33:44 +02:00
committed by Braks
parent f7dd7f1803
commit 082050b164
87 changed files with 640 additions and 0 deletions
+10
View File
@@ -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'
+167
View File
@@ -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
+29
View File
@@ -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 },
)
}
+252
View File
@@ -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)
}
+32
View File
@@ -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 },
)
}
+158
View File
@@ -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),
}
}