1// Keeps the recipe context menu (MyRecipes) inside the viewport. 2// 3// Two jobs: 4// 5// 1. clampCtxMenu() â called from OnAfterRenderAsync after every render 6// while the menu is open. The open coords are the raw cursor position 7// on desktop and only an estimate-clamp on touch (card-longpress.js), 8// and the menu's size changes while open (touch accordion expanding, 9// error row appearing, items dropping out after a pick) â so measure 10// the real box and nudge it back inside the right/bottom edges. 11// Writing style.left/top directly is safe against Blazor re-renders: 12// the diff compares successive virtual trees, and the style attribute 13// it renders only changes when ctxX/Y change â i.e. on a fresh open, 14// which is exactly when the stale nudge should be discarded. 15// 16// 2. Flyout flip â the hover submenus open at left:100%/top:0 of their 17// row via pure CSS, so there's no render event to hook. A delegated 18// mouseover/focusin listener measures the just-shown flyout and adds 19// .ctx-fly-left / .ctx-fly-up when it would cross the right/bottom 20// edge; the classes survive Blazor's child patches because the 21// virtual tree's class attribute never changes while the menu is 22// open, and a re-open recreates the DOM without them. 23 24const PAD_PX = 8; 25 26let _lastWrap = null; 27 28export function init() { 29 document.addEventListener('mouseover', onHoverFlip, true); 30 document.addEventListener('focusin', onHoverFlip, true); 31} 32 33export function clampCtxMenu() { 34 const m = document.querySelector('.ctx-menu'); 35 if (!m) return; 36 const r = m.getBoundingClientRect(); 37 const nl = Math.max(PAD_PX, Math.min(r.left, window.innerWidth - r.width - PAD_PX)); 38 const nt = Math.max(PAD_PX, Math.min(r.top, window.innerHeight - r.height - PAD_PX)); 39 if (nl !== r.left) m.style.left = nl + 'px'; 40 if (nt !== r.top) m.style.top = nt + 'px'; 41} 42 43// Find the card under a viewport point, seeing THROUGH the open 44// context-menu backdrop (which otherwise captures the hit). Lets a 45// right-click while a menu is open re-target the menu to the card under the 46// cursor instead of just closing. `attr` is 'data-recipe-id' or 47// 'data-collection-id'. Returns the attribute value, or null. 48export function cardAtPoint(x, y, attr) { 49 const backdrops = document.querySelectorAll('.ctx-backdrop'); 50 const prev = []; 51 backdrops.forEach(b => { prev.push(b.style.pointerEvents); b.style.pointerEvents = 'none'; }); 52 let el = null; 53 try { el = document.elementFromPoint(x, y); } 54 finally { backdrops.forEach((b, i) => { b.style.pointerEvents = prev[i]; }); } 55 const card = el && el.closest ? el.closest('[' + attr + ']') : null; 56 return card ? card.getAttribute(attr) : null; 57} 58 59function onHoverFlip(e) { 60 const wrap = e.target.closest ? e.target.closest('.ctx-menu .ctx-item-wrap') : null; 61 // Once per wrapper entry â mouseover refires on every child crossed, 62 // and a remove-measure-readd cycle mid-hover would be wasted work. 63 if (wrap === _lastWrap) return; 64 _lastWrap = wrap; 65 if (!wrap) return; 66 const sub = wrap.querySelector(':scope > .ctx-submenu'); 67 if (!sub) return; 68 69 sub.classList.remove('ctx-fly-left', 'ctx-fly-up'); 70 // Both classList writes and the measurement happen in this same 71 // task, before the next paint â no flicker from the reset. 72 const r = sub.getBoundingClientRect(); 73 if (!r.width && !r.height) return; // hidden (touch accordion closed, etc.) 74 if (r.right > window.innerWidth - PAD_PX) sub.classList.add('ctx-fly-left'); 75 if (r.bottom > window.innerHeight - PAD_PX) sub.classList.add('ctx-fly-up'); 76}
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.