1class clickToBuy { 2 constructor() { 3 this.structure = document.querySelector('.sn_click_to_buy'); 4 this.siteWrapper = document.querySelector('.sn_site_wrapper'); 5 6 this.markets = window.CTB_MARKETS || []; 7 this.products = window.CTB_PRODUCTS || []; 8 this.retailers = window.CTB_RETAILERS || {}; 9 this.onlineRetailers = window.CTB_ONLINE_RETAILERS_DATA || []; 10 this.selectedLocalization = window.CTB_FIRST_MARKET_INT || null; //NULL only if it's not international, at the beginning. 11 12 this.dynamicOnlineRetailers = []; 13 14 this.isStrictLocalization = this.markets.length === 1 && this.markets[0].code !== 'en'; // true single market, false international 15 16 this.marketFilterWrapper = document.querySelectorAll('.ctb_market_filter_wrapper'); 17 this.productFilterWrapper = document.querySelector('.ctb_product_filter_wrapper'); 18 19 this.marketSelects = document.querySelectorAll('.ctb__market_selector'); 20 this.productSelects = document.querySelectorAll('.ctb__product_selector'); 21 22 this.selectedMarket = null; 23 this.selectedProduct = null; 24 25 this.lastProdIDFromCTA = 0; 26 27 this.currentLoc = this.structure?.getAttribute('data-lang') || null; 28 this.first = true; 29 } 30 31 async fetchOnlineProducts() { 32 if (this.isStrictLocalization || !this.selectedLocalization) return; 33 34 const cacheKey = this.selectedLocalization; 35 36 try { 37 let data = null; 38 const cached = false; 39 40 if (cached) { 41 data = JSON.parse(cached); 42 } else { 43 let url = '/wp-json/wdf-data/v1/online-products'; 44 url += `?localization=${encodeURIComponent(this.selectedLocalization)}`; 45 46 const response = await fetch(url, { 47 credentials: 'same-origin' 48 }); 49 50 if (!response.ok) { 51 throw new Error(`HTTP ${response.status}`); 52 } 53 54 data = await response.json(); 55 56 this.products = data.products || []; 57 this.retailers = data.retailers || {}; 58 this.onlineRetailers = data.online_retailers_data || []; 59 60 sessionStorage.setItem( 61 'ctb_online_' + cacheKey, 62 JSON.stringify(data) 63 ); 64 } 65 66 this.dynamicOnlineRetailers = data.online_retailers_data || []; 67 68 } catch (error) { 69 console.error('[C2B FETCH ERROR]', error); 70 } 71 } 72 73 async start(first = true) { 74 this.first = first; 75 76 if (!this.structure) return; 77 78 if (this.isStrictLocalization) { 79 this.marketFilterWrapper.forEach(div => { 80 div.style.display = 'none'; 81 }); 82 83 this.selectedMarket = this.markets[0]; 84 } 85 else { 86 if (!this.selectedLocalization && this.markets.length > 0) { 87 this.selectedLocalization = this.markets[0].code; 88 } 89 await this.fetchOnlineProducts(); 90 } 91 92 this.buildMarketSelects(); 93 this.buildProductSelects(); 94 95 if (first) { 96 this.addEventListeners(); 97 } 98 } 99 100 buildMarketSelects() { 101 this.marketSelects.forEach(select => this.buildMarketSelect(select)); 102 } 103 104 buildMarketSelect(select) { 105 const menu = select.querySelector('.sn_click_to_buy_select_menu_in') 106 const label = select.querySelector('.sn_click_to_buy_select_label'); 107 108 menu.innerHTML = ''; 109 110 if (!this.selectedMarket) { 111 if (!this.isStrictLocalization && this.selectedLocalization) { 112 const match = this.markets.find( 113 m => m.code === this.selectedLocalization 114 ); 115 116 if (match) { 117 this.selectedMarket = match;
118 this.selectedLocalization = match.code; 119 } 120 } 121 } 122 123 this.markets.forEach((market, index) => { 124 if(market.code != 'int'){ 125 const el = document.createElement('div'); 126 el.className = 'sn_click_to_buy_select_menu_i'; 127 el.dataset.code = market.code; 128 el.innerText = market.country.label ?? market.label; 129 130 el.addEventListener('click', async () => { 131 this.selectedMarket = market; 132 this.selectedLocalization = market.code; 133 134 this.selectedProduct = null; 135 label.innerText = market.country.label ?? market.label; 136 label.dataset.code = market.code; 137 138 select.querySelector('.sn_click_to_buy_select_menu').classList.add('ctb_hide'); 139 140 if (!this.isStrictLocalization) { 141 await this.fetchOnlineProducts(); 142 } 143 144 this.buildProductSelects(); 145 this.populateRetailers(); 146 this.triggerEvent(select); 147 }); 148 149 menu.appendChild(el); 150 151 if (!this.selectedMarket && index === 0) { 152 this.selectedMarket = market; 153 this.selectedLocalization = market.code; 154 label.innerText = market.country.label ?? market.label; 155 label.dataset.code = market.code; 156 } 157 } 158 }); 159 160 if (this.selectedMarket) { 161 label.innerText = this.selectedMarket.country.label ?? this.selectedMarket.label; 162 label.dataset.code = this.selectedMarket.code; 163 } 164 165 this.triggerEvent(select); 166 } 167 168 /* ============================ 169 PRODUCT SELECT 170 ============================ */ 171 buildProductSelects() { 172 this.productSelects.forEach(select => this.buildProductSelect(select)); 173 } 174 175 buildProductSelect(select) { 176 const menu = select.querySelector('.sn_click_to_buy_select_menu_in'); 177 const label = select.querySelector('.sn_click_to_buy_select_label'); 178 179 const labelAllProducts = this.productFilterWrapper?.getAttribute('data-product-label-all') ?? 'All'; 180 181 menu.innerHTML = ''; 182 183 this.selectedProduct = null; 184 185 label.innerText = labelAllProducts; 186 label.dataset.id = '0'; 187 188 const all = document.createElement('div'); 189 all.className = 'sn_click_to_buy_select_menu_i'; 190 all.innerText = labelAllProducts; 191 all.dataset.id = '0'; 192 193 all.addEventListener('click', () => { 194 this.selectedProduct = null; 195 label.innerText = labelAllProducts; 196 label.dataset.id = '0'; 197 select.querySelector('.sn_click_to_buy_select_menu').classList.add('ctb_hide'); 198 this.populateRetailers(); 199 }); 200 201 menu.appendChild(all); 202 203 this.products 204 .filter(p => 205 !this.selectedMarket?.code || 206 p.localizations?.includes(this.selectedMarket.code) 207 ) 208 .forEach(product => { 209 if(product.sap == -1){ 210 return; 211 } 212 const el = document.createElement('div'); 213 el.className = 'sn_click_to_buy_select_menu_i'; 214 el.innerText = product.title; 215 el.dataset.id = product.id; 216 217 el.addEventListener('click', () => { 218 this.selectedProduct = product; 219 label.innerText = product.title; 220 label.dataset.id = product.id; 221 222 select.querySelector('.sn_click_to_buy_select_menu').classList.add('ctb_hide'); 223 this.populateRetailers(); 224 }); 225 226 menu.appendChild(el); 227 }); 228 229 if (this.lastProdIDFromCTA != 0) { 230 231 const product = this.products.find( 232 p => String(p.id) === String(this.lastProdIDFromCTA) 233 ); 234 235 if (product) { 236 this.selectedProduct = product; 237 label.innerText = product.title; 238 label.dataset.id = product.id; 239 240 select 241 .querySelector('.sn_click_to_buy_select_menu') 242 ?.classList.add('ctb_hide'); 243 244 this.populateRetailers(); 245 } 246 247 } else { 248 this.populateRetailers(); 249 } 250 } 251 252 /* ============================ 253 RETAILERS 254 ============================ */ 255 retailerMatchesFilters(retailer, productId = null) { 256 if (this.selectedMarket?.code) { 257 if (!retailer.localizations?.includes(this.selectedMarket.code)) { 258 return false; 259 } 260 } 261 262 if (productId && this.selectedProduct) { 263 return productId === this.selectedProduct.id; 264 } 265 266 return true; 267 } 268 269 populateRetailers() { 270 const wrapper = this.structure.querySelector('.retailer_wrapper'); 271 let html = ''; 272 273 const marketCode = this.selectedMarket?.code || null; 274 if (!this.selectedProduct) { 275 const allRetailersSource = (!this.isStrictLocalization && this.dynamicOnlineRetailers.length) 276 ? this.dynamicOnlineRetailers 277 : this.onlineRetailers; 278 279 allRetailersSource.forEach(retailer => { 280 if ( 281 marketCode && 282 (!retailer.localizations || !retailer.localizations.includes(marketCode)) 283 ) { 284 return; 285 } 286 287 html += this.buildRetailerCard(retailer, false); 288 }); 289 290 } else { 291 // CASE PRODUCT FILTERED 292 293 let productRetailers = []; 294 295 if (this.retailers[this.selectedProduct.id]) { 296 productRetailers = this.retailers[this.selectedProduct.id]; 297 } 298
299 productRetailers.forEach(retailer => { 300 if ( 301 marketCode && 302 (!retailer.localizations || !retailer.localizations.includes(marketCode)) 303 ) { 304 return; 305 } 306 307 html += this.buildRetailerCard(retailer, true); 308 }); 309 } 310 311 wrapper.innerHTML = html || '<p class="ctb_empty">No retailers available for this market</p>'; 312 313 wrapper.querySelectorAll('body:not(.coachella-lp) .ctb_retailer_cta a').forEach(a => { 314 a.addEventListener('click', () => { 315 316 const marketName = this.selectedMarket?.country?.label ?? this.selectedMarket.label; 317 window.dispatchEvent(new CustomEvent('sn:bb-event', { 318 detail: { 319 event: 'buy_now', 320 action_name: 'click-on-retailer', 321 bbname: 'click-to-buy', 322 bbcontext: 'drawer', 323 content: a.innerText, 324 language: this.currentLoc, 325 retailer: a.closest('.ctb_retailer') 326 ?.querySelector('.ctb_retailer_name')?.innerText, 327 product: this.selectedProduct?.title || 'All', 328 market: marketName || '', 329 destination: a.href 330 331 } 332 })); 333 }); 334 }); 335 } 336 337 buildRetailerCard(retailer, hasProductLink = true) { 338 const wrapper = this.structure.querySelector('.retailer_wrapper'); 339 const ctaLabel = wrapper.getAttribute('data-cta-label') ?? "Buy now"; 340 const link = hasProductLink && retailer.product_url 341 ? retailer.product_url 342 : retailer.website || retailer.product_url; 343 344 return ` 345 <div class="ctb_retailer"> 346 ${retailer.logo ? ` 347 <div class="ctb_retailer_logo"> 348 <img src="${retailer.logo}" alt="${retailer.name}"> 349 </div>` : ''} 350 <div class="ctb_retailer_action"> 351 <div class="ctb_retailer_name">${retailer.name}</div> 352 <div class="ctb_retailer_cta"> 353 <a href="${link}" target="_blank"> 354 ${ctaLabel} 355 </a> 356 </div> 357 </div> 358 </div>`; 359 } 360 361 /* ============================ 362 OPEN / CLOSE 363 ============================ */ 364 open() { 365 this.siteWrapper?.classList.add('_blur'); 366 this.structure.classList.add('_open'); 367 } 368 369 close() { 370 this.siteWrapper?.classList.remove('_blur'); 371 this.structure.classList.remove('_open'); 372 localStorage.removeItem('ctb_product'); 373 } 374 375 /* ============================ 376 EVENTS 377 ============================ */ 378 addEventListeners() { 379 document.querySelectorAll('[data-click-to-buy-cta]').forEach(el => { 380 381 el.addEventListener('click', e => { 382 e.preventDefault(); 383 this.lastProdIDFromCTA = el.getAttribute('ctb-product-id'); 384 let bbcontext = e.currentTarget.getAttribute('data-bbcontext') ?? 'blocks'; 385 let actionName = 'open-drawer-from-page'; 386 387 if(bbcontext == 'blocks'){ 388 bbcontext = 'product-page'; 389 } 390 else if(bbcontext == 'header'){ 391 actionName = 'open-drawer-from-header'; 392 } 393 const ctaText = el.innerText.trim(); 394 this.start(false); 395 this.open(); 396 if (!document.body.classList.contains('coachella-lp')) { 397 if (el.getAttribute('data-gtma-click') === 'click-to-buy-cta-party-planner') { 398 var peopleValue = el.getAttribute('data-people-value') || ''; 399 var perPersonValue = el.getAttribute('data-per-person-value') || ''; 400 window.dispatchEvent(new CustomEvent('sn:bb-event', { 401 detail: {
402 'event': 'buy_now', 403 'action_name': 'open-drawer-from-party-planner', 404 'bbname': 'click-to-buy-cta', 405 'bbcontext': 'drawer', 406 'language': this.currentLoc, 407 'param1': peopleValue, 408 'param2': perPersonValue 409 } 410 })); 411 } else { 412 413 if(this.lastProdIDFromCTA == 0){ 414 window.dispatchEvent(new CustomEvent('sn:bb-event', { 415 detail: { 416 event: 'buy_now', 417 action_name: actionName, 418 bbname: 'click-to-buy-cta', 419 bbcontext: bbcontext, 420 language: this.currentLoc, 421 event: "click_cta", 422 destination: "script", 423 content: ctaText, 424 is_custom_event: false 425 } 426 })); 427 } 428 else{ 429 window.dispatchEvent(new CustomEvent('sn:bb-event', { 430 detail: { 431 event: 'buy_now', 432 action_name: actionName, 433 bbname: 'click-to-buy-cta', 434 bbcontext: bbcontext, 435 language: this.currentLoc, 436 event: "buy_now", 437 destination: "script", 438 content: ctaText 439 } 440 })); 441 } 442 } 443 } 444 }); 445 }); 446 447 document.querySelectorAll('.sn_click_to_buy_close').forEach(el => 448 el.addEventListener('click', () => this.close()) 449 ); 450 451 this.marketSelects.forEach(select => { 452 select.querySelector('.sn_click_to_buy_select_toggle') 453 .addEventListener('click', () => 454 select.querySelector('.sn_click_to_buy_select_menu').classList.toggle('ctb_hide') 455 ); 456 }); 457 458 this.productSelects.forEach(select => { 459 select.querySelector('.sn_click_to_buy_select_toggle') 460 .addEventListener('click', () => 461 select.querySelector('.sn_click_to_buy_select_menu').classList.toggle('ctb_hide') 462 ); 463 }); 464 } 465 466 triggerEvent(el) { 467 el.dispatchEvent(new Event('change')); 468 } 469 470 checkCtbParameters() { 471 return new URLSearchParams(window.location.search).get('ctb') === 'true'; 472 } 473} 474 475/* ============================ 476 START 477============================ */ 478const ctb = new clickToBuy(); 479ctb.start(true);
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.