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.