PageSourceSearch

https://greatcarsandtrucksaz.com/wp-content/themes/website-theme-w…/spin-360/assets/spin-360.js?ver=1781169506

js greatcarsandtrucksaz.com collected 2026-10-08 22:55:39 UTC 8,697 bytes, 269 lines download raw bytes

1/**
2 * Spin 360 — global override-proof injector.
3 *
4 * Listing: finds vehicle cards by their /vehicles/{id} link (a contract that
5 * survives any child-theme markup), asks the server which have a 360 spin, and
6 * injects a badge that opens the spin in a modal. Re-runs when AJAX
7 * filtering/pagination swaps in new cards.
8 *
9 * VDP: the spin src is resolved server-side and passed via window.SPIN360; a
10 * floating button opens the same modal.
11 *
12 * Idempotent: if the (non-overridden) template already rendered a 360 badge or
13 * spin slide, this layer defers and injects nothing — so no duplicates.
14 */
15(function () {
16    'use strict';
17
18    var CFG = window.SPIN360 || {};
19    if (!CFG.ajaxUrl) {
20        return;
21    }
22
23    var TRIGGER = 'spin360-global-trigger';
24    var lookedUp = {}; // client-side cache: vehicle id -> spin hash | null
25    var modal, iframe;
26
27    function ready(fn) {
28        if (document.readyState !== 'loading') {
29            fn();
30        } else {
31            document.addEventListener('DOMContentLoaded', fn);
32        }
33    }
34
35    function getModal() {
36        if (!modal) {
37            modal = document.getElementById('adSpin360Modal');
38            iframe = modal && modal.querySelector('.spin360-iframe');
39        }
40        return modal;
41    }
42
43    function openModal(src) {
44        var m = getModal();
45        if (!m || !iframe) {
46            return;
47        }
48        iframe.src = src;
49        m.classList.add('is-open');
50        m.setAttribute('aria-hidden', 'false');
51        document.body.style.overflow = 'hidden';
52    }
53
54    function closeModal() {
55        var m = getModal();
56        if (!m) {
57            return;
58        }
59        m.classList.remove('is-open');
60        m.setAttribute('aria-hidden', 'true');
61        if (iframe) {
62            iframe.src = '';
63        }
64        document.body.style.overflow = '';
65    }
66
67    function vehicleIdFromHref(href) {
68        var match = /\/vehicles\/(\d+)/.exec(href || '');
69        return match ? match[1] : null;
70    }
71
72    function cardContainer(anchor) {
73        return anchor.closest('.inventory-card, .listing-vehicles-card, article, li') || anchor;
74    }
75
76    function makeBadge(spinId) {
77        var btn = document.createElement('button');
78        btn.type = 'button';
79        btn.className = 'spin360-badge ' + TRIGGER;
80        btn.setAttribute('data-spin-id', spinId);
81        btn.setAttribute('aria-label', 'View 360');
82
83        var img = document.createElement('img');
84        img.src = CFG.iconUrl;
85        img.alt = '360';
86        img.loading = 'lazy';
87        btn.appendChild(img);
88
89        return btn;
90    }
91
92    function injectBadge(anchor, spinId) {
93        var card = cardContainer(anchor);
94        if (card.getAttribute('data-spin360-done')) {
95            return;
96        }
97
98        // Idempotent: a template-rendered badge already covers this card.
99        if (card.querySelector('.ad-360-icon-button, .spin360-badge')) {
100            card.setAttribute('data-spin360-done', '1');
101            return;
102        }
103
104        // Only treat it as a real vehicle card if there's an image to overlay;
105        // skips plain text links to /vehicles/ (breadcrumbs, "back" links, etc).
106        var imageHost = anchor.querySelector('img') ? anchor : null;
107        if (!imageHost && !card.querySelector('img')) {
108            return;
109        }
110
111        var host = imageHost || card;
112        var hostStyle = window.getComputedStyle(host);
113        // Reproduce the original's block, relatively-positioned image container.
114        // In child markup the image is wrapped in an inline <a>, which gives an
115        // absolutely-positioned badge no proper containing box (breaks on mobile).
116        if (hostStyle.position === 'static') {
117            host.style.position = 'relative';
118        }
119        if (hostStyle.display === 'inline') {
120            host.style.display = 'block';
121        }
122        host.appendChild(makeBadge(spinId));
123        card.setAttribute('data-spin360-done', '1');
124    }
125
126    function collectAnchorsById() {
127        var byId = {};
128        document.querySelectorAll('a[href*="/vehicles/"]').forEach(function (a) {
129            var id = vehicleIdFromHref(a.getAttribute('href'));
130            if (!id) {
131                return;
132            }
133            (byId[id] = byId[id] || []).push(a);
134        });
135        return byId;
136    }
137
138    function applySpins(byId, spinMap) {
139        Object.keys(spinMap).forEach(function (id) {
140            var hash = spinMap[id];
141            if (!hash || !byId[id]) {
142                return;
143            }
144            byId[id].forEach(function (a) {
145                injectBadge(a, hash);
146            });
147        });
148    }
149
150    function scanListing() {
151        var byId = collectAnchorsById();
152        var ids = Object.keys(byId);
153        if (!ids.length) {
154            return;
155        }
156
157        // Apply spins we already know about (cards added by AJAX after the
158        // first lookup) without another round trip.
159        var known = {};
160        ids.forEach(function (id) {
161            if (lookedUp[id]) {
162                known[id] = lookedUp[id];
163            }
164        });
165        applySpins(byId, known);
166
167        var toQuery = ids.filter(function (id) {
168            return !(id in lookedUp);
169        });
170        if (!toQuery.length) {
171            return;
172        }
173
174        fetch(CFG.ajaxUrl, {
175            method: 'POST',
176            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
177            body: 'action=spin360_lookup&ids=' + encodeURIComponent(toQuery.join(',')),
178            credentials: 'same-origin'
179        })
180            .then(function (r) { return r.json(); })
181            .then(function (res) {
182                var map = (res && res.success && res.data) ? res.data : {};
183                toQuery.forEach(function (id) {
184                    lookedUp[id] = map[id] || null;
185                });
186                applySpins(byId, map);
187            })
188            .catch(function () { /* network error — silently skip badges */ });
189    }
190
191    function injectVdpButton() {
192        if (!CFG.isVdp || !CFG.vdpSrc) {
193            return;
194        }
195        // Idempotent: template already renders a spin slide/modal on this VDP.
196        if (document.querySelector('.orbit-spin, .autospin360, #spinModal')) {
197            return;
198        }
199        if (document.querySelector('.spin360-vdp-btn')) {
200            return;
201        }
202
203        var btn = document.createElement('button');
204        btn.type = 'button';
205        btn.className = 'spin360-vdp-btn ' + TRIGGER;
206        btn.setAttribute('data-spin-src', CFG.vdpSrc);
207
208        var img = document.createElement('img');
209        img.src = CFG.iconUrl;
210        img.alt = '';
211        var label = document.createElement('span');
212        label.textContent = 'View in 360°';
213        btn.appendChild(img);
214        btn.appendChild(label);
215
216        document.body.appendChild(btn);
217    }
218
219    ready(function () {
220        // Open from any trigger (listing badge or VDP button).
221        document.addEventListener('click', function (e) {
222            var trigger = e.target.closest('.' + TRIGGER);
223            if (!trigger) {
224                return;
225            }
226            // The badge can sit inside the /vehicles/ link (child markup wraps
227            // the image in the anchor) — stop the click from navigating.
228            e.preventDefault();
229            e.stopPropagation();
230            var src = trigger.getAttribute('data-spin-src');
231            if (!src) {
232                var sid = trigger.getAttribute('data-spin-id');
233                if (sid) {
234                    src = CFG.orbitBase + encodeURIComponent(sid);
235                }
236            }
237            if (src) {
238                openModal(src);
239            }
240        });
241
242        // Close interactions.
243        document.addEventListener('click', function (e) {
244            if (e.target.closest('.spin360-close')) {
245                e.preventDefault();
246                closeModal();
247            } else if (e.target.id === 'adSpin360Modal') {
248                closeModal();
249            }
250        });
251        document.addEventListener('keydown', function (e) {
252            if (e.key === 'Escape') {
253                closeModal();
254            }
255        });
256
257        scanListing();
258        injectVdpButton();
259
260        // Re-scan when AJAX filtering/pagination swaps in new cards. Debounced,
261        // and guarded by data-spin360-done so our own insertions don't loop.
262        var pending;
263        var observer = new MutationObserver(function () {
264            clearTimeout(pending);
265            pending = setTimeout(scanListing, 200);
266        });
267        observer.observe(document.body, { childList: true, subtree: true });
268    });
269})();

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.