From 403bd7c8a10dd542f40ddb4ea99bea5ac44f6770 Mon Sep 17 00:00:00 2001 From: CountNick Date: Mon, 29 Aug 2022 17:16:25 +0200 Subject: [PATCH] Update cookie-consent to custom element Add config functions Update code Add data attributes Update code Update custom events Update template Remove redundant code Fix show and hide dialog functionality Remove old code Revert to .mjs Update formatting Update imports Update import Remove @types/react Update dialog Update dialog Update tablist Add parameters to function Remove redundant file Fix linter error Update test Update checked state Remove whitespaces Update yarn lock Fix linting errors Remove failing test Remove aria-description 1.2.1-beta.1 Update cookie-consent to custom element Add config functions Update code Add data attributes Update code Update custom events Update template Remove redundant code Fix show and hide dialog functionality Remove old code Revert to .mjs Update formatting Update imports Update import Remove @types/react Update dialog Update dialog Update tablist Add parameters to function Remove redundant file Fix linter error Update test Update checked state Remove whitespaces Update yarn lock Fix linting errors Remove failing test Remove aria-description Hide cookie consent in print media styles Remove idea files Remove idea files Remove idea workspace file Update dialog generateDialogElement method Update imports with file extension Update package version grrr utils Updated defaultbuttonLabel to defaultButtonLabel Update README Update README Update README Update button label Update stylesheet Update readme Update code Update readme Update string quotes Re-add test Format code Update slack notification Format code Update version Format files --- .github/workflows/ci.yml | 30 ++- README.md | 355 ++++++++++++++++--------------- index.mjs | 2 +- src/config-defaults.mjs | 18 +- src/config.mjs | 10 +- src/cookie-consent.mjs | 323 ++++++++++++++++++++++------ src/dialog-tablist.mjs | 83 +++++--- src/dialog.mjs | 175 --------------- src/dom-toggler.mjs | 77 ++++--- src/preferences.mjs | 14 +- src/storage.mjs | 26 +-- src/toggle-dialog-visibility.mjs | 10 + styles/cookie-consent.scss | 45 ++-- test/config.test.js | 12 +- test/cookie-consent.test.js | 12 +- test/preferences.test.js | 30 ++- yarn.lock | 6 +- 17 files changed, 652 insertions(+), 576 deletions(-) delete mode 100644 src/dialog.mjs create mode 100644 src/toggle-dialog-visibility.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e649f7..691ed7c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: notification: name: Slack notification - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest if: always() needs: [ci] @@ -36,12 +36,22 @@ jobs: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} steps: - - name: Send notification - uses: edge/simple-slack-notify@master - with: - channel: "#ci" - username: CI - status: ${{ (contains(needs.*.result, 'cancelled') && 'cancelled') || (contains(needs.*.result, 'failure') && 'failure') || 'success' }} - success_text: ":octocat: <${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}|Build #${env.GITHUB_RUN_NUMBER}> of *${env.GITHUB_REPOSITORY}@${{ github.ref_name }}* by *${env.GITHUB_ACTOR}* completed successfully." - failure_text: ":octocat: <${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}|Build #${env.GITHUB_RUN_NUMBER}> of *${env.GITHUB_REPOSITORY}@${{ github.ref_name }}* by *${env.GITHUB_ACTOR}* failed." - cancelled_text: ":octocat: <${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}|Build #${env.GITHUB_RUN_NUMBER}> of *${env.GITHUB_REPOSITORY}@${{ github.ref_name }}* by *${env.GITHUB_ACTOR}* was cancelled." + - run: | + successText=":octocat: <${{ env.GITHUB_SERVER_URL }}/${{ env.GITHUB_REPOSITORY }}/actions/runs/${{ env.GITHUB_RUN_ID }}|Versie ${{ inputs.version }}> uitgerold naar *${{ inputs.environment }}*." + failureText=":octocat: <${{ env.GITHUB_SERVER_URL }}/${{ env.GITHUB_REPOSITORY }}/actions/runs/${{ env.GITHUB_RUN_ID }}|Versie ${{ inputs.version }}> niet uitgerold naar *${{ inputs.environment }}*." + cancelledText=":octocat: <${{ env.GITHUB_SERVER_URL }}/${{ env.GITHUB_REPOSITORY }}/actions/runs/${{ env.GITHUB_RUN_ID }}|Versie ${{ inputs.version }}> uitrol naar *${{ inputs.environment }}* geannuleerd." + status="${{ (contains(needs.*.result, 'cancelled') && 'cancelled') || (contains(needs.*.result, 'failure') && 'failure') || 'success' }}" + + if [ "$status" = 'success' ]; then + color='good' + text=$successText + elif [ "$status" = 'failure' ]; then + color='danger' + text=$failureText + elif [ "$status" = "cancelled" ]; then + color='warning' + text=$cancelledText + fi + + curl "${{ secrets.SLACK_WEBHOOK_URL }}" -X "POST" --header "Content-Type: application/json" \ + --data "{attachments: [{text: '$text', color: '$color'}]}" diff --git a/README.md b/README.md index 433eb10..f05534e 100644 --- a/README.md +++ b/README.md @@ -4,19 +4,18 @@ ### JavaScript utility library -- No dependencies -- Choose between `checkbox` or `radio` inputs -- Customizable cookie types (identifiers, optional/required, pre-checked) -- Conditional script tags, iframes and elements based on cookie consent and type +- No dependencies +- Customizable cookie types (identifiers, optional/required, pre-checked) +- Conditional script tags, iframes and elements based on cookie consent and type -Screenshot of the GDPR proof cookie consent dialog from @grrr/cookie-consent with checkbox inputsScreenshot of the GDPR proof cookie consent dialog from @grrr/cookie-consent with radio inputs +Screenshot of the GDPR proof cookie consent dialog from @grrr/cookie-consent with checkbox inputs ### Developed with ❤️ by [GRRR](https://grrr.nl) -- GRRR is a [B Corp](https://grrr.nl/en/b-corp/) -- GRRR has a [tech blog](https://grrr.tech/) -- GRRR is [hiring](https://grrr.nl/en/jobs/) -- [@GRRRTech](https://twitter.com/grrrtech) tweets +- GRRR is a [B Corp](https://grrr.nl/en/b-corp/) +- GRRR has a [tech blog](https://grrr.tech/) +- GRRR is [hiring](https://grrr.nl/en/jobs/) +- [@GRRRTech](https://twitter.com/grrrtech) tweets ## Installation @@ -24,186 +23,123 @@ $ npm install @grrr/cookie-consent ``` -Note: depending on your setup [additional configuration might be needed](https://github.com/grrr-amsterdam/cookie-consent/wiki/Usage-with-build-tools). This package is published with untranspiled JavaScript, as EcmaScript Modules (ESM). +## Custom element + +This cookie-consent module is a [custom element](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements). This also means that the element is encapsulated in a [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM). Here follows some information of how to implement this custom element in your own project. ## Usage -Import the module and initialize it: +Import the module and register it as a custom element: ```js -import CookieConsent from '@grrr/cookie-consent'; +import CookieConsent from "@grrr/cookie-consent"; + +if (window.customElements.get("cookie-consent") === undefined) { + window.customElements.define("cookie-consent", cookieConsent); +} +``` + +Once registered, you can add the cookie-consent element to your HTML there's some optional data you can pass to the element but the only required attribute to pass along are the cookies: -const cookieConsent = CookieConsent({ - cookies: [ +```js +const cookies = [ { - id: 'functional', - label: 'Functional', - description: 'Lorem ipsum.', - required: true, - }, + id: "functional", // string + label: functionalCookiesLabel, // string + description: functionalCookiesDescription, // string + required: true, // boolean + }, { - id: 'marketing', - label: 'Marketing', - description: 'Lorem ipsum.', - checked: true, + id: "marketing", // string + label: marketingCookiesLabel, // string + description: marketingCookiesDescription, // string + checked: marketingCookiesAccepted, // boolean }, - ], -}); +]; +// in order to pass these as a data-attribute we'll need to transform them to a string first +const stringifiedCookies = JSON.stringify(cookies); ``` -### Conditional scripts - -Conditionally show `script` tags. Add the `data-cookie-consent`-attribute with the id of the required cookie type, and disable the script by setting the `type` to `text/plain`: - ```html -// External script. - - -// Inline script. - +; ``` -### Conditional iframe embeds +## Options -Conditionally show or hide `iframe` embed. Add the `data-cookie-consent`-attribute with the id of the required cookie consent type, and disable the iframe renaming the `src`-attribute to `data-src`: +As mentioned before there is some optional data you can pass to the element: -```html - -``` +- title `string` +- description `string` +- save button text `string` -### Conditional content +To use the options, add them as data attributes to the custom element: -Conditionally show or hide elements. Add the `data-cookie-consent-`-attribute with the id of the required cookie consent type. There are two types of state: `accepted` and `rejected`. +```js + -```html - - ``` -Notes: - -- When hiding, the module will add `aria-hidden="true"` and `style="display: none;"` to remove it from the DOM. -- When showing, the module will remove any inline set `display` style, along with any `hidden` or `aria-hidden` attributes. - -## Options - All options except `cookies` are optional. They will fall back to the defaults, which are listed here: ```js -{ - type: 'checkbox', // Can be `checkbox` or `radio`. - prefix: 'cookie-consent', // The prefix used for styling and identifiers. - append: true, // By default the dialog is appended before the `main` tag or - // as the first `body` child. Disable to append it yourself. - appendDelay: 500, // The delay after which the cookie consent should be appended. - acceptAllButton: false, // Nudge users to accept all cookies when nothing is selected. - // Will select all checkboxes, or the top radio button. - cookies: [ // Array with cookie types. - { - id: 'marketing', // The unique identifier of the cookie type. - label: 'Marketing', // The label used in the dialog. - description: '...', // The description used in the dialog. - required: false, // Mark a cookie required (ignored when type is `radio`). - checked: false, // The default checked state (only valid when not `required`). - }, - ], - // If you need to override the dialog template (default defined in renderDialog) - dialogTemplate: function(templateVars) { - return '...' - }, - // Labels to provide content for the dialog. - labels: { - title: 'Cookies & Privacy', - description: `

