atagen

malice

public
Star0
Watch0
9090eb8cdf5f6e853fd8a08b68b8d5fcc31b062a
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: web/src/lib/longpress.ts

0e50e0d137afatagen2026-10-26 05:50 UTC1
import { readable, type Readable } from 'svelte/store';
0e50e0d137afatagen2026-10-26 05:50 UTC2
0e50e0d137afatagen2026-10-26 05:50 UTC3
export const LONG_PRESS_MS = 450;
0e50e0d137afatagen2026-10-26 05:50 UTC4
export const LONG_PRESS_SLOP = 8;
0e50e0d137afatagen2026-10-26 05:50 UTC5
0e50e0d137afatagen2026-10-26 05:50 UTC6
export interface PressEvent {
0e50e0d137afatagen2026-10-26 05:50 UTC7
  clientX: number;
0e50e0d137afatagen2026-10-26 05:50 UTC8
  clientY: number;
0e50e0d137afatagen2026-10-26 05:50 UTC9
  currentTarget: EventTarget | null;
0e50e0d137afatagen2026-10-26 05:50 UTC10
}
0e50e0d137afatagen2026-10-26 05:50 UTC11
0e50e0d137afatagen2026-10-26 05:50 UTC12
export interface Timers {
0e50e0d137afatagen2026-10-26 05:50 UTC13
  set(fn: () => void, ms: number): unknown;
0e50e0d137afatagen2026-10-26 05:50 UTC14
  clear(handle: unknown): void;
0e50e0d137afatagen2026-10-26 05:50 UTC15
}
0e50e0d137afatagen2026-10-26 05:50 UTC16
0e50e0d137afatagen2026-10-26 05:50 UTC17
const realTimers: Timers = {
0e50e0d137afatagen2026-10-26 05:50 UTC18
  set: (fn, ms) => setTimeout(fn, ms),
0e50e0d137afatagen2026-10-26 05:50 UTC19
  clear: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>)
