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',