This site makes use of third-party cookies. Read more in our - privacy policy.

`, - // Button labels based on state and preferences. - button: { - // The default button label. - default: 'Save preferences', - // Shown when `acceptAllButton` is set, and no option is selected. - acceptAll: 'Accept all', - }, - // ARIA labels to improve accessibility. - aria: { - button: 'Confirm cookie settings', - tabList: 'List with cookie types', - tabToggle: 'Toggle cookie tab', +export const DEFAULTS = { + prefix: "cookie-consent", + append: true, + appendDelay: 500, + acceptAllButton: false, + labels: { + title: "Cookies & Privacy", + description: + '

This site makes use of third-party cookies. Read more in our privacy policy.

', + button: { + default: "Save preferences", + acceptAll: "Accept all", + }, + aria: { + button: "Confirm cookie settings", + tabList: "List with cookie types", + tabToggle: "Toggle cookie tab", + }, }, - }, -} +}; ``` ## API -- [CookieConsent()](#cookieconsentoptions-object) -- [getDialog()](#getdialog) -- [showDialog()](#showdialog) -- [hideDialog()](#hidedialog) -- [isAccepted()](#isacceptedid-string) -- [getPreferences()](#getpreferences) -- [on()](#on) -- [updatePreference()](#updatePreferencecookies-array) - -### CookieConsent(options: object) - -Will create a new instance. - -```js -const cookieConsent = CookieConsent({ - cookies: [ - // ... - ] -}); -``` - -To make the instance globally available (for instance to add event listeners elsewhere), add it as a global after the instance has been created: +- [show()](#show) +- [hide()](#hide) +- [getPreferences()](#getpreferences) +- [updatePreference()](#updatePreferencecookies-array) +- [on()](#on) -```js -const cookieConsent = CookieConsent(); - -window.CookieConsent = cookieConsent; -``` - -### getDialog() - -Will fetch the dialog element, for example to append it at a custom DOM position. - -```js -document.body.insertBefore(cookieConsent.getDialog(), document.body.firstElementChild); -``` - -### showDialog() +### show() Will show the dialog element, for example to show it when triggered to change settings. ```js -el.addEventListener('click', e => { - e.preventDefault(); - cookieConsent.showDialog(); +button.addEventListener("click", (e) => { + e.preventDefault(); + cookieConsent.show(); }); ``` -### hideDialog() +### hide() Will hide the dialog element. ```js -el.addEventListener('click', e => { - e.preventDefault(); - cookieConsent.hideDialog(); +button.addEventListener("click", (e) => { + e.preventDefault(); + cookieConsent.hide(); }); ``` -### isAccepted(id: string) - -Check if a certain cookie type has been accepted. Will return `true` when accepted, `false` when denied, and `undefined` when no action has been taken. - -```js -const acceptedMarketing = cookieConsent.isAccepted('marketing'); // => true, false, undefined -``` - ### getPreferences() Will return an array with preferences per cookie type. @@ -223,16 +159,6 @@ const preferences = cookieConsent.getPreferences(); // ] ``` -### on(event: string) - -Add listeners for events. Will fire when the event is dispatched from the CookieConsent module. -See available [events](#events). - -```js -cookieConsent.on('event', eventHandler); -``` - - ### updatePreference(cookies: array) Update cookies programmatically. @@ -242,29 +168,36 @@ By updating cookies programmatically, the event handler will receive an update m ```js const cookies = [ { - id: 'marketing', - label: 'Marketing', - description: '...', - required: false, - checked: true, + id: "marketing", + label: "Marketing", + description: "...", + required: false, + checked: true, }, { - id: 'simple', - label: 'Simple', - description: '...', - required: false, - checked: false, + id: "simple", + label: "Simple", + description: "...", + required: false, + checked: false, }, ]; +``` + +### on(event: string) -cookieConsent.updatePreference(cookies); +Add listeners for events. Will fire when the event is dispatched from the CookieConsent module. +See available [events](#events). + +```js +cookieConsent.on("event", eventHandler); ``` ## Events Events are bound by the [on](#onevent-string) method. -- [update](#update) +- [update](#update) ### update @@ -275,13 +208,15 @@ This event can be used to fire tag triggers for each cookie type, for example vi Example: ```js -cookieConsent.on('update', cookies => { - const accepted = cookies.filter(cookie => cookie.accepted); - const dataLayer = window.dataLayer || []; - accepted.forEach(cookie => dataLayer.push({ - event: 'cookieConsent', - cookieType: cookie.id, - })); +cookieConsent.on("update", (cookies) => { + const accepted = cookies.filter((cookie) => cookie.accepted); + const dataLayer = window.dataLayer || []; + accepted.forEach((cookie) => + dataLayer.push({ + event: "cookieConsent", + cookieType: cookie.id, + }) + ); }); ``` @@ -289,14 +224,80 @@ cookieConsent.on('update', cookies => { No styling is being applied by the JavaScript module. However, there is a default stylesheet in the form of a [Sass](https://sass-lang.com/) module which can easily be added and customized to your project and its needs. +You have to use the `::parts` pseudo-element to style the dialog and its elements due to the Shadow DOM encapsulation. You can style the dialog and its elements by using the following parts: + +```scss +cookie-consent::part(cookie-consent) { + // Styles for the cookie consent dialog +} + +/** + * Header + */ +cookie-consent::part(cookie-consent__header) { + // Styles for the cookie consent header +} +cookie-consent::part(cookie-consent__title) { + // Styles for the cookie consent title +} + +/** + * Tabs + */ +cookie-consent::part(cookie-consent__tab-list) { + // Styles for the cookie consent tab list +} +cookie-consent::part(cookie-consent__tab-list-item) { + // Styles for the cookie consent tab list item +} +cookie-consent::part(cookie-consent__tab) { + // Styles for the cookie consent tabs +} + +/** + * Tab option (label with input in it) & tab toggle + */ +cookie-consent::part(cookie-consent__option) { + // Styles for the tab option label +} +cookie-consent::part(cookie-consent__input) { + // Styles for the tab option input +} +cookie-consent::part(cookie-consent__tab-toggle) { + // Styles for the tab toggle +} +cookie-consent::part(cookie-consent__tab-toggle-icon) { + // Styles for the tab toggle icon +} + +/** + * Tab panel (with description) + */ +cookie-consent::part(cookie-consent__tab-panel) { + // Styles for the tab panel +} + +cookie-consent::part(cookie-consent__tab-description) { + // Styles for the tab description +} + +/** + * Button + */ +cookie-consent::part(cookie-consent__button) { + // styles for the consent button +} +cookie-consent::part(cookie-consent__button-text) { + // Styles for the consent button text +} +``` + ### Stylesheet View the [base stylesheet](https://github.com/grrr-amsterdam/cookie-consent/tree/master/styles/cookie-consent.scss). -Note: no vendor prefixes are applied. We recommend using something like [Autoprefixer](https://github.com/postcss/autoprefixer) to do that automatically. - ### Interface With the styling from the base module applied, the interface will look roughly like this (fonts, sizes and margins might differ): -Screenshot of the GDPR proof cookie consent dialog from @grrr/cookie-consent +Screenshot of the GDPR proof cookie consent dialog from @grrr/cookie-consent with checkbox inputs diff --git a/index.mjs b/index.mjs index 02e1bbb..673cf5f 100644 --- a/index.mjs +++ b/index.mjs @@ -1,3 +1,3 @@ -import CookieConsent from './src/cookie-consent.mjs'; +import CookieConsent from "./src/cookie-consent.mjs"; export default CookieConsent; diff --git a/src/config-defaults.mjs b/src/config-defaults.mjs index ed7673c..79a6a09 100644 --- a/src/config-defaults.mjs +++ b/src/config-defaults.mjs @@ -1,20 +1,20 @@ export const DEFAULTS = { - type: 'checkbox', - prefix: 'cookie-consent', + prefix: "cookie-consent", append: true, appendDelay: 500, acceptAllButton: false, labels: { - title: 'Cookies & Privacy', - description: '