0e50e0d137afatagen2026-10-26 05:50 UTC20
};
0e50e0d137afatagen2026-10-26 05:50 UTC21
0e50e0d137afatagen2026-10-26 05:50 UTC22
export class PressTracker {
0e50e0d137afatagen2026-10-26 05:50 UTC23
  fired = false;
0e50e0d137afatagen2026-10-26 05:50 UTC24
  private handle: unknown = null;
0e50e0d137afatagen2026-10-26 05:50 UTC25
  private x = 0;
0e50e0d137afatagen2026-10-26 05:50 UTC26
  private y = 0;
0e50e0d137afatagen2026-10-26 05:50 UTC27
0e50e0d137afatagen2026-10-26 05:50 UTC28
  constructor(
0e50e0d137afatagen2026-10-26 05:50 UTC29
    private readonly onFire: (x: number, y: number) => void,
0e50e0d137afatagen2026-10-26 05:50 UTC30
    private readonly timers: Timers = realTimers,
0e50e0d137afatagen2026-10-26 05:50 UTC31
    private readonly delay = LONG_PRESS_MS,
0e50e0d137afatagen2026-10-26 05:50 UTC32
    private readonly slop = LONG_PRESS_SLOP
0e50e0d137afatagen2026-10-26 05:50 UTC33
  ) {}
0e50e0d137afatagen2026-10-26 05:50 UTC34
0e50e0d137afatagen2026-10-26 05:50 UTC35
  get pending(): boolean {
0e50e0d137afatagen2026-10-26 05:50 UTC36
    return this.handle !== null;
0e50e0d137afatagen2026-10-26 05:50 UTC37
  }
0e50e0d137afatagen2026-10-26 05:50 UTC38
0e50e0d137afatagen2026-10-26 05:50 UTC39
  start(x: number, y: number): void {
0e50e0d137afatagen2026-10-26 05:50 UTC40
    this.cancel();
0e50e0d137afatagen2026-10-26 05:50 UTC41
    this.fired = false;
0e50e0d137afatagen2026-10-26 05:50 UTC42
    this.x = x;
0e50e0d137afatagen2026-10-26 05:50 UTC43
    this.y = y;
0e50e0d137afatagen2026-10-26 05:50 UTC44
    this.handle = this.timers.set(() => {
0e50e0d137afatagen2026-10-26 05:50 UTC45
      this.handle = null;
0e50e0d137afatagen2026-10-26 05:50 UTC46
      this.fired = true;
0e50e0d137afatagen2026-10-26 05:50 UTC47
      this.onFire(this.x, this.y);
0e50e0d137afatagen2026-10-26 05:50 UTC48
    }, this.delay);
0e50e0d137afatagen2026-10-26 05:50 UTC49
  }
0e50e0d137afatagen2026-10-26 05:50 UTC50
0e50e0d137afatagen2026-10-26 05:50 UTC51
  move(x: number, y: number): void {
0e50e0d137afatagen2026-10-26 05:50 UTC52
    if (this.handle === null) return;
0e50e0d137afatagen2026-10-26 05:50 UTC53
    if (Math.hypot(x - this.x, y - this.y) > this.slop) this.cancel();
0e50e0d137afatagen2026-10-26 05:50 UTC54
  }
0e50e0d137afatagen2026-10-26 05:50 UTC55
0e50e0d137afatagen2026-10-26 05:50 UTC56
  flush(): void {
0e50e0d137afatagen2026-10-26 05:50 UTC57
    if (this.handle === null) return;
0e50e0d137afatagen2026-10-26 05:50 UTC58
    this.cancel();
0e50e0d137afatagen2026-10-26 05:50 UTC59
    this.fired = true;
0e50e0d137afatagen2026-10-26 05:50 UTC60
    this.onFire(this.x, this.y);
0e50e0d137afatagen2026-10-26 05:50 UTC61
  }
0e50e0d137afatagen2026-10-26 05:50 UTC62
0e50e0d137afatagen2026-10-26 05:50 UTC63
  cancel(): void {
0e50e0d137afatagen2026-10-26 05:50 UTC64
    if (this.handle === null) return;
0e50e0d137afatagen2026-10-26 05:50 UTC65
    this.timers.clear(this.handle);
0e50e0d137afatagen2026-10-26 05:50 UTC66
    this.handle = null;
0e50e0d137afatagen2026-10-26 05:50 UTC67
  }
0e50e0d137afatagen2026-10-26 05:50 UTC68
}
0e50e0d137afatagen2026-10-26 05:50 UTC69
0e50e0d137afatagen2026-10-26 05:50 UTC70
type Handler = ((ev: PressEvent) => void) | null | undefined;
0e50e0d137afatagen2026-10-26 05:50 UTC71
0e50e0d137afatagen2026-10-26 05:50 UTC72
interface Options {
0e50e0d137afatagen2026-10-26 05:50 UTC73
  on: Handler;
0e50e0d137afatagen2026-10-26 05:50 UTC74
  allowSelection?: boolean;
0e50e0d137afatagen2026-10-26 05:50 UTC75
  ignore?: string;
0e50e0d137afatagen2026-10-26 05:50 UTC76
}
0e50e0d137afatagen2026-10-26 05:50 UTC77
0e50e0d137afatagen2026-10-26 05:50 UTC78
type Param = Handler | Options;
0e50e0d137afatagen2026-10-26 05:50 UTC79
0e50e0d137afatagen2026-10-26 05:50 UTC80
function split(param: Param): Options {
0e50e0d137afatagen2026-10-26 05:50 UTC81
  return typeof param === 'function' || !param ? { on: param } : param;
0e50e0d137afatagen2026-10-26 05:50 UTC82
}
0e50e0d137afatagen2026-10-26 05:50 UTC83
0e50e0d137afatagen2026-10-26 05:50 UTC84
export function longpress(node: HTMLElement, param: Param) {
0e50e0d137afatagen2026-10-26 05:50 UTC85
  let opts = split(param);
0e50e0d137afatagen2026-10-26 05:50 UTC86
  let current = opts.on;
0e50e0d137afatagen2026-10-26 05:50 UTC87
  let pointerId = -1;
0e50e0d137afatagen2026-10-26 05:50 UTC88
0e50e0d137afatagen2026-10-26 05:50 UTC89
  const tracker = new PressTracker((x, y) => {
0e50e0d137afatagen2026-10-26 05:50 UTC90
    navigator.vibrate?.(10);
0e50e0d137afatagen2026-10-26 05:50 UTC91
    current?.({ clientX: x, clientY: y, currentTarget: node });
0e50e0d137afatagen2026-10-26 05:50 UTC92
  });
0e50e0d137afatagen2026-10-26 05:50 UTC93
0e50e0d137afatagen2026-10-26 05:50 UTC94
  const apply = () => {
0e50e0d137afatagen2026-10-26 05:50 UTC95
    node.style.setProperty('-webkit-touch-callout', 'none');
0e50e0d137afatagen2026-10-26 05:50 UTC96
    if (!opts.allowSelection) node.style.setProperty('user-select', 'none');
0e50e0d137afatagen2026-10-26 05:50 UTC97
  };
0e50e0d137afatagen2026-10-26 05:50 UTC98
  apply();
0e50e0d137afatagen2026-10-26 05:50 UTC99
0e50e0d137afatagen2026-10-26 05:50 UTC100
  function down(e: PointerEvent) {
0e50e0d137afatagen2026-10-26 05:50 UTC101
    if (!current || e.pointerType === 'mouse' || !e.isPrimary) return;
0e50e0d137afatagen2026-10-26 05:50 UTC102
    if (opts.ignore && (e.target as Element).closest(opts.ignore)) return;
0e50e0d137afatagen2026-10-26 05:50 UTC103
    pointerId = e.pointerId;
0e50e0d137afatagen2026-10-26 05:50 UTC104
    tracker.start(e.clientX, e.clientY);
0e50e0d137afatagen2026-10-26 05:50 UTC105
  }
0e50e0d137afatagen2026-10-26 05:50 UTC106
  function move(e: PointerEvent) {
0e50e0d137afatagen2026-10-26 05:50 UTC107
    if (e.pointerId === pointerId) tracker.move(e.clientX, e.clientY);
0e50e0d137afatagen2026-10-26 05:50 UTC108
  }
0e50e0d137afatagen2026-10-26 05:50 UTC109
  function end(e: PointerEvent) {
0e50e0d137afatagen2026-10-26 05:50 UTC110
    if (e.pointerId !== pointerId) return;
0e50e0d137afatagen2026-10-26 05:50 UTC111
    tracker.cancel();
0e50e0d137afatagen2026-10-26 05:50 UTC112
    if (tracker.fired) setTimeout(() => (tracker.fired = false), 400);
0e50e0d137afatagen2026-10-26 05:50 UTC113
  }
0e50e0d137afatagen2026-10-26 05:50 UTC114
  function click(e: MouseEvent) {
0e50e0d137afatagen2026-10-26 05:50 UTC115
    if (!tracker.fired) return;
0e50e0d137afatagen2026-10-26 05:50 UTC116
    tracker.fired = false;
0e50e0d137afatagen2026-10-26 05:50 UTC117
    e.preventDefault();
0e50e0d137afatagen2026-10-26 05:50 UTC118
    e.stopImmediatePropagation();
0e50e0d137afatagen2026-10-26 05:50 UTC119
  }
0e50e0d137afatagen2026-10-26 05:50 UTC120
  function contextmenu(e: Event) {
0e50e0d137afatagen2026-10-26 05:50 UTC121
    if (!tracker.fired && !tracker.pending) return;
0e50e0d137afatagen2026-10-26 05:50 UTC122
    e.preventDefault();
0e50e0d137afatagen2026-10-26 05:50 UTC123
    e.stopImmediatePropagation();
0e50e0d137afatagen2026-10-26 05:50 UTC124
    tracker.flush();
0e50e0d137afatagen2026-10-26 05:50 UTC125
  }
0e50e0d137afatagen2026-10-26 05:50 UTC126
  const cancel = () => tracker.cancel();
0e50e0d137afatagen2026-10-26 05:50 UTC127
0e50e0d137afatagen2026-10-26 05:50 UTC128
  node.addEventListener('pointerdown', down);
0e50e0d137afatagen2026-10-26 05:50 UTC129
  node.addEventListener('pointermove', move);
0e50e0d137afatagen2026-10-26 05:50 UTC130
  node.addEventListener('pointerup', end);
0e50e0d137afatagen2026-10-26 05:50 UTC131
  node.addEventListener('pointercancel', end);
0e50e0d137afatagen2026-10-26 05:50 UTC132
  node.addEventListener('click', click, true);
0e50e0d137afatagen2026-10-26 05:50 UTC133
  node.addEventListener('contextmenu', contextmenu, true);
0e50e0d137afatagen2026-10-26 05:50 UTC134
  window.addEventListener('scroll', cancel, true);
0e50e0d137afatagen2026-10-26 05:50 UTC135
0e50e0d137afatagen2026-10-26 05:50 UTC136
  return {
0e50e0d137afatagen2026-10-26 05:50 UTC137
    update(next: Param) {
0e50e0d137afatagen2026-10-26 05:50 UTC138
      opts = split(next);
0e50e0d137afatagen2026-10-26 05:50 UTC139
      current = opts.on;
0e50e0d137afatagen2026-10-26 05:50 UTC140
      apply();
0e50e0d137afatagen2026-10-26 05:50 UTC141
    },
0e50e0d137afatagen2026-10-26 05:50 UTC142
    destroy() {
0e50e0d137afatagen2026-10-26 05:50 UTC143
      tracker.cancel();
0e50e0d137afatagen2026-10-26 05:50 UTC144
      node.removeEventListener('pointerdown', down);
0e50e0d137afatagen2026-10-26 05:50 UTC145
      node.removeEventListener('pointermove', move);
0e50e0d137afatagen2026-10-26 05:50 UTC146
      node.removeEventListener('pointerup', end);
0e50e0d137afatagen2026-10-26 05:50 UTC147
      node.removeEventListener('pointercancel', end);
0e50e0d137afatagen2026-10-26 05:50 UTC148
      node.removeEventListener('click', click, true);
0e50e0d137afatagen2026-10-26 05:50 UTC149
      node.removeEventListener('contextmenu', contextmenu, true);
0e50e0d137afatagen2026-10-26 05:50 UTC150
      window.removeEventListener('scroll', cancel, true);
0e50e0d137afatagen2026-10-26 05:50 UTC151
    }
0e50e0d137afatagen2026-10-26 05:50 UTC152
  };
0e50e0d137afatagen2026-10-26 05:50 UTC153
}
0e50e0d137afatagen2026-10-26 05:50 UTC154
0e50e0d137afatagen2026-10-26 05:50 UTC155
export const finePointer: Readable<boolean> = readable(true, (set) => {
0e50e0d137afatagen2026-10-26 05:50 UTC156
  if (typeof window === 'undefined') return;
0e50e0d137afatagen2026-10-26 05:50 UTC157
  const query = window.matchMedia('(pointer: fine)');
0e50e0d137afatagen2026-10-26 05:50 UTC158
  set(query.matches);
0e50e0d137afatagen2026-10-26 05:50 UTC159
  const update = () => set(query.matches);
0e50e0d137afatagen2026-10-26 05:50 UTC160
  query.addEventListener('change', update);
0e50e0d137afatagen2026-10-26 05:50 UTC161
  return () => query.removeEventListener('change', update);
0e50e0d137afatagen2026-10-26 05:50 UTC162
});