From 58d788a475c187888e2c278ef2737b6fa38ecfe0 Mon Sep 17 00:00:00 2001 From: David Nuescheler Date: Thu, 27 Jun 2024 15:51:02 -0600 Subject: [PATCH 1/4] chore: change initial flow --- scripts/delayed.js | 7 ------- scripts/marketing-tech.js | 2 ++ scripts/scripts.js | 21 ++++++++++++++++++--- 3 files changed, 20 insertions(+), 10 deletions(-) delete mode 100644 scripts/delayed.js create mode 100644 scripts/marketing-tech.js diff --git a/scripts/delayed.js b/scripts/delayed.js deleted file mode 100644 index 4f632597be..0000000000 --- a/scripts/delayed.js +++ /dev/null @@ -1,7 +0,0 @@ -// eslint-disable-next-line import/no-cycle -import { sampleRUM } from './aem.js'; - -// Core Web Vitals RUM collection -sampleRUM('cwv'); - -// add more delayed functionality here diff --git a/scripts/marketing-tech.js b/scripts/marketing-tech.js new file mode 100644 index 0000000000..55768c77e0 --- /dev/null +++ b/scripts/marketing-tech.js @@ -0,0 +1,2 @@ +// add tag management code here, marketing or advertising functionality +// this file is gated by consent diff --git a/scripts/scripts.js b/scripts/scripts.js index 0211c1dd34..36a8db14d0 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -120,16 +120,31 @@ async function loadLazy(doc) { * Loads everything that happens a lot later, * without impacting the user experience. */ -function loadDelayed() { +async function loadMarketingTech() { + const mapStatus = (consentStatus) => { + if (consentStatus === 'declineAll') return false; + return true; + }; + const checkConsent = () => { + const consentStatus = localStorage.getItem('consentStatus'); + if (consentStatus !== null) return mapStatus(consentStatus); + return new Promise((resolve) => { + // display consent banner + document.addEventListener('aem:changeconsent', (e) => { + localStorage.setItem('consentStatus', e.detail.consentStatus); + resolve(mapStatus(e.detail.consentStatus)); + }); + }); + }; // eslint-disable-next-line import/no-cycle - window.setTimeout(() => import('./delayed.js'), 3000); + if (await checkConsent()) import('./marketing-tech.js'); // load anything that can be postponed to the latest here } async function loadPage() { await loadEager(document); await loadLazy(document); - loadDelayed(); + loadMarketingTech(); } loadPage(); From c2efe45ee7dc08f5d68b43711e96d8ac0aa8fcdc Mon Sep 17 00:00:00 2001 From: David Nuescheler Date: Wed, 10 Jul 2024 15:39:35 -0600 Subject: [PATCH 2/4] chore: refactor of consent based loading --- scripts/consent.js | 77 +++++++++++++++++++++ scripts/{marketing-tech.js => consented.js} | 3 + scripts/scripts.js | 32 +++------ styles/lazy-styles.css | 49 +++++++++++++ 4 files changed, 139 insertions(+), 22 deletions(-) create mode 100644 scripts/consent.js rename scripts/{marketing-tech.js => consented.js} (57%) diff --git a/scripts/consent.js b/scripts/consent.js new file mode 100644 index 0000000000..cbc0e2144b --- /dev/null +++ b/scripts/consent.js @@ -0,0 +1,77 @@ +/** + * Display Consent Banner and return consent details + */ + +// eslint-disable-next-line import/no-cycle +import { loadFragment } from '../blocks/fragment/fragment.js'; + +/* create button helper */ +function createButton(text, clickHandler, style, label) { + const button = document.createElement('button'); + button.textContent = text; + if (style) button.className = style; + if (label) button.ariaLabel = label; + button.addEventListener('click', clickHandler); + return button; +} + +/* display consent banner */ +function displayConsentBanner(consentBanner) { + const bannerClose = new Promise((resolve) => { + const dialog = document.createElement('dialog'); + + const storeAndClose = (status) => { + localStorage.setItem('consentStatus', status); + dialog.remove(); + resolve(status); + // eslint-disable-next-line no-use-before-define + displayConsentLink(consentBanner); + }; + + const close = () => { storeAndClose('declineAll'); }; + const accept = () => { storeAndClose('acceptAll'); }; + const decline = () => { storeAndClose('declineAll'); }; + + const config = consentBanner.dataset; + dialog.id = 'consent-banner'; + dialog.append(consentBanner); + const buttons = document.createElement('span'); + buttons.className = 'consent-banner-buttons'; + if (config.accept) buttons.append(createButton(config.accept, accept)); + if (config.decline) buttons.append(createButton(config.decline, decline, 'secondary')); + if (config.close) buttons.append(createButton('\u2715', close, 'consent-banner-close', config.close)); + dialog.append(buttons); + document.body.append(dialog); + dialog.show(); + }); + + return bannerClose; +} + +/* display consent link */ +function displayConsentLink(consentBanner) { + const div = document.createElement('div'); + div.id = 'consent-link'; + const button = createButton(consentBanner.dataset.shortTitle, async () => { + div.remove(); + await displayConsentBanner(consentBanner); + }, 'secondary'); + div.append(button); + document.body.append(div); +} + +/* main consent handler */ +export default async function handleConsent() { + const mapStatus = (consentStatus) => { + if (consentStatus === 'declineAll') return false; + return true; + }; + const consentStatus = localStorage.getItem('consentStatus'); + const consentBanner = (await loadFragment('/drafts/uncled/consent-banner')).firstElementChild; + + if (consentStatus === null) { + return mapStatus(await displayConsentBanner(consentBanner)); + } + displayConsentLink(consentBanner); + return mapStatus(consentStatus); +} diff --git a/scripts/marketing-tech.js b/scripts/consented.js similarity index 57% rename from scripts/marketing-tech.js rename to scripts/consented.js index 55768c77e0..907a4cf683 100644 --- a/scripts/marketing-tech.js +++ b/scripts/consented.js @@ -1,2 +1,5 @@ // add tag management code here, marketing or advertising functionality // this file is gated by consent + +// eslint-disable-next-line no-console +console.log('loading consented.js'); diff --git a/scripts/scripts.js b/scripts/scripts.js index 36a8db14d0..f6a5f31cdd 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -117,34 +117,22 @@ async function loadLazy(doc) { } /** - * Loads everything that happens a lot later, - * without impacting the user experience. + * Checks consent asynchronously and returns true if consented.js + * can be loaded (which should contain the tag manager). */ -async function loadMarketingTech() { - const mapStatus = (consentStatus) => { - if (consentStatus === 'declineAll') return false; - return true; - }; - const checkConsent = () => { - const consentStatus = localStorage.getItem('consentStatus'); - if (consentStatus !== null) return mapStatus(consentStatus); - return new Promise((resolve) => { - // display consent banner - document.addEventListener('aem:changeconsent', (e) => { - localStorage.setItem('consentStatus', e.detail.consentStatus); - resolve(mapStatus(e.detail.consentStatus)); - }); - }); - }; - // eslint-disable-next-line import/no-cycle - if (await checkConsent()) import('./marketing-tech.js'); - // load anything that can be postponed to the latest here +async function checkConsent() { + const mod = await import('./consent.js'); + return mod.default(); +} + +async function loadConsented() { + import('./consented.js'); } async function loadPage() { await loadEager(document); await loadLazy(document); - loadMarketingTech(); + if (await checkConsent()) loadConsented(); } loadPage(); diff --git a/styles/lazy-styles.css b/styles/lazy-styles.css index 84e7d6c971..04556b4f00 100644 --- a/styles/lazy-styles.css +++ b/styles/lazy-styles.css @@ -1 +1,50 @@ /* add global styles that can be loaded post LCP here */ + +dialog#consent-banner, div#consent-link { + position: fixed; + left: 0; + bottom: 0; + background-color: var(--light-color); + max-width: 600px; + padding: 16px; + font-size: var(--body-font-size-xs); + margin: 16px; + border-radius: 32px; + border: 0; + box-shadow: 3px 3px 10px #0004; +} + +dialog#consent-banner { + border-radius: 16px; + padding-right: 40px; +} + +dialog#consent-banner p { + margin: 0px; +} + +dialog#consent-banner .consent-banner-buttons { + display: flex; + gap: 8px; +} + +dialog#consent-banner .consent-banner-buttons button { + margin: 8px 0; +} + +dialog#consent-banner .consent-banner-close { + position: absolute; + margin: 0; + padding: 0; + right: 16px; + top: 12px; + border: unset; + padding: unset; + color: unset; + background-color: unset; + margin: 0; +} + +div#consent-link button { + margin: 0; +} From 84563a8a54b03fa3955bb3d37306c2323a77b309 Mon Sep 17 00:00:00 2001 From: David Nuescheler Date: Thu, 11 Jul 2024 11:08:54 -0600 Subject: [PATCH 3/4] chore: refactor number 2 --- blocks/fragment/fragment.js | 7 +++++++ scripts/consent.js | 40 +++++++++++++++++++------------------ scripts/scripts.js | 12 ++--------- styles/lazy-styles.css | 16 ++++----------- 4 files changed, 34 insertions(+), 41 deletions(-) diff --git a/blocks/fragment/fragment.js b/blocks/fragment/fragment.js index 648a70e103..bdab7eae1c 100644 --- a/blocks/fragment/fragment.js +++ b/blocks/fragment/fragment.js @@ -24,12 +24,19 @@ export async function loadFragment(path) { const main = document.createElement('main'); main.innerHTML = await resp.text(); + /* remove paths of self references */ + const { pathname } = new URL(path, window.location); + main.querySelectorAll(`a[href^="${pathname}#"]`).forEach((a) => { + a.href = `#${a.href.split('#')[1]}`; + }); + // reset base path for media to fragment base const resetAttributeBase = (tag, attr) => { main.querySelectorAll(`${tag}[${attr}^="./media_"]`).forEach((elem) => { elem[attr] = new URL(elem.getAttribute(attr), new URL(path, window.location)).href; }); }; + resetAttributeBase('img', 'src'); resetAttributeBase('source', 'srcset'); diff --git a/scripts/consent.js b/scripts/consent.js index cbc0e2144b..05a659422c 100644 --- a/scripts/consent.js +++ b/scripts/consent.js @@ -1,5 +1,8 @@ /** - * Display Consent Banner and return consent details + * Handles consent, including the loading of consented.js + * where needed. + * + * Dispatches `consent` event on document on change */ // eslint-disable-next-line import/no-cycle @@ -16,7 +19,9 @@ function createButton(text, clickHandler, style, label) { } /* display consent banner */ -function displayConsentBanner(consentBanner) { +export async function displayConsentBanner() { + const consentBanner = (await loadFragment('/drafts/uncled/consent-banner')).firstElementChild; + const bannerClose = new Promise((resolve) => { const dialog = document.createElement('dialog'); @@ -24,8 +29,9 @@ function displayConsentBanner(consentBanner) { localStorage.setItem('consentStatus', status); dialog.remove(); resolve(status); - // eslint-disable-next-line no-use-before-define - displayConsentLink(consentBanner); + document.dispatchEvent(new CustomEvent('consent', { detail: { consentStatus: status } })); + const loc = window.location; + if (loc.hash === '#consent') window.history.pushState({}, null, `${loc.pathname}${loc.search}`); }; const close = () => { storeAndClose('declineAll'); }; @@ -48,30 +54,26 @@ function displayConsentBanner(consentBanner) { return bannerClose; } -/* display consent link */ -function displayConsentLink(consentBanner) { - const div = document.createElement('div'); - div.id = 'consent-link'; - const button = createButton(consentBanner.dataset.shortTitle, async () => { - div.remove(); - await displayConsentBanner(consentBanner); - }, 'secondary'); - div.append(button); - document.body.append(div); -} - /* main consent handler */ export default async function handleConsent() { const mapStatus = (consentStatus) => { if (consentStatus === 'declineAll') return false; return true; }; + + document.addEventListener('consent', (e) => { + if (mapStatus(e.detail.consentStatus)) import('./consented.js'); + }); + const consentStatus = localStorage.getItem('consentStatus'); - const consentBanner = (await loadFragment('/drafts/uncled/consent-banner')).firstElementChild; if (consentStatus === null) { - return mapStatus(await displayConsentBanner(consentBanner)); + return mapStatus(await displayConsentBanner()); } - displayConsentLink(consentBanner); + + window.addEventListener('hashchange', (e) => { + if (new URL(e.newURL).hash === '#consent') displayConsentBanner(); + }); + return mapStatus(consentStatus); } diff --git a/scripts/scripts.js b/scripts/scripts.js index f6a5f31cdd..b7d6330677 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -116,23 +116,15 @@ async function loadLazy(doc) { sampleRUM.observe(main.querySelectorAll('picture > img')); } -/** - * Checks consent asynchronously and returns true if consented.js - * can be loaded (which should contain the tag manager). - */ -async function checkConsent() { +async function handleConsent() { const mod = await import('./consent.js'); return mod.default(); } -async function loadConsented() { - import('./consented.js'); -} - async function loadPage() { await loadEager(document); await loadLazy(document); - if (await checkConsent()) loadConsented(); + handleConsent(); } loadPage(); diff --git a/styles/lazy-styles.css b/styles/lazy-styles.css index 04556b4f00..3c8e4d764e 100644 --- a/styles/lazy-styles.css +++ b/styles/lazy-styles.css @@ -1,24 +1,20 @@ /* add global styles that can be loaded post LCP here */ -dialog#consent-banner, div#consent-link { +dialog#consent-banner { position: fixed; left: 0; bottom: 0; background-color: var(--light-color); max-width: 600px; padding: 16px; + padding-right: 40px; font-size: var(--body-font-size-xs); margin: 16px; - border-radius: 32px; + border-radius: 16px; border: 0; box-shadow: 3px 3px 10px #0004; } -dialog#consent-banner { - border-radius: 16px; - padding-right: 40px; -} - dialog#consent-banner p { margin: 0px; } @@ -43,8 +39,4 @@ dialog#consent-banner .consent-banner-close { color: unset; background-color: unset; margin: 0; -} - -div#consent-link button { - margin: 0; -} +} \ No newline at end of file From aaafd8193aad416778e1f003e49e8c853fd4b4e3 Mon Sep 17 00:00:00 2001 From: David Nuescheler Date: Wed, 24 Jul 2024 11:16:05 -0600 Subject: [PATCH 4/4] chore: focus cleanup --- scripts/consent.js | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/scripts/consent.js b/scripts/consent.js index 05a659422c..7b4a7f131e 100644 --- a/scripts/consent.js +++ b/scripts/consent.js @@ -19,11 +19,17 @@ function createButton(text, clickHandler, style, label) { } /* display consent banner */ -export async function displayConsentBanner() { +export async function displayConsentBanner(focus = false) { const consentBanner = (await loadFragment('/drafts/uncled/consent-banner')).firstElementChild; const bannerClose = new Promise((resolve) => { + const config = consentBanner.dataset; const dialog = document.createElement('dialog'); + dialog.id = 'consent-banner'; + dialog.ariaLabel = config.shortTitle; + dialog.ariaModal = false; + dialog.open = true; + dialog.append(consentBanner); const storeAndClose = (status) => { localStorage.setItem('consentStatus', status); @@ -38,9 +44,6 @@ export async function displayConsentBanner() { const accept = () => { storeAndClose('acceptAll'); }; const decline = () => { storeAndClose('declineAll'); }; - const config = consentBanner.dataset; - dialog.id = 'consent-banner'; - dialog.append(consentBanner); const buttons = document.createElement('span'); buttons.className = 'consent-banner-buttons'; if (config.accept) buttons.append(createButton(config.accept, accept)); @@ -48,7 +51,7 @@ export async function displayConsentBanner() { if (config.close) buttons.append(createButton('\u2715', close, 'consent-banner-close', config.close)); dialog.append(buttons); document.body.append(dialog); - dialog.show(); + if (focus) dialog.querySelector('button').focus(); }); return bannerClose; @@ -56,24 +59,22 @@ export async function displayConsentBanner() { /* main consent handler */ export default async function handleConsent() { - const mapStatus = (consentStatus) => { - if (consentStatus === 'declineAll') return false; - return true; - }; + const mapStatus = (consentStatus) => !(consentStatus === 'declineAll'); document.addEventListener('consent', (e) => { if (mapStatus(e.detail.consentStatus)) import('./consented.js'); }); - const consentStatus = localStorage.getItem('consentStatus'); + window.addEventListener('hashchange', (e) => { + if (new URL(e.newURL).hash === '#consent') { + displayConsentBanner(true); + } + }); - if (consentStatus === null) { + const consentStatus = localStorage.getItem('consentStatus'); + if (consentStatus === null || window.location.hash === '#consent') { return mapStatus(await displayConsentBanner()); } - window.addEventListener('hashchange', (e) => { - if (new URL(e.newURL).hash === '#consent') displayConsentBanner(); - }); - return mapStatus(consentStatus); }