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