PageSourceSearch

https://rootd510.com/assets/front/js/locations-picker.js?v=d5743b0122b039ff6c9619cc32c10455ddcf6832

js rootd510.com collected 2026-09-29 18:17:58 UTC 8,745 bytes, 197 lines download raw bytes

1/*
2 * Locations module — auto-prompt picker (modal/page) and on-demand drawer
3 * runtime. Loaded sitewide via FrontModel when the LOCATIONS module is
4 * enabled. Both surfaces share the same submit flow (POST /locations/select,
5 * mirror cookie client-side, navigate to redirect or reload) — extracted into
6 * `submitLocation` so the modal and drawer don't drift apart.
7 */
8(function () {
9    /**
10     * Shared submit flow used by both the modal and the drawer. Handles:
11     *   - busy/disabled state on the clicked button
12     *   - POST to action with credentials so the Set-Cookie header sticks
13     *   - graceful HTML-instead-of-JSON responses (CSRF page, 404, 500)
14     *   - belt-and-suspenders: also writes the cookie client-side before
15     *     navigating, since some browsers don't reliably commit the AJAX
16     *     response's Set-Cookie header before window.location navigates and
17     *     the picker would re-prompt on the next page load otherwise.
18     */
19    function submitLocation(action, btn, locationId, errorEl) {
20        function showError(msg) {
21            if (!errorEl) return;
22            errorEl.textContent = msg;
23            errorEl.hidden = false;
24        }
25        function resetBtn() {
26            btn.classList.remove('is-busy');
27            btn.removeAttribute('disabled');
28        }
29
30        if (errorEl) { errorEl.hidden = true; errorEl.textContent = ''; }
31        btn.classList.add('is-busy');
32        btn.setAttribute('disabled', 'disabled');
33
34        var fd = new FormData();
35        fd.append('location_id', locationId);
36        // The page being selected from. The endpoint is reached by XHR, so it
37        // can't see it otherwise, and it decides whether a location's Shop URL
38        // applies (shop pages only, see resolveSelectionRedirect).
39        fd.append('current_path', window.location.pathname + window.location.search);
40
41        fetch(action, {
42            method: 'POST',
43            body: fd,
44            credentials: 'same-origin',
45            headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }
46        })
47            .then(function (r) {
48                // Read as text so we can give a useful error if the server
49                // returned HTML instead of JSON.
50                return r.text().then(function (text) { return { status: r.status, text: text }; });
51            })
52            .then(function (res) {
53                var json;
54                try { json = JSON.parse(res.text); } catch (_) { json = null; }
55                if (!json) {
56                    showError('Could not save your selection (status ' + res.status + '). Please try again.');
57                    resetBtn();
58                    return;
59                }
60                if (!json.success) {
61                    showError(json.error || 'Could not save your selection.');
62                    resetBtn();
63                    return;
64                }
65                var cookie = 'selected_location_id=' + encodeURIComponent(locationId)
66                           + '; path=/; SameSite=Lax'
67                           + (location.protocol === 'https:' ? '; Secure' : '');
68                if (json.remember_days > 0) {
69                    cookie += '; max-age=' + (json.remember_days * 24 * 60 * 60);
70                }
71                document.cookie = cookie;
72
73                if (json.redirect) window.location.href = json.redirect;
74                else               window.location.reload();
75            })
76            .catch(function () {
77                showError('Network error. Please check your connection and try again.');
78                resetBtn();
79            });
80    }
81
82    // ---- Auto-prompt modal / page interstitial ---------------------------
83    function initModal() {
84        var modal = document.getElementById('locPickerModal');
85        if (!modal) return;
86        var required = modal.getAttribute('data-required') === '1';
87        var action   = modal.getAttribute('data-action');
88        var errorEl  = document.getElementById('locPickerError');
89        document.body.style.overflow = 'hidden';
90
91        function close() {
92            if (required) return; // hard gate — no dismiss path.
93            modal.remove();
94            document.body.style.overflow = '';
95        }
96
97        // Dismiss handlers — only attached when not required.
98        if (!required) {
99            modal.addEventListener('click', function (e) {
100                if (e.target.closest('[data-loc-picker-dismiss]')) close();
101            });
102            document.addEventListener('keydown', function onEsc(e) {
103                if (e.key === 'Escape') { close(); document.removeEventListener('keydown', onEsc); }
104            });
105        }
106
107        // Step-flow panel switching. Group axis is dynamic (state vs city —
108        // see the PHP layout decision); the JS just shuttles the chosen
109        // group key between panels.
110        var steps = modal.querySelector('.loc-picker-steps');
111        if (steps) {
112            steps.addEventListener('click', function (e) {
113                var btn = e.target.closest('[data-step-to]');
114                if (!btn) return;
115                var goTo = btn.getAttribute('data-step-to');
116                steps.querySelectorAll('.loc-picker-steps__panel').forEach(function (p) {
117                    p.hidden = p.getAttribute('data-panel') !== goTo;
118                });
119                if (goTo === 'locations') {
120                    var groupFilter = btn.getAttribute('data-group') || '';
121                    modal.querySelectorAll('.loc-picker-card[data-group-filter]').forEach(function (c) {
122                        c.hidden = c.getAttribute('data-group-filter') !== groupFilter;
123                    });
124                }
125            });
126        }
127
128        modal.addEventListener('click', function (e) {
129            var btn = e.target.closest('[data-location-id]');
130            if (!btn || btn.classList.contains('is-busy')) return;
131            var locationId = btn.getAttribute('data-location-id');
132            if (!locationId) return;
133            submitLocation(action, btn, locationId, errorEl);
134        });
135    }
136
137    // ---- On-demand drawer ------------------------------------------------
138    function initDrawer() {
139        var drawer = document.getElementById('locPickerDrawer');
140        if (!drawer) return;
141        var aside   = drawer.querySelector('.sg-drawer');
142        var action  = aside ? aside.getAttribute('data-action') : '';
143        var errorEl = document.getElementById('locDrawerError');
144
145        drawer.addEventListener('click', function (e) {
146            var btn = e.target.closest('[data-location-id]');
147            if (!btn || btn.classList.contains('is-busy')) return;
148            var locationId = btn.getAttribute('data-location-id');
149            if (!locationId) return;
150            submitLocation(action, btn, locationId, errorEl);
151        });
152    }
153
154    // ---- Picker trigger buttons -----------------------------------------
155    // The [location_picker_button] / SG Builder trigger renders a default label
156    // server-side (cache-safe); once a location is picked we swap in its name
157    // here, read from the JS-readable cookie + the drawer card markup, so a
158    // full-page-cached button never shows one visitor's choice to everyone.
159    function getCookie(name) {
160        var m = document.cookie.match('(?:^|; )' + name.replace(/([.*+?^${}()|[\]\\])/g, '\\$1') + '=([^;]*)');
161        return m ? decodeURIComponent(m[1]) : '';
162    }
163
164    function initTriggers() {
165        var triggers = document.querySelectorAll('[data-loc-picker-trigger]');
166        if (!triggers.length) return;
167
168        var id = getCookie('selected_location_id').replace(/\D/g, '');
169        if (!id) return; // no selection yet — leave the default label.
170
171        // The drawer (emitted alongside the button) already lists every
172        // location's name — reuse it instead of shipping a second id→name map.
173        var card = document.querySelector('#locPickerDrawer .loc-drawer-card[data-location-id="' + id + '"]');
174        var nameEl = card ? card.querySelector('.loc-drawer-card__name') : null;
175        var name = nameEl ? nameEl.textContent.trim() : '';
176        if (!name) return;
177
178        triggers.forEach(function (btn) {
179            var labelEl = btn.querySelector('.loc-picker-trigger__label');
180            if (labelEl) labelEl.textContent = name;
181            btn.classList.add('is-selected');
182            btn.setAttribute('title', name);
183        });
184    }
185
186    function init() {
187        initModal();
188        initDrawer();
189        initTriggers();
190    }
191
192    if (document.readyState === 'loading') {
193        document.addEventListener('DOMContentLoaded', init);
194    } else {
195        init();
196    }
197})();

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.