1/** 2 * Bmart Wishlist frontend. 3 * 4 * Cache-safety contract: server HTML is identical for all visitors; every 5 * per-user mark (button state, header count, list contents) is applied here 6 * after a REST GET. Guests are identified by a cookie key that only this 7 * script writes â page responses never set cookies. 8 */ 9( function () { 10 'use strict'; 11 12 var cfg = window.bmartWishlist || null; 13 // canUse arrives as "1"/"" (wp_localize_script stringifies booleans) and is 14 // falsy only on a stale cached page from a mode transition â the server 15 // never localizes this config for users who can't use the feature. A 16 // pre-1.1.0 cached page lacks the key entirely; treat that as usable. 17 if ( ! cfg || ( 'canUse' in cfg && ! cfg.canUse ) ) { 18 return; 19 } 20 21 var STORAGE_KEY = 'bmartWishlist:v1'; 22 23 var state = { 24 items: [], 25 count: 0, 26 loaded: false, 27 }; 28 29 // ââ Cookie helpers ââââââââââââââââââââââââââââââââââââââââââââââ 30 31 function getCookie( name ) { 32 var match = document.cookie.match( new RegExp( '(?:^|; )' + name + '=([a-f0-9]{32})' ) ); 33 return match ? match[ 1 ] : ''; 34 } 35 36 function setCookie( name, value ) { 37 document.cookie = name + '=' + value + '; path=/; max-age=' + 60 * 60 * 24 * 365 + '; SameSite=Lax' + 38 ( 'https:' === window.location.protocol ? '; Secure' : '' ); 39 } 40 41 function clearCookie( name ) { 42 document.cookie = name + '=; path=/; max-age=0; SameSite=Lax'; 43 } 44 45 function guestKey() { 46 // Sent even when logged in: the server merges a stray pre-login guest 47 // list during GET /list (registration/stale-tab paths), which is what 48 // makes clearing the cookie afterwards safe. 49 return getCookie( cfg.cookieName ); 50 } 51 52 // ââ REST helper âââââââââââââââââââââââââââââââââââââââââââââââââ 53 54 function api( path, options ) { 55 options = options || {}; 56 var headers = { 'Content-Type': 'application/json' }; 57 58 if ( cfg.loggedIn && cfg.nonce ) { 59 headers[ 'X-WP-Nonce' ] = cfg.nonce; 60 } 61 var key = guestKey(); 62 if ( key ) { 63 headers[ 'X-Bmart-Wishlist-Key' ] = key; 64 } 65 66 return window.fetch( cfg.restUrl + path, { 67 method: options.method || 'GET', 68 credentials: 'same-origin', 69 headers: headers, 70 body: options.body ? JSON.stringify( options.body ) : undefined, 71 } ).then( function ( response ) { 72 return response.json().catch( function () { 73 // Non-JSON body (edge error page, challenge) â friendly error, 74 // never a raw SyntaxError in an alert. 75 throw new Error( cfg.i18n.error ); 76 } ).then( function ( data ) { 77 if ( ! response.ok ) { 78 var error = new Error( ( data && data.message ) || cfg.i18n.error ); 79 error.code = data && data.code; 80 throw error; 81 } 82 return data; 83 } ); 84 } ); 85 } 86 87 // ââ State / storage âââââââââââââââââââââââââââââââââââââââââââââ 88 89 function hasIdentity() { 90 return cfg.loggedIn || !! guestKey(); 91 } 92 93 function setState( items, count ) { 94 state.items = items || []; 95 state.count = 'number' === typeof count ? count : state.items.length; 96 state.loaded = true; 97 98 try { 99 window.sessionStorage.setItem( STORAGE_KEY, JSON.stringify( { items: state.items, count: state.count } ) ); 100 } catch ( e ) { 101 // Storage unavailable (private mode etc.) â purely a paint optimization, ignore. 102 } 103 104 updateButtons(); 105 updateCounts(); 106 renderPage(); 107 } 108 109 function paintFromStorage() { 110 try { 111 var cached = JSON.parse( window.sessionStorage.getItem( STORAGE_KEY ) ); 112 if ( cached && cached.items ) { 113 state.items = cached.items; 114 state.count = cached.count || cached.items.length; 115 updateButtons(); 116 updateCounts(); 117 } 118 } catch ( e ) { 119 // Ignore bad cache. 120 } 121 } 122 123 function findItem( productId, variationId ) { 124 for ( var i = 0; i < state.items.length; i++ ) { 125 if ( Number( state.items[ i ].product_id ) === Number( productId ) && 126 Number( state.items[ i ].variation_id ) === Number( variationId || 0 ) ) { 127 return state.items[ i ]; 128 } 129 } 130 return null; 131 } 132 133 function findAnyByProduct( productId ) { 134 for ( var i = 0; i < state.items.length; i++ ) { 135 if ( Number( state.items[ i ].product_id ) === Number( productId ) ) { 136 return state.items[ i ]; 137 } 138 } 139 return null; 140 } 141 142 function loadList() { 143 if ( ! hasIdentity() ) { 144 // Logged-in merge already consumed the guest cookie server-side. 145 setState( [], 0 ); 146 return; 147 } 148 149 api( 'list' ).then( function ( data ) { 150 setState( data.items, data.count ); 151 if ( cfg.loggedIn && getCookie( cfg.cookieName ) ) { 152 // The key rode along on this very GET and the server merged the 153 // guest list into the account before responding â safe to retire. 154 clearCookie( cfg.cookieName ); 155 } 156 } ).catch( function () { 157 state.loaded = true; 158 renderPage(); 159 } ); 160 } 161 162 // ââ Buttons âââââââââââââââââââââââââââââââââââââââââââââââââââââ 163 164 function buttonContext( button ) {
165 var productId = Number( button.getAttribute( 'data-product-id' ) ); 166 var isVariable = '1' === button.getAttribute( 'data-variable' ); 167 var permalink = button.getAttribute( 'data-permalink' ) || ''; 168 var variationId = 0; 169 var attributes = {}; 170 var complete = true; 171 172 if ( isVariable ) { 173 var form = button.closest( 'form.variations_form' ); 174 if ( form ) { 175 var input = form.querySelector( 'input[name="variation_id"]' ); 176 variationId = input ? Number( input.value ) || 0 : 0; 177 var selects = form.querySelectorAll( 'select[name^="attribute_"]' ); 178 for ( var i = 0; i < selects.length; i++ ) { 179 if ( ! selects[ i ].value ) { 180 complete = false; 181 } 182 attributes[ selects[ i ].name ] = selects[ i ].value; 183 } 184 if ( ! variationId ) { 185 complete = false; 186 } 187 } else { 188 complete = false; 189 } 190 } 191 192 return { productId: productId, isVariable: isVariable, isLoop: !! permalink, permalink: permalink, variationId: variationId, attributes: attributes, complete: complete }; 193 } 194 195 function updateButtons() { 196 var buttons = document.querySelectorAll( '[data-bmart-wishlist-add]' ); 197 for ( var i = 0; i < buttons.length; i++ ) { 198 var ctx = buttonContext( buttons[ i ] ); 199 var added; 200 if ( ctx.isLoop && ctx.isVariable ) { 201 // Tile button for a variable product: filled when ANY saved 202 // entry matches the product; clicking navigates to the page. 203 added = !! findAnyByProduct( ctx.productId ); 204 } else if ( ctx.isVariable && ! ctx.complete ) { 205 added = false; 206 } else { 207 added = !! findItem( ctx.productId, ctx.variationId ); 208 } 209 buttons[ i ].classList.toggle( 'is-added', added ); 210 buttons[ i ].setAttribute( 'aria-pressed', added ? 'true' : 'false' ); 211 var label = buttons[ i ].querySelector( '.bmart-wishlist__label' ); 212 if ( label ) { 213 label.textContent = added ? cfg.i18n.added : cfg.i18n.add; 214 } 215 } 216 } 217 218 function updateCounts() { 219 var badges = document.querySelectorAll( '[data-bmart-wishlist-count]' ); 220 for ( var i = 0; i < badges.length; i++ ) { 221 badges[ i ].textContent = String( state.count ); 222 badges[ i ].hidden = state.count < 1; 223 } 224 } 225 226 function onButtonClick( button ) { 227 if ( ! cfg.loggedIn && ! cfg.guestEnabled ) { 228 window.location.href = cfg.accountUrl; 229 return; 230 } 231 232 var ctx = buttonContext( button ); 233 234 if ( ctx.isLoop && ctx.isVariable ) { 235 // The variation gets picked on the product page. 236 window.location.href = ctx.permalink; 237 return; 238 } 239 240 if ( ctx.isVariable && ! ctx.complete ) { 241 window.alert( cfg.i18n.selectOptions ); 242 return; 243 } 244 245 var existing = findItem( ctx.productId, ctx.variationId ); 246 button.disabled = true; 247 248 var request; 249 if ( existing ) { 250 request = api( 'items/' + existing.hash, { method: 'DELETE' } ).then( function ( data ) { 251 state.items = state.items.filter( function ( item ) { 252 return item.hash !== existing.hash; 253 } ); 254 setState( state.items, data.count ); 255 } ); 256 } else { 257 request = api( 'items', { 258 method: 'POST', 259 body: { 260 product_id: ctx.productId, 261 variation_id: ctx.variationId, 262 variation: ctx.attributes, 263 }, 264 } ).then( function ( data ) { 265 if ( data.guest_key ) { 266 setCookie( cfg.cookieName, data.guest_key ); 267 } 268 if ( data.item ) { 269 state.items.unshift( data.item ); 270 trackKlaviyo( data.item ); 271 } 272 setState( state.items, data.count ); 273 } ); 274 } 275 276 request.catch( function ( error ) { 277 if ( 'bmart_wishlist_login_required' === error.code ) { 278 window.location.href = cfg.accountUrl; 279 return; 280 } 281 window.alert( error.message || cfg.i18n.error ); 282 } ).then( function () { 283 button.disabled = false; 284 } ); 285 } 286 287 // ââ Klaviyo âââââââââââââââââââââââââââââââââââââââââââââââââââââ 288 289 function trackKlaviyo( item ) { 290 if ( ! cfg.klaviyo || 'undefined' === typeof window.klaviyo || ! window.klaviyo.push ) { 291 return; 292 } 293 window.klaviyo.push( [ 'track', 'Added to Wishlist', { 294 ProductID: item.product_id, 295 VariationID: item.variation_id || null, 296 ProductName: item.name, 297 Price: item.price || null, 298 URL: item.permalink, 299 ImageURL: item.image, 300 } ] ); 301 } 302 303 // ââ Wishlist page âââââââââââââââââââââââââââââââââââââââââââââââ 304 305 function el( tag, className, text ) { 306 var node = document.createElement( tag ); 307 if ( className ) { 308 node.className = className; 309 } 310 if ( text ) { 311 node.textContent = text; 312 } 313 return node; 314 } 315 316 function renderPage() { 317 var root = document.querySelector( '[data-bmart-wishlist-page]' ); 318 if ( ! root || ! state.loaded ) { 319 return; 320 } 321 322 root.innerHTML = ''; 323 324 if ( ! cfg.loggedIn && ! cfg.guestEnabled ) { 325 var login = el( 'p', 'bmart-wishlist__login-prompt' ); 326 var loginLink = el( 'a', '', cfg.i18n.loginRequired ); 327 loginLink.href = cfg.accountUrl; 328 login.appendChild( loginLink ); 329 root.appendChild( login ); 330 return; 331 } 332 333 if ( ! state.items.length ) { 334 var empty = el( 'div', 'bmart-wishlist__empty' ); 335 empty.appendChild( el( 'p', '', cfg.i18n.empty ) ); 336 var browse = el( 'a', 'button', cfg.i18n.browse ); 337 browse.href = cfg.shopUrl; 338 empty.appendChild( browse ); 339 root.appendChild( empty ); 340 return; 341 } 342 343 var list = el( 'div', 'bmart-wishlist__list' ); 344
345 state.items.forEach( function ( item ) { 346 var row = el( 'div', 'bmart-wishlist__row' + ( item.is_available ? '' : ' bmart-wishlist__row--unavailable' ) ); 347 348 var media = el( 'a', 'bmart-wishlist__row-image' ); 349 media.href = item.permalink || '#'; 350 if ( item.image ) { 351 var img = document.createElement( 'img' ); 352 img.src = item.image; 353 img.alt = item.name; 354 img.loading = 'lazy'; 355 media.appendChild( img ); 356 } 357 row.appendChild( media ); 358 359 var info = el( 'div', 'bmart-wishlist__row-info' ); 360 var name = el( 'a', 'bmart-wishlist__row-name', item.name ); 361 name.href = item.permalink || '#'; 362 info.appendChild( name ); 363 if ( item.variation_label ) { 364 info.appendChild( el( 'div', 'bmart-wishlist__row-variation', item.variation_label ) ); 365 } 366 var price = el( 'div', 'bmart-wishlist__row-price' ); 367 price.innerHTML = item.price_html || ''; // Server-generated by our own REST endpoint. 368 info.appendChild( price ); 369 if ( item.is_available && ! item.in_stock ) { 370 info.appendChild( el( 'div', 'bmart-wishlist__row-stock', cfg.i18n.outOfStock ) ); 371 } 372 row.appendChild( info ); 373 374 var actions = el( 'div', 'bmart-wishlist__row-actions' ); 375 if ( item.is_available ) { 376 var cart = el( 'a', 'button bmart-wishlist__row-cart', item.purchasable && item.in_stock ? cfg.i18n.addToCart : cfg.i18n.viewProduct ); 377 cart.href = item.purchasable && item.in_stock && item.add_to_cart_url ? item.add_to_cart_url : item.permalink; 378 actions.appendChild( cart ); 379 } 380 var remove = el( 'button', 'bmart-wishlist__row-remove', 'Ã' ); 381 remove.type = 'button'; 382 remove.setAttribute( 'aria-label', cfg.i18n.remove ); 383 remove.setAttribute( 'data-bmart-wishlist-remove', item.hash ); 384 actions.appendChild( remove ); 385 row.appendChild( actions ); 386 387 list.appendChild( row ); 388 } ); 389 390 root.appendChild( list ); 391 } 392 393 function onRemoveClick( button ) { 394 var hash = button.getAttribute( 'data-bmart-wishlist-remove' ); 395 button.disabled = true; 396 397 api( 'items/' + hash, { method: 'DELETE' } ).then( function ( data ) { 398 state.items = state.items.filter( function ( item ) { 399 return item.hash !== hash; 400 } ); 401 setState( state.items, data.count ); 402 } ).catch( function ( error ) { 403 button.disabled = false; 404 window.alert( error.message || cfg.i18n.error ); 405 } ); 406 } 407 408 // ââ Wiring ââââââââââââââââââââââââââââââââââââââââââââââââââââââ 409 410 // Event delegation: survives re-renders of wishlist rows. It does NOT 411 // recover an interaction that fired before this script executed â the 412 // script must stay out of Perfmatters delay (enforced in PHP via the 413 // perfmatters_delay_js_exclusions filter; verify on staging regardless). 414 document.addEventListener( 'click', function ( event ) { 415 var addButton = event.target.closest( '[data-bmart-wishlist-add]' ); 416 if ( addButton ) { 417 event.preventDefault(); 418 onButtonClick( addButton ); 419 return; 420 } 421 var removeButton = event.target.closest( '[data-bmart-wishlist-remove]' ); 422 if ( removeButton ) { 423 event.preventDefault(); 424 onRemoveClick( removeButton ); 425 } 426 } ); 427 428 // Woo's variation events are jQuery-triggered; refresh button state on change. 429 function bindVariationEvents() { 430 var $ = window.jQuery; 431 if ( ! $ ) { 432 return; 433 } 434 $( document.body ).on( 'found_variation reset_data hide_variation', 'form.variations_form', function () { 435 updateButtons(); 436 } ); 437 } 438 439 // Admins-only mode ships every wishlist element hidden (markup identical 440 // for all visitors â shared page/fragment caches); this script only loads 441 // for users allowed to see them, so reveal here. 442 function revealGated() { 443 var gated = document.querySelectorAll( '[data-bmart-wishlist-gated]' ); 444 for ( var i = 0; i < gated.length; i++ ) { 445 gated[ i ].hidden = false; 446 gated[ i ].style.display = ''; 447 } 448 } 449 450 function init() { 451 revealGated(); 452 bindVariationEvents(); 453 paintFromStorage(); 454 loadList(); 455 } 456 457 if ( 'loading' === document.readyState ) { 458 document.addEventListener( 'DOMContentLoaded', init ); 459 } else { 460 init(); 461 } 462 463 // Back/forward cache restore: state may be stale â refetch. 464 window.addEventListener( 'pageshow', function ( event ) { 465 if ( event.persisted ) { 466 loadList(); 467 } 468 } ); 469} )();
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.