1// ââ Render-size-based card image resolver ââââââââââââââââââââââââââââââââââ 2// Card thumbnails render with the smallest CDN crop (â¦/crops/160/â¦) as their 3// initial `src`. After layout we measure each image's ACTUAL rendered width (in 4// CSS pixels, deliberately IGNORING devicePixelRatio â a product decision to 5// favour smaller payloads over retina sharpness) and upgrade anything that 6// renders wider than 160px to the 256 crop. 7// 8// We don't use srcset/sizes for this: the browser's responsive-image system 9// always multiplies the rendered width by the device pixel ratio, so on a 2à 10// screen a 130px card would still pull 256. Measuring clientWidth in JS is the 11// only way to key the choice off the pure CSS render size. 12 13const CROPS = [160, 256]; // CDN-generated crop widths, ascending 14 15// Smallest available crop >= the rendered width (falls back to the largest). 16function pickWidth(rendered) { 17 for (const w of CROPS) if (w >= rendered) return w; 18 return CROPS[CROPS.length - 1]; 19} 20 21// Upgrade a single <img> still pointing at the /crops/160/ baseline. 22function resolve(img) { 23 const src = img.getAttribute('src') || ''; 24 if (!src.includes('/crops/160/')) return; 25 const rendered = img.clientWidth || img.parentElement?.clientWidth || 0; 26 if (!rendered) return; // not laid out yet â leave the 160 baseline in place 27 const want = pickWidth(rendered); 28 if (want !== 160) img.src = src.replace('/crops/160/', `/crops/${want}/`); 29} 30 31// Resolve every baseline card image under `root` (default: whole document). 32export function resolveCardImages(root = document) { 33 (root || document).querySelectorAll('img[src*="/crops/160/"]').forEach(resolve); 34} 35 36let raf = 0; 37function schedule() { 38 if (raf) return; 39 raf = requestAnimationFrame(() => { raf = 0; resolveCardImages(document); }); 40} 41 42// ââ Broken-image fallback ââââââââââââââââââââââââââââââââââââââââââââââââââ 43// CI3 parity: a missing or broken card image must show NOTHING â never the 44// browser's broken-image icon + alt text. Card thumbnails point at CDN crops 45// that can 404 (crop never generated, bad filename, wrong pId). On failure we 46// drop the <img>; the wrapper then collapses via the `:not(:has(img))` rules in 47// menu.css. This mirrors menu.js's delegated handler, but covers the static / 48// home-widget surfaces (featured rail, etc.) that the menu-grid handler doesn't. 49// 50// Card-image wrappers we actively manage (retry + sweep for broken thumbnails). 51const CARD_WRAP = '.item-img,.feat-img,.fav-row__img,.fav-lg__img,.srch-card-img,.wla-best__image,.featured-image,.op-menu-img,.op-gallery-cell,.op-gallery-thumb'; 52// Of those, the wrappers whose CSS collapses to nothing once their <img> is gone 53// (their empty-state is "show nothing"). `.featured-image` is handled separately 54// because its empty-state shows a styled placeholder, not a collapse. 55const COLLAPSE_WRAP = '.item-img,.feat-img,.fav-row__img,.fav-lg__img,.srch-card-img,.wla-best__image,.op-menu-img,.op-gallery-cell,.op-gallery-thumb'; 56// Standalone images (no card wrapper) that should simply vanish when broken 57// rather than show the browser's broken-image icon + alt text. The DB-driven 58// lead-form header banner is one: many forms store a /uploads/ URL that 404s on 59// the CDN, and a broken icon next to its alt text reads as a stray duplicate 60// heading above the real <h1>. Collapse it â no logo fallback (forms want the 61// banner gone, not replaced). CI3 achieved this server-side via getimagesize(). 62const DROP_IMG = '.form-page-card__header'; 63 64// Reverse cdn_card_crop(): â¦/uploads/123/crops/160/photo.jpg.webp â â¦/uploads/123/photo.jpg 65// Returns null when the src isn't a generated crop (nothing to fall back to). 66function decrop(src) { 67 const m = (src || '').match(/^(.*\/uploads\/\d+)\/crops\/\d+\/(.+)\.webp(\?.*)?$/); 68 return m ? `${m[1]}/${m[2]}${m[3] || ''}` : null; 69} 70 71// Brand logo for the image fallback (menu-popup parity). Prefer the square logo 72// since card image wells are ~1:1; fall back to the wordmark logo. 73function brandLogo() { 74 const c = window.AppConfig || {}; 75 return String(c.logoSquare || c.logo || ''); 76} 77 78function dropBroken(img) { 79 if (!img || img.tagName !== 'IMG') return; 80 // Standalone drop-on-error images (e.g. the form header banner): just remove 81 // them so nothing â no broken icon, no alt text â is left behind. 82 if (img.matches(DROP_IMG)) { img.remove(); return; } 83 if (!img.closest(CARD_WRAP)) return; // only manage card thumbnails 84 // The menu page owns broken images inside its grid / popups (menu.js) â don't 85 // double-handle and fight its own fallback logic. 86 if (img.closest('#dynamicMenuContent,.popup-overlay')) return; 87 // A failed CDN crop usually just means the /crops/<w>/ webp was never 88 // generated though the original upload exists â retry the original once 89 // before giving up, so a missing crop recovers its real image. 90 if (img.dataset.imgRetry !== '1') { 91 img.dataset.imgRetry = '1'; 92 const orig = decrop(img.getAttribute('src')); 93 if (orig) { img.removeAttribute('srcset'); img.src = orig; return; } 94 } 95 // Image is unavailable. Fill the well with the grayscaled brand logo, exactly 96 // like the menu item popup â nicer than a blank box or "item p
96hoto" text. 97 const logo = brandLogo(); 98 if (logo && !img.classList.contains('card-img-fallback')) { 99 img.classList.add('card-img-fallback'); 100 img.removeAttribute('srcset'); 101 img.alt = ''; 102 img.src = logo; 103 return; 104 } 105 // No brand logo configured (or the logo itself failed): match each surface's 106 // empty-state design. The ue_silver featured rail shows a styled placeholder; 107 // other listing surfaces collapse via the page CSS `:not(:has(img))` rules. 108 if (img.closest('.featured-image')) { 109 const ph = document.createElement('span'); 110 ph.className = 'feat-img-ph'; 111 ph.textContent = 'item photo'; 112 img.replaceWith(ph); 113 return; 114 } 115 if (img.closest(COLLAPSE_WRAP)) img.remove(); 116} 117 118// Catch images that already errored before our listener attached (the module 119// auto-inits, but some thumbnails may have failed during initial parse). 120function sweepBroken(root = document) { 121 const sel = CARD_WRAP.split(',').map(s => `${s} img`).join(',') + ',' + DROP_IMG; 122 (root || document).querySelectorAll(sel) 123 .forEach(img => { if (img.complete && img.naturalWidth === 0) dropBroken(img); }); 124} 125 126let inited = false; 127// Wire up the document-level passes: initial layout, a MutationObserver (so 128// JS-rendered cards and slick-carousel clones get upgraded as they appear), and 129// a debounced resize pass (breakpoints can change a card's rendered width). 130// Safe to call repeatedly; only the first call attaches listeners. 131export function initCardImages() { 132 if (inited) return; 133 inited = true; 134 // Image 'error' events don't bubble â capture them at the document. 135 document.addEventListener('error', e => dropBroken(e.target), true); 136 const start = () => { 137 schedule(); 138 sweepBroken(document); 139 // childList only â our own src swaps are attribute changes, so they 140 // never retrigger this (no feedback loop). rAF coalesces bursts. 141 new MutationObserver(muts => { 142 for (const m of muts) if (m.addedNodes.length) { schedule(); sweepBroken(document); break; } 143 }).observe(document.body, { childList: true, subtree: true }); 144 }; 145 if (document.readyState !== 'loading') start(); 146 else document.addEventListener('DOMContentLoaded', start); 147 let t; 148 window.addEventListener('resize', () => { clearTimeout(t); t = setTimeout(schedule, 200); }, { passive: true }); 149} 150 151// Auto-init when included directly on a server-rendered page. JS modules that 152// import resolveCardImages also trigger init here, which is harmless. 153initCardImages();
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.