1// Silktide Consent Manager - https://silktide.com/consent-manager/ 2 3class SilktideCookieBanner { 4 constructor(config) { 5 this.config = config; // Save config to the instance 6 7 this.wrapper = null; 8 this.banner = null; 9 this.modal = null; 10 this.cookieIcon = null; 11 this.backdrop = null; 12 13 this.createWrapper(); 14 15 if (this.shouldShowBackdrop()) { 16 this.createBackdrop(); 17 } 18 19 this.createCookieIcon(); 20 this.createModal(); 21 22 if (this.shouldShowBanner()) { 23 this.createBanner(); 24 this.showBackdrop(); 25 } else { 26 this.showCookieIcon(); 27 } 28 29 this.setupEventListeners(); 30 31 if (this.hasSetInitialCookieChoices()) { 32 this.loadRequiredCookies(); 33 this.runAcceptedCookieCallbacks(); 34 } 35 } 36 37 destroyCookieBanner() { 38 // Remove all cookie banner elements from the DOM 39 if (this.wrapper && this.wrapper.parentNode) { 40 this.wrapper.parentNode.removeChild(this.wrapper); 41 } 42 43 // Restore scrolling 44 this.allowBodyScroll(); 45 46 // Clear all references 47 this.wrapper = null; 48 this.banner = null; 49 this.modal = null; 50 this.cookieIcon = null; 51 this.backdrop = null; 52 } 53 54 // ---------------------------------------------------------------- 55 // Wrapper 56 // ---------------------------------------------------------------- 57 createWrapper() { 58 this.wrapper = document.createElement("div"); 59 this.wrapper.id = "silktide-wrapper"; 60 document.body.insertBefore(this.wrapper, document.body.firstChild); 61 } 62 63 // ---------------------------------------------------------------- 64 // Wrapper Child Generator 65 // ---------------------------------------------------------------- 66 createWrapperChild(htmlContent, id) { 67 // Create child element 68 const child = document.createElement("div"); 69 child.id = id; 70 child.innerHTML = htmlContent; 71 72 // Ensure wrapper exists 73 if (!this.wrapper || !document.body.contains(this.wrapper)) { 74 this.createWrapper(); 75 } 76 77 // Append child to wrapper 78 this.wrapper.appendChild(child); 79 return child; 80 } 81 82 // ---------------------------------------------------------------- 83 // Backdrop 84 // ---------------------------------------------------------------- 85 createBackdrop() { 86 this.backdrop = this.createWrapperChild(null, "silktide-backdrop"); 87 } 88 89 showBackdrop() { 90 if (this.backdrop) { 91 this.backdrop.style.display = "block"; 92 }
93 // Trigger optional onBackdropOpen callback 94 if (typeof this.config.onBackdropOpen === "function") { 95 this.config.onBackdropOpen(); 96 } 97 } 98 99 hideBackdrop() { 100 if (this.backdrop) { 101 this.backdrop.style.display = "none"; 102 } 103 104 // Trigger optional onBackdropClose callback 105 if (typeof this.config.onBackdropClose === "function") { 106 this.config.onBackdropClose(); 107 } 108 } 109 110 shouldShowBackdrop() { 111 return this.config?.background?.showBackground || false; 112 } 113 114 // update the checkboxes in the modal with the values from localStorage 115 updateCheckboxState(saveToStorage = false) { 116 const preferencesSection = this.modal.querySelector("#cookie-preferences"); 117 const checkboxes = preferencesSection.querySelectorAll('input[type="checkbox"]'); 118 119 checkboxes.forEach((checkbox) => { 120 const [, cookieId] = checkbox.id.split("cookies-"); 121 const cookieType = this.config.cookieTypes.find((type) => type.id === cookieId); 122 123 if (!cookieType) return; 124 125 if (saveToStorage) { 126 // Save the current state to localStorage and run callbacks 127 const currentState = checkbox.checked; 128 129 if (cookieType.required) { 130 localStorage.setItem(`silktideCookieChoice_${cookieId}${this.getBannerSuffix()}`, "true"); 131 } else { 132 localStorage.setItem( 133 `silktideCookieChoice_${cookieId}${this.getBannerSuffix()}`, 134 currentState.toString() 135 ); 136 137 // Run appropriate callback 138 if (currentState && typeof cookieType.onAccept === "function") { 139 cookieType.onAccept(); 140 } else if (!currentState && typeof cookieType.onReject === "function") { 141 cookieType.onReject(); 142 } 143 } 144 } else { 145 // When reading values (opening modal) 146 if (cookieType.required) { 147 checkbox.checked = true; 148 checkbox.disabled = true; 149 } else { 150 const storedValue = localStorage.getItem( 151 `silktideCookieChoice_${cookieId}${this.getBannerSuffix()}` 152 ); 153 154 if (storedValue !== null) { 155 checkbox.checked = storedValue === "true"; 156 } else { 157 checkbox.checked = !!cookieType.defaultValue; 158 } 159 } 160 } 161 }); 162 } 163 164 setInitialCookieChoiceMade() { 165 window.localStorage.setItem(`silktideCookieBanner_InitialChoice${this.getBannerSuffix()}`, 1); 166 } 167 168 // ---------------------------------------------------------------- 169 // Consent Handling 170 // ---------------------------------------------------------------- 171 handleCookieChoice(accepted) { 172 // We set that an initial choice was made regardless of what it was so we don't show the banner again 173 this.setInitialCookieChoiceMade(); 174 175 this.removeBanner(); 176 this.hideBackdrop(); 177 this.toggleModal(false); 178 this.showCookieIcon(); 179 180 this.config.cookieTypes.forEach((type) => { 181 // Set localStorage and run accept/reject callbacks 182 if (type.required == true) { 183 localStorage.setItem(`silktideCookieChoice_${type.id}${this.getBannerSuffix()}`, "true"); 184 if (typeof type.onAccept === "function") { 185 type.onAccept(); 186 } 187 } else { 188 localStorage.setItem( 189 `silktideCookieChoice_${type.id}${this.getBannerSuffix()}`, 190 accepted.toString() 191 ); 192 193 if (accepted) { 194 if (typeof type.onAccept === "function") { 195 type.onAccept(); 196 } 197 } else { 198 if (typeof type.onReject === "function") { 199 type.onReject(); 200 } 201 } 202 } 203 }); 204 205 // Trigger optional onAcceptAll/onRejectAll callbacks 206 if (accepted && typeof this.config.onAcceptAll === "function") { 207 if (typeof this.config.onAcceptAll === "function") { 208 this.config.onAcceptAll(); 209 } 210 } else if (typeof this.config.onRejectAll === "function") { 211 if (typeof this.config.onRejectAll === "function") { 212 this.config.onRejectAll(); 213 } 214 } 215 216 // finally update the checkboxes in the modal with the values from localStorage 217 this.updateCheckboxState(); 218 } 219 220 getAcceptedCookies() { 221 return (this.config.cookieTypes || []).reduce((acc, cookieType) => { 222 acc[cookieType.id] = 223 localStorage.getItem(`silktideCookieChoice_${cookieType.id}${this.getBannerSuffix()}`) === 224 "true"; 225 return acc; 226 }, {}); 227 } 228 229 runAcceptedCookieCallbacks() { 230 if (!this.config.cookieTypes) return; 231 232 const acceptedCookies = this.getAcceptedCookies();
233 this.config.cookieTypes.forEach((type) => { 234 if (type.required) return; // we run required cookies separately in loadRequiredCookies 235 if (acceptedCookies[type.id] && typeof type.onAccept === "function") { 236 if (typeof type.onAccept === "function") { 237 type.onAccept(); 238 } 239 } 240 }); 241 } 242 243 runRejectedCookieCallbacks() { 244 if (!this.config.cookieTypes) return; 245 246 const rejectedCookies = this.getRejectedCookies(); 247 this.config.cookieTypes.forEach((type) => { 248 if (rejectedCookies[type.id] && typeof type.onReject === "function") { 249 if (typeof type.onReject === "function") { 250 type.onReject(); 251 } 252 } 253 }); 254 } 255 256 /** 257 * Run through all of the cookie callbacks based on the current localStorage values 258 */ 259 runStoredCookiePreferenceCallbacks() { 260 this.config.cookieTypes.forEach((type) => { 261 const accepted = 262 localStorage.getItem(`silktideCookieChoice_${type.id}${this.getBannerSuffix()}`) === "true"; 263 // Set localStorage and run accept/reject callbacks 264 if (accepted) { 265 if (typeof type.onAccept === "function") { 266 type.onAccept(); 267 } 268 } else { 269 if (typeof type.onReject === "function") { 270 type.onReject(); 271 } 272 } 273 }); 274 } 275 276 loadRequiredCookies() { 277 if (!this.config.cookieTypes) return;
278 this.config.cookieTypes.forEach((cookie) => { 279 if (cookie.required && typeof cookie.onAccept === "function") { 280 if (typeof cookie.onAccept === "function") { 281 cookie.onAccept(); 282 } 283 } 284 }); 285 } 286 287 // ---------------------------------------------------------------- 288 // Banner 289 // ---------------------------------------------------------------- 290 getBannerContent() { 291 const bannerDescription = 292 this.config.text?.banner?.description || 293 `We use cookies on our site to enhance your user experience, provide personalized content, and analyze our traffic.`; 294 295 // Accept button 296 const acceptAllButtonText = this.config.text?.banner?.acceptAllButtonText || "Accept all"; 297 const acceptAllButtonLabel = this.config.text?.banner?.acceptAllButtonAccessibleLabel; 298 const acceptAllButton = `<button class="accept-all st-button st-button--primary"${ 299 acceptAllButtonLabel && acceptAllButtonLabel !== acceptAllButtonText 300 ? ` aria-label="${acceptAllButtonLabel}"` 301 : "" 302 }>${acceptAllButtonText}</button>`; 303 304 // Reject button 305 const rejectNonEssentialButtonText = 306 this.config.text?.banner?.rejectNonEssentialButtonText || "Reject non-essential"; 307 const rejectNonEssentialButtonLabel = 308 this.config.text?.banner?.rejectNonEssentialButtonAccessibleLabel; 309 const rejectNonEssentialButton = `<button class="reject-all st-button st-button--primary"${ 310 rejectNonEssentialButtonLabel && 311 rejectNonEssentialButtonLabel !== rejectNonEssentialButtonText 312 ? ` aria-label="${rejectNonEssentialButtonLabel}"` 313 : "" 314 }>${rejectNonEssentialButtonText}</button>`; 315 316 // Preferences button 317 const preferencesButtonText = this.config.text?.banner?.preferencesButtonText || "Preferences"; 318 const preferencesButtonLabel = this.config.text?.banner?.preferencesButtonAccessibleLabel; 319 const preferencesButton = `<button class="preferences"${ 320 preferencesButtonLabel && preferencesButtonLabel !== preferencesButtonText 321 ? ` aria-label="${preferencesButtonLabel}"` 322 : "" 323 }><span>${preferencesButtonText}</span></button>`; 324 325 // Silktide logo link 326 const silktideLogo = ` 327 <a class="silktide-logo" href="https://silktide.com/consent-manager" aria-label="Visit the Silktide Consent Manager page"> 328 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="25" viewBox="0 0 24 25" fill="inherit"> 329 <path fill-rule="evenodd" clip-rule="evenodd" d="M14.1096 16.7745C13.8895 17.2055 13.3537 17.3805 12.9129 17.1653L8.28443 14.9055L2.73192 17.7651L11.1025 21.9814C11.909 22.3876 12.8725 22.3591 13.6524 21.9058L20.4345 17.9645C21.2845 17.4704 21.7797 16.5522 21.7164 15.5872L21.7088 15.4704C21.6487 14.5561 21.0962 13.7419 20.2579 13.3326L15.6793 11.0972L10.2283 13.9045L13.71 15.6043C14.1507 15.8195 14.3297 16.3434 14.1096 16.7745ZM8.2627 12.9448L13.7136 10.1375L10.2889 8.46543C9.84803 8.25021 9.66911 7.72629 9.88916 7.29524C10.1093 6.86417 10.6451 6.68921 11.0859 6.90442L15.6575 9.13647L21.2171 6.27325L12.8808 2.03496C12.0675 1.62147 11.0928 1.65154 10.3078 2.11432L3.54908 6.09869C2.70732 6.59492 2.21846 7.50845 2.28139 8.46761L2.29003 8.59923C2.35002 9.51362 2.9026 10.3278 3.7409 10.7371L8.2627 12.9448ZM6.31884 13.9458L2.94386 12.2981C1.53727 11.6113 0.610092 10.2451 0.509431 8.71094L0.500795 8.57933C0.3952 6.96993 1.21547 5.4371 2.62787 4.60447L9.38662 0.620092C10.7038 -0.156419 12.3392 -0.206861 13.7039 0.486938L23.3799 5.40639C23.4551 5.44459 23.5224 5.4918 23.5811 5.54596C23.7105 5.62499 23.8209 5.73754 23.897 5.87906C24.1266 6.30534 23.9594 6.83293 23.5234 7.05744L17.6231 10.0961L21.0549 11.7716C22.4615 12.4583 23.3887 13.8245 23.4893 15.3587L23.497 15.4755C23.6033 17.0947 22.7724 18.6354 21.346 19.4644L14.5639 23.4057C13.2554 24.1661 11.6386 24.214 10.2854 23.5324L0.621855 18.6649C0.477299 18.592 0.361696 18.4859 0.279794 18.361C0.210188 18.2968 0.150054 18.2204 0.10296 18.133C-0.126635 17.7067 0.0406445 17.1792 0.47659 16.9546L6.31884 13.9458Z" fill="inherit"/> 330 </svg> 331 </a> 332 `; 333 334 const bannerContent = ` 335 ${bannerDescription} 336 <div class="actions"> 337 ${acceptAllButton} 338 ${rejectNonEssentialButton} 339 <div class="actions-row"> 340 ${preferencesButton} 341 ${silktideLogo} 342 </div> 343 </div> 344 `; 345 346 return bannerContent; 347 } 348 349 hasSetInitialCookieChoices() { 350 return !!localStorage.getItem(`silktideCookieBanner_InitialChoice${this.getBannerSuffix()}`); 351 } 352 353 createBanner() { 354 // Create banner element 355 this.banner = this.createWrapperChild(this.getBannerContent(), "silktide-banner"); 356 357 // Add positioning class from config 358 if (this.banner && this.config.position?.banner) { 359 this.banner.classList.add(this.config.position.banner); 360 } 361 362 // Trigger optional onBannerOpen callback 363 if (this.banner && typeof this.config.onBannerOpen === "function") { 364 this.config.onBannerOpen(); 365 } 366 } 367 368 removeBanner() { 369 if (this.banner && this.banner.parentNode) { 370 this.banner.parentNode.removeChild(this.banner); 371 this.banner = null; 372 373 // Trigger optional onBannerClose callback 374 if (typeof this.config.onBannerClose === "function") { 375 this.config.onBannerClose(); 376 } 377 } 378 } 379 380 shouldShowBanner() { 381 if (this.config.showBanner === false) { 382 return false;
383 } 384 return ( 385 localStorage.getItem(`silktideCookieBanner_InitialChoice${this.getBannerSuffix()}`) === null 386 ); 387 } 388 389 // ---------------------------------------------------------------- 390 // Modal 391 // ---------------------------------------------------------------- 392 getModalContent() { 393 const preferencesTitle = 394 this.config.text?.preferences?.title || "Customize your cookie preferences"; 395 396 const preferencesDescription = 397 this.config.text?.preferences?.description || 398 "<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>"; 399 400 // Preferences button 401 const preferencesButtonLabel = this.config.text?.banner?.preferencesButtonAccessibleLabel; 402 403 const closeModalButton = `<button class="modal-close"${preferencesButtonLabel ? ` aria-label="${preferencesButtonLabel}"` : ""}> 404 <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> 405 <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"/> 406 </svg> 407 </button>`; 408 409 const cookieTypes = this.config.cookieTypes || []; 410 const acceptedCookieMap = this.getAcceptedCookies(); 411 412 // Accept button 413 const acceptAllButtonText = this.config.text?.banner?.acceptAllButtonText || "Accept all"; 414 const acceptAllButtonLabel = this.config.text?.banner?.acceptAllButtonAccessibleLabel; 415 const acceptAllButton = `<button class="preferences-accept-all st-button st-button--primary"${ 416 acceptAllButtonLabel && acceptAllButtonLabel !== acceptAllButtonText 417 ? ` aria-label="${acceptAllButtonLabel}"` 418 : "" 419 }>${acceptAllButtonText}</button>`; 420 421 // Reject button 422 const rejectNonEssentialButtonText = 423 this.config.text?.banner?.rejectNonEssentialButtonText || "Reject non-essential"; 424 const rejectNonEssentialButtonLabel = 425 this.config.text?.banner?.rejectNonEssentialButtonAccessibleLabel; 426 const rejectNonEssentialButton = `<button class="preferences-reject-all st-button st-button--primary"${ 427 rejectNonEssentialButtonLabel && 428 rejectNonEssentialButtonLabel !== rejectNonEssentialButtonText 429 ? ` aria-label="${rejectNonEssentialButtonLabel}"` 430 : "" 431 }>${rejectNonEssentialButtonText}</button>`; 432 433 // Credit link 434 const creditLinkText = 435 this.config.text?.preferences?.creditLinkText || "Get this banner for free"; 436 const creditLinkAccessibleLabel = this.config.text?.preferences?.creditLinkAccessibleLabel; 437 const creditLink = `<a href="https://silktide.com/consent-manager"${ 438 creditLinkAccessibleLabel && creditLinkAccessibleLabel !== creditLinkText 439 ? ` aria-label="${creditLinkAccessibleLabel}"` 440 : "" 441 }>${creditLinkText}</a>`; 442 443 const modalContent = ` 444 <header> 445 <h1>${preferencesTitle}</h1> 446 ${closeModalButton} 447 </header> 448 ${preferencesDescription} 449 <section id="cookie-preferences"> 450 ${cookieTypes 451 .map((type) => { 452 const accepted = acceptedCookieMap[type.id]; 453 let isChecked = false; 454 455 // if it's accepted then show as checked 456 if (accepted) { 457 isChecked = true; 458 } 459 460 // if nothing has been accepted / rejected yet, then show as checked if the default value is true 461 if (!accepted && !this.hasSetInitialCookieChoices()) { 462 isChecked = type.defaultValue; 463 } 464 465 return ` 466 <fieldset> 467 <legend>${type.name}</legend> 468 <div class="cookie-type-content"> 469 <div class="cookie-type-description">${type.description}</div> 470 <label class="switch" for="cookies-${type.id}"> 471 <input type="checkbox" id="cookies-${type.id}" ${ 472 type.required ? "checked disabled" : isChecked ? "checked" : "" 473 } />
474 <span class="switch__pill" aria-hidden="true"></span> 475 <span class="switch__dot" aria-hidden="true"></span> 476 <span class="switch__off" aria-hidden="true">Off</span> 477 <span class="switch__on" aria-hidden="true">On</span> 478 </label> 479 </div> 480 </fieldset> 481 `; 482 }) 483 .join("")} 484 </section> 485 <footer> 486 ${acceptAllButton} 487 ${rejectNonEssentialButton} 488 ${creditLink} 489 </footer> 490 `; 491 492 return modalContent; 493 } 494 495 createModal() { 496 // Create banner element 497 this.modal = this.createWrapperChild(this.getModalContent(), "silktide-modal"); 498 } 499 500 toggleModal(show) { 501 if (!this.modal) return; 502 503 this.modal.style.display = show ? "flex" : "none"; 504 505 if (show) { 506 this.showBackdrop(); 507 this.hideCookieIcon(); 508 this.removeBanner(); 509 this.preventBodyScroll(); 510 511 // Focus the close button 512 const modalCloseButton = this.modal.querySelector(".modal-close"); 513 modalCloseButton.focus(); 514 515 // Trigger optional onPreferencesOpen callback 516 if (typeof this.config.onPreferencesOpen === "function") { 517 this.config.onPreferencesOpen(); 518 } 519 520 this.updateCheckboxState(false); // read from storage when opening 521 } else { 522 // Set that an initial choice was made when closing the modal 523 this.setInitialCookieChoiceMade(); 524 525 // Save current checkbox states to storage 526 this.updateCheckboxState(true); 527 528 this.hideBackdrop(); 529 this.showCookieIcon(); 530 this.allowBodyScroll(); 531 532 // Trigger optional onPreferencesClose callback 533 if (typeof this.config.onPreferencesClose === "function") { 534 this.config.onPreferencesClose(); 535 } 536 } 537 } 538 539 // ---------------------------------------------------------------- 540 // Cookie Icon 541 // ---------------------------------------------------------------- 542 getCookieIconContent() { 543 return ` 544 <svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg"> 545 <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" /> 546 </svg> 547 `; 548 } 549 550 createCookieIcon() { 551 this.cookieIcon = document.createElement("button"); 552 this.cookieIcon.id = "silktide-cookie-icon"; 553 this.cookieIcon.innerHTML = this.getCookieIconContent(); 554 555 if (this.config.text?.banner?.preferencesButtonAccessibleLabel) { 556 this.cookieIcon.ariaLabel = this.config.text?.banner?.preferencesButtonAccessibleLabel; 557 } 558 559 // Ensure wrapper exists 560 if (!this.wrapper || !document.body.contains(this.wrapper)) { 561 this.createWrapper(); 562 } 563 564 // Append child to wrapper 565 this.wrapper.appendChild(this.cookieIcon); 566 567 // Add positioning class from config 568 if (this.cookieIcon && this.config.cookieIcon?.position) { 569 this.cookieIcon.classList.add(this.config.cookieIcon.position); 570 } 571 572 // Add color scheme class from config 573 if (this.cookieIcon && this.config.cookieIcon?.colorScheme) { 574 this.cookieIcon.classList.add(this.config.cookieIcon.colorScheme); 575 } 576 } 577 578 showCookieIcon() { 579 if (this.cookieIcon) { 580 this.cookieIcon.style.display = "flex"; 581 } 582 } 583 584 hideCookieIcon() { 585 if (this.cookieIcon) { 586 this.cookieIcon.style.display = "none"; 587 } 588 } 589
590 /** 591 * This runs if the user closes the modal without making a choice for the first time 592 * We apply the default values and the necessary values as default 593 */ 594 handleClosedWithNoChoice() { 595 this.config.cookieTypes.forEach((type) => { 596 let accepted = true; 597 // Set localStorage and run accept/reject callbacks 598 if (type.required == true) { 599 localStorage.setItem( 600 `silktideCookieChoice_${type.id}${this.getBannerSuffix()}`, 601 accepted.toString() 602 ); 603 } else if (type.defaultValue) { 604 localStorage.setItem( 605 `silktideCookieChoice_${type.id}${this.getBannerSuffix()}`, 606 accepted.toString() 607 ); 608 } else { 609 accepted = false; 610 localStorage.setItem( 611 `silktideCookieChoice_${type.id}${this.getBannerSuffix()}`, 612 accepted.toString() 613 ); 614 } 615 616 if (accepted) { 617 if (typeof type.onAccept === "function") { 618 type.onAccept(); 619 } 620 } else { 621 if (typeof type.onReject === "function") { 622 type.onReject(); 623 } 624 } 625 // set the flag to say that the cookie choice has been made 626 this.setInitialCookieChoiceMade(); 627 this.updateCheckboxState(); 628 }); 629 } 630 631 // ---------------------------------------------------------------- 632 // Focusable Elements 633 // ---------------------------------------------------------------- 634 getFocusableElements(element) { 635 return element.querySelectorAll( 636 'button, a[href], input, select, textarea, [tabindex]:not([tabindex="-1"])' 637 ); 638 } 639 640 // ---------------------------------------------------------------- 641 // Event Listeners 642 // ---------------------------------------------------------------- 643 setupEventListeners() { 644 // Check Banner exists before trying to add event listeners 645 if (this.banner) { 646 // Get the buttons 647 const acceptButton = this.banner.querySelector(".accept-all"); 648 const rejectButton = this.banner.querySelector(".reject-all"); 649 const preferencesButton = this.banner.querySelector(".preferences"); 650 651 // Add event listeners to the buttons 652 acceptButton?.addEventListener("click", () => this.handleCookieChoice(true)); 653 rejectButton?.addEventListener("click", () => this.handleCookieChoice(false)); 654 preferencesButton?.addEventListener("click", () => { 655 this.showBackdrop(); 656 this.toggleModal(true); 657 }); 658 659 // Focus Trap 660 const focusableElements = this.getFocusableElements(this.banner); 661 const firstFocusableEl = focusableElements[0]; 662 const lastFocusableEl = focusableElements[focusableElements.length - 1]; 663 664 // Add keydown event listener to handle tab navigation 665 this.banner.addEventListener("keydown", (e) => { 666 if (e.key === "Tab") { 667 if (e.shiftKey) { 668 if (document.activeElement === firstFocusableEl) {
669 lastFocusableEl.focus(); 670 e.preventDefault(); 671 } 672 } else { 673 if (document.activeElement === lastFocusableEl) { 674 firstFocusableEl.focus(); 675 e.preventDefault(); 676 } 677 } 678 } 679 }); 680 681 // Set initial focus 682 if (this.config.mode !== "wizard") { 683 acceptButton?.focus(); 684 } 685 } 686 687 // Check Modal exists before trying to add event listeners 688 if (this.modal) { 689 const closeButton = this.modal.querySelector(".modal-close"); 690 const acceptAllButton = this.modal.querySelector(".preferences-accept-all"); 691 const rejectAllButton = this.modal.querySelector(".preferences-reject-all"); 692 693 closeButton?.addEventListener("click", () => { 694 this.toggleModal(false); 695 696 const hasMadeFirstChoice = this.hasSetInitialCookieChoices(); 697 698 if (hasMadeFirstChoice) { 699 // run through the callbacks based on the current localStorage state 700 this.runStoredCookiePreferenceCallbacks(); 701 } else { 702 // handle the case where the user closes without making a choice for the first time 703 this.handleClosedWithNoChoice(); 704 } 705 }); 706 acceptAllButton?.addEventListener("click", () => this.handleCookieChoice(true)); 707 rejectAllButton?.addEventListener("click", () => this.handleCookieChoice(false)); 708 709 // Banner Focus Trap 710 const focusableElements = this.getFocusableElements(this.modal); 711 const firstFocusableEl = focusableElements[0]; 712 const lastFocusableEl = focusableElements[focusableElements.length - 1]; 713 714 this.modal.addEventListener("keydown", (e) => { 715 if (e.key === "Tab") { 716 if (e.shiftKey) { 717 if (document.activeElement === firstFocusableEl) {
718 lastFocusableEl.focus(); 719 e.preventDefault(); 720 } 721 } else { 722 if (document.activeElement === lastFocusableEl) { 723 firstFocusableEl.focus(); 724 e.preventDefault(); 725 } 726 } 727 } 728 if (e.key === "Escape") { 729 this.toggleModal(false); 730 } 731 }); 732 733 closeButton?.focus(); 734 735 // Update the checkbox event listeners 736 const preferencesSection = this.modal.querySelector("#cookie-preferences"); 737 const checkboxes = preferencesSection.querySelectorAll('input[type="checkbox"]'); 738 739 checkboxes.forEach((checkbox) => { 740 checkbox.addEventListener("change", (event) => { 741 const [, cookieId] = event.target.id.split("cookies-"); 742 const isAccepted = event.target.checked; 743 const previousValue = 744 localStorage.getItem(`silktideCookieChoice_${cookieId}${this.getBannerSuffix()}`) === 745 "true"; 746 747 // Only proceed if the value has actually changed 748 if (isAccepted !== previousValue) { 749 // Find the corresponding cookie type 750 const cookieType = this.config.cookieTypes.find((type) => type.id === cookieId); 751 752 if (cookieType) { 753 // Update localStorage 754 localStorage.setItem( 755 `silktideCookieChoice_${cookieId}${this.getBannerSuffix()}`, 756 isAccepted.toString() 757 ); 758 759 // Run the appropriate callback only if the value changed 760 if (isAccepted && typeof cookieType.onAccept === "function") { 761 cookieType.onAccept(); 762 } else if (!isAccepted && typeof cookieType.onReject === "function") { 763 cookieType.onReject(); 764 } 765 } 766 } 767 }); 768 }); 769 } 770 771 // Check Cookie Icon exists before trying to add event listeners 772 if (this.cookieIcon) { 773 this.cookieIcon.addEventListener("click", () => { 774 // If modal is not found, create it 775 if (!this.modal) { 776 this.createModal(); 777 this.toggleModal(true); 778 this.hideCookieIcon(); 779 } 780 // If modal is hidden, show it 781 else if (this.modal.style.display === "none" || this.modal.style.display === "") { 782 this.toggleModal(true); 783 this.hideCookieIcon(); 784 } 785 // If modal is visible, hide it 786 else { 787 this.toggleModal(false); 788 } 789 }); 790 } 791 } 792 793 getBannerSuffix() { 794 if (this.config.bannerSuffix) { 795 return "_" + this.config.bannerSuffix; 796 } 797 return ""; 798 } 799 800 preventBodyScroll() { 801 document.body.style.overflow = "hidden"; 802 // Prevent iOS Safari scrolling 803 document.body.style.position = "fixed"; 804 document.body.style.width = "100%"; 805 } 806 807 allowBodyScroll() { 808 document.body.style.overflow = ""; 809 document.body.style.position = ""; 810 document.body.style.width = ""; 811 } 812} 813 814(function () { 815 window.silktideCookieBannerManager = {}; 816 817 let config = {}; 818 let cookieBanner; 819 820 function updateCookieBannerConfig(userConfig = {}) { 821 config = { ...config, ...userConfig }; 822 823 // If cookie banner exists, destroy and recreate it with new config 824 if (cookieBanner) { 825 cookieBanner.destroyCookieBanner(); // We'll need to add this method 826 cookieBanner = null; 827 } 828 829 // Only initialize if document.body exists 830 if (document.body) { 831 initCookieBanner(); 832 } else { 833 // Wait for DOM to be ready 834 document.addEventListener("DOMContentLoaded", initCookieBanner, { 835 once: true, 836 }); 837 } 838 } 839 840 function initCookieBanner() { 841 if (!cookieBanner) { 842 cookieBanner = new SilktideCookieBanner(config); // Pass config to the CookieBanner instance 843 } 844 } 845 846 function injectScript(url, loadOption) { 847 // Check if script with this URL already exists 848 const existingScript = document.querySelector(`script[src="${url}"]`); 849 if (existingScript) { 850 return; // Script already exists, don't add it again 851 } 852 853 const script = document.createElement("script"); 854 script.src = url; 855 856 // Apply the async or defer attribute based on the loadOption parameter 857 if (loadOption === "async") { 858 script.async = true; 859 } else if (loadOption === "defer") { 860 script.defer = true; 861 } 862 863 document.head.appendChild(script); 864 } 865 866 window.silktideCookieBannerManager.initCookieBanner = initCookieBanner; 867 window.silktideCookieBannerManager.updateCookieBannerConfig = updateCookieBannerConfig; 868 window.silktideCookieBannerManager.injectScript = injectScript; 869 870 if (document.readyState === "loading") { 871 document.addEventListener("DOMContentLoaded", initCookieBanner, { 872 once: true, 873 }); 874 } else { 875 initCookieBanner(); 876 } 877})();
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.