PageSourceSearch

https://goodtogostore.com/src/js/modules/cardImage.js?v=1791374065

js goodtogostore.com collected 2026-10-07 22:39:31 UTC 8,103 bytes, 153 lines download raw bytes

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.