PageSourceSearch

https://schedule.rothmanortho.com/cookie-banner/silktide-consent-manager.js

js rothmanortho.com collected 2026-10-03 22:11:06 UTC 28,280 bytes, 818 lines download raw bytes

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

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.