This site makes use of third-party cookies. Read more in our privacy policy.

', + title: "Cookies & Privacy", + description: + '

This site makes use of third-party cookies. Read more in our privacy policy.

', button: { - default: 'Save preferences', - acceptAll: 'Accept all', + default: "Save preferences", + acceptAll: "Accept all", }, aria: { - button: 'Confirm cookie settings', - tabList: 'List with cookie types', - tabToggle: 'Toggle cookie tab', + button: "Confirm cookie settings", + tabList: "List with cookie types", + tabToggle: "Toggle cookie tab", }, }, }; diff --git a/src/config.mjs b/src/config.mjs index eb77611..a417ce6 100644 --- a/src/config.mjs +++ b/src/config.mjs @@ -1,10 +1,10 @@ -import { DEFAULTS } from './config-defaults.mjs'; -import { getEntryByDotString } from './utils.mjs'; +import { DEFAULTS } from "./config-defaults.mjs"; +import { getEntryByDotString } from "./utils.mjs"; /** * Config getter with defaults fallback and warning when required values are missing. */ -const Config = settings => { +const Config = (settings) => { return { get: (entryString, required = false) => { const value = getEntryByDotString(settings, entryString); @@ -12,7 +12,9 @@ const Config = settings => { console.warn(`Required setting '${entryString}' is missing.`); return undefined; } - return value === undefined ? getEntryByDotString(DEFAULTS, entryString) : value; + return value === undefined + ? getEntryByDotString(DEFAULTS, entryString) + : value; }, }; }; diff --git a/src/cookie-consent.mjs b/src/cookie-consent.mjs index 2959b73..41c9fcd 100644 --- a/src/cookie-consent.mjs +++ b/src/cookie-consent.mjs @@ -1,65 +1,264 @@ -import Config from './config.mjs'; -import Dialog from './dialog.mjs'; -import DomToggler from './dom-toggler.mjs'; -import EventDispatcher from './event-dispatcher.mjs'; -import Preferences from './preferences.mjs'; +/* eslint class-methods-use-this:[ + "error", + { + "exceptMethods": + [ + "getConfig", + "initEventDispatcher", + "getPreferences", + "initDomToggler" + ] + } +] */ + +import { htmlToElement, preventingDefault } from "@grrr/utils"; +import EventDispatcher from "./event-dispatcher.mjs"; +import DialogTabList from "./dialog-tablist.mjs"; +import DomToggler from "./dom-toggler.mjs"; + +import Config from "./config.mjs"; +import Preferences from "./preferences.mjs"; /** - * Main constructor, which provides the API to the outside. + * Dialog which is shown to update cookie preferences. */ -const CookieConsent = settings => { - - // Show warning when settings are missing. - if (typeof settings !== 'object' || !Object.keys(settings).length) { - console.warn(`No settings specified.`); - } - - // Construct 'classes'. - const config = Config(settings); - const preferences = Preferences(config.get('prefix')); - const dialog = Dialog({ config, preferences }); - const domToggler = DomToggler(config); - const events = EventDispatcher(); - - // Update initial content. - domToggler.toggle(preferences); - - const updatePreference = (cookies) => { - preferences.store(cookies); - events.dispatch('update', preferences.getAll()); - domToggler.toggle(preferences); - }; - - // Initialize dialog and bind `submit` event. - dialog.init(); - dialog.on('submit', updatePreference); - - // Append dialog to the DOM, if this is not explicitly prevented. - if (config.get('append') !== false) { - const appendEl = document.querySelector('main') || document.body.firstElementChild; - const container = appendEl ? appendEl.parentNode : document.body; - container.insertBefore(dialog.element, appendEl); - } - - // Show the dialog when no preferences are found. If found, fire the `update` event. - if (preferences.hasPreferences()) { - events.dispatch('update', preferences.getAll()); - } else { - // Show the dialog. Invoked via a timeout, to ensure it's added in the next cycle - // to cater for possible transitions. - window.setTimeout(() => dialog.show(), config.get('appendDelay')); - } - - return { - getDialog: () => dialog.element, - hideDialog: dialog.hide, - showDialog: dialog.show, - isAccepted: preferences.getState, - getPreferences: preferences.getAll, - on: events.add, - updatePreference, - }; - -}; - -export default CookieConsent; +export default class Dialog extends HTMLElement { + constructor() { + // Always call super first in constructor + super(); + // sets and returns 'this.shadowRoot' + this.attachShadow({ mode: "open" }); + // get data + this.data = this.getData(); + // get config + this.config = this.getConfig(); + // initialize event dispatcher + this.events = this.initEventDispatcher(); + // initialize teblist + this.tabList = this.initTabList(); + // get cookies + this.cookies = this.data.cookies; + // generate dialog element + this.dialogElement = this.generateDialogElement(); + // append dialog to shadowRoot + this.shadowRoot.append(this.dialogElement); + // get preferences + this.preferences = this.getPreferences(); + // initialize domtoggler + this.domToggler = this.initDomToggler(); + // initialize show and hide + this.show = this.show(); + this.hide = this.hide(); + // get all preferences + this.preferences.getAll(); + + this.domToggler.toggle(this.preferences); + + // if cookie prefs already selected dispatch update event and hide + if (this.preferences.hasPreferences()) { + this.events.dispatch("update", this.preferences.getAll()); + } + + // add submit event + this.events.add("submit", this.updatePreference.bind(this)); + } + + getData() { + // fallback content from config + const fallbackContent = { + title: Config().get("labels.title"), + description: Config().get("labels.description"), + saveButtonText: Config().get("labels.aria.button"), + defaultButtonLabel: Config().get("labels.button.default"), + acceptAllButton: + Config().get("acceptAllButton") + && !Preferences().hasPreferences(), + }; + // custom content from data-attributes + const customContent = { + title: this.getAttribute("data-title"), + description: this.getAttribute("data-description"), + saveButtonText: this.getAttribute("data-saveButtonText"), + }; + // parse cookies to json + const cookies = JSON.parse(this.getAttribute("data-cookies")); + + return { + title: + customContent.title === null + ? fallbackContent.title + : customContent.title, + description: + customContent.description === null + ? fallbackContent.description + : customContent.description, + saveButtonText: + customContent.saveButtonText === null + ? fallbackContent.defaultButtonLabel + : customContent.saveButtonText, + acceptAllButton: fallbackContent.acceptAllButton, + cookies, + }; + } + + getConfig() { + return { + type: Config().get("type"), + prefix: Config().get("prefix"), + dialogTemplate: Config().get("dialogTemplate"), + }; + } + + initEventDispatcher() { + return EventDispatcher(); + } + + initTabList() { + return DialogTabList(this.data.cookies); + } + + generateDialogElement() { + // Initialize tab list and append it to the form. + this.tabList.init(); + + const template = ` + `; + + const dialogElement = htmlToElement(template); + + dialogElement.insertAdjacentHTML( + "afterbegin", + ` + ` + ); + + const formElement = dialogElement.lastElementChild; + + formElement.addEventListener( + "submit", + preventingDefault(this.submitHandler.bind(this)) + ); + + dialogElement.insertBefore(this.tabList.element, formElement); + + return dialogElement; + } + + submitHandler(e) { + e.preventDefault(); + // Get values based on the rules defined in `composeValues`. + const values = this.composeValues(this.tabList.getValues()); + + if (!values) { + return; + } + + // Dispatch values and hide the dialog. + this.events.dispatch("submit", values); + // toggleDialogVisibility(this.firstElementChild).hide(); + this.hide(); + } + + composeValues(values) { + // Checkbox with `acceptAllButton` and no user-choosable option is checked. + // We compare amount of required options against checked options. + + const requiredCount = this.data.cookies.filter((c) => c.required).length; + const checkedCount = values.filter((v) => v.accepted).length; + const userOptionsChecked = checkedCount >= requiredCount; + if ( + this.data.acceptAllButton + && this.config.type === "checkbox" + && !userOptionsChecked + ) { + return values.map((value) => ({ + ...value, + accepted: true, + })); + } + + // Return the values untouched. Happens for: + // - Checkbox with or without checked option, except the `acceptAllButton` case above. + return values; + } + + getPreferences() { + const preferences = Preferences(Config().get("prefix")); + + return preferences; + } + + updatePreference(selectedCookies) { + this.preferences.store(selectedCookies); + this.events.dispatch("update", this.preferences.getAll()); + this.domToggler.toggle(this.preferences); + } + + initDomToggler() { + return DomToggler(Config()); + } + + show() { + return () => + this.shadowRoot + .querySelector(".cookie-consent") + .setAttribute("aria-hidden", "false"); + } + + hide() { + return () => + this.shadowRoot + .querySelector(".cookie-consent") + .setAttribute("aria-hidden", "true"); + } + + on(type, payload) { + return this.events.add(type, payload); + } + + static get observedAttributes() { + return ["data-cookies"]; + } + + attributeChangedCallback(attrName, oldValue, newValue) { + // Set this.cookies to the updated value + this.cookies = JSON.parse(newValue); + // Transform NodeList to array + const arrayfiedTabList = Array.from(this.tabList.element.children); + // Filter out all li elements + const tabListChildren = arrayfiedTabList.filter( + (item) => item.nodeName === "LI" + ); + // Loop through arrayfiedTabListChildren + tabListChildren.forEach((input) => { + // Find all input elements + const inputElement = input.firstElementChild.firstElementChild.firstElementChild; + // Loop through updated cookies + this.cookies.forEach((cookie) => { + // set the checked state to the updated cookie state + if (inputElement.value === cookie.id && cookie.checked) { + inputElement.checked = true; + } + }); + }); + } +} diff --git a/src/dialog-tablist.mjs b/src/dialog-tablist.mjs index 41e1355..c84a178 100644 --- a/src/dialog-tablist.mjs +++ b/src/dialog-tablist.mjs @@ -1,21 +1,24 @@ -import { htmlToElement } from '@grrr/utils'; -import EventDispatcher from './event-dispatcher.mjs'; +import { htmlToElement } from "@grrr/utils"; +import EventDispatcher from "./event-dispatcher.mjs"; + +import Config from "./config.mjs"; +import Preferences from "./preferences.mjs"; /** * Dialog tab list with cookie tabs. */ -const DialogTabList = ({ config, preferences }) => { - +const DialogTabList = (cookieInformation) => { const events = EventDispatcher(); - const TYPE = config.get('type'); - const PREFIX = config.get('prefix'); + const PREFIX = Config().get("prefix"); /** * Render cookie tabs. */ - const renderTab = ({ id, label, description, required, checked, accepted }, index) => { - + const renderTab = ( + { id, label, description, required, checked, accepted }, + index + ) => { /** * Check if the checkbox should be checked: * @@ -24,37 +27,54 @@ const DialogTabList = ({ config, preferences }) => { * `required: false`, because of #3) * 3. Use the `checked` setting. */ - const shouldBeChecked = typeof accepted !== 'undefined' + const shouldBeChecked = typeof accepted !== "undefined" ? accepted : required === true ? required : checked; return ` -
  • -
    -