1/** 2 * Promo Banner Frontend JavaScript 3 * 4 * This file handles all frontend functionality for the promotional banners 5 * including animations, interactions, and tracking. 6 */ 7 8(function($) { 9 'use strict'; 10 11 // Promo Banner Class 12 class PromoBanner { 13 constructor(element) { 14 this.element = element; 15 this.bannerId = element.data('banner-id'); 16 this.position = element.data('position'); 17 this.isExpanded = false; 18 this.isAnimating = false; 19 20 this.init(); 21 } 22 23 init() { 24 this.bindEvents(); 25 this.setupAccessibility(); 26 27 // Show the banner first, then handle expand by default 28 this.show(); 29 } 30 31 bindEvents() { 32 const self = this; 33 34 // Toggle banner 35 this.element.find('.promo-banner-toggle').on('click', function(e) { 36 e.preventDefault(); 37 self.toggle(); 38 }); 39 40 // Close banner 41 this.element.find('.promo-banner-close').on('click', function(e) { 42 e.preventDefault(); 43 self.close(); 44 }); 45 46 // Close on outside click 47 $(document).on('click', function(e) { 48 if (!$(e.target).closest('.promo-banner').length) { 49 self.close(); 50 } 51 }); 52 53 // Close on escape key 54 $(document).on('keydown', function(e) { 55 if (e.keyCode === 27) { // ESC key 56 self.close(); 57 } 58 }); 59 60 // Handle window resize 61 $(window).on('resize', function() { 62 self.handleResize(); 63 }); 64 } 65 66 setupAccessibility() { 67 // Set ARIA attributes 68 this.element.attr('role', 'dialog'); 69 this.element.attr('aria-label', 'Promotional Banner'); 70 this.element.attr('aria-hidden', 'true'); 71 72 // Set focus management 73 this.element.find('.promo-banner-toggle').attr('aria-expanded', 'false'); 74 this.element.find('.promo-banner-toggle').attr('aria-controls', 'promo-banner-content-' + this.bannerId); 75 76 // Create content ID for ARIA 77 this.element.find('.promo-banner-content').attr('id', 'promo-banner-content-' + this.bannerId); 78 } 79 80 handleExpandByDefault() { 81 const expandByDefault = this.element.data('expand-by-default') === 'yes' || this.element.data('expand-by-default') === true; 82 const delay = this.element.data('delay') || 0; 83 const scrollPercentage = this.element.data('scroll-percentage') || 0; 84 85 // Only auto-expand if no other triggers are set and expand by default is enabled 86 if (expandByDefault && delay === 0 && scrollPercentage === 0) { 87 // For expand by default banners, clear any previous user closed cookie 88 // so they always expand on page load 89 this.clearCookie('promo_banner_' + this.bannerId + '_user_closed'); 90 91 // Handle expansion based on device type 92 setTimeout(() => { 93 if (this.isMobile()) { 94 // On mobile, show the modal instead of expanding the banner 95 this.showMobileModal(); 96 } else { 97 // On desktop, expand the banner normally 98 this.expand(); 99 } 100 }, 100); // Small delay to ensure smooth animation 101 } 102 } 103 104 setupDelay() { 105 const delay = this.element.data('delay') || 0; 106 const expandByDefault = this.element.data('expand-by-default') === 'yes'; 107 108 if (delay > 0) { 109 setTimeout(() => { 110 this.show(); 111 // If expand by default is enabled, handle expansion based on device type 112 if (expandByDefault) { 113 setTimeout(() => { 114 if (this.isMobile()) { 115 // On mobile, show the modal instead of expanding the banner 116 this.showMobileModal(); 117 } else { 118 // On desktop, expand the banner normally 119 this.expand(); 120 } 121 }, 100); // Small delay to ensure smooth animation 122 } 123 }, delay * 1000); 124 } else { 125 this.show(); 126 // If expand by default is enabled, handle expansion based on device type 127 if (expandByDefault) { 128 setTimeout(() => { 129 if (this.isMobile()) { 130 // On mobile, show the modal instead of expanding the banner 131 this.showMobileModal(); 132 } else { 133 // On desktop, expand the banner normally 134 this.expand(); 135 } 136 }, 100); // Small delay to ensure smooth animation 137 } 138 } 139 } 140 141 show() { 142 if (this.isExpanded || this.isAnimating) return; 143 144 this.element.addClass('promo-banner-visible'); 145 this.element.attr('aria-hidden', 'false'); 146 147 // Handle expand by default functionality 148 this.handleExpandByDefault(); 149 } 150 151 toggle() { 152 if (this.isAnimating) return; 153 154 // Check if mobile 155 if (this.isMobile()) { 156 this.showMobileModal(); 157 } else { 158 if (this.isExpanded) { 159 this.close(); 160 } else { 161 this.expand(); 162 } 163 } 164 } 165 166 // Check if mobile 167 isMobile() { 168 return $(window).width() <= 768; 169 } 170 171 // Show mobile modal 172 showMobileModal() { 173 const modalId = '#' + this.element.attr('id') + '-modal'; 174 const $modal = $(modalId); 175 176 if ($modal.length) { 177 $modal.addClass('active'); 178 $('body').addClass('modal-open'); 179 180 // Bind modal close events if not already bound 181 this.bindModalEvents($modal); 182 } 183 } 184 185 // Bind modal events 186 bindModalEvents($modal) { 187 const self = this; 188 189 // Close modal on close button click 190 $modal.find('.promo-banner-close').off('click.modal').on('click.modal', function(e) { 191 e.preventDefault(); 192 self.closeMobileModal($modal); 193 }); 194 195 // Close modal on overlay click 196 $modal.off('click.modal').on('click.modal', function(e) { 197 if (e.target === this) { 198 self.closeMobileModal($modal); 199 } 200 }); 201 202 // Close modal on escape key 203 $(document).off('keydown.modal').on('keydown.modal', function(e) { 204 if (e.keyCode === 27) { // ESC key 205 self.closeMobileModal($modal); 206 } 207 }); 208 } 209 210 // Close mobile modal 211 closeMobileModal($modal) { 212 $modal.removeClass('active'); 213 $('body').removeClass('modal-open'); 214 215 // Remember user preference if modal was shown by default 216 const expandByDefault = this.element.data('expand-by-default') === 'yes'; 217 if (expandByDefault) { 218 this.setCookie('promo_banner_' + this.bannerId + '_user_closed', 'true', 1); // Remember for 1 day 219 } 220 } 221 222 expand() { 223 if (this.isExpanded || this.isAnimating) { 224 return; 225 } 226 227 this.isAnimating = true; 228 this.element.addClass('promo-banner-expanded'); 229 this.element.find('.promo-banner-toggle').attr('aria-expanded', 'true'); 230 231 // Update aria-hidden when expanded 232 this.element.attr('aria-hidden', 'false'); 233 234 // Focus management 235 setTimeout(() => { 236 this.element.find('.promo-banner-content').focus(); 237 }, 300); 238 239 this.isExpanded = true; 240 this.isAnimating = false;
241 } 242 243 close() { 244 if (!this.isExpanded || this.isAnimating) return; 245 246 this.isAnimating = true; 247 this.element.removeClass('promo-banner-expanded'); 248 this.element.find('.promo-banner-toggle').attr('aria-expanded', 'false'); 249 250 // Remember user preference if banner was expanded by default 251 const expandByDefault = this.element.data('expand-by-default') === 'yes'; 252 if (expandByDefault) { 253 this.setCookie('promo_banner_' + this.bannerId + '_user_closed', 'true', 1); // Remember for 1 day 254 } 255 256 // Focus management - blur the button first, then set aria-hidden 257 this.element.find('.promo-banner-toggle').blur(); 258 259 setTimeout(() => { 260 // Update aria-hidden after focus is removed 261 this.element.attr('aria-hidden', 'true'); 262 this.isExpanded = false; 263 this.isAnimating = false; 264 }, 100); // Small delay to ensure blur completes 265 } 266 267 handleResize() { 268 // Note: Position classes are now maintained across all screen sizes 269 // CSS media queries handle responsive positioning without removing position classes 270
271 // Close mobile modal if switching to desktop 272 if (!this.isMobile()) { 273 const modalId = '#' + this.element.attr('id') + '-modal'; 274 const $modal = $(modalId); 275 if ($modal.hasClass('active')) { 276 this.closeMobileModal($modal); 277 278 // If expand by default is enabled and we're switching to desktop, expand the banner 279 const expandByDefault = this.element.data('expand-by-default') === 'yes'; 280 const userClosed = this.getCookie('promo_banner_' + this.bannerId + '_user_closed'); 281 if (expandByDefault && !userClosed && this.element.hasClass('promo-banner-visible')) { 282 setTimeout(() => { 283 this.expand(); 284 }, 100); 285 } 286 } 287 } else { 288 // If switching to mobile and expand by default is enabled, show modal instead 289 const expandByDefault = this.element.data('expand-by-default') === 'yes'; 290 const userClosed = this.getCookie('promo_banner_' + this.bannerId + '_user_closed'); 291 if (expandByDefault && !userClosed && this.element.hasClass('promo-banner-visible') && this.isExpanded) { 292 this.close(); // Close the expanded banner 293 setTimeout(() => { 294 this.showMobileModal(); // Show the modal instead 295 }, 100); 296 } 297 } 298 } 299 300 getCookie(name) { 301 const nameEQ = name + '='; 302 const ca = document.cookie.split(';'); 303 for (let i = 0; i < ca.length; i++) { 304 let c = ca[i]; 305 while (c.charAt(0) === ' ') c = c.substring(1, c.length); 306 if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length); 307 } 308 return null; 309 } 310 311 setCookie(name, value, days) { 312 let expires = ''; 313 if (days) { 314 const date = new Date(); 315 date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); 316 expires = '; expires=' + date.toUTCString(); 317 } 318 document.cookie = name + '=' + (value || '') + expires + '; path=/'; 319 } 320 321 clearCookie(name) { 322 document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; 323 } 324 } 325 326 // Initialize banners when DOM is ready 327 $(document).ready(function() { 328 // Initialize all promo banners 329 $('.promo-banner').each(function() { 330 const $banner = $(this); 331 332 // Check if this banner already has the PromoBanner class functionality 333 // by looking for the data attributes that the PromoBanner class sets 334 if ($banner.data('banner-id') && $banner.find('.promo-banner-toggle').length) { 335 // Use the new PromoBanner class 336 new PromoBanner($banner); 337 } else { 338 // Use legacy functionality for backwards compatibility 339 const $toggle = $banner.find('.promo-banner-toggle'); 340 const $modal = $('#' + $banner.attr('id') + '-modal'); 341 const $modalClose = $modal.find('.promo-banner-close'); 342 343 // Check if mobile 344 function isMobile() { 345 return $(window).width() <= 768; 346 } 347 348 // Toggle banner 349 $toggle.on('click.legacy', function(e) { 350 e.preventDefault(); 351 352 if (isMobile()) { 353 // Show modal on mobile 354 $modal.addClass('active'); 355 $('body').addClass('modal-open'); 356 } else { 357 // Show sliding content on desktop 358 $banner.toggleClass('promo-banner-expanded'); 359 } 360 }); 361 362 // Close modal (mobile) 363 $modalClose.on('click.legacy', function(e) { 364 e.preventDefault(); 365 $modal.removeClass('active'); 366 $('body').removeClass('modal-open'); 367 }); 368 369 // Close on outside click (desktop) 370 $(document).on('click.legacy', function(e) { 371 if (!$(e.target).closest('.promo-banner').length) { 372 $banner.removeClass('promo-banner-expanded'); 373 } 374 }); 375 376 // Close modal on overlay click (mobile) 377 $modal.on('click.legacy', function(e) { 378 if (e.target === this) { 379 $modal.removeClass('active'); 380 $('body').removeClass('modal-open'); 381 } 382 }); 383 384 // Close on escape key 385 $(document).on('keydown.legacy', function(e) { 386 if (e.keyCode === 27) { // ESC key 387 if (isMobile() && $modal.hasClass('active')) { 388 // Close modal on mobile 389 $modal.removeClass('active'); 390 $('body').removeClass('modal-open'); 391 } else { 392 // Close banner on desktop 393 $banner.removeClass('promo-banner-expanded'); 394 } 395 } 396 }); 397 398 // Handle window resize - close modal if switching to de
398sktop 399 $(window).on('resize.legacy', function() { 400 if (!isMobile() && $modal.hasClass('active')) { 401 $modal.removeClass('active'); 402 $('body').removeClass('modal-open'); 403 } 404 }); 405 } 406 }); 407 408 // Handle dynamic content loading 409 if (typeof MutationObserver !== 'undefined') { 410 const observer = new MutationObserver(function(mutations) { 411 mutations.forEach(function(mutation) { 412 if (mutation.type === 'childList') { 413 mutation.addedNodes.forEach(function(node) { 414 if (node.nodeType === 1 && $(node).hasClass('promo-banner')) { 415 const $banner = $(node); 416 417 // Check if this banner already has the PromoBanner class functionality 418 if ($banner.data('banner-id') && $banner.find('.promo-banner-toggle').length) { 419 // Use the new PromoBanner class 420 new PromoBanner($banner); 421 } 422 // Note: Legacy functionality for dynamically added content 423 // would need to be handled separately if needed 424 } 425 }); 426 } 427 }); 428 }); 429 430 observer.observe(document.body, { 431 childList: true, 432 subtree: true 433 }); 434 } 435 }); 436 437 // Utility functions 438 window.PromoBanner = { 439 // Public method to show banner by ID 440 show: function(bannerId) { 441 const banner = $('#promo-banner-' + bannerId); 442 if (banner.length) { 443 const promoBanner = banner.data('promo-banner-instance'); 444 if (promoBanner) { 445 promoBanner.expand(); 446 } 447 } 448 }, 449 450 // Public method to hide banner by ID 451 hide: function(bannerId) { 452 const banner = $('#promo-banner-' + bannerId); 453 if (banner.length) { 454 const promoBanner = banner.data('promo-banner-instance'); 455 if (promoBanner) { 456 promoBanner.close(); 457 } 458 } 459 }, 460 461 // Public method to get banner statistics (placeholder for future implementation) 462 getStats: function(bannerId) { 463 // This function is reserved for future analytics implementation 464 return null; 465 } 466 }; 467 468})(jQuery);
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.