PageSourceSearch

https://www.iwant2eat.com/js/card-longpress.xvkzaape1m.js

js iwant2eat.com collected 2026-09-24 14:45:35 UTC 5,931 bytes, 140 lines download raw bytes

1// Touch path for the per-card right-click context menu (MyRecipes).
2// iOS Safari never fires `contextmenu` for a touch long-press, so the
3// card's @oncontextmenu handler is mouse-only there. This module turns
4// a long-press (~450ms, same threshold as tag-drop-touch.js) on a
5// [data-recipe-id] card into a call to the page's TouchOpenContextMenu,
6// which opens the same menu desktop right-click does.
7//
8// Android Chrome DOES fire contextmenu on long-press — ~50ms after our
9// timer. By then the menu is opening server-side, so the native event
10// would land on the freshly rendered backdrop and instantly close the
11// menu again. While a long-press is being handled we swallow the native
12// contextmenu in the capture phase; conversely, if a native contextmenu
13// ever beats our timer we cancel the timer and let the regular
14// @oncontextmenu path handle it (its :preventDefault stops the OS menu).
15//
16// Releasing a long-press also synthesizes a click, which would either
17// navigate into the recipe (card @onclick) or close the fresh menu via
18// the backdrop, depending on render timing. preventDefault on touchend
19// stops it per spec; a short capture-phase swallow window catches
20// browsers that fire it anyway. The window is zeroed on the next
21// touchstart so it can never eat a genuine follow-up tap — any new
22// touch means the previous touch's ghost click already came or never
23// will.
24
25let _dotnetHelper = null;
26let _timer = null;
27let _startXY = null;
28let _fired = false;            // long-press fired for the touch in progress
29let _swallowClicksUntil = 0;   // post-release ghost-click guard
30
31const LONG_PRESS_MS = 450;
32const MOVE_TOLERANCE_PX = 10;
33// Rough menu footprint used to keep it on-screen when the press is near
34// a viewport edge. Width tracks the coarse-pointer max-width cap on
35// .ctx-menu (18rem = 288px + shadow margin); height covers the tallest
36// realistic menu (4 items + capped scrolling submenu). Only a first
37// guess so the initial paint lands close — ctx-menu-clamp.js measures
38// the real box after every render and fixes any residual overflow.
39const MENU_W_PX = 296;
40const MENU_H_PX = 340;
41const EDGE_PX = 8;
42
43export function initCardLongPress(dotnetHelper) {
44    _dotnetHelper = dotnetHelper;
45
46    // Mouse/trackpad users have real right-click; only attach for touch.
47    if (!matchMedia('(pointer: coarse)').matches) return;
48
49    document.addEventListener('touchstart', onTouchStart, { passive: true });
50    document.addEventListener('touchmove', onTouchMove, { passive: true });
51    // passive:false — we preventDefault the touchend that ends a long-
52    // press so the browser doesn't synthesize mouse events + click.
53    document.addEventListener('touchend', onTouchEnd, { passive: false });
54    document.addEventListener('touchcancel', onTouchCancel, { passive: true });
55    document.addEventListener('click', onClickCapture, true);
56    document.addEventListener('contextmenu', onContextMenuCapture, true);
57}
58
59function onTouchStart(e) {
60    clearPending();
61    _fired = false;
62    _swallowClicksUntil = 0;
63    if (e.touches.length !== 1) return;
64    // While a tag is "picked up" (tag-drop-touch.js) the next card tap
65    // means "drop the tag here" — don't arm the menu underneath it.
66    if (document.body.classList.contains('tag-drag-active')) return;
67    // Recipe cards and collection cards (IWA-31) both long-press into
68    // their own context menus; the data attribute decides which .NET
69    // entry point fires.
70    const card = e.target.closest('[data-recipe-id], [data-collection-id]');
71    if (!card) return;
72
73    const t = e.touches[0];
74    _startXY = { x: t.clientX, y: t.clientY };
75    const recipeId = card.dataset.recipeId;
76    const collectionId = card.dataset.collectionId;
77    _timer = setTimeout(() => {
78        _timer = null;
79        _fired = true;
80        // Clamp so the finger-anchored menu stays inside small viewports.
81        const x = Math.max(EDGE_PX, Math.min(_startXY.x, window.innerWidth - MENU_W_PX));
82        const y = Math.max(EDGE_PX, Math.min(_startXY.y, window.innerHeight - MENU_H_PX));
83        _startXY = null;
84        if (navigator.vibrate) navigator.vibrate(15);
85        const method = recipeId ? 'TouchOpenContextMenu' : 'TouchOpenCollectionContextMenu';
86        const id = recipeId || collectionId;
87        try {
88            _dotnetHelper.invokeMethodAsync(method, id, x, y)
89                .catch(() => { /* circuit lost — best effort */ });
90        } catch (_) { /* ditto */ }
91    }, LONG_PRESS_MS);
92}
93
94function onTouchMove(e) {
95    if (!_timer || !_startXY) return;
96    const t = e.touches[0];
97    if (Math.abs(t.clientX - _startXY.x) > MOVE_TOLERANCE_PX ||
98        Math.abs(t.clientY - _startXY.y) > MOVE_TOLERANCE_PX) {
99        clearPending();   // scroll intent, not a long-press
100    }
101}
102
103function onTouchEnd(e) {
104    clearPending();
105    if (!_fired) return;  // plain tap — let the click → OpenRecipe through
106    _fired = false;
107    if (e.cancelable) e.preventDefault();
108    _swallowClicksUntil = Date.now() + 350;
109}
110
111function onTouchCancel() {
112    clearPending();
113    _fired = false;
114}
115
116function onClickCapture(e) {
117    if (Date.now() >= _swallowClicksUntil) return;
118    _swallowClicksUntil = 0;   // one-shot
119    e.preventDefault();
120    e.stopImmediatePropagation();
121}
122
123function onContextMenuCapture(e) {
124    // Android's native long-press contextmenu for the press we already
125    // handled: swallow it before Blazor sees it. _fired covers "finger
126    // still down"; the click window covers "fired right at release".
127    if (_fired || Date.now() < _swallowClicksUntil) {
128        e.preventDefault();
129        e.stopImmediatePropagation();
130        return;
131    }
132    // Native event beat our timer — stand down and let the normal
133    // @oncontextmenu path open the menu.
134    clearPending();
135}
136
137function clearPending() {
138    if (_timer) { clearTimeout(_timer); _timer = null; }
139    _startXY = null;
140}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.