diff --git a/assets/src/blocks/Cookies/CookiesBlock.js b/assets/src/blocks/Cookies/CookiesBlock.js
new file mode 100644
index 0000000000..84f41f2df0
--- /dev/null
+++ b/assets/src/blocks/Cookies/CookiesBlock.js
@@ -0,0 +1,48 @@
+import {CookiesEditor} from './CookiesEditor.js';
+
+export const BLOCK_NAME = 'planet4-blocks/cookies';
+
+export const registerCookiesBlock = () => {
+ const {registerBlockType} = wp.blocks;
+
+ registerBlockType(BLOCK_NAME, {
+ title: 'Cookies',
+ icon: 'welcome-view-site',
+ category: 'planet4-blocks',
+ supports: {
+ multiple: false, // Use the block just once per post.
+ },
+ attributes: {
+ title: {
+ type: 'string',
+ default: '',
+ },
+ description: {
+ type: 'string',
+ default: '',
+ },
+ necessary_cookies_name: {
+ type: 'string',
+ },
+ necessary_cookies_description: {
+ type: 'string',
+ },
+ all_cookies_name: {
+ type: 'string',
+ },
+ all_cookies_description: {
+ type: 'string',
+ },
+ analytical_cookies_name: {
+ type: 'string',
+ },
+ analytical_cookies_description: {
+ type: 'string',
+ },
+ },
+ edit: CookiesEditor,
+ save() {
+ return null;
+ },
+ });
+};
diff --git a/assets/src/blocks/Cookies/CookiesEditor.js b/assets/src/blocks/Cookies/CookiesEditor.js
new file mode 100644
index 0000000000..5451fe8e7d
--- /dev/null
+++ b/assets/src/blocks/Cookies/CookiesEditor.js
@@ -0,0 +1,18 @@
+import {CookiesFrontend} from './CookiesFrontend';
+
+export const CookiesEditor = ({attributes, isSelected, setAttributes}) => {
+ const toAttribute = attributeName => value => {
+ if (isSelected) {
+ setAttributes({[attributeName]: value});
+ }
+ };
+
+ return (
+
+ );
+};
diff --git a/assets/src/blocks/Cookies/CookiesEditorScript.js b/assets/src/blocks/Cookies/CookiesEditorScript.js
new file mode 100644
index 0000000000..38eb0ac252
--- /dev/null
+++ b/assets/src/blocks/Cookies/CookiesEditorScript.js
@@ -0,0 +1,3 @@
+import {registerCookiesBlock} from './CookiesBlock';
+
+registerCookiesBlock();
diff --git a/assets/src/blocks/Cookies/CookiesFieldResetButton.js b/assets/src/blocks/Cookies/CookiesFieldResetButton.js
new file mode 100644
index 0000000000..a2890c1e37
--- /dev/null
+++ b/assets/src/blocks/Cookies/CookiesFieldResetButton.js
@@ -0,0 +1,28 @@
+const {__} = wp.i18n;
+
+import {Tooltip} from '@wordpress/components';
+
+const COOKIES_DEFAULT_COPY = window.p4_vars.cookies_default_copy || {};
+
+export const CookiesFieldResetButton = ({fieldName, toAttribute, currentValue}) => {
+ const defaultValue = COOKIES_DEFAULT_COPY[fieldName] || '';
+
+ if (!currentValue || !defaultValue || currentValue === defaultValue) {
+ return null;
+ }
+
+ return (
+
+ Cookies', 'planet4-blocks-backend')}>
+ i
+
+ toAttribute(fieldName)(undefined)}
+ role="presentation"
+ >
+ {__('Use default value', 'planet4-blocks-backend')}
+
+
+ );
+};
diff --git a/assets/src/blocks/Cookies/CookiesFrontend.js b/assets/src/blocks/Cookies/CookiesFrontend.js
new file mode 100644
index 0000000000..da76b4505f
--- /dev/null
+++ b/assets/src/blocks/Cookies/CookiesFrontend.js
@@ -0,0 +1,323 @@
+import {FrontendRichText} from '../components/FrontendRichText/FrontendRichText';
+import {removeCookie, useCookie, writeCookie} from './useCookie';
+import {useState, useEffect} from '@wordpress/element';
+import {CookiesFieldResetButton} from './CookiesFieldResetButton';
+
+const {__} = wp.i18n;
+
+const dataLayer = window.dataLayer || [];
+
+const COOKIES_DEFAULT_COPY = window.p4_vars.cookies_default_copy || {};
+
+function gtag() {
+ dataLayer.push(arguments);
+}
+
+// Planet4 settings(Planet 4 > Cookies > Enable Analytical Cookies).
+const ENABLE_ANALYTICAL_COOKIES = window.p4_vars.enable_analytical_cookies;
+
+// Planet4 settings (Planet 4 > Analytics > Enable Google Consent Mode).
+const ENABLE_GOOGLE_CONSENT_MODE = window.p4_vars.enable_google_consent_mode;
+
+const CONSENT_COOKIE = 'greenpeace';
+const NO_TRACK_COOKIE = 'no_track';
+const ACTIVE_CONSENT_COOKIE = 'active_consent_choice';
+const ONLY_NECESSARY = '1';
+const NECESSARY_MARKETING = '2';
+const NECESSARY_ANALYTICAL = '3';
+const NECESSARY_ANALYTICAL_MARKETING = '4';
+
+const hideCookiesBox = () => {
+ // the .cookie-notice element belongs to the P4 Master Theme
+ const cookiesBox = document.querySelector('#set-cookie');
+ if (cookiesBox) {
+ cookiesBox.classList.remove('shown');
+ }
+};
+
+export const CookiesFrontend = props => {
+ const {
+ isSelected,
+ title,
+ description,
+ necessary_cookies_name,
+ necessary_cookies_description,
+ analytical_cookies_name,
+ analytical_cookies_description,
+ all_cookies_name,
+ all_cookies_description,
+ isEditing,
+ className,
+ toAttribute = () => {},
+ } = props;
+
+ // Whether consent was revoked by the user since current page load.
+ const [userRevokedMarketingCookies, setUserRevokedMarketingCookies] = useState(false);
+ const [userRevokedAnalyticalCookies, setUserRevokedAnalyticalCookies] = useState(false);
+ const [consentCookie, setConsentCookie] = useCookie(CONSENT_COOKIE);
+ const analyticalCookiesChecked = [NECESSARY_ANALYTICAL, NECESSARY_ANALYTICAL_MARKETING].includes(consentCookie);
+ const marketingCookiesChecked = [NECESSARY_MARKETING, NECESSARY_ANALYTICAL_MARKETING].includes(consentCookie);
+ const hasConsent = marketingCookiesChecked || analyticalCookiesChecked;
+
+ const updateNoTrackCookie = () => {
+ if (hasConsent) {
+ removeCookie(NO_TRACK_COOKIE);
+ } else {
+ writeCookie(NO_TRACK_COOKIE, '1');
+ }
+ };
+ useEffect(updateNoTrackCookie, [userRevokedAnalyticalCookies, userRevokedMarketingCookies]);
+
+ const updateConsent = (key, granted) => {
+ dataLayer.push({
+ event: 'updateConsent',
+ });
+
+ if (!ENABLE_GOOGLE_CONSENT_MODE) {
+ return;
+ }
+
+ gtag('consent', 'update', {
+ [key]: granted ? 'granted' : 'denied',
+ });
+ dataLayer.push({
+ event: 'updateConsent',
+ [key]: granted ? 'granted' : 'denied',
+ });
+ };
+
+ const toggleHubSpotConsent = () => {
+ if (!marketingCookiesChecked && userRevokedMarketingCookies) {
+ const _hsp = window._hsp = window._hsp || [];
+ _hsp.push(['revokeCookieConsent']);
+ }
+ };
+ useEffect(toggleHubSpotConsent, [marketingCookiesChecked, userRevokedMarketingCookies]);
+
+ const updateActiveConsentChoice = () => {
+ if (hasConsent) {
+ writeCookie(ACTIVE_CONSENT_COOKIE, '1');
+ hideCookiesBox();
+ }
+ };
+ useEffect(updateActiveConsentChoice, [marketingCookiesChecked, analyticalCookiesChecked]);
+
+ const getFieldValue = fieldName => {
+ if (props[fieldName] === undefined) {
+ return COOKIES_DEFAULT_COPY[fieldName] || '';
+ }
+ return props[fieldName] || '';
+ };
+
+ const isFieldValid = fieldName => getFieldValue(fieldName).trim().length > 0;
+
+ return (
+ <>
+
+ {(isEditing || title) &&
+
+ }
+ {(isEditing || description) &&
+
+ }
+ {(isEditing || (isFieldValid('necessary_cookies_name') && isFieldValid('necessary_cookies_description'))) &&
+ <>
+
+
+ {__('Always enabled', 'planet4-blocks')}
+ {isEditing &&
+
+ }
+
+
+
+ {isEditing &&
+
+ }
+
+ >
+ }
+ {(ENABLE_ANALYTICAL_COOKIES && (isEditing || (isFieldValid('analytical_cookies_name') && isFieldValid('analytical_cookies_description')))) &&
+ <>
+
+
+ {isEditing &&
+
+ }
+
+
+
+ {isEditing &&
+
+ }
+
+ >
+ }
+ {(isEditing || (isFieldValid('all_cookies_name') && isFieldValid('all_cookies_description'))) &&
+ <>
+
+
+ {isEditing &&
+
+ }
+
+
+
+ {isEditing &&
+
+ }
+
+ >
+ }
+
+ >
+ );
+};
diff --git a/assets/src/blocks/Cookies/CookiesScript.js b/assets/src/blocks/Cookies/CookiesScript.js
new file mode 100644
index 0000000000..be9bcf7566
--- /dev/null
+++ b/assets/src/blocks/Cookies/CookiesScript.js
@@ -0,0 +1,12 @@
+import {createRoot} from 'react-dom/client';
+import {BLOCK_NAME} from './CookiesBlock';
+import {CookiesFrontend} from './CookiesFrontend';
+
+// Fallback for non migrated content. Remove after migration.
+document.querySelectorAll(`[data-render="${BLOCK_NAME}"]`).forEach(
+ blockNode => {
+ const attributes = JSON.parse(blockNode.dataset.attributes);
+ const rootElement = createRoot(blockNode);
+ rootElement.render();
+ }
+);
diff --git a/assets/src/blocks/Cookies/useCookie.js b/assets/src/blocks/Cookies/useCookie.js
new file mode 100644
index 0000000000..e91efd9595
--- /dev/null
+++ b/assets/src/blocks/Cookies/useCookie.js
@@ -0,0 +1,44 @@
+import {useState, useEffect} from '@wordpress/element';
+
+export const readCookie = name => {
+ const declarations = document.cookie.split(';');
+ let match = null;
+ declarations.forEach(part => {
+ const [key, value] = part.split('=');
+ if (key.trim() === name) {
+ match = value;
+ }
+ });
+ return match;
+};
+
+export const writeCookie = (name, value, days = 365) => {
+ const date = new Date();
+ date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
+ const secureMode = document.location.protocol === 'http:' ?
+ ';SameSite=Lax' :
+ ';SameSite=None;Secure';
+ document.cookie = encodeURI(name) + '=' + encodeURI(value) + ';domain=.' + document.domain + ';path=/;' + '; expires=' + date.toGMTString() + secureMode;
+};
+
+// Value should not matter as cookie is expired.
+export const removeCookie = name => writeCookie(name, '0', -1);
+
+export const useCookie = name => {
+ const [value, setValue] = useState(() => readCookie(name));
+
+ const saveCookie = () => {
+ if (value === null) {
+ removeCookie(name);
+ return;
+ }
+ writeCookie(name, value);
+ };
+ useEffect(saveCookie, [value]);
+
+ return [
+ value,
+ setValue,
+ ];
+};
+
diff --git a/assets/src/blocks/components/FrontendRichText/FrontendRichText.js b/assets/src/blocks/components/FrontendRichText/FrontendRichText.js
new file mode 100644
index 0000000000..8a617301fd
--- /dev/null
+++ b/assets/src/blocks/components/FrontendRichText/FrontendRichText.js
@@ -0,0 +1,13 @@
+const RichText = wp.blockEditor ? wp.blockEditor.RichText : null;
+
+export const FrontendRichText = ({editable, ...richTextProps}) => {
+ const renderAsRichText = RichText && editable;
+ const TagName = richTextProps.tagName;
+
+ return renderAsRichText ?
+ :
+ ;
+};
diff --git a/assets/src/scss/blocks.scss b/assets/src/scss/blocks.scss
new file mode 100644
index 0000000000..4713b29a6b
--- /dev/null
+++ b/assets/src/scss/blocks.scss
@@ -0,0 +1,5 @@
+@import "blocks/ActionsList";
+@import "blocks/PostsList";
+@import "blocks/CarouselHeader/CarouselHeaderStyle";
+@import "blocks/Accordion/AccordionStyle";
+@import "blocks/Cookies/CookiesStyle";
diff --git a/assets/src/scss/blocks/Cookies/CookiesEditorStyle.scss b/assets/src/scss/blocks/Cookies/CookiesEditorStyle.scss
new file mode 100644
index 0000000000..cbca4578e8
--- /dev/null
+++ b/assets/src/scss/blocks/Cookies/CookiesEditorStyle.scss
@@ -0,0 +1,33 @@
+.cookies-block {
+ .cookies-checkbox-description.rich-text {
+ display: inline-block;
+ }
+
+ .field-reset-button {
+ display: inline-block;
+ color: var(--grey-600);
+ margin-inline-start: 16px;
+ font-family: var(--font-family-tertiary);
+ font-size: 13px;
+ vertical-align: middle;
+ white-space: nowrap;
+
+ .cta {
+ cursor: pointer;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
+ .info {
+ margin-inline-end: 8px;
+ border-radius: 50%;
+ width: 18px;
+ height: 18px;
+ display: inline-block;
+ text-align: center;
+ border: solid 1px var(--grey-600);
+ }
+ }
+}
diff --git a/assets/src/scss/blocks/Cookies/CookiesStyle.scss b/assets/src/scss/blocks/Cookies/CookiesStyle.scss
new file mode 100644
index 0000000000..ef6fb293bf
--- /dev/null
+++ b/assets/src/scss/blocks/Cookies/CookiesStyle.scss
@@ -0,0 +1,58 @@
+.cookies-block {
+ .custom-control-description,
+ .custom-control input[type="checkbox"] ~ .custom-control-description {
+ _-- {
+ font-size: var(--font-size-xl--font-family-primary);
+ font-family: var(--font-family-primary);
+ }
+
+ @include x-large-and-up {
+ _-- {
+ font-size: var(--font-size-2xl--font-family-primary);
+ }
+ }
+ }
+
+ .always-enabled {
+ _-- {
+ font-size: var(--font-size-xxs--font-family-tertiary);
+ font-weight: var(--font-weight-bold);
+ }
+
+ font-family: var(--font-family-tertiary);
+ background: var(--grey-900);
+ color: white;
+ border-radius: $sp-x;
+ padding: $sp-x;
+ margin-inline-start: $sp-1;
+ }
+
+ .cookies-title,
+ .cookies-header-text,
+ .cookies-checkbox-description {
+ font-family: var(--font-family-tertiary) !important;
+ }
+
+ .cookies-title,
+ .cookies-header-text {
+ font-weight: var(--font-weight-bold) !important;
+ }
+
+ .cookies-title {
+ font-size: var(--font-size-xs--font-family-primary);
+ }
+
+ .cookies-description {
+ font-family: var(--font-family-tertiary);
+ font-size: var(--font-size-xxxs--font-family-primary);
+ }
+
+ .cookies-header-text {
+ font-size: var(--font-size-xxxs--font-family-primary) !important;
+ line-height: var(--line-height-xs--font-family-primary) !important;
+ }
+
+ .cookies-checkbox-description {
+ font-size: var(--font-size-xxs--font-family-tertiary);
+ }
+}
diff --git a/assets/src/scss/editorStyle.scss b/assets/src/scss/editorStyle.scss
index 25eb825dca..2f3e948e7e 100644
--- a/assets/src/scss/editorStyle.scss
+++ b/assets/src/scss/editorStyle.scss
@@ -28,9 +28,9 @@
@import "base/css-variables";
// Blocks
-@import "blocks/ActionsList";
-@import "blocks/PostsList";
-@import "blocks/CarouselHeader/CarouselHeaderStyle";
+@import "blocks";
+
+// Blocks editor styles
@import "blocks/CarouselHeader/CarouselHeaderEditorStyle";
-@import "blocks/Accordion/AccordionStyle";
@import "blocks/Accordion/AccordionEditorStyle";
+@import "blocks/Cookies/CookiesEditorStyle";
diff --git a/assets/src/scss/style.scss b/assets/src/scss/style.scss
index b27eb61472..71f235cbae 100644
--- a/assets/src/scss/style.scss
+++ b/assets/src/scss/style.scss
@@ -81,10 +81,7 @@ Text Domain: planet4-master-theme
@import "base/css-variables";
// Blocks
-@import "blocks/ActionsList";
-@import "blocks/PostsList";
-@import "blocks/CarouselHeader/CarouselHeaderStyle";
-@import "blocks/Accordion/AccordionStyle";
+@import "blocks";
// Hide WPML footer language switcher.
.wpml-ls-statics-footer {
diff --git a/src/Blocks/Cookies.php b/src/Blocks/Cookies.php
new file mode 100644
index 0000000000..594086380e
--- /dev/null
+++ b/src/Blocks/Cookies.php
@@ -0,0 +1,79 @@
+ [ self::class, 'render_frontend' ],
+ 'attributes' => [
+ 'title' => [
+ 'type' => 'string',
+ 'default' => '',
+ ],
+ 'description' => [
+ 'type' => 'string',
+ 'default' => '',
+ ],
+ 'necessary_cookies_name' => [
+ 'type' => 'string',
+ ],
+ 'necessary_cookies_description' => [
+ 'type' => 'string',
+ ],
+ 'all_cookies_name' => [
+ 'type' => 'string',
+ ],
+ 'all_cookies_description' => [
+ 'type' => 'string',
+ ],
+ 'analytical_cookies_name' => [
+ 'type' => 'string',
+ ],
+ 'analytical_cookies_description' => [
+ 'type' => 'string',
+ ],
+ ],
+ ]
+ );
+
+ add_action('enqueue_block_editor_assets', [ self::class, 'enqueue_editor_assets' ]);
+ add_action('wp_enqueue_scripts', [ self::class, 'enqueue_frontend_assets' ]);
+ }
+
+ /**
+ * Required by the `BaseBlock` class.
+ *
+ * @param array $fields Unused, required by the abstract function.
+ *
+ * @return array Array.
+ * @phpcs:disable SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter
+ */
+ public function prepare_data(array $fields): array
+ {
+ return [];
+ }
+}
diff --git a/src/Loader.php b/src/Loader.php
index 315842b6fc..e37fc2f5d6 100644
--- a/src/Loader.php
+++ b/src/Loader.php
@@ -158,6 +158,7 @@ public static function add_blocks(): void
new Blocks\GuestBook();//NOSONAR
new Blocks\CarouselHeader();//NOSONAR
new Blocks\Accordion();//NOSONAR
+ new Blocks\Cookies();//NOSONAR
if (!BetaBlocks::is_active()) {
return;
diff --git a/src/MasterBlocks.php b/src/MasterBlocks.php
index 64e31bb4bd..042e249c2e 100644
--- a/src/MasterBlocks.php
+++ b/src/MasterBlocks.php
@@ -59,6 +59,9 @@ public function enqueue_block_editor_script(): void
'wp-edit-post',
]
);
+
+ $reflection_vars = self::get_js_variables();
+ wp_localize_script('planet4-blocks-theme-editor-script', 'p4_vars', $reflection_vars);
}
/**
@@ -82,6 +85,34 @@ public function enqueue_block_public_assets(): void
$js_creation,
true
);
- wp_enqueue_script('planet4-blocks-script');
+ wp_enqueue_script('planet4-blocks-theme-script');
+
+ $reflection_vars = self::get_js_variables();
+ wp_localize_script('planet4-blocks-theme-script', 'p4_vars', $reflection_vars);
+ }
+
+ /**
+ * Add variables reflected from PHP to JS.
+ */
+ public function get_js_variables(): array
+ {
+ $option_values = get_option('planet4_options');
+
+ $cookies_default_copy = [
+ 'necessary_cookies_name' => $option_values['necessary_cookies_name'] ?? '',
+ 'necessary_cookies_description' => $option_values['necessary_cookies_description'] ?? '',
+ 'analytical_cookies_name' => $option_values['analytical_cookies_name'] ?? '',
+ 'analytical_cookies_description' => $option_values['analytical_cookies_description'] ?? '',
+ 'all_cookies_name' => $option_values['all_cookies_name'] ?? '',
+ 'all_cookies_description' => $option_values['all_cookies_description'] ?? '',
+ ];
+
+ $reflection_vars = [
+ 'enable_analytical_cookies' => $option_values['enable_analytical_cookies'] ?? '',
+ 'enable_google_consent_mode' => $option_values['enable_google_consent_mode'] ?? '',
+ 'cookies_default_copy' => $cookies_default_copy,
+ ];
+
+ return $reflection_vars;
}
}
diff --git a/webpack.config.js b/webpack.config.js
index 7a6dc24f6c..71c60c148b 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -39,6 +39,8 @@ module.exports = {
CarouselHeaderEditorScript: './assets/src/blocks/CarouselHeader/CarouselHeaderEditorScript.js',
AccordionScript: './assets/src/blocks/Accordion/AccordionScript.js',
AccordionEditorScript: './assets/src/blocks/Accordion/AccordionEditorScript.js',
+ CookiesScript: './assets/src/blocks/Cookies/CookiesScript.js',
+ CookiesEditorScript: './assets/src/blocks/Cookies/CookiesEditorScript.js',
},
output: {
filename: '[name].js',