your personal website on atproto - mirror
blento.app
1import type { QRContext } from './QRCodeModal.svelte';
2
3// Global state for QR modal
4let openModal: ((href: string, context: QRContext) => void) | null = null;
5
6export function registerQRModal(fn: (href: string, context: QRContext) => void) {
7 openModal = fn;
8}
9
10export function unregisterQRModal() {
11 openModal = null;
12}
13
14export function qrOverlay(
15 node: HTMLElement,
16 params: { href?: string; context?: QRContext; disabled?: boolean } = {}
17) {
18 const LONG_PRESS_DURATION = 500;
19 let longPressTimer: ReturnType<typeof setTimeout> | null = null;
20 let isLongPress = false;
21 let touchActive = false;
22
23 // Prevent iOS link preview on long-press
24 const originalCallout = node.style.getPropertyValue('-webkit-touch-callout');
25 node.style.setProperty('-webkit-touch-callout', 'none');
26
27 function getHref() {
28 return params.href || (node as HTMLAnchorElement).href || '';
29 }
30
31 function startLongPress(e: PointerEvent) {
32 if (params.disabled) return;
33 // Only start long press for primary button (touch/left-click), not right-click
34 if (e.button !== 0) return;
35 touchActive = e.pointerType === 'touch';
36 isLongPress = false;
37 longPressTimer = setTimeout(() => {
38 isLongPress = true;
39 openModal?.(getHref(), params.context ?? {});
40 }, LONG_PRESS_DURATION);
41 }
42
43 function cancelLongPress() {
44 if (longPressTimer) {
45 clearTimeout(longPressTimer);
46 longPressTimer = null;
47 }
48 touchActive = false;
49 }
50
51 function handleClick(e: MouseEvent) {
52 if (isLongPress) {
53 e.preventDefault();
54 isLongPress = false;
55 return;
56 }
57
58 // Shift-click opens QR modal
59 if (e.shiftKey && !params.disabled) {
60 e.preventDefault();
61 openModal?.(getHref(), params.context ?? {});
62 }
63 }
64
65 function handleContextMenu(e: Event) {
66 // Prevent context menu during touch to avoid iOS preview
67 if (touchActive || isLongPress) {
68 e.preventDefault();
69 }
70 }
71
72 node.addEventListener('pointerdown', startLongPress);
73 node.addEventListener('pointerup', cancelLongPress);
74 node.addEventListener('pointercancel', cancelLongPress);
75 node.addEventListener('pointerleave', cancelLongPress);
76 node.addEventListener('click', handleClick);
77 node.addEventListener('contextmenu', handleContextMenu);
78
79 return {
80 update(newParams: { href?: string; context?: QRContext; disabled?: boolean }) {
81 params = newParams;
82 },
83 destroy() {
84 node.removeEventListener('pointerdown', startLongPress);
85 node.removeEventListener('pointerup', cancelLongPress);
86 node.removeEventListener('pointercancel', cancelLongPress);
87 node.removeEventListener('pointerleave', cancelLongPress);
88 node.removeEventListener('click', handleClick);
89 node.removeEventListener('contextmenu', handleContextMenu);
90 cancelLongPress();
91 // Restore original style
92 if (originalCallout) {
93 node.style.setProperty('-webkit-touch-callout', originalCallout);
94 } else {
95 node.style.removeProperty('-webkit-touch-callout');
96 }
97 }
98 };
99}