PluginProbe ʕ •ᴥ•ʔ
FrontBlocks for Gutenberg/GeneratePress / 1.5.2
FrontBlocks for Gutenberg/GeneratePress v1.5.2
1.5.2 1.5.1 1.4.0 1.5.0 trunk 0.2.0 0.2.1 0.2.2 0.2.3 0.2.4 0.2.5 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.1.0 1.2.0 1.2.1 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 ci-artifacts
frontblocks / assets / cookie-notice / frontblocks-cookie-notice.js
frontblocks / assets / cookie-notice Last commit date
frontblocks-cookie-notice.css 2 days ago frontblocks-cookie-notice.js 2 days ago
frontblocks-cookie-notice.js
381 lines
1 /**
2 * FrontBlocks Cookie Notice
3 *
4 * @package FrontBlocks
5 * @version 1.0.0
6 */
7
8 (function () {
9 'use strict';
10
11 if (document.readyState === 'loading') {
12 document.addEventListener('DOMContentLoaded', init);
13 } else {
14 init();
15 }
16
17 function readCookie(name) {
18 var match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'));
19
20 if (!match) {
21 return '';
22 }
23
24 try {
25 return decodeURIComponent(match[1]);
26 } catch (e) {
27 // Malformed percent-encoding: treat it the same as no cookie at all.
28 return '';
29 }
30 }
31
32 function defineInjectHelper() {
33 window.frblCookieNoticeInject = window.frblCookieNoticeInject || function (gtmId, ga4Id, trackingIntegrations) {
34 if (gtmId) {
35 window.dataLayer = window.dataLayer || [];
36 window.dataLayer.push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
37
38 var gtmScript = document.createElement('script');
39 gtmScript.async = true;
40 gtmScript.src = 'https://www.googletagmanager.com/gtm.js?id=' + encodeURIComponent(gtmId);
41 document.head.appendChild(gtmScript);
42 }
43
44 if (ga4Id) {
45 var ga4Script = document.createElement('script');
46 ga4Script.async = true;
47 ga4Script.src = 'https://www.googletagmanager.com/gtag/js?id=' + encodeURIComponent(ga4Id);
48 document.head.appendChild(ga4Script);
49
50 window.dataLayer = window.dataLayer || [];
51 window.gtag = window.gtag || function () {
52 window.dataLayer.push(arguments);
53 };
54 window.gtag('js', new Date());
55 window.gtag('config', ga4Id);
56 }
57
58 if (!Array.isArray(trackingIntegrations)) {
59 trackingIntegrations = [];
60 }
61
62 trackingIntegrations.forEach(function (integration) {
63 var trackingType = integration && integration.type ? integration.type : '';
64 var trackingId = integration && integration.id ? integration.id : '';
65
66 if (!trackingId) {
67 return;
68 }
69
70 if (trackingType === 'clientify_analytics_plus') {
71 var clientifyPixel = document.createElement('script');
72 clientifyPixel.defer = true;
73 clientifyPixel.src = 'https://analyticsplusdev.clientify.net/analytics_plus/pixel/' + encodeURIComponent(trackingId);
74 document.head.appendChild(clientifyPixel);
75 } else if (trackingType === 'clientify_analytics_classic') {
76 (function (d, w, u, o) {
77 w[o] = w[o] || function () {
78 (w[o].q = w[o].q || []).push(arguments);
79 };
80 var a = d.createElement('script'),
81 m = d.getElementsByTagName('script')[0];
82 a.async = 1; a.src = u;
83 m.parentNode.insertBefore(a, m);
84 })(document, window, 'https://analytics.clientify.net/tracker.js', 'ana');
85 window.ana('setTrackerUrl', 'https://analytics.clientify.net');
86 window.ana('setTrackingCode', trackingId);
87 window.ana('trackPageview');
88 } else if (trackingType === 'brevo') {
89 var brevoScript = document.createElement('script');
90 brevoScript.async = true;
91 brevoScript.src = 'https://cdn.brevo.com/js/sdk-loader.js';
92 document.head.appendChild(brevoScript);
93
94 window.Brevo = window.Brevo || [];
95 window.Brevo.push(['init', { client_key: trackingId }]);
96 }
97 });
98 };
99 }
100
101 function fetchAndInjectScripts() {
102 var formData = new FormData();
103 formData.append('action', 'frbl_get_cookie_notice_config');
104
105 fetch(frblCookieNotice.ajaxUrl, {
106 method: 'POST',
107 credentials: 'same-origin',
108 body: formData
109 })
110 .then(function (response) {
111 return response.json();
112 })
113 .then(function (response) {
114 if (response && response.success && response.data && window.frblCookieNoticeInject) {
115 window.frblCookieNoticeInject(response.data.gtmId, response.data.ga4Id, response.data.trackingIntegrations);
116 }
117 })
118 .catch(function () {
119 // Network hiccup: consent is already stored locally; nothing else to do here.
120 });
121 }
122
123 /**
124 * For an already-decided visitor, request the tracking scripts (accepted)
125 * or simply do nothing further (rejected). Normally an inline bootstrap
126 * script printed on wp_head already does this as early as possible, well
127 * before this file even loads — this is the fallback for sites whose
128 * Content Security Policy blocks that unnonced inline script, so tracking
129 * still starts there too, just later.
130 *
131 * Deliberately separate from hiding the banner (see hideBannerIfDecided()):
132 * that inline copy runs on wp_head, before '#frbl-cookie-notice' exists in
133 * the DOM at all, so it can never do the hiding itself — only this file,
134 * running once the DOM is ready, can.
135 */
136 function requestTrackingIfNeeded() {
137 if (window.frblCookieNoticeBootstrapped) {
138 return;
139 }
140
141 defineInjectHelper();
142
143 var isStale = typeof window.frblCookieNoticeIsConsentStale === 'function' && window.frblCookieNoticeIsConsentStale();
144
145 if (readCookie(frblCookieNotice.cookieName) === 'accepted' && !isStale) {
146 fetchAndInjectScripts();
147 }
148
149 window.frblCookieNoticeBootstrapped = true;
150 }
151
152 function hideBannerIfDecided(banner) {
153 var consent = readCookie(frblCookieNotice.cookieName);
154
155 // An add-on tracking per-category consent (analytics vs. marketing) can
156 // define this to say "the categories cookie is stale — e.g. the site
157 // admin just added a new integration — so re-prompt even though the
158 // legacy accepted/rejected cookie here still looks decided."
159 if (typeof window.frblCookieNoticeIsConsentStale === 'function' && window.frblCookieNoticeIsConsentStale()) {
160 return false;
161 }
162
163 if (banner && (consent === 'accepted' || consent === 'rejected')) {
164 banner.style.display = 'none';
165 return true;
166 }
167
168 return false;
169 }
170
171 function init() {
172 if (typeof frblCookieNotice === 'undefined') {
173 return;
174 }
175
176 requestTrackingIfNeeded();
177
178 var banner = document.getElementById('frbl-cookie-notice');
179
180 if (!banner) {
181 return;
182 }
183
184 if (hideBannerIfDecided(banner)) {
185 // Already decided: nothing left to wire up.
186 return;
187 }
188
189 var acceptBtn = banner.querySelector('[data-frbl-cookie-action="accept"]');
190 var rejectBtn = banner.querySelector('[data-frbl-cookie-action="reject"]');
191 var isPopup = banner.classList.contains('frbl-cookie-notice--popup');
192 var previouslyFocused = document.activeElement;
193
194 revealBanner();
195
196 if (acceptBtn) {
197 acceptBtn.addEventListener('click', function () {
198 handleDecision('accepted');
199 });
200 }
201
202 if (rejectBtn) {
203 rejectBtn.addEventListener('click', function () {
204 handleDecision('rejected');
205 });
206 }
207
208 var customizeBtn = banner.querySelector('[data-frbl-cookie-action="customize"]');
209
210 if (customizeBtn) {
211 // Deliberately not routed through handleDecision(): clicking it isn't a
212 // decision by itself, just a request to see more detail. An add-on
213 // listens for this to open its own categories dialog.
214 customizeBtn.addEventListener('click', function () {
215 var event;
216
217 try {
218 event = new CustomEvent('frblCookieNoticeCustomize');
219 } catch (e) {
220 event = document.createEvent('CustomEvent');
221 event.initCustomEvent('frblCookieNoticeCustomize', true, true, null);
222 }
223
224 document.dispatchEvent(event);
225 });
226 }
227
228 /**
229 * Reveal the banner: removes the '--init' class printed by PHP so the
230 * CSS transition animates it in (slide up for the bar, slide in from
231 * its anchored edge for the box, scale/fade in for the popup). The
232 * popup gets a short extra delay first, per its layout's own request,
233 * so it doesn't feel jarring the instant the page loads.
234 */
235 function revealBanner() {
236 var delay = isPopup ? 150 : 20;
237
238 window.setTimeout(function () {
239 banner.classList.remove('frbl-cookie-notice--init');
240
241 if (isPopup) {
242 document.body.classList.add('frbl-cookie-notice-lock-scroll');
243
244 if (acceptBtn) {
245 acceptBtn.focus({ preventScroll: true });
246 }
247
248 document.addEventListener('keydown', trapFocus);
249 }
250 }, delay);
251 }
252
253 function trapFocus(event) {
254 if (event.key !== 'Tab') {
255 return;
256 }
257
258 var focusable = Array.prototype.slice.call(
259 banner.querySelectorAll('a[href], button')
260 );
261
262 if (!focusable.length) {
263 return;
264 }
265
266 var first = focusable[0];
267 var last = focusable[focusable.length - 1];
268
269 if (event.shiftKey && document.activeElement === first) {
270 event.preventDefault();
271 last.focus();
272 } else if (!event.shiftKey && document.activeElement === last) {
273 event.preventDefault();
274 first.focus();
275 }
276 }
277
278 var decided = false;
279
280 function handleDecision(decision) {
281 if (decided) {
282 return;
283 }
284
285 decided = true;
286
287 setConsentCookie(decision);
288 updateConsentMode(decision);
289 hideBanner();
290 dispatchConsentEvent(decision);
291 logDecision(decision);
292
293 if (decision === 'accepted') {
294 fetchAndInjectScripts();
295 }
296 }
297
298 function updateConsentMode(decision) {
299 var granted = decision === 'accepted' ? 'granted' : 'denied';
300
301 window.dataLayer = window.dataLayer || [];
302 window.dataLayer.push(['consent', 'update', {
303 ad_storage: granted,
304 ad_user_data: granted,
305 ad_personalization: granted,
306 analytics_storage: granted
307 }]);
308 }
309
310 function setConsentCookie(decision) {
311 var maxAge = parseInt(frblCookieNotice.expirationDays, 10) * 24 * 60 * 60;
312 var secure = window.location.protocol === 'https:' ? '; Secure' : '';
313
314 document.cookie = frblCookieNotice.cookieName + '=' + decision +
315 '; path=' + frblCookieNotice.cookiePath + '; max-age=' + maxAge + '; SameSite=Lax' + secure;
316 }
317
318 function hideBanner() {
319 banner.classList.add('frbl-cookie-notice--hidden');
320 document.body.classList.remove('frbl-cookie-notice-lock-scroll');
321 document.removeEventListener('keydown', trapFocus);
322
323 if (isPopup && previouslyFocused && typeof previouslyFocused.focus === 'function') {
324 previouslyFocused.focus({ preventScroll: true });
325 }
326
327 window.setTimeout(function () {
328 if (banner.parentNode) {
329 banner.parentNode.removeChild(banner);
330 }
331 }, 300);
332 }
333
334 function dispatchConsentEvent(decision) {
335 var event;
336
337 try {
338 event = new CustomEvent('frblCookieConsent', { detail: { consent: decision } });
339 } catch (e) {
340 event = document.createEvent('CustomEvent');
341 event.initCustomEvent('frblCookieConsent', true, true, { consent: decision });
342 }
343
344 document.dispatchEvent(event);
345 }
346
347 function logDecision(decision) {
348 var nonceForm = new FormData();
349 nonceForm.append('action', 'frbl_get_cookie_notice_log_nonce');
350
351 fetch(frblCookieNotice.ajaxUrl, {
352 method: 'POST',
353 credentials: 'same-origin',
354 body: nonceForm
355 })
356 .then(function (response) {
357 return response.json();
358 })
359 .then(function (response) {
360 if (!response || !response.success || !response.data) {
361 return;
362 }
363
364 var formData = new FormData();
365 formData.append('action', 'frbl_log_cookie_consent');
366 formData.append('nonce', response.data.nonce);
367 formData.append('decision', decision);
368
369 return fetch(frblCookieNotice.ajaxUrl, {
370 method: 'POST',
371 credentials: 'same-origin',
372 body: formData
373 });
374 })
375 .catch(function () {
376 // Best-effort: the aggregate stat is not critical to the consent flow.
377 });
378 }
379 }
380 })();
381