import type { DirectiveBinding, ObjectDirective } from 'vue'; export const START_HOLD_EVENT = 'start-hold'; export interface StartDirectiveValue { threshold: number; fn?: () => void; /** 默认派发 `start-hold`;`false` 时不派发 */ emit?: boolean; } type StartBinding = DirectiveBinding; type Entry = { binding: StartBinding; }; const entries = new WeakMap(); const listenOpts: AddEventListenerOptions = { capture: true }; function resolveMode(modifiers: StartBinding['modifiers']): 'duration' | 'distance' { if (modifiers.distance && modifiers.duration) { console.warn('[v-start] Both .duration and .distance are set; using .distance.'); return 'distance'; } if (modifiers.distance) return 'distance'; return 'duration'; } function readValue(binding: StartBinding): StartDirectiveValue | null { const v = binding.value; if (v == null || typeof v !== 'object') return null; if (typeof v.threshold !== 'number' || Number.isNaN(v.threshold)) return null; return v; } function mount(el: HTMLElement, binding: StartBinding) { let down: { timeStamp: number; clientX: number; clientY: number; pointerId: number } | null = null; const onPointerDown = (e: PointerEvent) => { down = { timeStamp: e.timeStamp, clientX: e.clientX, clientY: e.clientY, pointerId: e.pointerId, }; }; const onPointerUp = (e: PointerEvent) => { if (!down || e.pointerId !== down.pointerId) return; const entry = entries.get(el); if (!entry) return; const value = readValue(entry.binding); const mode = resolveMode(entry.binding.modifiers); const start = down; down = null; if (!value) return; const delta = mode === 'duration' ? e.timeStamp - start.timeStamp : Math.hypot(e.clientX - start.clientX, e.clientY - start.clientY); if (delta <= value.threshold) return; value.fn?.(); if (value.emit !== false) { const detail = mode === 'duration' ? { durationMs: delta } : { distancePx: delta }; el.dispatchEvent( new CustomEvent(START_HOLD_EVENT, { detail, bubbles: true, }), ); } }; el.addEventListener('pointerdown', onPointerDown, listenOpts); el.addEventListener('pointerup', onPointerUp, listenOpts); entries.set(el, { binding }); return () => { el.removeEventListener('pointerdown', onPointerDown, listenOpts); el.removeEventListener('pointerup', onPointerUp, listenOpts); entries.delete(el); }; } export const startDirective: ObjectDirective = { mounted(el, binding) { (el as HTMLElement & { __startCleanup?: () => void }).__startCleanup = mount(el, binding); }, updated(el, binding) { const e = entries.get(el); if (e) e.binding = binding; }, unmounted(el) { (el as HTMLElement & { __startCleanup?: () => void }).__startCleanup?.(); (el as HTMLElement & { __startCleanup?: () => void }).__startCleanup = undefined; }, };