1/* eslint-disable max-len */ 2/* eslint-disable func-names */ 3/* eslint-disable no-use-before-define */ 4/* eslint-disable no-nested-ternary */ 5/* eslint-disable no-param-reassign */ 6// Silktide Consent Manager - https://silktide.com/consent-manager/ 7 8class SilktideCookieBanner { 9 constructor(config) { 10 this.config = config; // Save config to the instance 11 12 this.wrapper = null; 13 this.banner = null; 14 this.modal = null; 15 this.cookieIcon = null; 16 this.backdrop = null; 17 18 this.createWrapper(); 19 20 if (this.shouldShowBackdrop()) { 21 this.createBackdrop(); 22 } 23 24 this.createCookieIcon(); 25 this.createModal(); 26 27 if (this.shouldShowBanner()) { 28 this.createBanner(); 29 this.showBackdrop(); 30 } else { 31 this.showCookieIcon(); 32 } 33 34 this.setupEventListeners(); 35 36 if (this.hasSetInitialCookieChoices()) { 37 this.loadRequiredCookies(); 38 this.runAcceptedCookieCallbacks(); 39 } 40 } 41 42 destroyCookieBanner() { 43 // Remove all cookie banner elements from the DOM 44 if (this.wrapper && this.wrapper.parentNode) { 45 this.wrapper.parentNode.removeChild(this.wrapper); 46 } 47 48 // Restore scrolling 49 this.allowBodyScroll(); 50 51 // Clear all references 52 this.wrapper = null; 53 this.banner = null; 54 this.modal = null; 55 this.cookieIcon = null; 56 this.backdrop = null; 57 } 58 59 // ---------------------------------------------------------------- 60 // Wrapper 61 // ---------------------------------------------------------------- 62 createWrapper() { 63 this.wrapper = document.createElement('div'); 64 this.wrapper.id = 'silktide-wrapper'; 65 document.body.insertBefore(this.wrapper, document.body.firstChild); 66 } 67 68 // ---------------------------------------------------------------- 69 // Wrapper Child Generator 70 // ---------------------------------------------------------------- 71 createWrapperChild(htmlContent, id) { 72 // Create child element 73 const child = document.createElement('div'); 74 child.id = id; 75 child.innerHTML = htmlContent; 76 77 // Ensure wrapper exists 78 if (!this.wrapper || !document.body.contains(this.wrapper)) { 79 this.createWrapper(); 80 } 81 82 // Append child to wrapper 83 this.wrapper.appendChild(child); 84 return child; 85 } 86 87 // ---------------------------------------------------------------- 88 // Backdrop 89 // ---------------------------------------------------------------- 90 createBackdrop() { 91 this.backdrop = this.createWrapperChild(null, 'silktide-backdrop'); 92 } 93 94 showBackdrop() { 95 if (this.backdrop) { 96 this.backdrop.style.display = 'block'; 97 }
98 // Trigger optional onBackdropOpen callback 99 if (typeof this.config.onBackdropOpen === 'function') { 100 this.config.onBackdropOpen(); 101 } 102 } 103 104 hideBackdrop() { 105 if (this.backdrop) { 106 this.backdrop.style.display = 'none'; 107 } 108 109 // Trigger optional onBackdropClose callback 110 if (typeof this.config.onBackdropClose === 'function') { 111 this.config.onBackdropClose(); 112 } 113 } 114 115 shouldShowBackdrop() { 116 return this.config?.background?.showBackground || false; 117 } 118 119 // update the checkboxes in the modal with the values from localStorage 120 updateCheckboxState(saveToStorage = false) { 121 const preferencesSection = this.modal.querySelector('#cookie-preferences'); 122 const checkboxes = preferencesSection.querySelectorAll('input[type="checkbox"]'); 123 124 checkboxes.forEach((checkbox) => { 125 const [, cookieId] = checkbox.id.split('cookies-'); 126 const cookieType = this.config.cookieTypes.find(type => type.id === cookieId); 127 128 if (!cookieType) return; 129 130 if (saveToStorage) { 131 // Save the current state to localStorage and run callbacks 132 const currentState = checkbox.checked; 133 134 if (cookieType.required) { 135 localStorage.setItem( 136 `silktideCookieChoice_${cookieId}${this.getBannerSuffix()}`, 137 'true', 138 ); 139 } else { 140 localStorage.setItem( 141 `silktideCookieChoice_${cookieId}${this.getBannerSuffix()}`, 142 currentState.toString(), 143 ); 144 145 // Run appropriate callback 146 if (currentState && typeof cookieType.onAccept === 'function') { 147 cookieType.onAccept(); 148 } else if (!currentState && typeof cookieType.onReject === 'function') { 149 cookieType.onReject(); 150 } 151 } 152 } else if (cookieType.required) { 153 checkbox.checked = true; 154 checkbox.disabled = true; 155 } else { 156 const storedValue = localStorage.getItem( 157 `silktideCookieChoice_${cookieId}${this.getBannerSuffix()}`, 158 ); 159 160 if (storedValue !== null) { 161 checkbox.checked = storedValue === 'true'; 162 } else { 163 checkbox.checked = !!cookieType.defaultValue; 164 } 165 } 166 }); 167 } 168 169 setInitialCookieChoiceMade() { 170 window.localStorage.setItem(`silktideCookieBanner_InitialChoice${this.getBannerSuffix()}`, 1); 171 } 172 173 // ---------------------------------------------------------------- 174 // Consent Handling 175 // ---------------------------------------------------------------- 176 handleCookieChoice(accepted) { 177 // We set that an initial choice was made regardless of what it was so we don't show the banner again 178 this.setInitialCookieChoiceMade(); 179 180 this.removeBanner(); 181 this.hideBackdrop(); 182 this.toggleModal(false); 183 this.showCookieIcon(); 184 185 this.config.cookieTypes.forEach((type) => { 186 // Set localStorage and run accept/reject callbacks 187 if (type.required === true) { 188 localStorage.setItem(`silktideCookieChoice_${type.id}${this.getBannerSuffix()}`, 'true'); 189 if (typeof type.onAccept === 'function') { type.onAccept(); } 190 } else { 191 localStorage.setItem( 192 `silktideCookieChoice_${type.id}${this.getBannerSuffix()}`, 193 accepted.toString(), 194 ); 195 196 if (accepted) { 197 if (typeof type.onAccept === 'function') { type.onAccept(); } 198 } else if (typeof type.onReject === 'function') { type.onReject(); } 199 } 200 }); 201 202 // Trigger optional onAcceptAll/onRejectAll callbacks 203 if (accepted && typeof this.config.onAcceptAll === 'function') { 204 if (typeof this.config.onAcceptAll === 'function') { this.config.onAcceptAll(); } 205 } else if (typeof this.config.onRejectAll === 'function') { 206 if (typeof this.config.onRejectAll === 'function') { this.config.onRejectAll(); } 207 } 208 209 // finally update the checkboxes in the modal with the values from localStorage 210 this.updateCheckboxState(); 211 } 212 213 getAcceptedCookies() { 214 return (this.config.cookieTypes || []).reduce((acc, cookieType) => { 215 acc[cookieType.id] = localStorage.getItem(`silktideCookieChoice_${cookieType.id}${this.getBannerSuffix()}`) 216 === 'true'; 217 return acc; 218 }, {}); 219 } 220 221 runAcceptedCookieCallbacks() { 222 if (!this.config.cookieTypes) return; 223 224 const acceptedCookies = this.getAcceptedCookies(); 225 this.config.cookieTypes.forEach((type) => { 226 if (type.required) return; // we run required cookies separately in loadRequiredCookies
227 if (acceptedCookies[type.id] && typeof type.onAccept === 'function') { 228 if (typeof type.onAccept === 'function') { type.onAccept(); } 229 } 230 }); 231 } 232 233 runRejectedCookieCallbacks() { 234 if (!this.config.cookieTypes) return; 235 236 const rejectedCookies = this.getRejectedCookies(); 237 this.config.cookieTypes.forEach((type) => { 238 if (rejectedCookies[type.id] && typeof type.onReject === 'function') { 239 if (typeof type.onReject === 'function') { type.onReject(); } 240 } 241 }); 242 } 243 244 /** 245 * Run through all of the cookie callbacks based on the current localStorage values 246 */ 247 runStoredCookiePreferenceCallbacks() { 248 this.config.cookieTypes.forEach((type) => { 249 const accepted = localStorage.getItem(`silktideCookieChoice_${type.id}${this.getBannerSuffix()}`) === 'true'; 250 // Set localStorage and run accept/reject callbacks 251 if (accepted) { 252 if (typeof type.onAccept === 'function') { type.onAccept(); } 253 } else if (typeof type.onReject === 'function') { type.onReject(); } 254 }); 255 } 256 257 loadRequiredCookies() { 258 if (!this.config.cookieTypes) return;
259 this.config.cookieTypes.forEach((cookie) => { 260 if (cookie.required && typeof cookie.onAccept === 'function') { 261 if (typeof cookie.onAccept === 'function') { cookie.onAccept(); } 262 } 263 }); 264 } 265 266 // ---------------------------------------------------------------- 267 // Banner 268 // ---------------------------------------------------------------- 269 getBannerContent() { 270 const bannerDescription = this.config.text?.banner?.description 271 || '<p>We use cookies on our site to enhance your user experience, provide personalized content, and analyze our traffic.</p>'; 272 273 // Accept button 274 const acceptAllButtonText = this.config.text?.banner?.acceptAllButtonText || 'Accept all'; 275 const acceptAllButtonLabel = this.config.text?.banner?.acceptAllButtonAccessibleLabel; 276 const acceptAllButton = `<button class="accept-all st-button st-button--primary"${ 277 acceptAllButtonLabel && acceptAllButtonLabel !== acceptAllButtonText 278 ? ` aria-label="${acceptAllButtonLabel}"` 279 : '' 280 }>${acceptAllButtonText}</button>`; 281 282 // Reject button 283 const rejectNonEssentialButtonText = this.config.text?.banner?.rejectNonEssentialButtonText || 'Reject non-essential'; 284 const rejectNonEssentialButtonLabel = this.config.text?.banner?.rejectNonEssentialButtonAccessibleLabel; 285 const rejectNonEssentialButton = `<button class="reject-all st-button st-button--primary"${ 286 rejectNonEssentialButtonLabel && rejectNonEssentialButtonLabel !== rejectNonEssentialButtonText 287 ? ` aria-label="${rejectNonEssentialButtonLabel}"` 288 : '' 289 }>${rejectNonEssentialButtonText}</button>`; 290 291 // Preferences button 292 const preferencesButtonText = this.config.text?.banner?.preferencesButtonText || 'Preferences'; 293 const preferencesButtonLabel = this.config.text?.banner?.preferencesButtonAccessibleLabel; 294 const preferencesButton = `<button class="preferences"${ 295 preferencesButtonLabel && preferencesButtonLabel !== preferencesButtonText 296 ? ` aria-label="${preferencesButtonLabel}"` 297 : '' 298 }><span>${preferencesButtonText}</span></button>`; 299 300 const bannerContent = ` 301 ${bannerDescription} 302 <div class="actions"> 303 ${acceptAllButton} 304 ${rejectNonEssentialButton} 305 <div class="actions-row"> 306 ${preferencesButton} 307 </div> 308 </div> 309 `; 310 311 return bannerContent; 312 } 313 314 hasSetInitialCookieChoices() { 315 return !!localStorage.getItem(`silktideCookieBanner_InitialChoice${this.getBannerSuffix()}`); 316 } 317 318 createBanner() { 319 // Create banner element 320 this.banner = this.createWrapperChild(this.getBannerContent(), 'silktide-banner'); 321 322 // Add positioning class from config 323 if (this.banner && this.config.position?.banner) { 324 this.banner.classList.add(this.config.position.banner); 325 } 326 327 // Trigger optional onBannerOpen callback 328 if (this.banner && typeof this.config.onBannerOpen === 'function') { 329 this.config.onBannerOpen(); 330 } 331 } 332 333 removeBanner() { 334 if (this.banner && this.banner.parentNode) { 335 this.banner.parentNode.removeChild(this.banner); 336 this.banner = null; 337 338 // Trigger optional onBannerClose callback 339 if (typeof this.config.onBannerClose === 'function') { 340 this.config.onBannerClose(); 341 } 342 } 343 } 344 345 shouldShowBanner() { 346 if (this.config.showBanner === false) { 347 return false; 348 } 349 return ( 350 localStorage.getItem(`silktideCookieBanner_InitialChoice${this.getBannerSuffix()}`) === null 351 ); 352 } 353 354 // ---------------------------------------------------------------- 355 // Modal 356 // ---------------------------------------------------------------- 357 getModalContent() { 358 const preferencesTitle = this.config.text?.preferences?.title || 'Customize your cookie preferences'; 359 360 const preferencesDescription = this.config.text?.preferences?.description 361 || '<p>We respect your right to privacy. You can choose not to allow some types of cookies. Your cookie preferences will apply across our website.</p>'; 362 363 // Preferences button 364 const preferencesButtonLabel = this.config.text?.banner?.preferencesButtonAccessibleLabel; 365 366 const closeModalButton = `<button class="modal-close"${preferencesButtonLabel ? ` aria-label="${preferencesButtonLabel}"` : ''}> 367 <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> 368 <path d="M19.4081 3.41559C20.189 2.6347 20.189 1.36655 19.4081 0.585663C18.6272 -0.195221 17.3591 -0.195221 16.5782 0.585663L10 7.17008L3.41559 0.59191C2.6347 -0.188974 1.36655 -0.188974 0.585663 0.59191C-0.195221 1.37279 -0.195221 2.64095 0.585663 3.42183L7.17008 10L0.59191 16.5844C-0.188974 17.3653 -0.188974 18.6335 0.59191 19.4143C1.37279 20.1952 2.64095 20.1952 3.42183 19.4143L10 12.8299L16.5844 19.4081C17.3653 20.189 18.6335 20.189 19.4143 19.4081C20.1952 18.6272 20.1952 17.3591 19.4143 16.5782L12.8299 10L19.4081 3.41559Z"/> 369 </svg> 370 </button>`; 371 372 const cookieTypes = this.config.cookieTypes || []; 373 const acceptedCookieMap = this.getAcceptedCookies(); 374 375 // Accept button 376 const acceptAllButtonText = this.config.text?.banner?.acceptAllButtonText || 'Accept all'; 377 const acceptAllButtonLabel = this.config.text?.banner?.acceptAllButtonAccessibleLabel; 378 const acceptAllButton = `<button class="preferences-accept-all st-button st-button--primary"${ 379 acceptAllButtonLabel && acceptAllButtonLabel !== acceptAllButtonText 380 ? ` aria-label="${acceptAllButtonLabel}"` 381 : '' 382 }>${acceptAllButtonText}</button>`; 383 384 // Reject button 385 const rejectNonEssentialButtonText = this.config.text?.banner?.rejectNonEssentialButtonText || 'Reject non-e
385ssential'; 386 const rejectNonEssentialButtonLabel = this.config.text?.banner?.rejectNonEssentialButtonAccessibleLabel; 387 const rejectNonEssentialButton = `<button class="preferences-reject-all st-button st-button--primary"${ 388 rejectNonEssentialButtonLabel && rejectNonEssentialButtonLabel !== rejectNonEssentialButtonText 389 ? ` aria-label="${rejectNonEssentialButtonLabel}"` 390 : '' 391 }>${rejectNonEssentialButtonText}</button>`; 392 393 const modalContent = ` 394 <header> 395 <h1>${preferencesTitle}</h1> 396 ${closeModalButton} 397 </header> 398 ${preferencesDescription} 399 <section id="cookie-preferences"> 400 ${cookieTypes 401 .map((type) => { 402 const accepted = acceptedCookieMap[type.id]; 403 let isChecked = false; 404 405 // if it's accepted then show as checked 406 if (accepted) { 407 isChecked = true; 408 } 409 410 // if nothing has been accepted / rejected yet, then show as checked if the default value is true 411 if (!accepted && !this.hasSetInitialCookieChoices()) { 412 isChecked = type.defaultValue; 413 } 414 415 return ` 416 <fieldset> 417 <legend>${type.name}</legend> 418 <div class="cookie-type-content"> 419 <div class="cookie-type-description">${type.description}</div> 420 <label class="switch" for="cookies-${type.id}"> 421 <input type="checkbox" id="cookies-${type.id}" ${ 422 type.required ? 'checked disabled' : isChecked ? 'checked' : '' 423} /> 424 <span class="switch__pill" aria-hidden="true"></span> 425 <span class="switch__dot" aria-hidden="true"></span> 426 <span class="switch__off" aria-hidden="true">Off</span> 427 <span class="switch__on" aria-hidden="true">On</span> 428 </label> 429 </div> 430 </fieldset> 431 `; 432 }) 433 .join('')} 434 </section> 435 <footer> 436 ${acceptAllButton} 437 ${rejectNonEssentialButton} 438 </footer> 439 `; 440 441 return modalContent; 442 } 443 444 createModal() { 445 // Create banner element 446 this.modal = this.createWrapperChild(this.getModalContent(), 'silktide-modal'); 447 } 448 449 toggleModal(show) { 450 if (!this.modal) return; 451 452 this.modal.style.display = show ? 'flex' : 'none'; 453 454 if (show) { 455 this.showBackdrop(); 456 this.hideCookieIcon(); 457 this.removeBanner(); 458 this.preventBodyScroll(); 459 460 // Focus the close button 461 const modalCloseButton = this.modal.querySelector('.modal-close'); 462 modalCloseButton.focus(); 463 464 // Trigger optional onPreferencesOpen callback 465 if (typeof this.config.onPreferencesOpen === 'function') { 466 this.config.onPreferencesOpen(); 467 } 468 469 this.updateCheckboxState(false); // read from storage when opening 470 } else { 471 // Set that an initial choice was made when closing the modal 472 this.setInitialCookieChoiceMade(); 473 474 // Save current checkbox states to storage 475 this.updateCheckboxState(true); 476 477 this.hideBackdrop(); 478 this.showCookieIcon(); 479 this.allowBodyScroll(); 480 481 // Trigger optional onPreferencesClose callback 482 if (typeof this.config.onPreferencesClose === 'function') { 483 this.config.onPreferencesClose(); 484 } 485 } 486 } 487 488 // ---------------------------------------------------------------- 489 // Cookie Icon 490 // ---------------------------------------------------------------- 491 getCookieIconContent() { 492 return ` 493 <svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg"> 494 <path d="M19.1172 1.15625C19.0547 0.734374 18.7344 0.390624 18.3125 0.328124C16.5859 0.0859365 14.8281 0.398437 13.2813 1.21875L7.5 4.30469C5.96094 5.125 4.71875 6.41406 3.95313 7.98437L1.08594 13.8906C0.320314 15.4609 0.0703136 17.2422 0.375001 18.9609L1.50781 25.4297C1.8125 27.1562 2.64844 28.7344 3.90625 29.9531L8.61719 34.5156C9.875 35.7344 11.4766 36.5156 13.2031 36.7578L19.6875 37.6719C21.4141 37.9141 23.1719 37.6016 24.7188 36.7812L30.5 33.6953C32.0391 32.875 33.2813 31.5859 34.0469 30.0078L36.9141 24.1094C37.6797 22.5391 37.9297 20.7578 37.625 19.0391C37.5547 18.625 37.2109 18.3125 36.7969 18.25C32.7734 17.6094 29.5469 14.5703 28.6328 10.6406C28.4922 10.0469 28.0078 9.59375 27.4063 9.5C23.1406 8.82031 19.7734 5.4375 19.1094 1.15625H19.1172ZM15.25 10.25C15.913 10.25 16.5489 10.5134 17.0178 10.9822C17.4866 11.4511 17.75 12.087 17.75 12.75C17.75 13.413 17.4866 14.0489 17.0178 14.5178C16.5489 14.9866 15.913 15.25 15.25 15.25C14.587 15.25 13.9511 14.9866 13.4822 14.5178C13.0134 14.0489 12.75 13.413 12.75 12.75C12.75 12.087 13.0134 11.4511 13.4822 10.9822C13.9511 10.5134 14.587 10.25 15.25 10.25ZM10.25 25.25C10.25 24.587 10.5134 23.9511 10.9822 23.4822C11.4511 23.0134 12.087 22.75 12.75 22.75C13.413 22.75 14.0489 23.0134 14.5178 23.4822C14.9866 23.9511 15.25 24.587 15.25 25.25C15.25 25.913 14.9866 26.5489 14.5178 27.0178C14.0489 27.4866 13.413 27.75 12.75 27.75C12.087 27.75 11.4511 27.4866 10.9822 27.0178C10.5134 26.5489 10.25 25.913 10.25 25.25ZM27.75 20.25C28.413 20.25 29.0489 20.5134 29.5178 20.9822C29.9866 21.4511 30.25 22.087 30.25 22.75C30.25 23.413 29.9866 24.0489 29.5178 24.5178C29.0489 24.9866 28.413 25.25 27.75 25.25C27.087 25.25 26.4511 24.9866 25.9822 24.5178C25.5134 24.0489 25.25 23.413 25.25 22.75C25.25 22.087 25.5134 21.4511 25.9822 20.9822C26.4511 20.5134 27.087 20.25 27.75 20.25Z" /> 495 </svg> 496 `; 497 } 498 499 createCookieIcon() { 500 this.cookieIcon = document.createElement('button'); 501 this.cookieIcon.id = 'silktide-cookie-icon'; 502 this.cookieIcon.title = 'Manage your cookie preferences for this site'; 503 this.cookieIcon.innerHTML = this.getCookieIconContent(); 504 505 if (this.config.text?.banner?.preferencesButtonAccessibleLabel) { 506 this.cookieIcon.ariaLabel = this.config.text?.banner?.preferencesButtonAccessibleLabel; 507 } 508 509 // Ensure wrapper exists 510 if (!this.wrapper || !document.body.contains(this.wrapper)) { 511 this.createWrapper(); 512 } 513 514 // Append child to wrapper 515 this.wrapper.appendChild(this.cookieIcon); 516 517 // Add positioning class from config 518 if (this.cookieIcon && this.config.cookieIcon?.position) { 519 this.cookieIcon.classList.add(this.config.cookieIcon.position); 520 } 521 522 // Add color scheme class from config 523 if (this.cookieIcon && this.config.cookieIcon?.colorScheme) { 524 this.cookieIcon.classList.add(this.config.cookieIcon.colorScheme); 525 } 526 } 527 528 showCookieIcon() { 529 if (this.cookieIcon) { 530 this.cookieIcon.style.display = 'flex'; 531 } 532 } 533 534 hideCookieIcon() { 535 if (this.cookieIcon) { 536 this.cookieIcon.style.display = 'none'; 537 } 538 } 539
540 /** 541 * This runs if the user closes the modal without making a choice for the first time 542 * We apply the default values and the necessary values as default 543 */ 544 handleClosedWithNoChoice() { 545 this.config.cookieTypes.forEach((type) => { 546 let accepted = true; 547 // Set localStorage and run accept/reject callbacks 548 if (type.required === true || type.defaultValue) { 549 localStorage.setItem( 550 `silktideCookieChoice_${type.id}${this.getBannerSuffix()}`, 551 accepted.toString(), 552 ); 553 } else { 554 accepted = false; 555 localStorage.setItem( 556 `silktideCookieChoice_${type.id}${this.getBannerSuffix()}`, 557 accepted.toString(), 558 ); 559 } 560 561 if (accepted) { 562 if (typeof type.onAccept === 'function') { type.onAccept(); } 563 } else if (typeof type.onReject === 'function') { type.onReject(); } 564 // set the flag to say that the cookie choice has been made 565 this.setInitialCookieChoiceMade(); 566 this.updateCheckboxState(); 567 }); 568 } 569 570 // ---------------------------------------------------------------- 571 // Focusable Elements 572 // ---------------------------------------------------------------- 573 getFocusableElements(element) { 574 return element.querySelectorAll( 575 'button, a[href], input, select, textarea, [tabindex]:not([tabindex="-1"])', 576 ); 577 } 578 579 // ---------------------------------------------------------------- 580 // Event Listeners 581 // ---------------------------------------------------------------- 582 setupEventListeners() { 583 // Check Banner exists before trying to add event listeners 584 if (this.banner) { 585 // Get the buttons 586 const acceptButton = this.banner.querySelector('.accept-all'); 587 const rejectButton = this.banner.querySelector('.reject-all'); 588 const preferencesButton = this.banner.querySelector('.preferences'); 589 590 // Add event listeners to the buttons 591 acceptButton?.addEventListener('click', () => this.handleCookieChoice(true)); 592 rejectButton?.addEventListener('click', () => this.handleCookieChoice(false)); 593 preferencesButton?.addEventListener('click', () => { 594 this.showBackdrop(); 595 this.toggleModal(true); 596 }); 597 598 // Focus Trap 599 const focusableElements = this.getFocusableElements(this.banner); 600 const firstFocusableEl = focusableElements[0]; 601 const lastFocusableEl = focusableElements[focusableElements.length - 1]; 602 603 // Add keydown event listener to handle tab navigation 604 this.banner.addEventListener('keydown', (e) => { 605 if (e.key === 'Tab') { 606 if (e.shiftKey) { 607 if (document.activeElement === firstFocusableEl) {
608 lastFocusableEl.focus(); 609 e.preventDefault(); 610 } 611 } else if (document.activeElement === lastFocusableEl) { 612 firstFocusableEl.focus(); 613 e.preventDefault(); 614 } 615 } 616 }); 617 618 // Set initial focus 619 if (this.config.mode !== 'wizard') { 620 acceptButton?.focus(); 621 } 622 } 623 624 // Check Modal exists before trying to add event listeners 625 if (this.modal) { 626 const closeButton = this.modal.querySelector('.modal-close'); 627 const acceptAllButton = this.modal.querySelector('.preferences-accept-all'); 628 const rejectAllButton = this.modal.querySelector('.preferences-reject-all'); 629 630 closeButton?.addEventListener('click', () => { 631 this.toggleModal(false); 632 633 const hasMadeFirstChoice = this.hasSetInitialCookieChoices(); 634 635 if (hasMadeFirstChoice) { 636 // run through the callbacks based on the current localStorage state 637 this.runStoredCookiePreferenceCallbacks(); 638 } else { 639 // handle the case where the user closes without making a choice for the first time 640 this.handleClosedWithNoChoice(); 641 } 642 }); 643 acceptAllButton?.addEventListener('click', () => this.handleCookieChoice(true)); 644 rejectAllButton?.addEventListener('click', () => this.handleCookieChoice(false)); 645 646 // Banner Focus Trap 647 const focusableElements = this.getFocusableElements(this.modal); 648 const firstFocusableEl = focusableElements[0]; 649 const lastFocusableEl = focusableElements[focusableElements.length - 1]; 650 651 this.modal.addEventListener('keydown', (e) => { 652 if (e.key === 'Tab') { 653 if (e.shiftKey) { 654 if (document.activeElement === firstFocusableEl) {
655 lastFocusableEl.focus(); 656 e.preventDefault(); 657 } 658 } else if (document.activeElement === lastFocusableEl) { 659 firstFocusableEl.focus(); 660 e.preventDefault(); 661 } 662 } 663 if (e.key === 'Escape') { 664 this.toggleModal(false); 665 } 666 }); 667 668 closeButton?.focus(); 669 670 // Update the checkbox event listeners 671 const preferencesSection = this.modal.querySelector('#cookie-preferences'); 672 const checkboxes = preferencesSection.querySelectorAll('input[type="checkbox"]'); 673 674 checkboxes.forEach((checkbox) => { 675 checkbox.addEventListener('change', (event) => { 676 const [, cookieId] = event.target.id.split('cookies-'); 677 const isAccepted = event.target.checked; 678 const previousValue = localStorage.getItem( 679 `silktideCookieChoice_${cookieId}${this.getBannerSuffix()}`, 680 ) === 'true'; 681 682 // Only proceed if the value has actually changed 683 if (isAccepted !== previousValue) { 684 // Find the corresponding cookie type 685 const cookieType = this.config.cookieTypes.find(type => type.id === cookieId); 686 687 if (cookieType) { 688 // Update localStorage 689 localStorage.setItem( 690 `silktideCookieChoice_${cookieId}${this.getBannerSuffix()}`, 691 isAccepted.toString(), 692 ); 693 694 // Run the appropriate callback only if the value changed 695 if (isAccepted && typeof cookieType.onAccept === 'function') { 696 cookieType.onAccept(); 697 } else if (!isAccepted && typeof cookieType.onReject === 'function') { 698 cookieType.onReject(); 699 } 700 } 701 } 702 }); 703 }); 704 } 705 706 // Check Cookie Icon exists before trying to add event listeners 707 if (this.cookieIcon) { 708 this.cookieIcon.addEventListener('click', () => { 709 // If modal is not found, create it 710 if (!this.modal) { 711 this.createModal(); 712 this.toggleModal(true); 713 this.hideCookieIcon(); 714 } else if (this.modal.style.display === 'none' || this.modal.style.display === '') { 715 this.toggleModal(true); 716 this.hideCookieIcon(); 717 } else { 718 this.toggleModal(false); 719 } 720 }); 721 } 722 } 723 724 openModal() { 725 if (!this.modal) { 726 this.createModal(); 727 this.toggleModal(true); 728 this.hideCookieIcon(); 729 } else if (this.modal.style.display === 'none' || this.modal.style.display === '') { 730 this.toggleModal(true); 731 this.hideCookieIcon(); 732 } else { 733 this.toggleModal(false); 734 } 735 } 736 737 getBannerSuffix() { 738 if (this.config.bannerSuffix) { 739 return `_${this.config.bannerSuffix}`; 740 } 741 return ''; 742 } 743 744 preventBodyScroll() { 745 document.body.style.overflow = 'hidden'; 746 // Prevent iOS Safari scrolling 747 document.body.style.position = 'fixed'; 748 document.body.style.width = '100%'; 749 } 750 751 allowBodyScroll() { 752 document.body.style.overflow = ''; 753 document.body.style.position = ''; 754 document.body.style.width = ''; 755 } 756} 757 758(function () { 759 window.silktideCookieBannerManager = {}; 760 761 let config = {}; 762 let cookieBanner; 763 764 function updateCookieBannerConfig(userConfig = {}) { 765 config = { ...config, ...userConfig }; 766 767 // If cookie banner exists, destroy and recreate it with new config 768 if (cookieBanner) { 769 cookieBanner.destroyCookieBanner(); // We'll need to add this method 770 cookieBanner = null; 771 } 772 773 // Only initialize if document.body exists 774 if (document.body) { 775 initCookieBanner(); 776 } else { 777 // Wait for DOM to be ready 778 document.addEventListener('DOMContentLoaded', initCookieBanner, { once: true }); 779 } 780 } 781 782 function initCookieBanner() { 783 if (!cookieBanner) { 784 cookieBanner = new SilktideCookieBanner(config); // Pass config to the CookieBanner instance 785 window.silktideCookieBannerManager.cookieBanner = cookieBanner; 786 } 787 } 788 789 function injectScript(url, loadOption) { 790 // Check if script with this URL already exists 791 const existingScript = document.querySelector(`script[src="${url}"]`); 792 if (existingScript) { 793 return; // Script already exists, don't add it again 794 } 795 796 const script = document.createElement('script'); 797 script.src = url; 798 799 // Apply the async or defer attribute based on the loadOption parameter 800 if (loadOption === 'async') { 801 script.async = true; 802 } else if (loadOption === 'defer') { 803 script.defer = true; 804 } 805 806 document.head.appendChild(script); 807 } 808 809 window.silktideCookieBannerManager.initCookieBanner = initCookieBanner; 810 window.silktideCookieBannerManager.updateCookieBannerConfig = updateCookieBannerConfig; 811 window.silktideCookieBannerManager.injectScript = injectScript; 812 813 if (document.readyState === 'loading') { 814 document.addEventListener('DOMContentLoaded', initCookieBanner, { once: true }); 815 } else { 816 initCookieBanner(); 817 } 818}());
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.