// Silktide Consent Manager - https://silktide.com/consent-manager/
class SilktideCookieBanner {
constructor(config) {
this.config = config; // Save config to the instance
this.wrapper = null;
this.banner = null;
this.modal = null;
this.cookieIcon = null;
this.backdrop = null;
this.createWrapper();
if (this.shouldShowBackdrop()) {
this.createBackdrop();
}
this.createCookieIcon();
this.createModal();
if (this.shouldShowBanner()) {
this.createBanner();
this.showBackdrop();
} else {
this.showCookieIcon();
}
this.setupEventListeners();
if (this.hasSetInitialCookieChoices()) {
this.loadRequiredCookies();
this.runAcceptedCookieCallbacks();
}
}
destroyCookieBanner() {
// Remove all cookie banner elements from the DOM
if (this.wrapper && this.wrapper.parentNode) {
this.wrapper.parentNode.removeChild(this.wrapper);
}
// Restore scrolling
this.allowBodyScroll();
// Clear all references
this.wrapper = null;
this.banner = null;
this.modal = null;
this.cookieIcon = null;
this.backdrop = null;
}
// ----------------------------------------------------------------
// Wrapper
// ----------------------------------------------------------------
createWrapper() {
this.wrapper = document.createElement('div');
this.wrapper.id = 'silktide-wrapper';
document.body.insertBefore(this.wrapper, document.body.firstChild);
}
// ----------------------------------------------------------------
// Wrapper Child Generator
// ----------------------------------------------------------------
createWrapperChild(htmlContent, id) {
// Create child element
const child = document.createElement('div');
child.id = id;
child.innerHTML = htmlContent;
// Ensure wrapper exists
if (!this.wrapper || !document.body.contains(this.wrapper)) {
this.createWrapper();
}
// Append child to wrapper
this.wrapper.appendChild(child);
return child;
}
// ----------------------------------------------------------------
// Backdrop
// ----------------------------------------------------------------
createBackdrop() {
this.backdrop = this.createWrapperChild(null, 'silktide-backdrop');
}
showBackdrop() {
if (this.backdrop) {
this.backdrop.style.display = 'block';
}
// Trigger optional onBackdropOpen callback
if (typeof this.config.onBackdropOpen === 'function') {
this.config.onBackdropOpen();
}
}
hideBackdrop() {
if (this.backdrop) {
this.backdrop.style.display = 'none';
}
// Trigger optional onBackdropClose callback
if (typeof this.config.onBackdropClose === 'function') {
this.config.onBackdropClose();
}
}
shouldShowBackdrop() {
return this.config?.background?.showBackground || false;
}
// update the checkboxes in the modal with the values from localStorage
updateCheckboxState(saveToStorage = false) {
const preferencesSection = this.modal.querySelector('#cookie-preferences');
const checkboxes = preferencesSection.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach((checkbox) => {
const [, cookieId] = checkbox.id.split('cookies-');
const cookieType = this.config.cookieTypes.find(type => type.id === cookieId);
if (!cookieType) return;
if (saveToStorage) {
// Save the current state to localStorage and run callbacks
const currentState = checkbox.checked;
if (cookieType.required) {
localStorage.setItem(
`silktideCookieChoice_${cookieId}${this.getBannerSuffix()}`,
'true'
);
} else {
localStorage.setItem(
`silktideCookieChoice_${cookieId}${this.getBannerSuffix()}`,
currentState.toString()
);
// Run appropriate callback
if (currentState && typeof cookieType.onAccept === 'function') {
cookieType.onAccept();
} else if (!currentState && typeof cookieType.onReject === 'function') {
cookieType.onReject();
}
}
} else {
// When reading values (opening modal)
if (cookieType.required) {
checkbox.checked = true;
checkbox.disabled = true;
} else {
const storedValue = localStorage.getItem(
`silktideCookieChoice_${cookieId}${this.getBannerSuffix()}`
);
if (storedValue !== null) {
checkbox.checked = storedValue === 'true';
} else {
checkbox.checked = !!cookieType.defaultValue;
}
}
}
});
}
setInitialCookieChoiceMade() {
window.localStorage.setItem(`silktideCookieBanner_InitialChoice${this.getBannerSuffix()}`, 1);
}
// ----------------------------------------------------------------
// Consent Handling
// ----------------------------------------------------------------
handleCookieChoice(accepted) {
// We set that an initial choice was made regardless of what it was so we don't show the banner again
this.setInitialCookieChoiceMade();
this.removeBanner();
this.hideBackdrop();
this.toggleModal(false);
this.showCookieIcon();
this.config.cookieTypes.forEach((type) => {
// Set localStorage and run accept/reject callbacks
if (type.required == true) {
localStorage.setItem(`silktideCookieChoice_${type.id}${this.getBannerSuffix()}`, 'true');
if (typeof type.onAccept === 'function') { type.onAccept() }
} else {
localStorage.setItem(
`silktideCookieChoice_${type.id}${this.getBannerSuffix()}`,
accepted.toString(),
);
if (accepted) {
if (typeof type.onAccept === 'function') { type.onAccept(); }
} else {
if (typeof type.onReject === 'function') { type.onReject(); }
}
}
});
// Trigger optional onAcceptAll/onRejectAll callbacks
if (accepted && typeof this.config.onAcceptAll === 'function') {
if (typeof this.config.onAcceptAll === 'function') { this.config.onAcceptAll(); }
} else if (typeof this.config.onRejectAll === 'function') {
if (typeof this.config.onRejectAll === 'function') { this.config.onRejectAll(); }
}
// finally update the checkboxes in the modal with the values from localStorage
this.updateCheckboxState();
}
getAcceptedCookies() {
return (this.config.cookieTypes || []).reduce((acc, cookieType) => {
acc[cookieType.id] =
localStorage.getItem(`silktideCookieChoice_${cookieType.id}${this.getBannerSuffix()}`) ===
'true';
return acc;
}, {});
}
runAcceptedCookieCallbacks() {
if (!this.config.cookieTypes) return;
const acceptedCookies = this.getAcceptedCookies();
this.config.cookieTypes.forEach((type) => {
if (type.required) return; // we run required cookies separately in loadRequiredCookies
if (acceptedCookies[type.id] && typeof type.onAccept === 'function') {
if (typeof type.onAccept === 'function') { type.onAccept(); }
}
});
}
runRejectedCookieCallbacks() {
if (!this.config.cookieTypes) return;
const rejectedCookies = this.getRejectedCookies();
this.config.cookieTypes.forEach((type) => {
if (rejectedCookies[type.id] && typeof type.onReject === 'function') {
if (typeof type.onReject === 'function') { type.onReject(); }
}
});
}
/**
* Run through all of the cookie callbacks based on the current localStorage values
*/
runStoredCookiePreferenceCallbacks() {
this.config.cookieTypes.forEach((type) => {
const accepted =
localStorage.getItem(`silktideCookieChoice_${type.id}${this.getBannerSuffix()}`) === 'true';
// Set localStorage and run accept/reject callbacks
if (accepted) {
if (typeof type.onAccept === 'function') { type.onAccept(); }
} else {
if (typeof type.onReject === 'function') { type.onReject(); }
}
});
}
loadRequiredCookies() {
if (!this.config.cookieTypes) return;
this.config.cookieTypes.forEach((cookie) => {
if (cookie.required && typeof cookie.onAccept === 'function') {
if (typeof cookie.onAccept === 'function') { cookie.onAccept(); }
}
});
}
// ----------------------------------------------------------------
// Banner
// ----------------------------------------------------------------
getBannerContent() {
const bannerDescription =
this.config.text?.banner?.description ||
"
We use cookies on our site to enhance your user experience, provide personalized content, and analyze our traffic.
";
// Accept button
const acceptAllButtonText = this.config.text?.banner?.acceptAllButtonText || 'Accept all';
const acceptAllButtonLabel = this.config.text?.banner?.acceptAllButtonAccessibleLabel;
const acceptAllButton = ``;
// Reject button
const rejectNonEssentialButtonText = this.config.text?.banner?.rejectNonEssentialButtonText || 'Reject non-essential';
const rejectNonEssentialButtonLabel = this.config.text?.banner?.rejectNonEssentialButtonAccessibleLabel;
const rejectNonEssentialButton = ``;
// Preferences button
const preferencesButtonText = this.config.text?.banner?.preferencesButtonText || 'Preferences';
const preferencesButtonLabel = this.config.text?.banner?.preferencesButtonAccessibleLabel;
const preferencesButton = ``;
// Silktide logo link
const silktideLogo = `
`;
const bannerContent = `
${bannerDescription}
${acceptAllButton}
${rejectNonEssentialButton}
${preferencesButton}
${silktideLogo}
`;
return bannerContent;
}
hasSetInitialCookieChoices() {
return !!localStorage.getItem(`silktideCookieBanner_InitialChoice${this.getBannerSuffix()}`);
}
createBanner() {
// Create banner element
this.banner = this.createWrapperChild(this.getBannerContent(), 'silktide-banner');
// Add positioning class from config
if (this.banner && this.config.position?.banner) {
this.banner.classList.add(this.config.position.banner);
}
// Trigger optional onBannerOpen callback
if (this.banner && typeof this.config.onBannerOpen === 'function') {
this.config.onBannerOpen();
}
}
removeBanner() {
if (this.banner && this.banner.parentNode) {
this.banner.parentNode.removeChild(this.banner);
this.banner = null;
// Trigger optional onBannerClose callback
if (typeof this.config.onBannerClose === 'function') {
this.config.onBannerClose();
}
}
}
shouldShowBanner() {
if (this.config.showBanner === false) {
return false;
}
return (
localStorage.getItem(`silktideCookieBanner_InitialChoice${this.getBannerSuffix()}`) === null
);
}
// ----------------------------------------------------------------
// Modal
// ----------------------------------------------------------------
getModalContent() {
const preferencesTitle =
this.config.text?.preferences?.title || 'Customize your cookie preferences';
const preferencesDescription =
this.config.text?.preferences?.description ||
"We respect your right to privacy. You can choose not to allow some types of cookies. Your cookie preferences will apply across our website.
";
// Preferences button
const preferencesButtonLabel = this.config.text?.banner?.preferencesButtonAccessibleLabel;
const closeModalButton = ``;
const cookieTypes = this.config.cookieTypes || [];
const acceptedCookieMap = this.getAcceptedCookies();
// Accept button
const acceptAllButtonText = this.config.text?.banner?.acceptAllButtonText || 'Accept all';
const acceptAllButtonLabel = this.config.text?.banner?.acceptAllButtonAccessibleLabel;
const acceptAllButton = ``;
// Reject button
const rejectNonEssentialButtonText = this.config.text?.banner?.rejectNonEssentialButtonText || 'Reject non-essential';
const rejectNonEssentialButtonLabel = this.config.text?.banner?.rejectNonEssentialButtonAccessibleLabel;
const rejectNonEssentialButton = ``;
// Credit link
const creditLinkText = this.config.text?.preferences?.creditLinkText || 'Get this banner for free';
const creditLinkAccessibleLabel = this.config.text?.preferences?.creditLinkAccessibleLabel;
const creditLink = `${creditLinkText}`;
const modalContent = `
${preferencesTitle}
${closeModalButton}
${preferencesDescription}
${cookieTypes
.map((type) => {
const accepted = acceptedCookieMap[type.id];
let isChecked = false;
// if it's accepted then show as checked
if (accepted) {
isChecked = true;
}
// if nothing has been accepted / rejected yet, then show as checked if the default value is true
if (!accepted && !this.hasSetInitialCookieChoices()) {
isChecked = type.defaultValue;
}
return `
`;
})
.join('')}
`;
return modalContent;
}
createModal() {
// Create banner element
this.modal = this.createWrapperChild(this.getModalContent(), 'silktide-modal');
}
toggleModal(show) {
if (!this.modal) return;
this.modal.style.display = show ? 'flex' : 'none';
if (show) {
this.showBackdrop();
this.hideCookieIcon();
this.removeBanner();
this.preventBodyScroll();
// Focus the close button
const modalCloseButton = this.modal.querySelector('.modal-close');
modalCloseButton.focus();
// Trigger optional onPreferencesOpen callback
if (typeof this.config.onPreferencesOpen === 'function') {
this.config.onPreferencesOpen();
}
this.updateCheckboxState(false); // read from storage when opening
} else {
// Set that an initial choice was made when closing the modal
this.setInitialCookieChoiceMade();
// Save current checkbox states to storage
this.updateCheckboxState(true);
this.hideBackdrop();
this.showCookieIcon();
this.allowBodyScroll();
// Trigger optional onPreferencesClose callback
if (typeof this.config.onPreferencesClose === 'function') {
this.config.onPreferencesClose();
}
}
}
// ----------------------------------------------------------------
// Cookie Icon
// ----------------------------------------------------------------
getCookieIconContent() {
return `
`;
}
createCookieIcon() {
this.cookieIcon = document.createElement('button');
this.cookieIcon.id = 'silktide-cookie-icon';
this.cookieIcon.title = 'Manage your cookie preferences for this site';
this.cookieIcon.innerHTML = this.getCookieIconContent();
if (this.config.text?.banner?.preferencesButtonAccessibleLabel) {
this.cookieIcon.ariaLabel = this.config.text?.banner?.preferencesButtonAccessibleLabel;
}
// Ensure wrapper exists
if (!this.wrapper || !document.body.contains(this.wrapper)) {
this.createWrapper();
}
// Append child to wrapper
this.wrapper.appendChild(this.cookieIcon);
// Add positioning class from config
if (this.cookieIcon && this.config.cookieIcon?.position) {
this.cookieIcon.classList.add(this.config.cookieIcon.position);
}
// Add color scheme class from config
if (this.cookieIcon && this.config.cookieIcon?.colorScheme) {
this.cookieIcon.classList.add(this.config.cookieIcon.colorScheme);
}
}
showCookieIcon() {
if (this.cookieIcon) {
this.cookieIcon.style.display = 'flex';
}
}
hideCookieIcon() {
if (this.cookieIcon) {
this.cookieIcon.style.display = 'none';
}
}
/**
* This runs if the user closes the modal without making a choice for the first time
* We apply the default values and the necessary values as default
*/
handleClosedWithNoChoice() {
this.config.cookieTypes.forEach((type) => {
let accepted = true;
// Set localStorage and run accept/reject callbacks
if (type.required == true || type.defaultValue) {
localStorage.setItem(
`silktideCookieChoice_${type.id}${this.getBannerSuffix()}`,
accepted.toString(),
);
} else {
accepted = false;
localStorage.setItem(
`silktideCookieChoice_${type.id}${this.getBannerSuffix()}`,
accepted.toString(),
);
}
if (accepted) {
if (typeof type.onAccept === 'function') { type.onAccept(); }
} else {
if (typeof type.onReject === 'function') { type.onReject(); }
}
// set the flag to say that the cookie choice has been made
this.setInitialCookieChoiceMade();
this.updateCheckboxState();
});
}
// ----------------------------------------------------------------
// Focusable Elements
// ----------------------------------------------------------------
getFocusableElements(element) {
return element.querySelectorAll(
'button, a[href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
);
}
// ----------------------------------------------------------------
// Event Listeners
// ----------------------------------------------------------------
setupEventListeners() {
// Check Banner exists before trying to add event listeners
if (this.banner) {
// Get the buttons
const acceptButton = this.banner.querySelector('.accept-all');
const rejectButton = this.banner.querySelector('.reject-all');
const preferencesButton = this.banner.querySelector('.preferences');
// Add event listeners to the buttons
acceptButton?.addEventListener('click', () => this.handleCookieChoice(true));
rejectButton?.addEventListener('click', () => this.handleCookieChoice(false));
preferencesButton?.addEventListener('click', () => {
this.showBackdrop();
this.toggleModal(true);
});
// Focus Trap
const focusableElements = this.getFocusableElements(this.banner);
const firstFocusableEl = focusableElements[0];
const lastFocusableEl = focusableElements[focusableElements.length - 1];
// Add keydown event listener to handle tab navigation
this.banner.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
if (e.shiftKey) {
if (document.activeElement === firstFocusableEl) {
lastFocusableEl.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastFocusableEl) {
firstFocusableEl.focus();
e.preventDefault();
}
}
}
});
// Set initial focus
if (this.config.mode !== 'wizard') {
acceptButton?.focus();
}
}
// Check Modal exists before trying to add event listeners
if (this.modal) {
const closeButton = this.modal.querySelector('.modal-close');
const acceptAllButton = this.modal.querySelector('.preferences-accept-all');
const rejectAllButton = this.modal.querySelector('.preferences-reject-all');
closeButton?.addEventListener('click', () => {
this.toggleModal(false);
const hasMadeFirstChoice = this.hasSetInitialCookieChoices();
if (hasMadeFirstChoice) {
// run through the callbacks based on the current localStorage state
this.runStoredCookiePreferenceCallbacks();
} else {
// handle the case where the user closes without making a choice for the first time
this.handleClosedWithNoChoice();
}
});
acceptAllButton?.addEventListener('click', () => this.handleCookieChoice(true));
rejectAllButton?.addEventListener('click', () => this.handleCookieChoice(false));
// Banner Focus Trap
const focusableElements = this.getFocusableElements(this.modal);
const firstFocusableEl = focusableElements[0];
const lastFocusableEl = focusableElements[focusableElements.length - 1];
this.modal.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
if (e.shiftKey) {
if (document.activeElement === firstFocusableEl) {
lastFocusableEl.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastFocusableEl) {
firstFocusableEl.focus();
e.preventDefault();
}
}
}
if (e.key === 'Escape') {
this.toggleModal(false);
}
});
closeButton?.focus();
// Update the checkbox event listeners
const preferencesSection = this.modal.querySelector('#cookie-preferences');
const checkboxes = preferencesSection.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', (event) => {
const [, cookieId] = event.target.id.split('cookies-');
const isAccepted = event.target.checked;
const previousValue = localStorage.getItem(
`silktideCookieChoice_${cookieId}${this.getBannerSuffix()}`
) === 'true';
// Only proceed if the value has actually changed
if (isAccepted !== previousValue) {
// Find the corresponding cookie type
const cookieType = this.config.cookieTypes.find(type => type.id === cookieId);
if (cookieType) {
// Update localStorage
localStorage.setItem(
`silktideCookieChoice_${cookieId}${this.getBannerSuffix()}`,
isAccepted.toString()
);
// Run the appropriate callback only if the value changed
if (isAccepted && typeof cookieType.onAccept === 'function') {
cookieType.onAccept();
} else if (!isAccepted && typeof cookieType.onReject === 'function') {
cookieType.onReject();
}
}
}
});
});
}
// Check Cookie Icon exists before trying to add event listeners
if (this.cookieIcon) {
this.cookieIcon.addEventListener('click', () => {
// If modal is not found, create it
if (!this.modal) {
this.createModal();
this.toggleModal(true);
this.hideCookieIcon();
}
// If modal is hidden, show it
else if (this.modal.style.display === 'none' || this.modal.style.display === '') {
this.toggleModal(true);
this.hideCookieIcon();
}
// If modal is visible, hide it
else {
this.toggleModal(false);
}
});
}
}
getBannerSuffix() {
if (this.config.bannerSuffix) {
return '_' + this.config.bannerSuffix;
}
return '';
}
preventBodyScroll() {
document.body.style.overflow = 'hidden';
// Prevent iOS Safari scrolling
document.body.style.position = 'fixed';
document.body.style.width = '100%';
}
allowBodyScroll() {
document.body.style.overflow = '';
document.body.style.position = '';
document.body.style.width = '';
}
}
(function () {
window.silktideCookieBannerManager = {};
let config = {};
let cookieBanner;
function updateCookieBannerConfig(userConfig = {}) {
config = {...config, ...userConfig};
// If cookie banner exists, destroy and recreate it with new config
if (cookieBanner) {
cookieBanner.destroyCookieBanner(); // We'll need to add this method
cookieBanner = null;
}
// Only initialize if document.body exists
if (document.body) {
initCookieBanner();
} else {
// Wait for DOM to be ready
document.addEventListener('DOMContentLoaded', initCookieBanner, {once: true});
}
}
function initCookieBanner() {
if (!cookieBanner) {
cookieBanner = new SilktideCookieBanner(config); // Pass config to the CookieBanner instance
}
}
function injectScript(url, loadOption) {
// Check if script with this URL already exists
const existingScript = document.querySelector(`script[src="${url}"]`);
if (existingScript) {
return; // Script already exists, don't add it again
}
const script = document.createElement('script');
script.src = url;
// Apply the async or defer attribute based on the loadOption parameter
if (loadOption === 'async') {
script.async = true;
} else if (loadOption === 'defer') {
script.defer = true;
}
document.head.appendChild(script);
}
window.silktideCookieBannerManager.initCookieBanner = initCookieBanner;
window.silktideCookieBannerManager.updateCookieBannerConfig = updateCookieBannerConfig;
window.silktideCookieBannerManager.injectScript = injectScript;
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initCookieBanner, {once: true});
} else {
initCookieBanner();
}
})();