start.directive.ts 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. import type { DirectiveBinding, ObjectDirective } from 'vue';
  2. export const START_HOLD_EVENT = 'start-hold';
  3. export interface StartDirectiveValue {
  4. threshold: number;
  5. fn?: () => void;
  6. /** 默认派发 `start-hold`;`false` 时不派发 */
  7. emit?: boolean;
  8. }
  9. type StartBinding = DirectiveBinding<StartDirectiveValue>;
  10. type Entry = {
  11. binding: StartBinding;
  12. };
  13. const entries = new WeakMap<HTMLElement, Entry>();
  14. const listenOpts: AddEventListenerOptions = { capture: true };
  15. function resolveMode(modifiers: StartBinding['modifiers']): 'duration' | 'distance' {
  16. if (modifiers.distance && modifiers.duration) {
  17. console.warn('[v-start] Both .duration and .distance are set; using .distance.');
  18. return 'distance';
  19. }
  20. if (modifiers.distance) return 'distance';
  21. return 'duration';
  22. }
  23. function readValue(binding: StartBinding): StartDirectiveValue | null {
  24. const v = binding.value;
  25. if (v == null || typeof v !== 'object') return null;
  26. if (typeof v.threshold !== 'number' || Number.isNaN(v.threshold)) return null;
  27. return v;
  28. }
  29. function mount(el: HTMLElement, binding: StartBinding) {
  30. let down: { timeStamp: number; clientX: number; clientY: number; pointerId: number } | null =
  31. null;
  32. const onPointerDown = (e: PointerEvent) => {
  33. down = {
  34. timeStamp: e.timeStamp,
  35. clientX: e.clientX,
  36. clientY: e.clientY,
  37. pointerId: e.pointerId,
  38. };
  39. };
  40. const onPointerUp = (e: PointerEvent) => {
  41. if (!down || e.pointerId !== down.pointerId) return;
  42. const entry = entries.get(el);
  43. if (!entry) return;
  44. const value = readValue(entry.binding);
  45. const mode = resolveMode(entry.binding.modifiers);
  46. const start = down;
  47. down = null;
  48. if (!value) return;
  49. const delta =
  50. mode === 'duration'
  51. ? e.timeStamp - start.timeStamp
  52. : Math.hypot(e.clientX - start.clientX, e.clientY - start.clientY);
  53. if (delta <= value.threshold) return;
  54. value.fn?.();
  55. if (value.emit !== false) {
  56. const detail =
  57. mode === 'duration' ? { durationMs: delta } : { distancePx: delta };
  58. el.dispatchEvent(
  59. new CustomEvent(START_HOLD_EVENT, {
  60. detail,
  61. bubbles: true,
  62. }),
  63. );
  64. }
  65. };
  66. el.addEventListener('pointerdown', onPointerDown, listenOpts);
  67. el.addEventListener('pointerup', onPointerUp, listenOpts);
  68. entries.set(el, { binding });
  69. return () => {
  70. el.removeEventListener('pointerdown', onPointerDown, listenOpts);
  71. el.removeEventListener('pointerup', onPointerUp, listenOpts);
  72. entries.delete(el);
  73. };
  74. }
  75. export const startDirective: ObjectDirective<HTMLElement, StartDirectiveValue> = {
  76. mounted(el, binding) {
  77. (el as HTMLElement & { __startCleanup?: () => void }).__startCleanup = mount(el, binding);
  78. },
  79. updated(el, binding) {
  80. const e = entries.get(el);
  81. if (e) e.binding = binding;
  82. },
  83. unmounted(el) {
  84. (el as HTMLElement & { __startCleanup?: () => void }).__startCleanup?.();
  85. (el as HTMLElement & { __startCleanup?: () => void }).__startCleanup = undefined;
  86. },
  87. };