From 824404a489963424752fce366423686b0d27a1ee Mon Sep 17 00:00:00 2001 From: Mikko Tapionlinna Date: Tue, 2 Apr 2024 11:53:01 +0300 Subject: [PATCH 01/13] UHF-9791: UHF-9791: Refactor javascript code to be more readable --- src/js/hds-cc/hds-cc.js | 315 ++++++++++------------ src/js/hds-cc/hds-cc_translations.js | 17 +- templates/misc/cookie-container.html.twig | 2 +- 3 files changed, 157 insertions(+), 177 deletions(-) diff --git a/src/js/hds-cc/hds-cc.js b/src/js/hds-cc/hds-cc.js index 4bff586134..a90a5b9d10 100644 --- a/src/js/hds-cc/hds-cc.js +++ b/src/js/hds-cc/hds-cc.js @@ -6,26 +6,25 @@ import { parse, serialize, } from 'cookie/index'; + import { getCookieBannerHtml, getGroupHtml, getTableRowHtml, } from './template'; + import { getTranslation, getTranslationKeys, } from './hds-cc_translations'; -class HdsCc { +class HDSCookieConsent { constructor() { this.COOKIE_NAME = 'city-of-helsinki-cookie-consents'; // Overridable default value this.COOKIE_DAYS = 100; this.UNCHANGED = 'unchanged'; this.ESSENTIAL_GROUP_NAME = 'essential'; document.addEventListener('DOMContentLoaded', () => this.init()); - - // Debug helper. Open banner and run on console to see the box updated. - window.aaa_chatcheck = () => dispatchEvent(new CustomEvent('CONSENTS_CHANGED', { detail: { groups: ['chat'] } })); } /** @@ -57,57 +56,50 @@ class HdsCc { * with specified name doesn't exist. */ async getCookie() { - let cookie; try { const cookieString = parse(document.cookie)[this.COOKIE_NAME]; if (!cookieString) { console.log('Cookie is not set'); return false; } - cookie = JSON.parse(cookieString); + return JSON.parse(cookieString); } catch (err) { - // If cookie parsing fails, show banner console.log(`Cookie parsing unsuccessful:\n${err}`); return false; } - - return cookie; } async setCookie(cookieData) { - console.log('setCookie', cookieData); + // console.log('setCookie', cookieData); const expiryDate = new Date(); expiryDate.setDate(expiryDate.getDate() + this.COOKIE_DAYS); - document.cookie = serialize(this.COOKIE_NAME, JSON.stringify(cookieData), {expires: expiryDate}); + document.cookie = serialize(this.COOKIE_NAME, JSON.stringify(cookieData), { expires: expiryDate }); } saveAcceptedGroups(cookieSettings, acceptedGroupNames = [], showBanner = false) { - console.log('Accepting cookies from groups:', acceptedGroupNames); + // console.log('Accepting cookies from groups:', acceptedGroupNames); const acceptedGroups = {}; - // Required should not come in every case; the category hash might have changed - cookieSettings.requiredCookies.groups.forEach(group => { - if (acceptedGroupNames.includes(group.groupId)) { - acceptedGroups[group.groupId] = group.checksum; - } - }); - // Add accepted group names to acceptedGroups, assume others to be declined (overrides previous selections) - cookieSettings.optionalCookies.groups.forEach(group => { + // Find group checksums for accepted groups + const bothGroups = [...cookieSettings.requiredCookies.groups, ...cookieSettings.optionalCookies.groups]; + bothGroups.forEach(group => { if (acceptedGroupNames.includes(group.groupId)) { acceptedGroups[group.groupId] = group.checksum; } }); - this.setCookie({ - showBanner, + const data = { checksum: cookieSettings.checksum, groups: acceptedGroups, - }); + ...(showBanner && { showBanner: true }), // Only add showBanner if it's true + }; + + this.setCookie(data); } - async removeInvalidGroupsFromBrowserCookie(cookieSettingsGroups, browserCookieState, cookieSettings) { - console.log('removeInvalidGroupsFromBrowserCookie', cookieSettingsGroups, browserCookieState, cookieSettings); + async removeInvalidGroupsFromCookie(cookieSettingsGroups, browserCookieState, cookieSettings) { + // console.log('removeInvalidGroupsFromCookie', cookieSettingsGroups, browserCookieState, cookieSettings); const newCookieGroups = []; @@ -127,100 +119,114 @@ class HdsCc { if (browserCookieState.groups[browserGroupName] === cookieSettingsGroup.checksum) { newCookieGroups.push(cookieSettingsGroup.groupId); } else { - console.log('Checksums do not match for group', cookieSettingsGroup.groupId); + console.log(`Checksums do not match for group: '${cookieSettingsGroup.groupId}', removing from cookie accepted.`); } } } }); }; - console.log('newCookieGroups', newCookieGroups); - // Because global checksum did not match, group checksums were checked and non-matching groups were removed, save the cleaned cookie const showBanner = true; this.saveAcceptedGroups(cookieSettings, newCookieGroups, showBanner); - return cookieSettings; } - async getCookieSettings() { + async getCookieSettingsFromJsonFile() { + // Get the site settings (prefetch) link element + const siteSettingsElem = document.querySelector('link#hdsCookieConsentSiteSettings'); + if (!siteSettingsElem) { + throw new Error('Cookie consent: Site settings link not found'); + } + // Use prefetch link elements href to fetch the site settings string + let cookieSettingsRaw; try { - const cookieSettingsRaw = await fetch(window.hdsCookieConsentPageSettings.jsonUrl).then((response) => response.text()); - const cookieSettingsChecksum = await this.getChecksum(cookieSettingsRaw); - - const cookieSettings = JSON.parse(cookieSettingsRaw); - this.COOKIE_NAME = cookieSettings.cookieName || this.COOKIE_NAME; // Optional override for cookie name - - // Compare file checksum with browser cookie checksum if the file has not changed and return false for no change (no banner needed) - const browserCookie = await this.getCookie(); - if (browserCookie) { - // Check if settings have not changed and browser cookie has 'showBanner' set to false - if (!browserCookie.showBanner && (cookieSettingsChecksum === browserCookie.checksum)) { - console.log('Settings were unchanged'); - return this.UNCHANGED; - } - } + cookieSettingsRaw = await fetch(siteSettingsElem.href).then((response) => response.text()); + } catch (error) { + throw new Error(`Cookie consent: Unable to fetch cookie consent settings: ${error}`); + } - cookieSettings.checksum = cookieSettingsChecksum; + // Calculate checksum for the site settings string + const cookieSettingsChecksum = await this.getChecksum(cookieSettingsRaw); - const essentialGroup = cookieSettings.requiredCookies.groups.find(group => group.groupId === this.ESSENTIAL_GROUP_NAME); - if (!essentialGroup) { - // The site cookie settings must have required group named by ESSENTIAL_GROUP_NAME - throw new Error(`Cookie consent error: '${this.ESSENTIAL_GROUP_NAME}' group missing`); - } - const requiredCookieFound = essentialGroup.cookies.find(cookie => cookie.name === this.COOKIE_NAME); - if (!requiredCookieFound) { - // The required "essential" group must have cookie with name matching the root level 'cookieName' - throw new Error(`Cookie consent error: Missing cookie entry for '${this.COOKIE_NAME}' in group '${this.ESSENTIAL_GROUP_NAME}'`); - } - - const cookieSettingsGroups = [...cookieSettings.requiredCookies.groups, ...cookieSettings.optionalCookies.groups]; + // Parse the fetched site settings string to JSON + let cookieSettings; + try { + cookieSettings = JSON.parse(cookieSettingsRaw); + } catch (error) { + throw new Error(`Cookie consent settings parsing failed: ${error}`); + } - const cookieNames = []; - cookieSettingsGroups.forEach(cookie => { - if (cookieNames.includes(cookie.groupId)) { - // The cookie settings must not contain cookie groups that have identical names - throw new Error(`Cookie consent error: Group '${cookie.groupId}' found multiple times in settings.`); - } - cookieNames.push(cookie.groupId); - }); + // Add checksum to the settings object, so that we do not need to recalculate it when saving the cookie + cookieSettings.checksum = cookieSettingsChecksum; + return cookieSettings; + } - // eslint-disable-next-line no-restricted-syntax - for (const group of cookieSettingsGroups) { - // eslint-disable-next-line no-await-in-loop - const groupChecksum = await this.getChecksum(group); - group.checksum = groupChecksum; - } + async getCookieSettings() { + const cookieSettings = await this.getCookieSettingsFromJsonFile(); - return await this.removeInvalidGroupsFromBrowserCookie(cookieSettingsGroups, browserCookie, cookieSettings); + this.COOKIE_NAME = cookieSettings.cookieName || this.COOKIE_NAME; // Optional override for cookie name - } catch (err) { - if (err.message.includes('undefined')) { - throw new Error(`Cookie settings not found: ${err}`); + // Compare file checksum with browser cookie checksum if the file has not changed and return false for no change (no banner needed) + const browserCookie = await this.getCookie(); + if (browserCookie) { + // Check if settings have not changed and browser cookie has 'showBanner' set to false + if (!browserCookie.showBanner && (cookieSettings.checksum === browserCookie.checksum)) { + console.log('Settings were unchanged'); + return this.UNCHANGED; } + } - throw new Error(err.message); + const essentialGroup = cookieSettings.requiredCookies.groups.find(group => group.groupId === this.ESSENTIAL_GROUP_NAME); + if (!essentialGroup) { + // The site cookie settings must have required group named by ESSENTIAL_GROUP_NAME + throw new Error(`Cookie consent error: '${this.ESSENTIAL_GROUP_NAME}' group missing`); + } + const requiredCookieFound = essentialGroup.cookies.find(cookie => cookie.name === this.COOKIE_NAME); + if (!requiredCookieFound) { + // The required "essential" group must have cookie with name matching the root level 'cookieName' + throw new Error(`Cookie consent error: Missing cookie entry for '${this.COOKIE_NAME}' in group '${this.ESSENTIAL_GROUP_NAME}'`); } - } - /** - * Go through form and get accepted groups. Return a list of group ID's. - */ - readGroupSelections(form, all = false) { - const groupSelections = []; - const formCheckboxes = form.querySelectorAll('input'); - formCheckboxes.forEach(check => { - if(check.checked || all) { - groupSelections.push(check.dataset.group); + const cookieSettingsGroups = [...cookieSettings.requiredCookies.groups, ...cookieSettings.optionalCookies.groups]; + + const cookieNames = []; + cookieSettingsGroups.forEach(cookie => { + if (cookieNames.includes(cookie.groupId)) { + // The cookie settings must not contain cookie groups that have identical names + throw new Error(`Cookie consent error: Group '${cookie.groupId}' found multiple times in settings.`); } + cookieNames.push(cookie.groupId); }); - return groupSelections; + // Checksums for all groups calculated in parallel without waiting for each + await Promise.all(cookieSettingsGroups.map(async (group) => { + group.checksum = await this.getChecksum(group); // This await is needed to ensure that all checksums are calculated before continuing + })); + + return this.removeInvalidGroupsFromCookie(cookieSettingsGroups, browserCookie, cookieSettings); } + handleButtonEvents(selection, formReference, cookieSettings) { + + /** + * Go through form and get accepted groups. Return a list of group ID's. + */ + function readGroupSelections(form, all = false) { + const groupSelections = []; + const formCheckboxes = form.querySelectorAll('input'); + formCheckboxes.forEach(check => { + if(check.checked || all) { + groupSelections.push(check.dataset.group); + } + }); + + return groupSelections; + } + switch (selection) { case 'required': { const acceptedGroups = [this.ESSENTIAL_GROUP_NAME]; @@ -228,12 +234,12 @@ class HdsCc { break; } case 'all': { - const acceptedGroups = this.readGroupSelections(formReference, true); + const acceptedGroups = readGroupSelections(formReference, true); this.saveAcceptedGroups(cookieSettings, acceptedGroups); break; } case 'selected': { - const acceptedGroups = this.readGroupSelections(formReference); + const acceptedGroups = readGroupSelections(formReference); this.saveAcceptedGroups(cookieSettings, acceptedGroups); break; } @@ -244,20 +250,17 @@ class HdsCc { window.location.reload(); } - /** - * Go through cookie group object and check if it has - * property with given key - */ - isGroupAccepted(groupName, browserCookie = null) { - const browserCookieState = browserCookie || this.getCookie(); + async areTheseGroupsAccepted(groupNamesArray) { + const browserCookieState = await this.getCookie(); // Check if our cookie exists and has groups set if (!browserCookieState || !browserCookieState.groups) { + console.log('Cookie is not set properly', browserCookieState, browserCookieState.groups); return false; } - // Return true if group is in accepted groups - return !!browserCookieState.groups[groupName]; + // Check if all groups are in accepted groups + return groupNamesArray.every(groupName => !!browserCookieState.groups[groupName]); } /** @@ -266,28 +269,23 @@ class HdsCc { * check 1. if cookie exists 2. essentials approved 3. hash match - show banner * else show banner */ - async checkBannerNeed(cookieSettings) { + async shouldDisplayBanner(cookieSettings) { if (cookieSettings !== this.UNCHANGED) { console.log('Cookie settings changed since approval, show banner'); return true; } - const browserCookieState = await this.getCookie(); - if (!browserCookieState) { + const browserCookie = await this.getCookie(); + if (!browserCookie) { console.log('Cookie doesn\'t exist, show banner'); return true; } - if (browserCookieState.showBanner) { + if (browserCookie.showBanner) { console.log('Cookie wants to show banner'); return true; } - if (!this.isGroupAccepted(this.ESSENTIAL_GROUP_NAME, browserCookieState)) { - console.log('Cookie settings essentials not yet approved, show banner'); - return true; - } - console.log('All checks passed, no need for banner'); return false; } @@ -303,7 +301,7 @@ class HdsCc { * @param {string} lang - Language key, e.g., 'fi' for Finnish. * @return {string} - Translated string based on the provided language key, or the original string if `translationObj` is not an object. */ - translate(translationObj, lang) { + translateSetting(translationObj, lang) { if (typeof (translationObj) === 'object') { if (translationObj[lang] === undefined) { return translationObj.en; // fallback to English translation @@ -313,39 +311,28 @@ class HdsCc { return translationObj; } - /** - * Builder template functions - * - * - group - * - tableRows - */ - - buildTableRows(cookies, lang) { - let tableRows = ''; - - cookies.forEach(cookie => { - tableRows += getTableRowHtml( - { - name: this.translate(cookie.name, lang), - host: this.translate(cookie.host, lang), - description: this.translate(cookie.description, lang), - expiration: this.translate(cookie.expiration, lang), - type: getTranslation(`type_${cookie.type}`, lang), - } - ); - }); - - return tableRows; - } - getCookieGroupsHtml(cookieGroupList, lang, translations, groupRequired, groupName, acceptedGroups) { let groupsHtml = ''; let groupNumber = 0; cookieGroupList.forEach(cookieGroup => { - const title = this.translate(cookieGroup.title, lang); - const description = this.translate(cookieGroup.description, lang); + const title = this.translateSetting(cookieGroup.title, lang); + const description = this.translateSetting(cookieGroup.description, lang); const {groupId} = cookieGroup; - const tableRowsHtml = this.buildTableRows(cookieGroup.cookies, lang); + + // Build table rows + let tableRowsHtml = ''; + cookieGroup.cookies.forEach(cookie => { + tableRowsHtml += getTableRowHtml( + { + name: this.translateSetting(cookie.name, lang), + host: this.translateSetting(cookie.host, lang), + description: this.translateSetting(cookie.description, lang), + expiration: this.translateSetting(cookie.expiration, lang), + type: getTranslation(`type_${cookie.type}`, lang), + } + ); + }); + const isAccepted = acceptedGroups.includes(cookieGroup.groupId); groupsHtml += getGroupHtml({...translations, title, description }, groupId, `${groupName}_${groupNumber}`, tableRowsHtml, groupRequired, isAccepted); groupNumber += 1; @@ -386,7 +373,7 @@ class HdsCc { }); } - async createShadowRoot(lang, cookieSettings) { + async renderBanner(lang, cookieSettings) { const targetSelector = window.hdsCookieConsentPageSettings.targetSelector || 'body'; const bannerTarget = document.querySelector(targetSelector); if (!bannerTarget) { @@ -481,36 +468,32 @@ class HdsCc { // Add chat cookie functions to window async createChatConsentAPI() { const chatUserConsent = { + retrieveUserConsent() { return this.isGroupAccepted('chat'); }, + async confirmUserConsent() { const showBanner = true; - let browserCookieState = null; + // Check if our cookie exists try { - try { - browserCookieState = JSON.parse(parse(document.cookie)[this.COOKIE_NAME]); - } catch (err) { - // Doesn't handle the state where form is open but cookie doesn't exist - console.log('no cookie:D'); + const browserCookie = this.getCookie(); + + let currentlyAccepted = []; + + if (browserCookie) { + currentlyAccepted = Object.keys(browserCookie.groups); } - // This is duplicate code from getCookieSettings - // TODO: refactor the function return values, refactor showBanner logic - const currentlyAccepted = Object.keys(browserCookieState.groups); - const cookieSettingsRaw = await fetch(window.hdsCookieConsentPageSettings.jsonUrl).then((response) => response.text()); - const cookieSettingsChecksum = await this.getChecksum(cookieSettingsRaw); - const cookieSettings = JSON.parse(cookieSettingsRaw); - cookieSettings.checksum = cookieSettingsChecksum; + const cookieSettings = await this.getCookieSettingsFromJsonFile(); const cookieSettingsGroups = [...cookieSettings.requiredCookies.groups, ...cookieSettings.optionalCookies.groups]; - // eslint-disable-next-line no-restricted-syntax - for (const group of cookieSettingsGroups) { - // eslint-disable-next-line no-await-in-loop - const groupChecksum = await this.getChecksum(group); - group.checksum = groupChecksum; - } + // Checksums for all groups calculated in parallel without waiting for each + await Promise.all(cookieSettingsGroups.map(async (group) => { + group.checksum = await this.getChecksum(group); + })); + this.saveAcceptedGroups(cookieSettings, [...currentlyAccepted, 'chat'], showBanner); } catch (err) { // If consent setting fails for some reason @@ -530,28 +513,22 @@ class HdsCc { async init() { const lang = window.hdsCookieConsentPageSettings.language; - // If cookie settings can't be loaded, do not show banner - let cookieSettings; - try { - cookieSettings = await this.getCookieSettings(); - } catch (err) { - throw new Error(`Cookie settings not available, cookie banner won't render: \n${err}`); - } + const cookieSettings = await this.getCookieSettings(); if (window.hdsCookieConsentPageSettings.exposeChatFunctions) { this.createChatConsentAPI(); // Create chat consent functions } - // TODO: consider naming // If cookie settings have not changed, do not show banner, otherwise, check - const showBanner = await this.checkBannerNeed(cookieSettings); - if (showBanner) { - await this.createShadowRoot(lang, cookieSettings); + const shouldDisplayBanner = await this.shouldDisplayBanner(cookieSettings); + if (shouldDisplayBanner) { + await this.renderBanner(lang, cookieSettings); } }; } -// eslint-disable-next-line no-unused-vars -const hdsCc = new HdsCc(); +window.hdsCookieConsents = new HDSCookieConsent(); +// Debug helper. Open banner and run on console to see the box updated. +window.aaa_chatcheck = () => dispatchEvent(new CustomEvent('CONSENTS_CHANGED', { detail: { groups: ['chat'] } })); diff --git a/src/js/hds-cc/hds-cc_translations.js b/src/js/hds-cc/hds-cc_translations.js index 5bd341e736..07cf64cbdc 100644 --- a/src/js/hds-cc/hds-cc_translations.js +++ b/src/js/hds-cc/hds-cc_translations.js @@ -128,7 +128,7 @@ export function getTranslation(key, lang, parameters) { } // Find translation based on key, fallback to English - const translation = translations[key] ? translations[key][lang] || translations[key].en : null; + const translation = translations[key]?.[lang] || translations[key]?.en || null; if (translation) { @@ -138,12 +138,15 @@ export function getTranslation(key, lang, parameters) { const parameter = index(parameters, stripDollarAndParenthesis); // Parameters may be either string or an language object - if (typeof parameter === 'object' && parameter[lang]) { - return parameter[lang]; - } - // Use English as fallback language where possible - if (typeof parameter === 'object' && parameter.en) { - return parameter.en; + if (typeof parameter === 'object') { + if (parameter[lang]) { + return parameter[lang]; + } + + // Use English as fallback language where possible + if (parameter.en) { + return parameter.en; + } } return parameter; }); diff --git a/templates/misc/cookie-container.html.twig b/templates/misc/cookie-container.html.twig index e64e7b96b6..e4fd45e119 100644 --- a/templates/misc/cookie-container.html.twig +++ b/templates/misc/cookie-container.html.twig @@ -1,10 +1,10 @@ + - From 2f19045b74e16c2756d2a663fb227530fa9d87f8 Mon Sep 17 00:00:00 2001 From: Mikko Tapionlinna Date: Tue, 2 Apr 2024 15:50:33 +0300 Subject: [PATCH 04/13] UHF-9791: UHF-9791: Fix chat API function to be non-async and get rid of the event to update checkboxes --- src/js/hds-cc/hds-cc.js | 124 ++++++++++++++++------------------------ 1 file changed, 49 insertions(+), 75 deletions(-) diff --git a/src/js/hds-cc/hds-cc.js b/src/js/hds-cc/hds-cc.js index d5f75a3a28..22b76f529b 100644 --- a/src/js/hds-cc/hds-cc.js +++ b/src/js/hds-cc/hds-cc.js @@ -45,6 +45,7 @@ class HDSCookieConsent { this.UNCHANGED = 'unchanged'; this.ESSENTIAL_GROUP_NAME = 'essential'; + this.shadowRoot = null; this.cookie_name = 'city-of-helsinki-cookie-consents'; // Overridable default value if (document.readyState === 'loading') { @@ -125,6 +126,20 @@ class HDSCookieConsent { }; this.setCookie(data); + + // Update the checkboxes to reflect the new state (if called outside) + if (this.shadowRoot) { + const form = this.shadowRoot.querySelector('form'); + if (form) { + const formCheckboxes = form.querySelectorAll('input'); + + formCheckboxes.forEach(check => { + if (acceptedGroupNames.includes(check.dataset.group)) { + check.checked = true; + } + }); + } + } } async removeInvalidGroupsFromCookie(cookieSettingsGroups, browserCookieState, cookieSettings) { @@ -363,40 +378,6 @@ class HDSCookieConsent { return groupsHtml; } - /* - * ================================================================ - * ===== ======== - * ===== INIT SEQUENCE ======== - * ===== ======== - * ================================================================ - */ - - /** - * Control form checkbox states - * - * Due to shadowroot approach we use window scoped custom events - * to trigger the form checking in cases where the form is open - * and user gives chat consent from chat window instead of the - * form checkboxes. The form reference is passed here on init phase. - */ - controlForm(form) { - const checkGroupBox = (groups) => { - const formCheckboxes = form.querySelectorAll('input'); - - formCheckboxes.forEach(check => { - if (groups.includes(check.dataset.group)) { - check.checked = true; - } - }); - }; - - window.addEventListener('CONSENTS_CHANGED', e => { - // TODO: Remove this console.log - console.log(`CONSENTS_CHANGED event received. Groups: '${ e.detail.groups }'`); - checkGroupBox(e.detail.groups); - }); - } - async renderBanner(lang, cookieSettings) { const bannerTarget = document.querySelector(this.TARGET_SELECTOR); if (!bannerTarget) { @@ -415,6 +396,7 @@ class HDSCookieConsent { bannerContainer.style.all = 'initial'; bannerTarget.prepend(bannerContainer); const shadowRoot = bannerContainer.attachShadow({ mode: 'open' }); + this.shadowRoot = shadowRoot; // TODO: Replace this temporary CSS file hack with proper preprocess CSS inlining // Fetch the external CSS file @@ -481,59 +463,55 @@ class HDSCookieConsent { this.handleButtonEvents(e.target.dataset.approved, shadowRootForm, cookieSettings); })); - // Add eventHandler for form - this.controlForm(shadowRootForm); - shadowRoot.querySelector('.hds-cc').focus(); } - // Add chat cookie functions to window + // Add chat cookie functions to window scope async createChatConsentAPI() { - const chatUserConsent = { - - retrieveUserConsent() { - return this.isGroupAccepted('chat'); - }, + const that = this; - async confirmUserConsent() { - const showBanner = true; + async function asyncConfirmUserConsent() { + const browserCookie = await that.getCookie(); + const showBanner = browserCookie?.showBanner || false; - // Check if our cookie exists - try { - const browserCookie = this.getCookie(); + // If cookie is set, get the accepted groups + let currentlyAccepted = []; + if (browserCookie) { + currentlyAccepted = Object.keys(browserCookie.groups); + } - let currentlyAccepted = []; + // Try to fetch the cookie settings from the JSON file + let cookieSettings = null; + try { + cookieSettings = await that.getCookieSettingsFromJsonFile(); + } catch (err) { + console.error(`Cookie consent: Unable to fetch cookie consent settings: ${err}`); + return false; + } - if (browserCookie) { - currentlyAccepted = Object.keys(browserCookie.groups); - } + const cookieSettingsGroups = [...cookieSettings.requiredCookies.groups, ...cookieSettings.optionalCookies.groups]; - const cookieSettings = await this.getCookieSettingsFromJsonFile(); - const cookieSettingsGroups = [...cookieSettings.requiredCookies.groups, ...cookieSettings.optionalCookies.groups]; + // Checksums for all groups calculated in parallel without waiting for each + await Promise.all(cookieSettingsGroups.map(async (group) => { + group.checksum = await that.getChecksum(group); + })); - // Checksums for all groups calculated in parallel without waiting for each - await Promise.all(cookieSettingsGroups.map(async (group) => { - group.checksum = await this.getChecksum(group); - })); + // Save chat group to accepted groups and update checkbox state + that.saveAcceptedGroups(cookieSettings, [...currentlyAccepted, 'chat'], showBanner); + } - this.saveAcceptedGroups(cookieSettings, [...currentlyAccepted, 'chat'], showBanner); - } catch (err) { - // If consent setting fails for some reason - console.log('Consent failed.\n', err); - return false; - } + window.chat_user_consent = { + retrieveUserConsent() { + return this.isGroupAccepted('chat'); + }, - // Doesn't handle the state where form is open but cookie doesn't exist - // See controlForm-function for more information about this - dispatchEvent(new CustomEvent('CONSENTS_CHANGED', { detail: { groups: ['chat'] } })); - } + confirmUserConsent() { + asyncConfirmUserConsent(); + }, }; - - window.chat_user_consent = chatUserConsent; } async init() { - const cookieSettings = await this.getCookieSettings(); if (this.EXPOSE_CHAT_FUNCTIONS) { @@ -550,7 +528,3 @@ class HDSCookieConsent { window.HDSCookieConsent = HDSCookieConsent; -// TODO: Remove this debug tool -// Debug helper. Open banner and run on console to see the box updated. -window.aaa_chatcheck = () => dispatchEvent(new CustomEvent('CONSENTS_CHANGED', { detail: { groups: ['chat'] } })); - From 948aa5edc1d2fbfd00882fda90b7747a901411f6 Mon Sep 17 00:00:00 2001 From: Mikko Tapionlinna Date: Wed, 3 Apr 2024 14:38:44 +0300 Subject: [PATCH 05/13] UHF-9791: UHF-9791: Make class methods and properties private, remove expose-chat-functions --- src/js/hds-cc/hds-cc.js | 320 ++++++++++++---------- templates/misc/cookie-container.html.twig | 11 +- 2 files changed, 184 insertions(+), 147 deletions(-) diff --git a/src/js/hds-cc/hds-cc.js b/src/js/hds-cc/hds-cc.js index 22b76f529b..b354130eeb 100644 --- a/src/js/hds-cc/hds-cc.js +++ b/src/js/hds-cc/hds-cc.js @@ -18,7 +18,17 @@ import { getTranslationKeys, } from './hds-cc_translations'; -class HDSCookieConsent { +class HdsCookieConsentClass { + #SITE_SETTINGS_JSON_URL; + #LANGUAGE; + #TARGET_SELECTOR; + #SPACER_PARENT_SELECTOR; + #PAGE_CONTENT_SELECTOR; + #TEMP_CSS_PATH; + #COOKIE_DAYS; + #UNCHANGED; + #ESSENTIAL_GROUP_NAME; + constructor( { siteSettingsJsonUrl, // Path to JSON file with cookie settings @@ -26,35 +36,92 @@ class HDSCookieConsent { targetSelector = 'body', // Where to inject the banner spacerParentSelector = 'body', // Where to inject the spacer pageContentSelector = 'body', // Where to add scroll-margin-bottom - exposeChatFunctions = false, // Expose chat consent functions to window object tempCssPath = '/path/to/external.css', // TODO: Remove this tempoarry path to external CSS file } ) { if (!siteSettingsJsonUrl) { throw new Error('Cookie consent: siteSettingsJsonUrl is required'); } - this.SITE_SETTINGS_JSON_URL = siteSettingsJsonUrl; - this.LANGUAGE = language; - this.TARGET_SELECTOR = targetSelector; - this.SPACER_PARENT_SELECTOR = spacerParentSelector; - this.PAGE_CONTENT_SELECTOR = pageContentSelector; - this.EXPOSE_CHAT_FUNCTIONS = exposeChatFunctions; - this.TEMP_CSS_PATH = tempCssPath; - - this.COOKIE_DAYS = 100; - this.UNCHANGED = 'unchanged'; - this.ESSENTIAL_GROUP_NAME = 'essential'; + this.#SITE_SETTINGS_JSON_URL = siteSettingsJsonUrl; + this.#LANGUAGE = language; + this.#TARGET_SELECTOR = targetSelector; + this.#SPACER_PARENT_SELECTOR = spacerParentSelector; + this.#PAGE_CONTENT_SELECTOR = pageContentSelector; + this.#TEMP_CSS_PATH = tempCssPath; + + this.#COOKIE_DAYS = 100; + this.#UNCHANGED = 'unchanged'; + this.#ESSENTIAL_GROUP_NAME = 'essential'; this.shadowRoot = null; this.cookie_name = 'city-of-helsinki-cookie-consents'; // Overridable default value + window.hds = window.hds || {}; + window.hds.cookieConsent = this; + if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { - this.init(); + this.#init(); }); } else { - this.init(); + this.#init(); + } + } + + /** + * Get the consent status for the specified cookie group names. + * @param {string[]} groupNamesArray - An array of group names. + * @returns {Promise} A promise that resolves to true if all the groups are accepted, otherwise false. + */ + getConsentStatus(groupNamesArray) { + + // Check if group names are provided as an array and not empty + if (!Array.isArray(groupNamesArray) || groupNamesArray.length === 0) { + console.error('Cookie consent: Group names must be provided as an non-empty array.'); + return false; + } + + const browserCookieState = this.#getCookie(); + + // Check if our cookie exists and has groups set + if (!browserCookieState || !browserCookieState.groups) { + // console.log('Cookie is not set properly', browserCookieState, browserCookieState.groups); + return false; + } + + // Check if all groups are in accepted groups + return groupNamesArray.every(groupName => !!browserCookieState.groups[groupName]); + } + + async setGroupsStatusToAccepted(acceptedGroupsArray) { + const browserCookie = this.#getCookie(); + const showBanner = browserCookie?.showBanner || false; + + // If cookie is set, get the accepted groups + let currentlyAccepted = []; + if (browserCookie) { + currentlyAccepted = Object.keys(browserCookie.groups); + } + + // Try to fetch the cookie settings from the JSON file + let cookieSettings = null; + try { + cookieSettings = await this.#getCookieSettingsFromJsonFile(); + } catch (err) { + console.error(`Cookie consent: Unable to fetch cookie consent settings: ${err}`); + return false; } + + const cookieSettingsGroups = [...cookieSettings.requiredCookies.groups, ...cookieSettings.optionalCookies.groups]; + + // Checksums for all groups calculated in parallel without waiting for each + await Promise.all(cookieSettingsGroups.map(async (group) => { + group.checksum = await this.#getChecksum(group); + })); + + // Save chat group to accepted groups and update checkbox state + this.#saveAcceptedGroups(cookieSettings, [...currentlyAccepted, ...acceptedGroupsArray], showBanner); + return true; } /** @@ -64,7 +131,7 @@ class HDSCookieConsent { * * Reference: https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest */ - async getChecksum(message, length = 8) { + async #getChecksum(message, length = 8) { let messageString = message; if (typeof message !== 'string') { messageString = JSON.stringify(message); @@ -77,15 +144,11 @@ class HDSCookieConsent { return hashHex.substring(0, length); } - /** - * Cookie section - */ - /** * Centralized browser cookie reading. Returns false if the cookie * with specified name doesn't exist. */ - async getCookie() { + #getCookie() { try { const cookieString = parse(document.cookie)[this.cookie_name]; if (!cookieString) { @@ -99,15 +162,30 @@ class HDSCookieConsent { } } - async setCookie(cookieData) { - // console.log('setCookie', cookieData); + + /** + * Sets a cookie with the provided data. + * + * @private + * @param {Object} cookieData - The data to be stored in the cookie. + */ + #setCookie(cookieData) { + // console.log('#setCookie', cookieData); const expiryDate = new Date(); - expiryDate.setDate(expiryDate.getDate() + this.COOKIE_DAYS); + expiryDate.setDate(expiryDate.getDate() + this.#COOKIE_DAYS); document.cookie = serialize(this.cookie_name, JSON.stringify(cookieData), { expires: expiryDate }); } - saveAcceptedGroups(cookieSettings, acceptedGroupNames = [], showBanner = false) { - // console.log('Accepting cookies from groups:', acceptedGroupNames); + + /** + * Saves the accepted cookie groups to cookie, unsets others. + * + * @param {Object} cookieSettings - Site specific settings + * @param {Array} acceptedGroupNames - The names of the accepted cookie groups. + * @param {boolean} showBanner - Whether to show the banner or not. + */ + #saveAcceptedGroups(cookieSettings, acceptedGroupNames = [], showBanner = false) { + // console.log('Saving accepted cookie groups:', acceptedGroupNames); const acceptedGroups = {}; @@ -125,7 +203,7 @@ class HDSCookieConsent { ...(showBanner && { showBanner: true }), // Only add showBanner if it's true }; - this.setCookie(data); + this.#setCookie(data); // Update the checkboxes to reflect the new state (if called outside) if (this.shadowRoot) { @@ -142,8 +220,8 @@ class HDSCookieConsent { } } - async removeInvalidGroupsFromCookie(cookieSettingsGroups, browserCookieState, cookieSettings) { - // console.log('removeInvalidGroupsFromCookie', cookieSettingsGroups, browserCookieState, cookieSettings); + async #removeInvalidGroupsFromCookie(cookieSettingsGroups, browserCookieState, cookieSettings) { + // console.log('#removeInvalidGroupsFromCookie', cookieSettingsGroups, browserCookieState, cookieSettings); const newCookieGroups = []; @@ -172,22 +250,22 @@ class HDSCookieConsent { // Because global checksum did not match, group checksums were checked and non-matching groups were removed, save the cleaned cookie const showBanner = true; - this.saveAcceptedGroups(cookieSettings, newCookieGroups, showBanner); + this.#saveAcceptedGroups(cookieSettings, newCookieGroups, showBanner); return cookieSettings; } - async getCookieSettingsFromJsonFile() { + async #getCookieSettingsFromJsonFile() { // Fetch the site settings JSON file let cookieSettingsRaw; try { - cookieSettingsRaw = await fetch(this.SITE_SETTINGS_JSON_URL).then((response) => response.text()); + cookieSettingsRaw = await fetch(this.#SITE_SETTINGS_JSON_URL).then((response) => response.text()); } catch (error) { throw new Error(`Cookie consent: Unable to fetch cookie consent settings: ${error}`); } // Calculate checksum for the site settings string - const cookieSettingsChecksum = await this.getChecksum(cookieSettingsRaw); + const cookieSettingsChecksum = await this.#getChecksum(cookieSettingsRaw); // Parse the fetched site settings string to JSON let cookieSettings; @@ -202,30 +280,30 @@ class HDSCookieConsent { return cookieSettings; } - async getCookieSettings() { - const cookieSettings = await this.getCookieSettingsFromJsonFile(); + async #getCookieSettings() { + const cookieSettings = await this.#getCookieSettingsFromJsonFile(); this.cookie_name = cookieSettings.cookieName || this.cookie_name; // Optional override for cookie name // Compare file checksum with browser cookie checksum if the file has not changed and return false for no change (no banner needed) - const browserCookie = await this.getCookie(); + const browserCookie = this.#getCookie(); if (browserCookie) { // Check if settings have not changed and browser cookie has 'showBanner' set to false if (!browserCookie.showBanner && (cookieSettings.checksum === browserCookie.checksum)) { console.log('Settings were unchanged'); - return this.UNCHANGED; + return this.#UNCHANGED; } } - const essentialGroup = cookieSettings.requiredCookies.groups.find(group => group.groupId === this.ESSENTIAL_GROUP_NAME); + const essentialGroup = cookieSettings.requiredCookies.groups.find(group => group.groupId === this.#ESSENTIAL_GROUP_NAME); if (!essentialGroup) { // The site cookie settings must have required group named by ESSENTIAL_GROUP_NAME - throw new Error(`Cookie consent error: '${this.ESSENTIAL_GROUP_NAME}' group missing`); + throw new Error(`Cookie consent error: '${this.#ESSENTIAL_GROUP_NAME}' group missing`); } const requiredCookieFound = essentialGroup.cookies.find(cookie => cookie.name === this.cookie_name); if (!requiredCookieFound) { // The required "essential" group must have cookie with name matching the root level 'cookieName' - throw new Error(`Cookie consent error: Missing cookie entry for '${this.cookie_name}' in group '${this.ESSENTIAL_GROUP_NAME}'`); + throw new Error(`Cookie consent error: Missing cookie entry for '${this.cookie_name}' in group '${this.#ESSENTIAL_GROUP_NAME}'`); } const cookieSettingsGroups = [...cookieSettings.requiredCookies.groups, ...cookieSettings.optionalCookies.groups]; @@ -241,44 +319,43 @@ class HDSCookieConsent { // Checksums for all groups calculated in parallel without waiting for each await Promise.all(cookieSettingsGroups.map(async (group) => { - group.checksum = await this.getChecksum(group); // This await is needed to ensure that all checksums are calculated before continuing + group.checksum = await this.#getChecksum(group); // This await is needed to ensure that all checksums are calculated before continuing })); - return this.removeInvalidGroupsFromCookie(cookieSettingsGroups, browserCookie, cookieSettings); + return this.#removeInvalidGroupsFromCookie(cookieSettingsGroups, browserCookie, cookieSettings); } - handleButtonEvents(selection, formReference, cookieSettings) { - - /** - * Go through form and get accepted groups. Return a list of group ID's. - */ - function readGroupSelections(form, all = false) { - const groupSelections = []; - const formCheckboxes = form.querySelectorAll('input'); - formCheckboxes.forEach(check => { - if(check.checked || all) { - groupSelections.push(check.dataset.group); - } - }); + /** + * Go through form and get accepted groups. Return a list of group ID's. + */ + #readGroupSelections(form, all = false) { + const groupSelections = []; + const formCheckboxes = form.querySelectorAll('input'); + formCheckboxes.forEach(check => { + if(check.checked || all) { + groupSelections.push(check.dataset.group); + } + }); - return groupSelections; - } + return groupSelections; + } + #handleButtonEvents(selection, formReference, cookieSettings) { switch (selection) { case 'required': { - const acceptedGroups = [this.ESSENTIAL_GROUP_NAME]; - this.saveAcceptedGroups(cookieSettings, acceptedGroups); + const acceptedGroups = [this.#ESSENTIAL_GROUP_NAME]; + this.#saveAcceptedGroups(cookieSettings, acceptedGroups); break; } case 'all': { - const acceptedGroups = readGroupSelections(formReference, true); - this.saveAcceptedGroups(cookieSettings, acceptedGroups); + const acceptedGroups = this.#readGroupSelections(formReference, true); + this.#saveAcceptedGroups(cookieSettings, acceptedGroups); break; } case 'selected': { - const acceptedGroups = readGroupSelections(formReference); - this.saveAcceptedGroups(cookieSettings, acceptedGroups); + const acceptedGroups = this.#readGroupSelections(formReference); + this.#saveAcceptedGroups(cookieSettings, acceptedGroups); break; } default: @@ -288,32 +365,19 @@ class HDSCookieConsent { window.location.reload(); } - async areTheseGroupsAccepted(groupNamesArray) { - const browserCookieState = await this.getCookie(); - - // Check if our cookie exists and has groups set - if (!browserCookieState || !browserCookieState.groups) { - console.log('Cookie is not set properly', browserCookieState, browserCookieState.groups); - return false; - } - - // Check if all groups are in accepted groups - return groupNamesArray.every(groupName => !!browserCookieState.groups[groupName]); - } - /** * logic for cookie banner spawn * compare cookieSettings and browser cookie state * check 1. if cookie exists 2. essentials approved 3. hash match - show banner * else show banner */ - async shouldDisplayBanner(cookieSettings) { - if (cookieSettings !== this.UNCHANGED) { + #shouldDisplayBanner(cookieSettings) { + if (cookieSettings !== this.#UNCHANGED) { console.log('Cookie settings changed since approval, show banner'); return true; } - const browserCookie = await this.getCookie(); + const browserCookie = this.#getCookie(); if (!browserCookie) { console.log('Cookie doesn\'t exist, show banner'); return true; @@ -339,7 +403,7 @@ class HDSCookieConsent { * @param {string} lang - Language key, e.g., 'fi' for Finnish. * @return {string} - Translated string based on the provided language key, or the original string if `translationObj` is not an object. */ - translateSetting(translationObj, lang) { + #translateSetting(translationObj, lang) { if (typeof (translationObj) === 'object') { if (translationObj[lang] === undefined) { return translationObj.en; // fallback to English translation @@ -349,12 +413,12 @@ class HDSCookieConsent { return translationObj; } - getCookieGroupsHtml(cookieGroupList, lang, translations, groupRequired, groupName, acceptedGroups) { + #getCookieGroupsHtml(cookieGroupList, lang, translations, groupRequired, groupName, acceptedGroups) { let groupsHtml = ''; let groupNumber = 0; cookieGroupList.forEach(cookieGroup => { - const title = this.translateSetting(cookieGroup.title, lang); - const description = this.translateSetting(cookieGroup.description, lang); + const title = this.#translateSetting(cookieGroup.title, lang); + const description = this.#translateSetting(cookieGroup.description, lang); const {groupId} = cookieGroup; // Build table rows @@ -362,10 +426,10 @@ class HDSCookieConsent { cookieGroup.cookies.forEach(cookie => { tableRowsHtml += getTableRowHtml( { - name: this.translateSetting(cookie.name, lang), - host: this.translateSetting(cookie.host, lang), - description: this.translateSetting(cookie.description, lang), - expiration: this.translateSetting(cookie.expiration, lang), + name: this.#translateSetting(cookie.name, lang), + host: this.#translateSetting(cookie.host, lang), + description: this.#translateSetting(cookie.description, lang), + expiration: this.#translateSetting(cookie.expiration, lang), type: getTranslation(`type_${cookie.type}`, lang), } ); @@ -378,17 +442,17 @@ class HDSCookieConsent { return groupsHtml; } - async renderBanner(lang, cookieSettings) { - const bannerTarget = document.querySelector(this.TARGET_SELECTOR); + async #renderBanner(lang, cookieSettings) { + const bannerTarget = document.querySelector(this.#TARGET_SELECTOR); if (!bannerTarget) { - throw new Error(`Cookie consent: targetSelector element '${this.TARGET_SELECTOR}' was not found`); + throw new Error(`Cookie consent: targetSelector element '${this.#TARGET_SELECTOR}' was not found`); } - const spacerParent = document.querySelector(this.SPACER_PARENT_SELECTOR); + const spacerParent = document.querySelector(this.#SPACER_PARENT_SELECTOR); if (!spacerParent) { - throw new Error(`Cookie consent: The spacerParentSelector element '${this.SPACER_PARENT_SELECTOR}' was not found`); + throw new Error(`Cookie consent: The spacerParentSelector element '${this.#SPACER_PARENT_SELECTOR}' was not found`); } - if (!document.querySelector(this.PAGE_CONTENT_SELECTOR)) { - throw new Error(`Cookie consent: contentSelector element '${this.PAGE_CONTENT_SELECTOR}' was not found`); + if (!document.querySelector(this.#PAGE_CONTENT_SELECTOR)) { + throw new Error(`Cookie consent: contentSelector element '${this.#PAGE_CONTENT_SELECTOR}' was not found`); } const bannerContainer = document.createElement('div'); @@ -401,7 +465,7 @@ class HDSCookieConsent { // TODO: Replace this temporary CSS file hack with proper preprocess CSS inlining // Fetch the external CSS file try { - const response = await fetch(this.TEMP_CSS_PATH); + const response = await fetch(this.#TEMP_CSS_PATH); const cssText = await response.text(); // Create and inject the style @@ -422,22 +486,22 @@ class HDSCookieConsent { let listOfAcceptedGroups = []; try { // Check which cookies are accepted at this point to check boxes on form render. - browserCookie = await this.getCookie(); + browserCookie = this.#getCookie(); listOfAcceptedGroups = [...Object.keys(browserCookie.groups)]; } catch (err) { // There was no cookie, the list stays empty. } let groupsHtml = ''; - groupsHtml += this.getCookieGroupsHtml(cookieSettings.requiredCookies.groups, lang, translations, true, 'required', listOfAcceptedGroups); - groupsHtml += this.getCookieGroupsHtml(cookieSettings.optionalCookies.groups, lang, translations, false, 'optional', listOfAcceptedGroups); + groupsHtml += this.#getCookieGroupsHtml(cookieSettings.requiredCookies.groups, lang, translations, true, 'required', listOfAcceptedGroups); + groupsHtml += this.#getCookieGroupsHtml(cookieSettings.optionalCookies.groups, lang, translations, false, 'optional', listOfAcceptedGroups); // Create banner HTML shadowRoot.innerHTML += getCookieBannerHtml(translations, groupsHtml); // Add scroll-margin-bottom to all elements inside the contentSelector const style = document.createElement('style'); - style.innerHTML = `${this.PAGE_CONTENT_SELECTOR} * {scroll-margin-bottom: calc(var(--hds-cc-height, -8px) + 8px);}`; + style.innerHTML = `${this.#PAGE_CONTENT_SELECTOR} * {scroll-margin-bottom: calc(var(--hds-cc-height, -8px) + 8px);}`; document.head.appendChild(style); // Add spacer inside spacerParent (to the bottom of the page) @@ -460,71 +524,37 @@ class HDSCookieConsent { const cookieButtons = shadowRoot.querySelectorAll('button[type=submit]'); const shadowRootForm = shadowRoot.querySelector('form'); cookieButtons.forEach(button => button.addEventListener('click', e => { - this.handleButtonEvents(e.target.dataset.approved, shadowRootForm, cookieSettings); + this.#handleButtonEvents(e.target.dataset.approved, shadowRootForm, cookieSettings); })); shadowRoot.querySelector('.hds-cc').focus(); } // Add chat cookie functions to window scope - async createChatConsentAPI() { - const that = this; - - async function asyncConfirmUserConsent() { - const browserCookie = await that.getCookie(); - const showBanner = browserCookie?.showBanner || false; - - // If cookie is set, get the accepted groups - let currentlyAccepted = []; - if (browserCookie) { - currentlyAccepted = Object.keys(browserCookie.groups); - } - - // Try to fetch the cookie settings from the JSON file - let cookieSettings = null; - try { - cookieSettings = await that.getCookieSettingsFromJsonFile(); - } catch (err) { - console.error(`Cookie consent: Unable to fetch cookie consent settings: ${err}`); - return false; - } - - const cookieSettingsGroups = [...cookieSettings.requiredCookies.groups, ...cookieSettings.optionalCookies.groups]; - - // Checksums for all groups calculated in parallel without waiting for each - await Promise.all(cookieSettingsGroups.map(async (group) => { - group.checksum = await that.getChecksum(group); - })); - - // Save chat group to accepted groups and update checkbox state - that.saveAcceptedGroups(cookieSettings, [...currentlyAccepted, 'chat'], showBanner); - } + async #createChatConsentAPI() { + const cookieConsentReference = this; window.chat_user_consent = { retrieveUserConsent() { - return this.isGroupAccepted('chat'); + return this.getConsentStatus(['chat']); }, - confirmUserConsent() { - asyncConfirmUserConsent(); + async confirmUserConsent() { + return cookieConsentReference.setGroupsStatusToAccepted(['chat']); }, }; } - async init() { - const cookieSettings = await this.getCookieSettings(); - - if (this.EXPOSE_CHAT_FUNCTIONS) { - this.createChatConsentAPI(); // Create chat consent functions - } + async #init() { + const cookieSettings = await this.#getCookieSettings(); // If cookie settings have not changed, do not show banner, otherwise, check - const shouldDisplayBanner = await this.shouldDisplayBanner(cookieSettings); + const shouldDisplayBanner = this.#shouldDisplayBanner(cookieSettings); if (shouldDisplayBanner) { - await this.renderBanner(this.LANGUAGE, cookieSettings); + await this.#renderBanner(this.#LANGUAGE, cookieSettings); } }; } - -window.HDSCookieConsent = HDSCookieConsent; +window.hds = window.hds || {}; +window.hds.CookieConsentClass = HdsCookieConsentClass; diff --git a/templates/misc/cookie-container.html.twig b/templates/misc/cookie-container.html.twig index 92f2e6c8ef..83dc8bb156 100644 --- a/templates/misc/cookie-container.html.twig +++ b/templates/misc/cookie-container.html.twig @@ -8,14 +8,21 @@ //targetSelector: 'body', // Defaults to 'body' //spacerParentSelector: 'body', // Defaults to 'body' //pageContentSelector: 'body', // Defaults to 'body' - exposeChatFunctions: true, // Defaults to false tempCssPath: '{{ theme_path }}/dist/css/cookie-consent.min.css', // TODO: Remove this when the real build process can include css files } }; const script = document.createElement('script'); script.src = conf.src; script.onerror = console.error; - script.onload = () => new window.HDSCookieConsent(conf.options); + script.onload = () => new window.hds.CookieConsentClass(conf.options); document.head.appendChild(script); })(); + +{# HDBT specific stuff #} + From be3c19bd69a6d551fe64d4c777d67617f3d6bbcf Mon Sep 17 00:00:00 2001 From: Mikko Tapionlinna Date: Thu, 4 Apr 2024 09:10:16 +0300 Subject: [PATCH 06/13] UHF-9791: UHF-9791: Support SPA approach with closeable banner and triggered events --- src/js/hds-cc/hds-cc.js | 72 ++++++++++++++++------- src/scss/styles.scss | 2 +- templates/misc/cookie-container.html.twig | 10 +++- 3 files changed, 60 insertions(+), 24 deletions(-) diff --git a/src/js/hds-cc/hds-cc.js b/src/js/hds-cc/hds-cc.js index b354130eeb..661785d3db 100644 --- a/src/js/hds-cc/hds-cc.js +++ b/src/js/hds-cc/hds-cc.js @@ -28,6 +28,7 @@ class HdsCookieConsentClass { #COOKIE_DAYS; #UNCHANGED; #ESSENTIAL_GROUP_NAME; + #SUBMIT_EVENT = false; constructor( { @@ -36,6 +37,7 @@ class HdsCookieConsentClass { targetSelector = 'body', // Where to inject the banner spacerParentSelector = 'body', // Where to inject the spacer pageContentSelector = 'body', // Where to add scroll-margin-bottom + submitEvent = false, // if string, do not reload page, but submit the string as event after consent tempCssPath = '/path/to/external.css', // TODO: Remove this tempoarry path to external CSS file } ) { @@ -48,12 +50,21 @@ class HdsCookieConsentClass { this.#SPACER_PARENT_SELECTOR = spacerParentSelector; this.#PAGE_CONTENT_SELECTOR = pageContentSelector; this.#TEMP_CSS_PATH = tempCssPath; + this.#SUBMIT_EVENT = submitEvent; this.#COOKIE_DAYS = 100; this.#UNCHANGED = 'unchanged'; this.#ESSENTIAL_GROUP_NAME = 'essential'; this.shadowRoot = null; + this.bannerElements = { + bannerContainer: null, + spacer: null, + }; + this.resizeReference = { + resizeObserver: null, + bannerHeightElement: null, + }; this.cookie_name = 'city-of-helsinki-cookie-consents'; // Overridable default value window.hds = window.hds || {}; @@ -163,6 +174,26 @@ class HdsCookieConsentClass { } + /** + * Removes the banner elements from DOM like the shadow root and spacer. + */ + #removeBanner() { + // Remove banner size observer + if (this.resizeReference.resizeObserver && this.resizeReference.bannerHeightElement) { + this.resizeReference.resizeObserver.unobserve(this.resizeReference.bannerHeightElement); + } + // Remove banner elements + if (this.bannerElements.bannerContainer) { + this.bannerElements.bannerContainer.remove(); + } + if (this.bannerElements.spacer) { + this.bannerElements.spacer.remove(); + } + // Remove scroll-margin-bottom variable from all elements inside the contentSelector + document.documentElement.style.removeProperty('--hds-cookie-consent-height'); + } + + /** * Sets a cookie with the provided data. * @@ -342,19 +373,20 @@ class HdsCookieConsentClass { } #handleButtonEvents(selection, formReference, cookieSettings) { + let acceptedGroups = []; switch (selection) { case 'required': { - const acceptedGroups = [this.#ESSENTIAL_GROUP_NAME]; + acceptedGroups = [this.#ESSENTIAL_GROUP_NAME]; this.#saveAcceptedGroups(cookieSettings, acceptedGroups); break; } case 'all': { - const acceptedGroups = this.#readGroupSelections(formReference, true); + acceptedGroups = this.#readGroupSelections(formReference, true); this.#saveAcceptedGroups(cookieSettings, acceptedGroups); break; } case 'selected': { - const acceptedGroups = this.#readGroupSelections(formReference); + acceptedGroups = this.#readGroupSelections(formReference); this.#saveAcceptedGroups(cookieSettings, acceptedGroups); break; } @@ -362,7 +394,13 @@ class HdsCookieConsentClass { // We should not be here, better do nothing break; } - window.location.reload(); + if (!this.#SUBMIT_EVENT) { + window.location.reload(); + } else { + window.dispatchEvent(new CustomEvent(this.#SUBMIT_EVENT, { detail: { acceptedGroups } })); + this.#removeBanner(); + } + } /** @@ -459,6 +497,7 @@ class HdsCookieConsentClass { bannerContainer.classList.add('hds-cc__target'); bannerContainer.style.all = 'initial'; bannerTarget.prepend(bannerContainer); + this.bannerElements.bannerContainer = bannerContainer; const shadowRoot = bannerContainer.attachShadow({ mode: 'open' }); this.shadowRoot = shadowRoot; @@ -501,24 +540,27 @@ class HdsCookieConsentClass { // Add scroll-margin-bottom to all elements inside the contentSelector const style = document.createElement('style'); - style.innerHTML = `${this.#PAGE_CONTENT_SELECTOR} * {scroll-margin-bottom: calc(var(--hds-cc-height, -8px) + 8px);}`; + style.innerHTML = `${this.#PAGE_CONTENT_SELECTOR} * {scroll-margin-bottom: calc(var(--hds-cookie-consent-height, -8px) + 8px);}`; document.head.appendChild(style); // Add spacer inside spacerParent (to the bottom of the page) const spacer = document.createElement('div'); + this.bannerElements.spacer = spacer; spacer.id = 'hds-cc__spacer'; spacerParent.appendChild(spacer); - spacer.style.height = 'var(--hds-cc-height, 0)'; + spacer.style.height = 'var(--hds-cookie-consent-height, 0)'; // Update spacer and scroll-margin-bottom on banner resize const resizeObserver = new ResizeObserver(entries => { entries.forEach(entry => { - document.documentElement.style.setProperty('--hds-cc-height', `${parseInt(entry.contentRect.height, 10) + parseInt(getComputedStyle(entry.target).borderTopWidth, 10)}px`); + document.documentElement.style.setProperty('--hds-cookie-consent-height', `${parseInt(entry.contentRect.height, 10) + parseInt(getComputedStyle(entry.target).borderTopWidth, 10)}px`); // spacer.style.height = `${entry.contentRect.height + parseInt(getComputedStyle(entry.target).borderTopWidth, 10)}px`; }); }); const bannerHeightElement = shadowRoot.querySelector('.hds-cc__container'); resizeObserver.observe(bannerHeightElement); + this.resizeReference.resizeObserver = resizeObserver; + this.resizeReference.bannerHeightElement = bannerHeightElement; // Add button events const cookieButtons = shadowRoot.querySelectorAll('button[type=submit]'); @@ -530,22 +572,8 @@ class HdsCookieConsentClass { shadowRoot.querySelector('.hds-cc').focus(); } - // Add chat cookie functions to window scope - async #createChatConsentAPI() { - const cookieConsentReference = this; - - window.chat_user_consent = { - retrieveUserConsent() { - return this.getConsentStatus(['chat']); - }, - - async confirmUserConsent() { - return cookieConsentReference.setGroupsStatusToAccepted(['chat']); - }, - }; - } - async #init() { + this.#removeBanner(); const cookieSettings = await this.#getCookieSettings(); // If cookie settings have not changed, do not show banner, otherwise, check diff --git a/src/scss/styles.scss b/src/scss/styles.scss index 08610dc768..9ccb79b370 100644 --- a/src/scss/styles.scss +++ b/src/scss/styles.scss @@ -26,7 +26,7 @@ body{ .dialog-off-canvas-main-canvas { height: auto; - min-height: calc(100% - var(--hds-cc-height, 0)); + min-height: calc(100% - var(--hds-cookie-consent-height, 0)); } .nav-toggle-dropdown--menu { diff --git a/templates/misc/cookie-container.html.twig b/templates/misc/cookie-container.html.twig index 83dc8bb156..3add8ccb5d 100644 --- a/templates/misc/cookie-container.html.twig +++ b/templates/misc/cookie-container.html.twig @@ -1,3 +1,10 @@ + + -{# HDBT specific stuff #} +