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( 131 `silktideCookieChoice_${cookieId}${this.getBannerSuffix()}`, 132 'true' 133 ); 134 } else { 135 localStorage.setItem( 136 `silktideCookieChoice_${cookieId}${this.getBannerSuffix()}`, 137 currentState.toString() 138 ); 139 140 // Run appropriate callback 141 if (currentState && typeof cookieType.onAccept === 'function') { 142 cookieType.onAccept(); 143 } else if (!currentState && typeof cookieType.onReject === 'function') { 144 cookieType.onReject(); 145 } 146 } 147 } else { 148 // When reading values (opening modal) 149 if (cookieType.required) { 150 checkbox.checked = true; 151 checkbox.disabled = true; 152 } else { 153 const storedValue = localStorage.getItem( 154 `silktideCookieChoice_${cookieId}${this.getBannerSuffix()}` 155 ); 156 157 if (storedValue !== null) { 158 checkbox.checked = storedValue === 'true'; 159 } else { 160 checkbox.checked = !!cookieType.defaultValue; 161 } 162 } 163 } 164 }); 165 } 166 167 setInitialCookieChoiceMade() { 168 window.localStorage.setItem(`silktideCookieBanner_InitialChoice${this.getBannerSuffix()}`, 1); 169 } 170 171 // ---------------------------------------------------------------- 172 // Consent Handling 173 // ---------------------------------------------------------------- 174 handleCookieChoice(accepted) { 175 // We set that an initial choice was made regardless of what it was so we don't show the banner again 176 this.setInitialCookieChoiceMade(); 177 178 this.removeBanner(); 179 this.hideBackdrop(); 180 this.toggleModal(false); 181 this.showCookieIcon(); 182 183 this.config.cookieTypes.forEach((type) => { 184 // Set localStorage and run accept/reject callbacks 185 if (type.required == true) { 186 localStorage.setItem(`silktideCookieChoice_${type.id}${this.getBannerSuffix()}`, 'true'); 187 if (typeof type.onAccept === 'function') { type.onAccept() } 188 } else { 189 localStorage.setItem( 190 `silktideCookieChoice_${type.id}${this.getBannerSuffix()}`, 191 accepted.toString(), 192 ); 193 194 if (accepted) { 195 if (typeof type.onAccept === 'function') { type.onAccept(); } 196 } else { 197 if (typeof type.onReject === 'function') { type.onReject(); } 198 } 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] = 216 localStorage.getItem(`silktideCookieChoice_${cookieType.id}${this.getBannerSuffix()}`) === 217 'true'; 218 return acc; 219 }, {}); 220 } 221 222 runAcceptedCookieCallbacks() { 223 if (!this.config.cookieTypes) return; 224 225 const acceptedCookies = this.getAcceptedCookies();
226 this.config.cookieTypes.forEach((type) => { 227 if (type.required) return; // we run required cookies separately in loadRequiredCookies 228 if (acceptedCookies[type.id] && typeof type.onAccept === 'function') { 229 if (typeof type.onAccept === 'function') { type.onAccept(); } 230 } 231 }); 232 } 233 234 runRejectedCookieCallbacks() { 235 if (!this.config.cookieTypes) return; 236 237 const rejectedCookies = this.getRejectedCookies(); 238 this.config.cookieTypes.forEach((type) => { 239 if (rejectedCookies[type.id] && typeof type.onReject === 'function') { 240 if (typeof type.onReject === 'function') { type.onReject(); } 241 } 242 }); 243 } 244 245 /** 246 * Run through all of the cookie callbacks based on the current localStorage values 247 */ 248 runStoredCookiePreferenceCallbacks() { 249 // BEGIN Addition by Maurice Lebon [2026-04-24] 250 // if (!this.config.cookieTypes) return; 251 // END Addition 252 253 this.config.cookieTypes.forEach((type) => { 254 const accepted = 255 localStorage.getItem(`silktideCookieChoice_${type.id}${this.getBannerSuffix()}`) === 'true'; 256 // Set localStorage and run accept/reject callbacks 257 if (accepted) { 258 if (typeof type.onAccept === 'function') { type.onAccept(); } 259 } else { 260 if (typeof type.onReject === 'function') { type.onReject(); } 261 } 262 }); 263 } 264 265 loadRequiredCookies() { 266 if (!this.config.cookieTypes) return;
267 this.config.cookieTypes.forEach((cookie) => { 268 if (cookie.required && typeof cookie.onAccept === 'function') { 269 if (typeof cookie.onAccept === 'function') { cookie.onAccept(); } 270 } 271 }); 272 } 273 274 // ---------------------------------------------------------------- 275 // Banner 276 // ---------------------------------------------------------------- 277 getBannerContent() { 278 const bannerDescription = 279 this.config.text?.banner?.description || 280 "<p>We use cookies on our site to enhance your user experience, provide personalized content, and analyze our traffic.</p>"; 281 282 // Accept button 283 const acceptAllButtonText = this.config.text?.banner?.acceptAllButtonText || 'Accept all'; 284 const acceptAllButtonLabel = this.config.text?.banner?.acceptAllButtonAccessibleLabel; 285 const acceptAllButton = `<button class="accept-all st-button st-button--primary"${ 286 acceptAllButtonLabel && acceptAllButtonLabel !== acceptAllButtonText 287 ? ` aria-label="${acceptAllButtonLabel}"` 288 : '' 289 }>${acceptAllButtonText}</button>`; 290 291 // Reject button 292 const rejectNonEssentialButtonText = this.config.text?.banner?.rejectNonEssentialButtonText || 'Reject non-essential'; 293 const rejectNonEssentialButtonLabel = this.config.text?.banner?.rejectNonEssentialButtonAccessibleLabel; 294 const rejectNonEssentialButton = `<button class="reject-all st-button st-button--primary"${ 295 rejectNonEssentialButtonLabel && rejectNonEssentialButtonLabel !== rejectNonEssentialButtonText 296 ? ` aria-label="${rejectNonEssentialButtonLabel}"` 297 : '' 298 }>${rejectNonEssentialButtonText}</button>`; 299 300 // Preferences button 301 const preferencesButtonText = this.config.text?.banner?.preferencesButtonText || 'Preferences'; 302 const preferencesButtonLabel = this.config.text?.banner?.preferencesButtonAccessibleLabel; 303 const preferencesButton = `<button class="preferences"${ 304 preferencesButtonLabel && preferencesButtonLabel !== preferencesButtonText 305 ? ` aria-label="${preferencesButtonLabel}"` 306 : '' 307 }><span>${preferencesButtonText}</span></button>`; 308 309 310 // Silktide logo link 311 // BEGIN Modified by Maurice Lebon [2026-04-24] 312 const silktideLogo = ''; /* ` 313 <a class="silktide-logo" href="https://silktide.com/consent-manager" target="_blank" rel="noreferrer" aria-label="Visit the Silktide Consent Manager page"> 314 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="25" viewBox="0 0 24 25" fill="inherit"> 315 <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"/> 316 </svg> 317 </a> 318 `; */ 319 // END Modified 320 321 const bannerContent = ` 322 ${bannerDescription} 323 <div class="actions"> 324 ${acceptAllButton} 325 ${rejectNonEssentialButton} 326 <div class="actions-row"> 327 ${preferencesButton} 328 ${silktideLogo} 329 </div> 330 </div> 331 `; 332 333 return bannerContent; 334 } 335 336 hasSetInitialCookieChoices() { 337 return !!localStorage.getItem(`silktideCookieBanner_InitialChoice${this.getBannerSuffix()}`); 338 } 339 340 createBanner() { 341 // Create banner element 342 this.banner = this.createWrapperChild(this.getBannerContent(), 'silktide-banner'); 343 344 // Add positioning class from config 345 if (this.banner && this.config.position?.banner) { 346 this.banner.classList.add(this.config.position.banner); 347 } 348 349 // Trigger optional onBannerOpen callback 350 if (this.banner && typeof this.config.onBannerOpen === 'function') { 351 this.config.onBannerOpen(); 352 } 353 } 354 355 removeBanner() { 356 if (this.banner && this.banner.parentNode) { 357 this.banner.parentNode.removeChild(this.banner); 358 this.banner = null; 359 360 // Trigger optional onBannerClose callback 361 if (typeof this.config.onBannerClose === 'function') { 362 this.config.onBannerClose(); 363 } 364 } 365 } 366 367 shouldShowBanner() { 368 if (this.config.showBanner === false) { 369 return false;
370 } 371 return ( 372 localStorage.getItem(`silktideCookieBanner_InitialChoice${this.getBannerSuffix()}`) === null 373 ); 374 } 375 376 // ---------------------------------------------------------------- 377 // Modal 378 // ---------------------------------------------------------------- 379 getModalContent() { 380 const preferencesTitle = 381 this.config.text?.preferences?.title || 'Customize your cookie preferences'; 382 383 const preferencesDescription = 384 this.config.text?.preferences?.description || 385 "<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>"; 386 387 // Preferences button 388 const preferencesButtonLabel = this.config.text?.banner?.preferencesButtonAccessibleLabel; 389 390 const closeModalButton = `<button class="modal-close"${preferencesButtonLabel ? ` aria-label="${preferencesButtonLabel}"` : ''}> 391 <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> 392 <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"/> 393 </svg> 394 </button>`; 395 396 397 const cookieTypes = this.config.cookieTypes || []; 398 const acceptedCookieMap = this.getAcceptedCookies(); 399 400 // Accept button 401 const acceptAllButtonText = this.config.text?.banner?.acceptAllButtonText || 'Accept all'; 402 const acceptAllButtonLabel = this.config.text?.banner?.acceptAllButtonAccessibleLabel; 403 const acceptAllButton = `<button class="preferences-accept-all st-button st-button--primary"${ 404 acceptAllButtonLabel && acceptAllButtonLabel !== acceptAllButtonText 405 ? ` aria-label="${acceptAllButtonLabel}"` 406 : '' 407 }>${acceptAllButtonText}</button>`; 408 409 // Reject button 410 const rejectNonEssentialButtonText = this.config.text?.banner?.rejectNonEssentialButtonText || 'Reject non-essential'; 411 const rejectNonEssentialButtonLabel = this.config.text?.banner?.rejectNonEssentialButtonAccessibleLabel; 412 const rejectNonEssentialButton = `<button class="preferences-reject-all st-button st-button--primary"${ 413 rejectNonEssentialButtonLabel && rejectNonEssentialButtonLabel !== rejectNonEssentialButtonText 414 ? ` aria-label="${rejectNonEssentialButtonLabel}"` 415 : '' 416 }>${rejectNonEssentialButtonText}</button>`; 417 418 // Credit link 419 const creditLinkText = this.config.text?.preferences?.creditLinkText || 'Get this banner for free'; 420 const creditLinkAccessibleLabel = this.config.text?.preferences?.creditLinkAccessibleLabel; 421 const creditLink = `<a href="https://silktide.com/consent-manager" target="_blank" rel="noreferrer"${ 422 creditLinkAccessibleLabel && creditLinkAccessibleLabel !== creditLinkText 423 ? ` aria-label="${creditLinkAccessibleLabel}"` 424 : '' 425 }>${creditLinkText}</a>`; 426 427 428 429 const modalContent = ` 430 <header> 431 <h1>${preferencesTitle}</h1> 432 ${closeModalButton} 433 </header> 434 ${preferencesDescription} 435 <section id="cookie-preferences"> 436 ${cookieTypes 437 .map((type) => { 438 const accepted = acceptedCookieMap[type.id]; 439 let isChecked = false; 440 441 // if it's accepted then show as checked 442 if (accepted) { 443 isChecked = true; 444 } 445 446 // if nothing has been accepted / rejected yet, then show as checked if the default value is true 447 if (!accepted && !this.hasSetInitialCookieChoices()) { 448 isChecked = type.defaultValue; 449 } 450 451 return ` 452 <fieldset> 453 <legend>${type.name}</legend> 454 <div class="cookie-type-content"> 455 <div class="cookie-type-description">${type.description}</div> 456 <label class="switch" for="cookies-${type.id}"> 457 <input type="checkbox" id="cookies-${type.id}" ${ 458 type.required ? 'checked disabled' : isChecked ? 'checked' : '' 459 } />
460 <span class="switch__pill" aria-hidden="true"></span> 461 <span class="switch__dot" aria-hidden="true"></span> 462 <span class="switch__off" aria-hidden="true">Off</span> 463 <span class="switch__on" aria-hidden="true">On</span> 464 </label> 465 </div> 466 </fieldset> 467 `; 468 }) 469 .join('')} 470 </section> 471 <footer> 472 ${acceptAllButton} 473 ${rejectNonEssentialButton} 474 ${creditLink} 475 </footer> 476 `; 477 478 return modalContent; 479 } 480 481 createModal() { 482 // Create banner element 483 this.modal = this.createWrapperChild(this.getModalContent(), 'silktide-modal'); 484 } 485 486 toggleModal(show) { 487 if (!this.modal) return; 488 489 this.modal.style.display = show ? 'flex' : 'none'; 490 491 if (show) { 492 this.showBackdrop(); 493 this.hideCookieIcon(); 494 this.removeBanner(); 495 this.preventBodyScroll(); 496 497 // Focus the close button 498 const modalCloseButton = this.modal.querySelector('.modal-close'); 499 modalCloseButton.focus(); 500 501 // Trigger optional onPreferencesOpen callback 502 if (typeof this.config.onPreferencesOpen === 'function') { 503 this.config.onPreferencesOpen(); 504 } 505 506 this.updateCheckboxState(false); // read from storage when opening 507 } else { 508 // Set that an initial choice was made when closing the modal 509 this.setInitialCookieChoiceMade(); 510 511 // Save current checkbox states to storage 512 this.updateCheckboxState(true); 513 514 this.hideBackdrop(); 515 this.showCookieIcon(); 516 this.allowBodyScroll(); 517 518 // Trigger optional onPreferencesClose callback 519 if (typeof this.config.onPreferencesClose === 'function') { 520 this.config.onPreferencesClose(); 521 } 522 } 523 } 524 525 // ---------------------------------------------------------------- 526 // Cookie Icon 527 // ---------------------------------------------------------------- 528 getCookieIconContent() { 529 return ` 530 <svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg"> 531 <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" /> 532 </svg> 533 `; 534 } 535 536 createCookieIcon() { 537 this.cookieIcon = document.createElement('button'); 538 this.cookieIcon.id = 'silktide-cookie-icon'; 539 this.cookieIcon.title = 'Manage your cookie preferences for this site'; 540 this.cookieIcon.innerHTML = this.getCookieIconContent(); 541 542 if (this.config.text?.banner?.preferencesButtonAccessibleLabel) { 543 this.cookieIcon.ariaLabel = this.config.text?.banner?.preferencesButtonAccessibleLabel; 544 } 545 546 // Ensure wrapper exists 547 if (!this.wrapper || !document.body.contains(this.wrapper)) { 548 this.createWrapper(); 549 } 550 551 // Append child to wrapper 552 this.wrapper.appendChild(this.cookieIcon); 553 554 // Add positioning class from config 555 if (this.cookieIcon && this.config.cookieIcon?.position) { 556 this.cookieIcon.classList.add(this.config.cookieIcon.position); 557 } 558 559 // Add color scheme class from config 560 if (this.cookieIcon && this.config.cookieIcon?.colorScheme) { 561 this.cookieIcon.classList.add(this.config.cookieIcon.colorScheme); 562 } 563 } 564 565 showCookieIcon() { 566 if (this.cookieIcon) { 567 this.cookieIcon.style.display = 'flex'; 568 } 569 } 570 571 hideCookieIcon() { 572 if (this.cookieIcon) { 573 this.cookieIcon.style.display = 'none'; 574 } 575 } 576 577 /** 578 * This runs if the user closes the modal without making a choice for the first time 579 * We apply the default values and the necessary values as default 580 */ 581 handleClosedWithNoChoice() {
582 this.config.cookieTypes.forEach((type) => { 583 let accepted = true; 584 // Set localStorage and run accept/reject callbacks 585 if (type.required == true || type.defaultValue) { 586 localStorage.setItem( 587 `silktideCookieChoice_${type.id}${this.getBannerSuffix()}`, 588 accepted.toString(), 589 ); 590 } else { 591 accepted = false; 592 localStorage.setItem( 593 `silktideCookieChoice_${type.id}${this.getBannerSuffix()}`, 594 accepted.toString(), 595 ); 596 } 597 598 if (accepted) { 599 if (typeof type.onAccept === 'function') { type.onAccept(); } 600 } else { 601 if (typeof type.onReject === 'function') { type.onReject(); } 602 } 603 // set the flag to say that the cookie choice has been made 604 this.setInitialCookieChoiceMade(); 605 this.updateCheckboxState(); 606 }); 607 } 608 609 // ---------------------------------------------------------------- 610 // Focusable Elements 611 // ---------------------------------------------------------------- 612 getFocusableElements(element) { 613 return element.querySelectorAll( 614 'button, a[href], input, select, textarea, [tabindex]:not([tabindex="-1"])', 615 ); 616 } 617 618 // ---------------------------------------------------------------- 619 // Event Listeners 620 // ---------------------------------------------------------------- 621 setupEventListeners() { 622 // Check Banner exists before trying to add event listeners 623 if (this.banner) { 624 // Get the buttons 625 const acceptButton = this.banner.querySelector('.accept-all'); 626 const rejectButton = this.banner.querySelector('.reject-all'); 627 const preferencesButton = this.banner.querySelector('.preferences'); 628 629 // Add event listeners to the buttons 630 acceptButton?.addEventListener('click', () => this.handleCookieChoice(true)); 631 rejectButton?.addEventListener('click', () => this.handleCookieChoice(false)); 632 preferencesButton?.addEventListener('click', () => { 633 this.showBackdrop(); 634 this.toggleModal(true); 635 }); 636 637 // Focus Trap 638 const focusableElements = this.getFocusableElements(this.banner); 639 const firstFocusableEl = focusableElements[0]; 640 const lastFocusableEl = focusableElements[focusableElements.length - 1]; 641 642 // Add keydown event listener to handle tab navigation 643 this.banner.addEventListener('keydown', (e) => { 644 if (e.key === 'Tab') { 645 if (e.shiftKey) { 646 if (document.activeElement === firstFocusableEl) {
647 lastFocusableEl.focus(); 648 e.preventDefault(); 649 } 650 } else { 651 if (document.activeElement === lastFocusableEl) { 652 firstFocusableEl.focus(); 653 e.preventDefault(); 654 } 655 } 656 } 657 }); 658 659 // Set initial focus 660 if (this.config.mode !== 'wizard') { 661 acceptButton?.focus(); 662 } 663 } 664 665 // Check Modal exists before trying to add event listeners 666 if (this.modal) { 667 const closeButton = this.modal.querySelector('.modal-close'); 668 const acceptAllButton = this.modal.querySelector('.preferences-accept-all'); 669 const rejectAllButton = this.modal.querySelector('.preferences-reject-all'); 670 671 closeButton?.addEventListener('click', () => { 672 this.toggleModal(false); 673 674 const hasMadeFirstChoice = this.hasSetInitialCookieChoices(); 675 676 if (hasMadeFirstChoice) { 677 // run through the callbacks based on the current localStorage state 678 this.runStoredCookiePreferenceCallbacks(); 679 } else { 680 // handle the case where the user closes without making a choice for the first time 681 this.handleClosedWithNoChoice(); 682 } 683 }); 684 acceptAllButton?.addEventListener('click', () => this.handleCookieChoice(true)); 685 rejectAllButton?.addEventListener('click', () => this.handleCookieChoice(false)); 686 687 // Banner Focus Trap 688 const focusableElements = this.getFocusableElements(this.modal); 689 const firstFocusableEl = focusableElements[0]; 690 const lastFocusableEl = focusableElements[focusableElements.length - 1]; 691 692 this.modal.addEventListener('keydown', (e) => { 693 if (e.key === 'Tab') { 694 if (e.shiftKey) { 695 if (document.activeElement === firstFocusableEl) {
696 lastFocusableEl.focus(); 697 e.preventDefault(); 698 } 699 } else { 700 if (document.activeElement === lastFocusableEl) { 701 firstFocusableEl.focus(); 702 e.preventDefault(); 703 } 704 } 705 } 706 if (e.key === 'Escape') { 707 this.toggleModal(false); 708 } 709 }); 710 711 closeButton?.focus(); 712 713 // Update the checkbox event listeners 714 const preferencesSection = this.modal.querySelector('#cookie-preferences'); 715 const checkboxes = preferencesSection.querySelectorAll('input[type="checkbox"]'); 716 717 checkboxes.forEach(checkbox => { 718 checkbox.addEventListener('change', (event) => { 719 const [, cookieId] = event.target.id.split('cookies-'); 720 const isAccepted = event.target.checked; 721 const previousValue = localStorage.getItem( 722 `silktideCookieChoice_${cookieId}${this.getBannerSuffix()}` 723 ) === 'true'; 724 725 // Only proceed if the value has actually changed 726 if (isAccepted !== previousValue) { 727 // Find the corresponding cookie type 728 const cookieType = this.config.cookieTypes.find(type => type.id === cookieId); 729 730 if (cookieType) { 731 // Update localStorage 732 localStorage.setItem( 733 `silktideCookieChoice_${cookieId}${this.getBannerSuffix()}`, 734 isAccepted.toString() 735 ); 736 737 // Run the appropriate callback only if the value changed 738 if (isAccepted && typeof cookieType.onAccept === 'function') { 739 cookieType.onAccept(); 740 } else if (!isAccepted && typeof cookieType.onReject === 'function') { 741 cookieType.onReject(); 742 } 743 } 744 } 745 }); 746 }); 747 } 748 749 // BEGIN Modified by Maurice Lebon [2026-04-24] 750 // Check Cookie Icon exists before trying to add event listeners 751 // if (this.cookieIcon) { 752 753 /* this.cookieIcon.addEventListener('click', () => { 754 // If modal is not found, create it 755 if (!this.modal) { 756 this.createModal(); 757 this.toggleModal(true); 758 this.hideCookieIcon(); 759 } 760 // If modal is hidden, show it 761 else if (this.modal.style.display === 'none' || this.modal.style.display === '') { 762 this.toggleModal(true); 763 this.hideCookieIcon(); 764 } 765 // If modal is visible, hide it 766 else { 767 this.toggleModal(false); 768 } 769 }); */ 770 771 // Addition by ML 772 const cookieLink = document.querySelectorAll('.dcw-cookie-link'); 773 if (cookieLink) { 774 const cookieLinkArray = [...cookieLink]; 775 cookieLinkArray.forEach(link => { 776 link.addEventListener('click', () => { 777 // If modal is not found, create it 778 if (!this.modal) { 779 this.createModal(); 780 this.toggleModal(true); 781 } 782 // If modal is hidden, show it 783 else if (this.modal.style.display === 'none' || this.modal.style.display === '') { 784 this.toggleModal(true); 785 } 786 // If modal is visible, hide it 787 else { 788 this.toggleModal(false); 789 } 790 }); 791 }) 792 } 793 /* if (cookieLink) { 794 795 cookieLink.addEventListener('click', () => { 796 // If modal is not found, create it 797 if (!this.modal) { 798 this.createModal(); 799 this.toggleModal(true); 800 } 801 // If modal is hidden, show it 802 else if (this.modal.style.display === 'none' || this.modal.style.display === '') { 803 this.toggleModal(true); 804 } 805 // If modal is visible, hide it 806 else { 807 this.toggleModal(false); 808 } 809 }); 810 } */ 811 //} 812 // END Modified 813 } 814 815 getBannerSuffix() { 816 if (this.config.bannerSuffix) { 817 return '_' + this.config.bannerSuffix; 818 } 819 return ''; 820 } 821 822 preventBodyScroll() { 823 document.body.style.overflow = 'hidden'; 824 // Prevent iOS Safari scrolling 825 document.body.style.position = 'fixed'; 826 document.body.style.width = '100%'; 827 } 828 829 allowBodyScroll() { 830 document.body.style.overflow = ''; 831 document.body.style.position = ''; 832 document.body.style.width = ''; 833 } 834} 835 836(function () { 837 window.silktideCookieBannerManager = {}; 838 839 let config = {}; 840 let cookieBanner; 841 842 function updateCookieBannerConfig(userConfig = {}) { 843 config = {...config, ...userConfig}; 844 845 // If cookie banner exists, destroy and recreate it with new config 846 if (cookieBanner) { 847 cookieBanner.destroyCookieBanner(); // We'll need to add this method 848 cookieBanner = null; 849 } 850 851 // Only initialize if document.body exists 852 if (document.body) { 853 initCookieBanner(); 854 } else { 855 // Wait for DOM to be ready 856 document.addEventListener('DOMContentLoaded', initCookieBanner, {once: true}); 857 } 858 } 859 860 function initCookieBanner() { 861 if (!cookieBanner) { 862 cookieBanner = new SilktideCookieBanner(config); // Pass config to the CookieBanner instance 863 } 864 } 865 866 function injectScript(url, loadOption) { 867 // Check if script with this URL already exists 868 const existingScript = document.querySelector(`script[src="${url}"]`); 869 if (existingScript) { 870 return; // Script already exists, don't add it again 871 } 872 873 const script = document.createElement('script'); 874 script.src = url; 875 876 // Apply the async or defer attribute based on the loadOption parameter 877 if (loadOption === 'async') { 878 script.async = true; 879 } else if (loadOption === 'defer') { 880 script.defer = true; 881 } 882 883 document.head.appendChild(script); 884 } 885 886 window.silktideCookieBannerManager.initCookieBanner = initCookieBanner; 887 window.silktideCookieBannerManager.updateCookieBannerConfig = updateCookieBannerConfig; 888 window.silktideCookieBannerManager.injectScript = injectScript; 889 890 if (document.readyState === 'loading') { 891 document.addEventListener('DOMContentLoaded', initCookieBanner, {once: true}); 892 } else {
893 initCookieBanner(); 894 } 895})();
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.