PageSourceSearch

https://www.jrsreal.cz/www/theme/CookieBar_cs/CookieBar.js

js jrsreal.cz collected 2026-10-08 09:09:40 UTC 19,208 bytes, 793 lines download raw bytes

1/**
2 * @description CookieBar IMPnet s.r.o.
3 * @author Daniel Žůrek
4 * @version 2.0
5  */
6class CookieBar
7{
8
9	#UI;
10	#logged;
11
12	constructor()
13	{
14		window.cookieBar = this;
15		this.#UI = (document.currentScript.src).split('/').slice(0,-1).join('/') + '/' + 'Api/';
16		this.#logged = false;
17
18		this.#initConsent();
19		// Update posíláme až když návštěvník opravdu rozhodl. Stará komponenta ho před
20		// rozhodnutím neposílala vůbec (consent.latte, n:if="!$show"), takže okno
21		// wait_for_update zůstalo otevřené. Okamžitý update s "denied" ho zavře a GTM
22		// ho čte jako rozhodnutí uživatele — marketingu tím mizí consent mode modelování.
23		if(this.#hasDecision()) {
24			this.#updateConsent();
25		}
26
27		document.addEventListener('DOMContentLoaded', () => {
28
29			this.#initEvents();
30
31			this.#renderCss('default').then(
32				data => this.#message(data)
33			).catch(
34				error => this.#error(error)
35			);
36
37			let operational = new CookieBarStorage('operational');
38			let marketing = new CookieBarStorage('marketing');
39			let analytic = new CookieBarStorage('analytic');
40
41			if(operational.value === null || marketing.value === null || analytic.value === null) {
42				this.#renderCookieBar().then(
43					data => this.#message(data)
44				).catch(
45					error => this.#error(error)
46				);
47			} else {
48				this.#renderButton().then(
49					data => this.#message(data)
50				).catch(
51					error => this.#error(error)
52				);
53			}
54
55		});
56	}
57
58	#initEvents()
59	{
60		let CookieBar = this;
61
62		// Rozbalování akordeonu řešíme v capture fázi a cílový stav vynutíme až po doběhnutí
63		// ostatních posluchačů. Některé weby mají v theme JS vlastní jQuery handler, který dělá
64		// to samé (toggleClass('active')); dva toggly na jeden klik se vyruší a lišta se pak
65		// tváří, že na ni nejde kliknout. Takhle je jedno, kolik dalších přepínačů tam visí.
66		document.addEventListener('click', function(event){
67			let toggle = event.target.closest('[data-cookie-bar-accordion-toggle]');
68			if(!toggle) {
69				return;
70			}
71
72			let accordion = toggle.closest('[data-cookie-bar-accordion]');
73			if(!accordion) {
74				return;
75			}
76
77			let target = !accordion.classList.contains('active');
78			setTimeout(() => accordion.classList.toggle('active', target), 0);
79		}, true);
80
81		document.addEventListener('click', function(event){
82			let setup = event.target.closest('[data-cookie-bar-setup]');
83			if(setup) {
84				event.preventDefault();
85				let type = setup.getAttribute('data-cookie-bar-setup');
86				let inputs = document.querySelectorAll('input[data-cookie-bar-input]');
87				inputs.forEach(el => {
88					let name = el.getAttribute('data-cookie-bar-input');
89					let value = type === 'enable' || (type === 'select' && el.checked) ? 'enable' : 'disable';
90					let cookieStorage = new CookieBarStorage(name);
91					cookieStorage.value = value;
92					cookieStorage.expires = 60 * 60 * 24 * 365;
93					cookieStorage.save();
94				});
95				CookieBar.#updateConsent();
96				CookieBar.#redraw();
97				CookieBar.#renderButton().then(
98					data => CookieBar.#message(data)
99				).catch(
100					error => CookieBar.#error(error)
101				);
102			}
103
104			let addGroup = event.target.closest('[data-cookie-bar-settings-add-group]');
105			if(addGroup) {
106				event.preventDefault();
107
108				let xhttpParams = {
109					'action': 'html',
110					'data': 'terminalGroup'
111				};
112
113				CookieBar.#xhttp(xhttpParams).then(
114					data => {
115						let groupTemplate = data.content;
116						let group = {'name': '------', 'url': '', 'index': Math.floor(Math.random() * 1000)};
117						let groupHtml = CookieBar.#interpolate(groupTemplate, group);
118						addGroup.insertAdjacentHTML('afterend', groupHtml);
119						CookieBar.#renderImages();
120					}
121				).catch(
122					error => CookieBar.#error(error)
123				);
124			}
125
126			let addItem = event.target.closest('[data-cookie-bar-settings-add-item]');
127			if(addItem) {
128				event.preventDefault();
129
130				let xhttpParams = {
131					'action': 'html',
132					'data': 'terminalItem'
133				};
134
135				CookieBar.#xhttp(xhttpParams).then(
136					data => {
137						let itemTemplate = data.content;
138						let item = {'name': '------', 'description': '', 'expire': '', 'type': '', 'group': {'index': Math.floor(Math.random() * 1000)}, 'index': Math.floor(Math.random() * 1000)};
139						let itemHtml = CookieBar.#interpolate(itemTemplate, item);
140						addItem.insertAdjacentHTML('afterend', itemHtml);
141						CookieBar.#renderImages();
142					}
143				).catch(
144					error => CookieBar.#error(error)
145				);
146			}
147
148			let removeGroup = event.target.closest('[data-cookie-bar-settings-remove-group]');
149			if(removeGroup) {
150				event.preventDefault();
151				removeGroup.closest('[data-cookie-bar-settings-group]').remove();
152			}
153
154			let removeItem = event.target.closest('[data-cookie-bar-settings-remove-item]');
155			if(removeItem) {
156				event.preventDefault();
157				removeItem.closest('[data-cookie-bar-settings-item]').remove();
158			}
159		});
160
161		document.addEventListener('submit', function(event){
162			if(event.target.hasAttribute('data-cookie-bar-login')) {
163				event.preventDefault();
164				let formData = new FormData(event.target);
165				CookieBar.#getLogin(formData.get('username'), formData.get('password')).then(
166					data => {
167						CookieBar.#logged = true;
168						CookieBar.#message(data);
169						CookieBar.#renderTerminal();
170					}
171				).catch(
172					error => CookieBar.#error(error)
173				);
174			}
175
176			if(event.target.hasAttribute('data-cookie-bar-terminal')) {
177				event.preventDefault();
178				let formData = new FormData(event.target);
179				CookieBar.#setSettings(formData).then(
180					data => CookieBar.#message(data)
181				).catch(
182					error => CookieBar.#error(error)
183				);
184			}
185
186			if(event.target.hasAttribute('data-cookie-bar-button')) {
187				event.preventDefault();
188				CookieBar.#renderCookieBar();
189
190			}
191		});
192	}
193
194	/**
195	 * @param {object} params
196	 */
197	#xhttp(params)
198	{
199		return new Promise((resolve, reject) => {
200			let xhttp = new XMLHttpRequest();
201			xhttp.open('POST', this.#UI, true);
202			xhttp.setRequestHeader('Content-type', 'application/json');
203			xhttp.ontimeout = () => reject('Request timed out');
204			xhttp.onerror = () => reject('Request error: ' + xhttp.status);
205			xhttp.onload = () => {
206				if(xhttp.readyState === 4) {
207					if (xhttp.status === 200) {
208						try {
209							resolve(JSON.parse(xhttp.responseText));
210						} catch(error) {
211							reject('Response error: ' + error);
212						}
213					} else {
214						reject('Response error: ' + xhttp.status);
215					}
216				}
217			};
218			xhttp.send(JSON.stringify(params));
219		});
220	}
221
222	/**
223	 * @param {string} message
224	 */
225	#error(message)
226	{
227		console.error('%cCookie Bar%c' + message, 'background: #f0506e; color: #fff');
228	}
229
230	/**
231	 * @param {string} message
232	 */
233	#message(message)
234	{
235		console.info('%cCookie Bar%c' + message, 'background: #41ba55; color: #fff');
236	}
237
238	/**
239	 *
240	 * @param {string} template
241	 * @param {object} object
242	 */
243	#interpolate(template, object)
244	{
245		return (new Function(...Object.keys(object), 'return `' + template + '`;'))(...Object.values(object));
246	}
247
248	/**
249	 * @param {FormData} formData
250	 */
251	#setSettings(formData)
252	{
253		let data = [];
254		let groupIndex = 0;
255		let itemIndex = 0;
256		for(let [key, value] of formData.entries()) {
257			if(key === 'sklik') {
258				if(value !== '') {
259					groupIndex = data.length;
260					data[groupIndex] = {'sklik': value};
261				}
262				continue;
263			}
264
265			let [,name,,isItem] = key.match(/([^\]]*)\[(\d+)?\](\[(\d+)?\])?/);
266
267			if(name === 'name' && isItem === undefined) {
268				groupIndex = data.length;
269				itemIndex = 0;
270			}
271
272			data[groupIndex] = data[groupIndex] || {};
273
274			if(name === 'name' && isItem !== undefined) {
275				data[groupIndex].items = data[groupIndex].items || [];
276				itemIndex = data[groupIndex].items.length
277			}
278
279			if(isItem !== undefined) {
280				data[groupIndex].items[itemIndex] = data[groupIndex].items[itemIndex] || {};
281				data[groupIndex].items[itemIndex][name] = value;
282			} else {
283				data[groupIndex][name] = value;
284			}
285		}
286
287		return new Promise((resolve, reject) => {
288			let xhttpParams = {
289				'action': 'settings',
290				'data': JSON.stringify(data)
291			};
292
293			this.#xhttp(xhttpParams).then(
294				() => resolve('Settings saved')
295			).catch(
296				error => reject(error)
297			);
298		});
299	}
300
301	#setDataLayer()
302	{
303		window.dataLayer = window.dataLayer || [];
304		window.dataLayer.push(arguments);
305	}
306
307	/**
308	 * @param {bool} consent
309	 */
310	#setSklik(consent)
311	{
312		this.#getSettings().then(groups => {
313			groups.forEach((group) => {
314				if(typeof group['sklik'] === 'undefined') {
315					return;
316				}
317
318				if(typeof window.rc === 'undefined' || typeof window.sznIVA === 'undefined' || typeof window.sznIVA.IS === 'undefined') {
319					this.#error('Missing implements Sklik retargeting code.');
320					return;
321				}
322
323				window.sznIVA.IS.updateIdentities({
324					eid: null
325				});
326
327				window.rc.retargetingHit({
328					rtgId: group['sklik'],
329					consent: consent ? 1 : 0,
330				});
331			});
332		});
333	}
334
335	#initConsent()
336	{
337		this.#setDataLayer(
338			'consent',
339			'default',
340			{
341				// Technická a bezpečnostní kategorie je vždy povolená (nejde vypnout),
342				// stará lišta je proto grantovala už v defaultu — držíme stejně.
343				'functionality_storage': 'granted',
344				'security_storage': 'granted',
345				'personalization_storage': 'denied',
346				'ad_storage': 'denied',
347				'analytics_storage': 'denied',
348				'ad_user_data': 'denied',
349				'ad_personalization': 'denied',
350				'wait_for_update': 2000,
351			}
352		);
353
354		this.#setSklik(false);
355	}
356
357	/**
358	 * Rozhodl už návštěvník o souhlasu? (všechny tři kategorie mají uloženou hodnotu)
359	 *
360	 * @return {boolean}
361	 */
362	#hasDecision()
363	{
364		return ['operational', 'marketing', 'analytic']
365			.every(name => new CookieBarStorage(name).value !== null);
366	}
367
368	#updateConsent()
369	{
370		let operational = new CookieBarStorage('operational');
371		let marketing = new CookieBarStorage('marketing');
372		let analytic = new CookieBarStorage('analytic');
373
374		this.#setDataLayer(
375			'consent',
376			'update',
377			{
378				'functionality_storage': 'granted',
379				'security_storage': 'granted',
380				'personalization_storage': operational.value === 'enable' ? 'granted' : 'denied',
381				'ad_storage': marketing.value === 'enable' ? 'granted' : 'denied',
382				'ad_personalization': marketing.value === 'enable' ? 'granted' : 'denied',
383				'ad_user_data': marketing.value === 'enable' ? 'granted' : 'denied',
384				'analytics_storage': analytic.value === 'enable' ? 'granted' : 'denied',
385			}
386		);
387
388		this.#setSklik(marketing.value === 'enable');
389	}
390
391	#getSettings()
392	{
393		return new Promise((resolve, reject) => {
394			let xhttpParams = {
395				'action': 'settings',
396			};
397
398			this.#xhttp(xhttpParams).then(
399				data => resolve(data)
400			).catch(
401				error => reject(error)
402			);
403		});
404	}
405
406	/**
407	 * @param {string} username
408	 * @param {string} password
409	 */
410	#getLogin(username, password)
411	{
412		return new Promise((resolve, reject) => {
413			let xhttpParams = {
414				'action': 'login',
415				'data': username + ':' + password
416			};
417
418			this.#xhttp(xhttpParams).then(
419				data => resolve('Logged in')
420			).catch(
421				error => reject(error)
422			);
423		});
424	}
425
426	/**
427	 * @param {string} filename
428	 */
429	#getImage(filename)
430	{
431		return new Promise((resolve, reject) => {
432			let xhttpParams = {
433				'action': 'image',
434				'data': filename
435			};
436
437			this.#xhttp(xhttpParams).then(
438				data => resolve(data)
439			).catch(
440				error => reject(error)
441			);
442		});
443	}
444
445	#renderlogin()
446	{
447		return new Promise((resolve, reject) => {
448			let xhttpParams = {
449				'action': 'html',
450				'data': 'login'
451			};
452
453			this.#renderCss('terminal').then(
454				data => this.#message(data)
455			).catch(
456				error => this.#error(error)
457			);
458
459			this.#xhttp(xhttpParams).then(
460				data => {
461					this.#redraw();
462					document.body.insertAdjacentHTML('beforeend', data.content);
463					this.#renderImages();
464					resolve('Login loaded');
465				}
466			).catch(
467				error => reject(error)
468			);
469		});
470	}
471
472	/**
473	 * @param {string} filename
474	 */
475	#renderCss(filename)
476	{
477		return new Promise((resolve, reject) => {
478			let xhttpParams = {
479				'action': 'css',
480				'data': filename,
481			};
482
483			this.#xhttp(xhttpParams).then(
484				data => {
485					let style = document.createElement('style');
486					style.textContent = data.content;
487					document.head.insertAdjacentElement('beforeend', style);
488					resolve('CSS loaded')
489				}
490			).catch(
491				error => reject(error)
492			);
493		});
494	}
495
496	#renderImages()
497	{
498		let images = document.querySelectorAll('[data-cookie-bar-image]');
499		images.forEach(el => {
500			this.#getImage( el.getAttribute('data-cookie-bar-image') ).then(
501				data => {
502					el.src = data.content;
503					el.removeAttribute('data-cookie-bar-image');
504					this.#message('Image loaded');
505				}
506			);
507		});
508	}
509
510	#renderButton()
511	{
512		return new Promise((resolve, reject) => {
513			let xhttpParams = {
514				'action': 'html',
515				'data': 'button'
516			};
517
518			this.#xhttp(xhttpParams).then(
519				data => {
520					this.#redraw();
521					document.body.insertAdjacentHTML('beforeend', data.content);
522					this.#renderImages();
523					resolve('Button loaded');
524				}
525			).catch(
526				error => reject( error )
527			);
528		});
529	}
530
531	#renderCookieBar()
532	{
533		return new Promise((resolve, reject) => {
534			let xhttpParams = {
535				'action': 'html',
536				'data': 'default'
537			};
538
539			this.#xhttp(xhttpParams).then(
540				data => {
541					this.#redraw();
542					document.body.insertAdjacentHTML('beforeend', data.content);
543
544					let checkboxes = document.querySelectorAll('[data-cookie-bar-input]');
545					checkboxes.forEach(el => {
546						let name = el.getAttribute('data-cookie-bar-input');
547						let cookieStorage = new CookieBarStorage(name);
548						if(cookieStorage.value === 'enable') {
549							el.setAttribute('checked', '');
550						}
551					});
552
553					let xhttpParams = {
554						'action': 'html',
555						'data': 'defaultGroup'
556					};
557					this.#xhttp(xhttpParams).then(
558						data => {
559							let groupTemplate = data.content;
560
561							let xhttpParams = {
562								'action': 'html',
563								'data': 'defaultItem'
564							};
565
566							this.#xhttp(xhttpParams).then(
567								data => {
568									let itemTemplate = data.content;
569
570									this.#getSettings().then(
571										groups => {
572											let divs = document.querySelectorAll('[data-cookie-bar-cookies-group]');
573											divs.forEach(el => {
574												let type = el.getAttribute('data-cookie-bar-cookies-group');
575												let divCount = document.querySelector('[data-cookie-bar-cookies-group-count="' + type + '"]');
576												let groupCount = 0;
577
578												groups.forEach((group) => {
579													if(typeof group[type] === 'undefined') {
580														return;
581													}
582													groupCount++;
583
584													group.renderItems = '';
585													if(group.items !== undefined) {
586														group.items.forEach(item => {
587															let itemHtml = this.#interpolate(itemTemplate, item);
588															group.renderItems += ' ' + itemHtml;
589														});
590													}
591
592													let groupHtml = this.#interpolate(groupTemplate, group);
593													el.insertAdjacentHTML('beforeend', groupHtml);
594												});
595												divCount.textContent = groupCount ? ' - ' + groupCount : ''
596											});
597
598											this.#renderImages();
599										}
600									).catch(
601										error => this.#error(error)
602									);
603								}
604							).catch(
605								error => this.#error(error)
606							);
607						}
608					).catch(
609						error => this.#error(error)
610					);
611					this.#renderImages();
612					resolve('Default loaded');
613				}
614			).catch(
615				error => reject( error )
616			);
617		});
618	}
619
620
621
622	#renderSettings()
623	{
624		let div = document.querySelector('[data-cookie-bar-settings]');
625
626		let xhttpParams = {
627			'action': 'html',
628			'data': 'terminalGroup'
629		};
630
631		this.#xhttp(xhttpParams).then(
632			data => {
633				let groupTemplate = data.content;
634
635				let xhttpParams = {
636					'action': 'html',
637					'data': 'terminalItem'
638				};
639
640				this.#xhttp(xhttpParams).then(
641					data => {
642						let itemTemplate = data.content;
643
644						this.#getSettings().then(
645							groups => {
646								groups.forEach((group, groupIndex) => {
647									if(typeof group['sklik'] !== 'undefined') {
648										document.querySelector('input[name="sklik"]').value = group['sklik'];
649										return;
650									}
651
652									group.index = groupIndex;
653									let groupHtml = this.#interpolate(groupTemplate, group);
654									div.insertAdjacentHTML('beforeend', groupHtml);
655
656									if(group.items === undefined) {
657										return;
658									}
659
660									group.items.forEach((item, itemIndex) => {
661										item.group = group;
662										item.index = itemIndex;
663										let groupItemHtml = this.#interpolate(itemTemplate, item);
664										div.querySelector('[data-cookie-bar-settings-group-items]').insertAdjacentHTML('beforeend', groupItemHtml);
665									});
666
667									this.#renderImages();
668								});
669							}
670						).catch(
671							error => this.#error(error)
672						);
673					}
674				).catch(
675					error => this.#error(error)
676				);
677			}
678		).catch(
679			error => this.#error(error)
680		);
681	}
682
683	#renderTerminal()
684	{
685		return new Promise((resolve, reject) => {
686			let xhttpParams = {
687				'action': 'html',
688				'data': 'terminal'
689			};
690
691			this.#xhttp(xhttpParams).then(data => {
692				this.#redraw();
693				document.body.insertAdjacentHTML('beforeend', data.content);
694				this.#renderSettings();
695
696				this.#renderImages();
697				resolve('Terminal loaded');
698			})
699			.catch(
700				error => reject(error)
701			);
702		});
703	}
704
705	#redraw()
706	{
707		let elements = document.querySelectorAll('[data-cookie-bar-modal],[data-cookie-bar-button]');
708		elements.forEach(element => {
709			element.remove();
710		});
711	}
712
713	get terminal()
714	{
715		if(!this.#logged) {
716			this.#renderlogin().then(
717				data => this.#message(data)
718			).catch(
719				error => this.#error(error)
720			);
721		} else {
722			this.#renderTerminal().then(
723				data => this.#message(data)
724			).catch(
725				error => this.#error(error)
726			);
727		}
728
729		return null;
730	}
731
732}
733
734class CookieBarStorage
735{
736
737	/**
738	 * @type {string}
739	 */
740	#name;
741
742	/**
743	 * @type {int}
744	 */
745	#expires = 0;
746
747	/**
748	 * @type {string}
749	 */
750	#prefix;
751
752	/**
753	 * @param {string} name
754	 */
755	constructor(name)
756	{
757		this.#prefix = `_${this.constructor.name}_`;
758		this.#name = name;
759		let match = document.cookie.match(RegExp(this.#prefix + this.#name + '=([^;]*)'));
760		this.value = match ? decodeURIComponent(match[1]) : null;
761	}
762
763	/**
764	 * @param {int} seconds
765	 */
766	set expires(seconds)
767	{
768		this.#expires = parseInt(seconds);
769	}
770
771	save()
772	{
773		let date = new Date();
774		date.setTime(date.getTime() + this.#expires * 1000);
775
776		let name = this.#prefix + this.#name;
777		let value = encodeURIComponent(this.value);
778		let expires = date.toUTCString();
779
780		document.cookie = `${name}=${value}; expires=${expires}; path=/`;
781
782		// Legacy cookie pro serverovou část Brave CMS. Čte ji vkládání skriptů z administrace
783		// (App/FrontendModule/Components/Scripts/Scripts.php přes CookieBar::getCookie) a
784		// serverové měření CAPI/sGTM (App/Domain/Tracking/Consent/ConsentReader). Obojí zná
785		// jen jméno `cookie-bar-<kategorie>` s hodnotou 1/0 — bez ní by marketingové skripty
786		// a server-side eventy po přechodu na tuhle lištu tiše přestaly chodit.
787		let legacyValue = this.value === 'enable' ? '1' : '0';
788		document.cookie = `cookie-bar-${this.#name}=${legacyValue}; expires=${expires}; path=/`;
789	}
790
791}
792
793new CookieBar();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.