|
@@ -0,0 +1,110 @@
|
|
|
|
|
+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<StartDirectiveValue>;
|
|
|
|
|
+
|
|
|
|
|
+type Entry = {
|
|
|
|
|
+ binding: StartBinding;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const entries = new WeakMap<HTMLElement, Entry>();
|
|
|
|
|
+
|
|
|
|
|
+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<HTMLElement, StartDirectiveValue> = {
|
|
|
|
|
+ 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;
|
|
|
|
|
+ },
|
|
|
|
|
+};
|