PageSourceSearch

https://geckohost.nz/js/silktide-consent-manager.js

js geckohost.nz collected 2026-10-06 03:07:35 UTC 32,819 bytes, 847 lines download raw bytes

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.