1/** 2 * PACustomer â turns the guest header rendered by the server into the 3 * signed-in header on the client. 4 * 5 * Loaded globally (bundled via script_footer.php) in the order api.js -> 6 * section.js -> mini-cart.js -> customer.js -> init.js. 7 * 8 * Pages are shared by every visitor through the Full Page Cache, so templates 9 * always render the guest variant and mark the pieces that differ once we know 10 * who the visitor is. Three declarative hooks cover the whole header: 11 * 12 * data-auth="guest" / data-auth="member" 13 * Shown only to that audience. Guest nodes are visible in the cached HTML 14 * and hidden once a signed-in customer is known; member nodes are the 15 * reverse, and therefore must be rendered with the `hide` class so they 16 * never flash for guests. Visibility is toggled via the `hide` class, which 17 * is the site-wide convention. 18 * 19 * data-auth-label 20 * Optional text slot (PCR account chrome). Stays empty until the customer 21 * section is known so "Login / Register" does not paint and then jump to 22 * "Account". data-label-guest / data-label-member supply the copy. 23 * 24 * data-customer-firstname 25 * textContent is replaced with the customer's first name (or email when the 26 * account has no name). Only meaningful inside a data-auth="member" node. 27 * 28 * data-init-profile-menu 29 * The account dropdown. FPC pages ship the guest menu (identical for every 30 * visitor on that URL). Uncached pages set data-menu-audience to member when 31 * the session is signed in, so Account Settings does not flash Sign In. 32 * A signed-in visitor on an FPC page fetches GET /rest/web/mine/profile-menu 33 * once the customer section says who they are, or on hover if the on-page 34 * menu is still the guest variant. While that fetch is outstanding the 35 * dropdown stays closed (data-menu-pending) so Sign In cannot flash. 36 * Uncached pages that already shipped the member menu are never held. 37 * 38 * The customer section is hydrated by SectionStore ('customer' section, see 39 * SectionController::loadCustomerData). SectionStore returns the cached payload 40 * synchronously and delivers fresh data through onUpdated, so a returning 41 * visitor sees the signed-in header without waiting for the network. 42 */ 43window.PACustomer = (function () { 44 var AUTH_ATTR = 'data-auth'; 45 var LABEL_ATTR = 'data-auth-label'; 46 var NAME_ATTR = 'data-customer-firstname'; 47 var MENU_ATTR = 'data-init-profile-menu'; 48 var HIDE_CLASS = 'hide'; 49 var ENDPOINT = '/rest/web/mine/profile-menu'; 50 51 // { logged_in, guest, id, email, firstname } from SectionStore('customer'). 52 var cache = null; 53 // Who the menu currently on the page is for. FPC pages start as guest; 54 // uncached /secure pages may already ship the member menu (data-menu-audience). 55 var menuAudience = 'guest'; 56 var fetchInFlight = null; 57 // False until SectionStore delivers the first customer payload this page load. 58 var customerSectionKnown = false; 59 // True while the pointer is over the account icon; used to open the menu 60 // after a pending fetch without flashing the guest Sign In links first. 61 var pointerOverProfile = false; 62 63 function readInitialMenuAudience() { 64 var node = document.querySelector('[' + MENU_ATTR + ']'); 65 if (!node) return 'guest'; 66 return node.getAttribute('data-menu-audience') === 'member' ? 'member' : 'guest'; 67 } 68 69 /** 70 * PHPSESSID is HttpOnly and invisible to JavaScript, so it cannot be used 71 * as a live-session check. Trust the customer section (localStorage until 72 * the first sync, then the server). Logout clears localStorage; a stale 73 * "logged in" after the session dies is corrected when the sync lands. 74 */ 75 function isLoggedIn() { 76 return !!(cache && cache.logged_in); 77 } 78 79 function audienceForVisitor() { 80 return isLoggedIn() ? 'member' : 'guest'; 81 } 82 83 function toggle(el, visible) { 84 if (visible) el.classList.remove(HIDE_CLASS); 85 else el.classList.add(HIDE_CLASS); 86 } 87 88 function renderAudience(scope) { 89 var loggedIn = isLoggedIn(); 90 var nodes = scope.querySelectorAll('[' + AUTH_ATTR + ']'); 91 Array.prototype.forEach.call(nodes, function (el) { 92 var audience = el.getAttribute(AUTH_ATTR); 93 if (audience === 'member') toggle(el, loggedIn); 94 else if (audience === 'guest') toggle(el, !loggedIn); 95 }); 96 } 97 98 /** 99 * Status.user_id / Status.cart_id used to be printed into the page footer. 100 * They identify the visitor, so shared cached HTML cannot carry them; the 101 * image-retry diagnostics in functions.js read them from Status, which is 102 * populated here instead. cart_id comes straight from its cookie (the name 103 * is site-scoped, so the server still supplies it as CART_ID_COOKIE). 104 */ 105 function publishStatus() { 106 if (typeof Status === 'undefined' || !Status.set) return; 107 Status.set('user_id', (isLoggedIn() && cache && cache.token) || 0); 108 if (typeof CART_ID_COOKIE !== 'undefined' && typeof readCookie === 'function') { 109 Status.set('cart_id', readCookie(CART_ID_COOKIE) || ''); 110 } 111 } 112 113 /** 114 * gtm.php emits the identity keys in their guest form because the page is 115 * shared by the Full Page Cache; the real values are pushed here. `events`
116 * carries the one-shot login / sign_up events the section consumed on the 117 * server, so each is pushed exactly once per payload. 118 */ 119 function pushGtm(data) { 120 if (!data || typeof window.dataLayer === 'undefined') return; 121 if (data.gtm) window.dataLayer.push(data.gtm); 122 123 var events = data.events || []; 124 if (!events.length) return; 125 // The section is persisted in localStorage and relayed to other tabs, so 126 // clear the one-shot events before pushing them or they fire again there. 127 data.events = []; 128 if (typeof window.SectionStore !== 'undefined') { 129 window.SectionStore.setLocal('customer', data); 130 } 131 events.forEach(function (event) { 132 window.dataLayer.push(event); 133 }); 134 } 135 136 function renderName(scope) { 137 var name = (isLoggedIn() && cache && (cache.firstname || cache.email)) || ''; 138 var nodes = scope.querySelectorAll('[' + NAME_ATTR + ']'); 139 Array.prototype.forEach.call(nodes, function (el) { 140 el.textContent = name; 141 }); 142 } 143 144 function isCustomerKnown() { 145 return customerSectionKnown || cache !== null; 146 } 147 148 function renderAuthLabels(scope) { 149 var known = isCustomerKnown(); 150 var loggedIn = isLoggedIn(); 151 var nodes = scope.querySelectorAll('[' + LABEL_ATTR + ']'); 152 Array.prototype.forEach.call(nodes, function (el) { 153 if (!known) { 154 el.textContent = ''; 155 return; 156 } 157 var guest = el.getAttribute('data-label-guest') || ''; 158 var member = el.getAttribute('data-label-member') || ''; 159 el.textContent = loggedIn ? member : guest; 160 }); 161 } 162 163 function render(scope) { 164 var root = scope || document; 165 renderAudience(root); 166 renderAuthLabels(root); 167 renderName(root); 168 publishStatus(); 169 } 170 171 function eachMenu(fn) { 172 Array.prototype.forEach.call(document.querySelectorAll('[' + MENU_ATTR + ']'), fn); 173 } 174 175 function injectMenu(html) { 176 eachMenu(function (n) { n.innerHTML = html; }); 177 } 178 179 function menuFitsVisitor() { 180 return menuAudience === audienceForVisitor(); 181 } 182 183 /** 184 * Hold the dropdown closed only while FPC guest markup would flash Sign In. 185 * Uncached /secure pages ship the member menu; do not treat a missing or 186 * guest-shaped customer cache as pending â needsMenuFetch already skips 187 * replacing that markup until the section answers. 188 */ 189 function shouldHoldMenuClosed() { 190 if (menuAudience === 'member') return false; 191 return isLoggedIn() || !!fetchInFlight; 192 } 193 194 /** 195 * top_menu.php shows the dropdown on hover. Mark the node pending so that 196 * handler does not reveal Sign In while we still owe the signed-in markup. 197 */ 198 function syncMenuPending() { 199 var pending = shouldHoldMenuClosed(); 200 eachMenu(function (n) { 201 if (pending) n.setAttribute('data-menu-pending', '1'); 202 else n.removeAttribute('data-menu-pending'); 203 }); 204 } 205 206 function hideMenus() { 207 eachMenu(function (n) { n.classList.add(HIDE_CLASS); }); 208 } 209 210 function revealMenuIfHovering() { 211 if (!pointerOverProfile || shouldHoldMenuClosed()) return; 212 eachMenu(function (n) { n.classList.remove(HIDE_CLASS); }); 213 } 214 215 function fetchMenu() { 216 if (fetchInFlight) return fetchInFlight; 217 if (typeof window.PAApi === 'undefined') return Promise.resolve(null); 218 fetchInFlight = window.PAApi.request(ENDPOINT, { 219 query: { path: window.location.pathname }, 220 }).then(function (res) { 221 if (res && typeof res.html === 'string') { 222 injectMenu(res.html); 223 // The server renders from the live session; trust that over the 224 // client cache when the fetch started before SectionStore answered. 225 menuAudience = res.signed_in ? 'member' : 'guest'; 226 } 227 return res; 228 }).catch(function () { 229 return null; 230 }).then(function (res) { 231 fetchInFlight = null; 232 syncMenuPending(); 233 revealMenuIfHovering(); 234 return res; 235 }); 236 syncMenuPending(); 237 return fetchInFlight; 238 } 239 240 /** 241 * Fetch when the on-page menu is for the wrong audience. Do not fetch while 242 * an uncached page already shipped the member menu and the section has not 243 * answered yet â that would race and can replace Sign Out with Sign In. 244 */ 245 function needsMenuFetch() {
246 if (fetchInFlight) return false; 247 if (!customerSectionKnown && menuAudience === 'member') return false; 248 return !menuFitsVisitor(); 249 } 250 251 function onHover() { 252 pointerOverProfile = true; 253 if (needsMenuFetch()) fetchMenu(); 254 } 255 256 function onLeave() { 257 pointerOverProfile = false; 258 } 259 260 /** 261 * Replace the menu when it is for the wrong audience. Hide guest Sign In 262 * links rather than leave them on screen until the member HTML lands. 263 * Member markup already on the page stays hoverable. 264 */ 265 function refreshMenuIfStale() { 266 syncMenuPending(); 267 if (shouldHoldMenuClosed()) hideMenus(); 268 if (needsMenuFetch()) fetchMenu(); 269 else revealMenuIfHovering(); 270 } 271 272 function onUpdatedCustomer(data) { 273 customerSectionKnown = true; 274 var wasLoggedIn = isLoggedIn(); 275 cache = data || null; 276 // Guest favorites live under the cart cookie version key; after sign-in 277 // the favorite section is keyed by customer id. Drop the stale guest 278 // localStorage payload so init.js re-renders hearts and the badge from 279 // the server instead of keeping count=0 / design_ids=[] until CDC catches up. 280 if (!wasLoggedIn && isLoggedIn() && typeof window.SectionStore !== 'undefined') {
281 window.SectionStore.invalidate('favorite'); 282 window.SectionStore.invalidate('cart'); 283 } 284 render(document); 285 pushGtm(cache); 286 refreshMenuIfStale(); 287 } 288 289 function prime() { 290 menuAudience = readInitialMenuAudience(); 291 if (typeof window.SectionStore === 'undefined') return; 292 var cached = window.SectionStore.get('customer'); 293 if (cached) cache = cached; 294 window.SectionStore.onUpdated('customer', onUpdatedCustomer); 295 refreshMenuIfStale(); 296 } 297 298 function init() { 299 prime(); 300 // Runs even without a cached section: it re-asserts the guest state the 301 // HTML already shows and publishes the cart id, which comes from a cookie. 302 var ready = function () { render(document); }; 303 if (typeof jQuery !== 'undefined') { 304 jQuery(function () { ready(); }); 305 } else if (document.readyState === 'loading') { 306 document.addEventListener('DOMContentLoaded', ready); 307 } else { 308 ready(); 309 } 310 var hoverTargets = document.querySelectorAll('[e-show-mini-profile]'); 311 Array.prototype.forEach.call(hoverTargets, function (el) { 312 el.addEventListener('mouseenter', onHover); 313 el.addEventListener('mouseleave', onLeave); 314 }); 315 } 316 317 init(); 318 319 return { 320 render: render, 321 fetchMenu: fetchMenu, 322 isLoggedIn: isLoggedIn, 323 getCachedCustomer: function () { return cache; }, 324 }; 325})();
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.