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.