Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"vue": "^3.3.0"
},
"dependencies": {
"@vueuse/core": "^10.5.0",
"@vueuse/core": "^14.1.0",
"d3-drag": "^3.0.0",
"d3-interpolate": "^3.0.1",
"d3-selection": "^3.0.0",
Expand Down
33 changes: 18 additions & 15 deletions packages/core/src/composables/useWatchProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,23 @@ export function useWatchProps(
let immediateStore = !!(store.nodes.value.length || store.edges.value.length)

// eslint-disable-next-line prefer-const
pauseModel = watchPausable([models.modelValue, () => models.modelValue?.value?.length], ([elements]) => {
if (elements && Array.isArray(elements)) {
pauseStore?.pause()

store.setElements(elements)

// only trigger store watcher immediately if we actually set any elements to the store
if (!pauseStore && !immediateStore && elements.length) {
immediateStore = true
} else {
pauseStore?.resume()
pauseModel = watchPausable(
[() => models.modelValue?.value, () => models.modelValue?.value?.length] as const,
([elements]) => {
if (elements && Array.isArray(elements)) {
pauseStore?.pause()

store.setElements(elements)

// only trigger store watcher immediately if we actually set any elements to the store
if (!pauseStore && !immediateStore && elements.length) {
immediateStore = true
} else {
pauseStore?.resume()
}
}
}
})
},
)

pauseStore = watchPausable(
[store.nodes, store.edges, () => store.edges.value.length, () => store.nodes.value.length],
Expand Down Expand Up @@ -75,7 +78,7 @@ export function useWatchProps(
let immediateStore = !!store.nodes.value.length

// eslint-disable-next-line prefer-const
pauseModel = watchPausable([models.nodes, () => models.nodes?.value?.length], ([nodes]) => {
pauseModel = watchPausable([() => models.nodes?.value, () => models.nodes?.value?.length] as const, ([nodes]) => {
if (nodes && Array.isArray(nodes)) {
pauseStore?.pause()

Expand Down Expand Up @@ -121,7 +124,7 @@ export function useWatchProps(
let immediateStore = !!store.edges.value.length

// eslint-disable-next-line prefer-const
pauseModel = watchPausable([models.edges, () => models.edges?.value?.length], ([edges]) => {
pauseModel = watchPausable([() => models.edges?.value, () => models.edges?.value?.length] as const, ([edges]) => {
if (edges && Array.isArray(edges)) {
pauseStore?.pause()

Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/types/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ export interface EdgeUpdateEvent {
}

export interface FlowEvents {
nodesChange: NodeChange[]
edgesChange: EdgeChange[]
nodesChange: [NodeChange[]]
edgesChange: [EdgeChange[]]
nodeDoubleClick: NodeMouseEvent
nodeClick: NodeMouseEvent
nodeMouseEnter: NodeMouseEvent
Expand All @@ -44,8 +44,8 @@ export interface FlowEvents {
nodeDragStart: NodeDragEvent
nodeDrag: NodeDragEvent
nodeDragStop: NodeDragEvent
nodesInitialized: GraphNode[]
updateNodeInternals: string[]
nodesInitialized: [GraphNode[]]
updateNodeInternals: [string[]]
miniMapNodeClick: NodeMouseEvent
miniMapNodeDoubleClick: NodeMouseEvent
miniMapNodeMouseEnter: NodeMouseEvent
Expand Down
46 changes: 23 additions & 23 deletions packages/core/src/utils/createExtendedEventHook.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import type { EventHook } from '@vueuse/core'
import type { EventHook, EventHookOn, EventHookTrigger } from '@vueuse/core'
import { tryOnScopeDispose } from '@vueuse/core'

// Extract VueUse's actual callback type for T (includes tuple/void/any behavior)
type HookCallback<T> = Parameters<EventHookOn<T>>[0]
type HookArgs<T> = Parameters<HookCallback<T>>

export interface EventHookExtended<T> extends EventHook<T> {
/** true if any user listeners are registered (emitter ignored) */
hasListeners: () => boolean
/** current user listeners (read-only; do not mutate externally) */
listeners: ReadonlySet<(param: T) => void>
listeners: ReadonlySet<HookCallback<T>>
/** wire a single external emitter (e.g., for `emit`) */
setEmitter: (fn: (param: T) => void) => void
setEmitter: (fn: HookCallback<T>) => void
/** remove the external emitter */
removeEmitter: () => void
/** wire a function to detect if any emit listeners exist (e.g., for `$listeners` in Vue 2) */
Expand All @@ -16,23 +20,21 @@ export interface EventHookExtended<T> extends EventHook<T> {
removeHasEmitListeners: () => void
}

type Handler<T = any> = (param: T) => any | Promise<any>

const noop: Handler = () => {}
function noop(..._args: any[]) {}

export function createExtendedEventHook<T = any>(defaultHandler?: (param: T) => void): EventHookExtended<T> {
const listeners = new Set<Handler>()
let emitter: Handler = noop
export function createExtendedEventHook<T = any>(defaultHandler?: HookCallback<T>): EventHookExtended<T> {
const listeners = new Set<HookCallback<T>>()
let emitter: HookCallback<T> = noop as HookCallback<T>
let hasEmitListeners = () => false

const hasListeners = () => listeners.size > 0 || hasEmitListeners()

const setEmitter = (fn: Handler) => {
const setEmitter = (fn: HookCallback<T>) => {
emitter = fn
}

const removeEmitter = () => {
emitter = noop
emitter = noop as HookCallback<T>
}

const setHasEmitListeners = (fn: () => boolean) => {
Expand All @@ -43,11 +45,11 @@ export function createExtendedEventHook<T = any>(defaultHandler?: (param: T) =>
hasEmitListeners = () => false
}

const off = (fn: Handler) => {
const off = (fn: HookCallback<T>) => {
listeners.delete(fn)
}

const on = (fn: Handler) => {
const on: EventHookOn<T> = (fn) => {
listeners.add(fn)

const offFn = () => off(fn)
Expand All @@ -56,29 +58,27 @@ export function createExtendedEventHook<T = any>(defaultHandler?: (param: T) =>
return { off: offFn }
}

/**
* Trigger order:
* 1) If any user listeners OR an emitter exist -> call all of those (defaultHandler is skipped)
* 2) Else (no listeners and no emitter) -> call defaultHandler (if provided)
*
* Errors are isolated via allSettled so one failing handler doesn't break others.
*/
const trigger = (param: T) => {
const queue: Handler[] = [emitter]
const clear = () => {
listeners.clear()
}

const trigger: EventHookTrigger<T> = async (...args: HookArgs<T>) => {
const queue: HookCallback<T>[] = [emitter]

if (hasListeners()) {
queue.push(...listeners)
} else if (defaultHandler) {
queue.push(defaultHandler)
}

return Promise.allSettled(queue.map((fn) => fn(param)))
return Promise.all(queue.map((fn) => fn(...args)))
}

return {
on,
off,
trigger,
clear,
hasListeners,
listeners,
setEmitter,
Expand Down
Loading
Loading