PageSourceSearch

https://www.radimposvom.com.hr/wp-content/plugins/bl-consent/assets/js/bl-consent.js?ver=1.0.2.0

js radimposvom.com.hr collected 2026-10-09 05:15:05 UTC 32,786 bytes, 1,180 lines download raw bytes

1/*!
2 * BL Kolačići - sučelje sajta: banner, Postavke kolačića, plutajuća tipka i okviri.
3 *
4 * Vanilla JS (ES2017), bez ovisnosti i bez build koraka. HTML ispisuje PHP u
5 * podnožju; ovdje se samo mijenjaju klase i atributi (nema innerHTML gradnje).
6 * Ugovor prema drugim pluginovima: SPEC-jezgra.md, odjeljak 8.
7 */
8(function () {
9	'use strict';
10
11	var w = window;
12	var d = document;
13	var html = d.documentElement;
14	var CFG = w.blConsentConfig || {};
15	var KEY = { preferences: 'p', statistics: 's', marketing: 'm' };
16	var SERVICES = ['youtube', 'spotify', 'maps'];
17	var MOBILE_MAX = 767;
18
19	if (w.blConsent && w.blConsent.__full) {
20		return; // Već učitano (skripta dvaput na stranici).
21	}
22
23	// ---------------------------------------------------------------- pomoćno
24
25	function log() {
26		if (CFG.debug && w.console && console.log) {
27			var a = Array.prototype.slice.call(arguments);
28			a.unshift('[BL Kolačići]');
29			console.log.apply(console, a);
30		}
31	}
32
33	function bit(v) {
34		return v === true || v === 1 || v === '1' || v === 'true' ? 1 : 0;
35	}
36
37	function days(n, def) {
38		n = parseInt(n, 10);
39		return n > 0 ? n : def;
40	}
41
42	function readCookie(name) {
43		var parts = d.cookie ? d.cookie.split('; ') : [];
44		for (var i = 0; i < parts.length; i++) {
45			var eq = parts[i].indexOf('=');
46			if (eq > 0 && parts[i].slice(0, eq) === name) {
47				return parts[i].slice(eq + 1);
48			}
49		}
50		return null;
51	}
52
53	function setCookie(name, value, nDays, domain) {
54		var c = name + '=' + value + '; Max-Age=' + Math.round(nDays * 86400) + '; Path=/; SameSite=Lax';
55		if (domain) {
56			c += '; Domain=' + domain;
57		}
58		if (w.location.protocol === 'https:') {
59			c += '; Secure';
60		}
61		d.cookie = c;
62	}
63
64	function newId() {
65		var bytes = new Uint8Array(16);
66		if (w.crypto && w.crypto.getRandomValues) {
67			w.crypto.getRandomValues(bytes);
68		} else {
69			for (var i = 0; i < 16; i++) {
70				bytes[i] = Math.floor(Math.random() * 256);
71			}
72		}
73		var s = '';
74		for (var j = 0; j < bytes.length; j++) {
75			s += String.fromCharCode(bytes[j]);
76		}
77		return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
78	}
79
80	function isMobile() {
81		return (w.innerWidth || html.clientWidth) <= MOBILE_MAX;
82	}
83
84	function device() {
85		var wd = w.innerWidth || html.clientWidth;
86		if (wd <= MOBILE_MAX) {
87			return 'm';
88		}
89		var coarse = w.matchMedia && w.matchMedia('(pointer: coarse)').matches;
90		return wd <= 1024 && coarse ? 't' : 'd';
91	}
92
93	// Izvor prometa za dnevne zbrojeve (od 1.0.2): meta, google, newsletter, other
94	// ili direct. Određuje se jednom po posjetu (kartici), iz prve stranice posjeta;
95	// šalje se samo kao naziv kanala, bez adrese, referera i identifikatora.
96	function channel() {
97		var ch = '';
98		try {
99			ch = w.sessionStorage.getItem('blc_ch') || '';
100		} catch (e) {}
101		if (/^(direct|google|meta|newsletter|other)$/.test(ch)) {
102			return ch;
103		}
104		ch = 'direct';
105		try {
106			var q = new URLSearchParams(w.location.search);
107			var us = String(q.get('utm_source') || '').toLowerCase();
108			var um = String(q.get('utm_medium') || '').toLowerCase();
109			var ref = '';
110			try {
111				ref = d.referrer ? new URL(d.referrer).hostname.toLowerCase() : '';
112			} catch (e) {}
113			var root = String(CFG.domain || '').replace(/^\./, '').toLowerCase();
114			if (ref && (ref === w.location.hostname || (root && (ref === root || ref.slice(-root.length - 1) === '.' + root)))) {
115				ref = '';
116			}
117			if (/^(newsletter|getresponse|mailerlite|email|e-mail|gr)$/.test(us) || /e-?mail|newsletter/.test(um)) {
118				ch = 'newsletter';
119			} else if (q.get('fbclid') || /^(facebook|fb|instagram|ig|meta)/.test(us) || /(^|\.)(facebook|instagram|fb)\.com$/.test(ref)) {
120				ch = 'meta';
121			} else if (q.get('gclid') || q.get('gbraid') || q.get('wbraid') || /^google/.test(us) || /(^|\.)google\.[a-z.]+$/.test(ref)) {
122				ch = 'google';
123			} else if (us || ref) {
124				ch = 'other';
125			}
126		} catch (e) {}
127		try {
128			w.sessionStorage.setItem('blc_ch', ch);
129		} catch (e) {}
130		return ch;
131	}
132
133	function visible(el) {
134		return !!(el && (el.offsetWidth || el.offsetHeight || el.getClientRects().length));
135	}
136
137	function emit(name, detail) {
138		var ev;
139		try {
140			ev = new CustomEvent(name, { detail: detail });
141		} catch (e) {
142			ev = d.createEvent('CustomEvent');
143			ev.initCustomEvent(name, false, false, detail);
144		}
145		d.dispatchEvent(ev);
146	}
147
148	// ---------------------------------------------------------------- stanje
149
150	function normalize(s) {
151		s = s && typeof s === 'object' ? s : {};
152		var c = s.c && typeof s.c === 'object' ? s.c : {};
153		var svcIn = s.svc && typeof s.svc === 'object' ? s.svc : {};
154		var svc = {};
155		SERVICES.forEach(function (k) {
156			svc[k] = bit(svcIn[k]);
157		});
158		return {
159			decided: !!s.decided,
160			c: { p: bit(c.p), s: bit(c.s), m: bit(c.m) },
161			svc: svc,
162			id: typeof s.id === 'string' && /^[A-Za-z0-9_-]{22}$/.test(s.id) ? s.id : null,
163			rev: parseInt(s.rev, 10) || parseInt(CFG.rev, 10) || 1
164		};
165	}
166
167	function anyAllowed(st) {
168		return !!(st.c.p || st.c.s || st.c.m || SERVICES.some(function (k) { return st.svc[k]; }));
169	}
170
171	// Kolačić bl_consent, kad glava nije postavila stanje (npr. optimizator je pomaknuo skriptu).
172	function fromCookie() {
173		var raw = readCookie('bl_consent');
174		if (!raw) {
175			return null;
176		}
177		var o;
178		try {
179			o = JSON.parse(decodeURIComponent(raw));
180		} catch (e) {
181			return null;
182		}
183		if (!o || o.v !== 1 || !o.c || typeof o.c !== 'object') {
184			return null;
185		}
186		if ((parseInt(o.rev, 10) || 0) !== (parseInt(CFG.rev, 10) || 1)) {
187			return null;
188		}
189		var st = normalize({ decided: true, c: o.c, svc: o.svc, id: o.id, rev: o.rev });
190		var maxAge = (anyAllowed(st) ? days(CFG.acceptDays, 365) : days(CFG.rejectDays, 90)) * 86400;
191		if (!o.ts || Date.now() / 1000 - o.ts > maxAge) {
192			return null;
193		}
194		return st;
195	}
196
197	var head = w.blConsentState;
198	var state = head && typeof head === 'object' && typeof head.decided === 'boolean' ? normalize(head) : (fromCookie() || normalize({ decided: false }));
199
200	function copy() {
201		return {
202			decided: state.decided,
203			c: { p: state.c.p, s: state.c.s, m: state.c.m },
204			svc: Object.assign({}, state.svc),
205			id: state.id,
206			rev: state.rev
207		};
208	}
209
210	function has(what) {
211		if (what === 'necessary' || what === 'functional') {
212			return true;
213		}
214		if (KEY[what]) {
215			return !!state.c[KEY[what]];
216		}
217		if (SERVICES.indexOf(what) !== -1) {
218			return !!(state.c.m || state.svc[what]);
219		}
220		return false;
221	}
222
223	// Klase iz glave; ako ih nema, postavi ih ovdje.
224	if (!html.classList.contains('blc-decided') && !html.classList.contains('blc-undecided')) {
225		html.classList.add(state.decided ? 'blc-decided' : 'blc-undecided');
226	}
227	html.classList.add('blc-js');
228
229	var listeners = [];
230
231	// ---------------------------------------------------------------- slanje
232
233	var queue = Promise.resolve();
234
235	var epBroken = false;
236
237	function post(path, body) {
238		if (!w.fetch || (!CFG.ep && !CFG.rest)) {
239			return Promise.reject(new Error('fetch'));
240		}
241		var json = JSON.stringify(body);
242		function send(url) {
243			return w.fetch(url, {
244				method: 'POST',
245				credentials: 'same-origin',
246				keepalive: true,
247				headers: { 'Content-Type': 'application/json' },
248				body: json
249			}).then(function (r) {
250				if (!r.ok) {
251					var e = new Error('HTTP ' + r.status);
252					e.status = r.status;
253					throw e;
254				}
255				return r.json();
256			});
257		}
258		if (!CFG.ep || epBroken) {
259			return send(CFG.rest + path);
260		}
261		// Lagani ulaz (bez učitavanja ostalih dodataka). Ako ga poslužitelj
262		// blokira ili ne radi, REST; 400 je pravilo, ne kvar, pa se ne ponavlja.
263		return send(CFG.ep + '?a=' + path).catch(function (err) {
264			if (err && err.status === 400) {
265				throw err;
266			}
267			epBroken = true;
268			log('Lagani ulaz nije uspio, šaljem preko REST-a.', err && err.message ? err.message : err);
269			if (!CFG.rest) {
270				throw err;
271			}
272			return send(CFG.rest + path);
273		});
274	}
275
276	// Rezerva kad REST ne uspije: isti format kao kolačić s poslužitelja.
277	function fallbackCookie(st) {
278		if (!state.id) {
279			state.id = newId();
280		}
281		var payload = {
282			v: 1,
283			id: state.id,
284			rev: parseInt(CFG.rev, 10) || 1,
285			tv: parseInt(CFG.tv, 10) || 1,
286			ts: Math.floor(Date.now() / 1000),
287			src: String(CFG.src || ''),
288			c: { p: st.c.p, s: st.c.s, m: st.c.m },
289			svc: Object.assign({}, st.svc)
290		};
291		var n = anyAllowed(st) ? days(CFG.acceptDays, 365) : days(CFG.rejectDays, 90);
292		setCookie('bl_consent', encodeURIComponent(JSON.stringify(payload)), n, CFG.domain || '');
293		if (w.blConsentState) {
294			w.blConsentState.id = state.id;
295		}
296	}
297
298	// Odluke idu redom, da druga odluka na istoj stranici pošalje isti id.
299	function sendConsent(action, st, service) {
300		queue = queue.then(function () {
301			var body = {
302				action: action,
303				c: { p: st.c.p, s: st.c.s, m: st.c.m },
304				svc: Object.assign({}, st.svc),
305				lang: String(CFG.lang || 'hr'),
306				device: device(),
307				ch: channel()
308			};
309			if (service) {
310				body.service = service;
311			}
312			if (state.id) {
313				body.id = state.id;
314			}
315			return post('consent', body).then(function (res) {
316				if (!res || res.ok !== true || !res.consent || typeof res.consent !== 'object') {
317					throw new Error('odgovor');
318				}
319				if (typeof res.consent.id === 'string' && /^[A-Za-z0-9_-]{22}$/.test(res.consent.id)) {
320					state.id = res.consent.id;
321					if (w.blConsentState) {
322						w.blConsentState.id = state.id;
323					}
324				}
325			}).catch(function (err) {
326				fallbackCookie(st);
327				log('REST /consent nije uspio, kolačić je postavljen iz JS-a.', err && err.message ? err.message : err);
328			});
329		});
330	}
331
332	var sentEvents = {};
333
334	// Događaj se broji jednom po posjetu (kartici), ne na svakoj stranici: manje zahtjeva prema poslužitelju.
335	function seenThisVisit(k) {
336		try {
337			var list = JSON.parse(w.sessionStorage.getItem('blc_ev') || '[]');
338			if (Array.isArray(list) && list.indexOf(k) !== -1) {
339				return true;
340			}
341			list = Array.isArray(list) ? list.slice(-20) : [];
342			list.push(k);
343			w.sessionStorage.setItem('blc_ev', JSON.stringify(list));
344		} catch (e) {}
345		return false;
346	}
347
348	function sendEvent(type, service) {
349		var k = type + '|' + (service || '');
350		if (sentEvents[k]) {
351			return;
352		}
353		sentEvents[k] = true;
354		if (seenThisVisit(k)) {
355			return;
356		}
357		var body = { type: type, device: device() };
358		if ('banner_view' === type) {
359			body.ch = channel();
360		}
361		if (service) {
362			body.service = service;
363		}
364		post('event', body).catch(function (err) {
365			log('Zapis događaja nije uspio.', err && err.message ? err.message : err);
366		});
367	}
368
369	// ---------------------------------------------------------------- primjena odluke
370
371	function gtagFn() {
372		w.dataLayer = w.dataLayer || [];
373		if (typeof w.gtag === 'function') {
374			return w.gtag;
375		}
376		return function () {
377			w.dataLayer.push(arguments);
378		};
379	}
380
381	function applyLocal(st, source) {
382		var G = function (v) { return v ? 'granted' : 'denied'; };
383		var cats = { preferences: !!st.c.p, statistics: !!st.c.s, marketing: !!st.c.m };
384
385		// 1. Prijelazni Complianz sloj: kolačići SINKRONO prije bilo kojeg dataLayer.push
386		//    (GTM ih čita 200 ms nakon klika na cmplz-btn).
387		if (CFG.complianz) {
388			var ad = function (v) { return v ? 'allow' : 'deny'; };
389			setCookie('cmplz_functional', 'allow', 365, '');
390			setCookie('cmplz_preferences', ad(cats.preferences), 365, '');
391			setCookie('cmplz_statistics', ad(cats.statistics), 365, '');
392			setCookie('cmplz_marketing', ad(cats.marketing), 365, '');
393			setCookie('cmplz_banner-status', 'dismissed', 365, '');
394			if (typeof w.cmplz_has_consent !== 'function') {
395				w.cmplz_has_consent = function (cat) {
396					return cat === 'functional' ? true : has(cat);
397				};
398			}
399		}
400
401		// 2. Google Consent Mode.
402		gtagFn()('consent', 'update', {
403			ad_storage: G(st.c.m),
404			ad_user_data: G(st.c.m),
405			ad_personalization: G(st.c.m),
406			analytics_storage: G(st.c.s),
407			functionality_storage: G(st.c.p),
408			personalization_storage: G(st.c.p)
409		});
410
411		// 3. dataLayer događaji.
412		w.dataLayer.push({ event: 'bl_consent_update', bl_consent: { preferences: cats.preferences, statistics: cats.statistics, marketing: cats.marketing, source: source || 'banner' } });
413		if (CFG.complianz) {
414			w.dataLayer.push({ event: 'cmplz_event_functional' });
415			Object.keys(cats).forEach(function (k) {
416				if (cats[k]) {
417					w.dataLayer.push({ event: 'cmplz_event_' + k });
418				}
419			});
420		}
421
422		// 4. WP Consent API.
423		if (CFG.wpConsentApi !== false && typeof w.wp_set_consent === 'function') {
424			try {
425				var ad2 = function (v) { return v ? 'allow' : 'deny'; };
426				w.wp_set_consent('functional', 'allow');
427				w.wp_set_consent('preferences', ad2(cats.preferences));
428				w.wp_set_consent('statistics', ad2(cats.statistics));
429				w.wp_set_consent('statistics-anonymous', ad2(cats.statistics));
430				w.wp_set_consent('marketing', ad2(cats.marketing));
431			} catch (e) {
432				log('wp_set_consent', e);
433			}
434		}
435
436		// 5. Meta piksel.
437		if (typeof w.fbq === 'function') {
438			try {
439				w.fbq('consent', cats.marketing ? 'grant' : 'revoke');
440			} catch (e2) {
441				log('fbq', e2);
442			}
443		}
444
445		// 6. mv-analitika.
446		try {
447			w.localStorage.setItem('mv_consent_analytics', cats.statistics ? '1' : '0');
448		} catch (e3) {
449			// localStorage nedostupan (privatni način); kolačić ispod je dovoljan.
450		}
451		setCookie('bl_consent_statistics', cats.statistics ? '1' : '0', anyAllowed(st) ? days(CFG.acceptDays, 365) : days(CFG.rejectDays, 90), CFG.domain || '');
452	}
453
454	function notify() {
455		var detail = copy();
456		emit('bl:consent', detail);
457		listeners.slice().forEach(function (fn) {
458			try {
459				fn(copy());
460			} catch (e) {
461				log('Slušatelj bl:consent je bacio grešku.', e);
462			}
463		});
464	}
465
466	// Upravljane skripte: <script type="text/plain" data-blc-category> postaje prava skripta.
467	function runScripts() {
468		var list = d.querySelectorAll('script[type="text/plain"][data-blc-category]');
469		Array.prototype.forEach.call(list, function (old) {
470			var cat = old.getAttribute('data-blc-category');
471			if (!has(cat) || !old.parentNode) {
472				return;
473			}
474			var s = d.createElement('script');
475			Array.prototype.forEach.call(old.attributes, function (a) {
476				if (a.name !== 'type' && a.name !== 'data-blc-category' && a.name !== 'data-blc-type') {
477					s.setAttribute(a.name, a.value);
478				}
479			});
480			var realType = old.getAttribute('data-blc-type');
481			if (realType) {
482				s.type = realType;
483			}
484			s.setAttribute('data-blc-run', cat);
485			if (old.src || old.getAttribute('src')) {
486				s.async = old.hasAttribute('async');
487			} else {
488				s.text = old.textContent;
489			}
490			old.parentNode.replaceChild(s, old);
491		});
492	}
493
494	// ---------------------------------------------------------------- okviri
495
496	// Elementor Video: isključi Elementorov rukovatelj prije pokretanja (inače učita YouTube API).
497	function neutralizeElementor() {
498		var list = d.querySelectorAll('.blc-embed[data-blc-elementor]');
499		Array.prototype.forEach.call(list, function (ph) {
500			var wrap = ph.closest('[data-widget_type]');
501			if (wrap && /^video\./.test(wrap.getAttribute('data-widget_type') || '')) {
502				wrap.setAttribute('data-blc-widget-type', wrap.getAttribute('data-widget_type'));
503				wrap.setAttribute('data-widget_type', 'blc-embed.default');
504			}
505		});
506	}
507
508	function showService(svc, clicked) {
509		var list = d.querySelectorAll('.blc-embed[data-blc-service="' + svc + '"]');
510		var focusTarget = null;
511		Array.prototype.forEach.call(list, function (ph) {
512			var tpl = ph.querySelector('template.blc-embed__tpl');
513			if (!tpl || !tpl.content || !ph.parentNode) {
514				return;
515			}
516			var frag = d.importNode(tpl.content, true);
517			var first = frag.querySelector('iframe') || frag.firstElementChild;
518			ph.parentNode.replaceChild(frag, ph);
519			if (clicked && ph === clicked && first) {
520				focusTarget = first;
521			}
522		});
523		if (focusTarget && focusTarget.focus) {
524			focusTarget.focus({ preventScroll: true });
525		}
526	}
527
528	function showAllowedEmbeds() {
529		SERVICES.forEach(function (svc) {
530			if (has(svc)) {
531				showService(svc, null);
532			}
533		});
534	}
535
536	// embed_view jednom po usluzi po stranici, kad okvir stvarno uđe u prikaz.
537	function watchEmbeds() {
538		var list = d.querySelectorAll('.blc-embed[data-blc-service]');
539		if (!list.length) {
540			return;
541		}
542		var seen = function (el) {
543			sendEvent('embed_view', el.getAttribute('data-blc-service'));
544		};
545		if (!('IntersectionObserver' in w)) {
546			Array.prototype.forEach.call(list, seen);
547			return;
548		}
549		var io = new IntersectionObserver(function (entries) {
550			entries.forEach(function (e) {
551				if (e.isIntersecting) {
552					io.unobserve(e.target);
553					if (d.contains(e.target)) {
554						seen(e.target);
555					}
556				}
557			});
558		}, { threshold: 0.3 });
559		Array.prototype.forEach.call(list, function (el) {
560			io.observe(el);
561		});
562	}
563
564	// ---------------------------------------------------------------- sučelje
565
566	var root = d.getElementById('blc-root');
567	var banner = d.getElementById('blc-banner');
568	var settings = d.getElementById('blc-settings');
569	var fab = d.getElementById('blc-fab');
570	var invoker = null;
571	var bannerViewed = false;
572
573	function isModal() {
574		return !!(root && root.classList.contains('blc-dim') && !html.classList.contains('blc-nodim'));
575	}
576
577	function bannerShown() {
578		return !!banner && !html.classList.contains('blc-settings-open') &&
579			(html.classList.contains('blc-undecided') || html.classList.contains('blc-banner-open'));
580	}
581
582	function currentDialog() {
583		if (settings && html.classList.contains('blc-settings-open')) {
584			return settings;
585		}
586		return bannerShown() ? banner : null;
587	}
588
589	function syncModal() {
590		var m = isModal() ? 'true' : 'false';
591		if (banner) {
592			banner.setAttribute('aria-modal', m);
593		}
594		if (settings) {
595			settings.setAttribute('aria-modal', m);
596		}
597	}
598
599	function setUiOpen(open) {
600		var was = html.classList.contains('blc-ui-open');
601		html.classList.toggle('blc-ui-open', open);
602		if (was !== open) {
603			emit('bl:consent-ui', { open: open });
604		}
605	}
606
607	function focusables(el) {
608		var sel = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
609		return Array.prototype.filter.call(el.querySelectorAll(sel), visible);
610	}
611
612	function focusDialog(dlg) {
613		if (dlg && dlg.focus) {
614			dlg.focus({ preventScroll: true });
615		}
616	}
617
618	// CSS se učitava bez blokiranja prikaza: prozor je vidljiv tek kad CSS stigne.
619	// Fokus se premješta tek tada, i samo ako ga posjetitelj u međuvremenu nije sam pomaknuo.
620	function focusWhenVisible(dlg) {
621		var tries = 0;
622		var tick = function () {
623			if (dlg !== currentDialog()) {
624				return;
625			}
626			if (visible(dlg)) {
627				var a = d.activeElement;
628				if (!a || a === d.body || a === html) {
629					focusDialog(dlg);
630				}
631				return;
632			}
633			if (++tries < 60) {
634				setTimeout(tick, 100);
635			}
636		};
637		tick();
638	}
639
640	function syncSwitches() {
641		if (!settings) {
642			return;
643		}
644		Array.prototype.forEach.call(settings.querySelectorAll('[data-blc-switch]'), function (sw) {
645			var k = KEY[sw.getAttribute('data-blc-switch')];
646			sw.setAttribute('aria-checked', k && state.c[k] ? 'true' : 'false');
647		});
648	}
649
650	function onBannerVisible() {
651		if (!bannerViewed && root && bannerShown()) {
652			bannerViewed = true;
653			sendEvent('banner_view');
654		}
655	}
656
657	function openSettings(from) {
658		if (!settings) {
659			return;
660		}
661		if (from && from !== d.body) {
662			invoker = from;
663		}
664		syncSwitches();
665		syncModal();
666		layoutAlly();
667		html.classList.remove('blc-banner-open');
668		html.classList.add('blc-settings-open');
669		setUiOpen(true);
670		var list = settings.querySelector('.blc-settings__list');
671		if (list) {
672			list.scrollTop = 0;
673		}
674		focusDialog(settings);
675	}
676
677	function openBanner(from) {
678		if (!banner) {
679			return;
680		}
681		if (from && from !== d.body) {
682			invoker = from;
683		}
684		syncModal();
685		layoutAlly();
686		html.classList.remove('blc-settings-open');
687		html.classList.add('blc-banner-open');
688		setUiOpen(true);
689		onBannerVisible();
690		focusDialog(banner);
691	}
692
693	function restoreFocus() {
694		var t = invoker;
695		invoker = null;
696		if (t && d.contains(t) && visible(t) && t.focus) {
697			t.focus({ preventScroll: true });
698			return;
699		}
700		// Nema pozivatelja (banner otvoren pri učitavanju): fokus na početak dokumenta.
701		var a = d.activeElement;
702		if (a && root && root.contains(a)) {
703			d.body.setAttribute('tabindex', '-1');
704			d.body.focus({ preventScroll: true });
705			d.body.removeAttribute('tabindex');
706		}
707	}
708
709	function closeUI() {
710		var wasOpen = !!currentDialog();
711		html.classList.remove('blc-banner-open', 'blc-settings-open');
712		setUiOpen(false);
713		if (wasOpen) {
714			restoreFocus();
715		}
716	}
717
718	/**
719	 * Odluka posjetitelja.
720	 *
721	 * @param {string} action accept_all|reject_all|save|x|embed
722	 * @param {Object} c      { p, s, m }
723	 * @param {Object} svc    { youtube, spotify, maps }
724	 */
725	function decide(action, c, svc) {
726		var before = state.decided ? copy() : null;
727		var next = normalize({ decided: true, c: c, svc: svc, id: state.id, rev: CFG.rev });
728		state = next;
729		w.blConsentState = copy();
730
731		html.classList.remove('blc-undecided');
732		html.classList.add('blc-decided');
733
734		sendConsent(action, next);
735		applyLocal(next, 'banner');
736		notify();
737		closeUI();
738
739		// Povlačenje privole: obriši kolačiće tih kategorija i učitaj stranicu
740		// ponovno, jer već pokrenute oznake (GTM, Jetpack, okviri) ne staju same.
741		var gone = before ? revoked(before, next) : { cats: [], any: false };
742		if (gone.any) {
743			clearCookies(gone.cats);
744			reloadAfterSave();
745			return;
746		}
747		runScripts();
748		showAllowedEmbeds();
749		layoutAlly();
750	}
751
752	/**
753	 * Što je bilo dopušteno, a više nije?
754	 *
755	 * @param {Object} a Prije.
756	 * @param {Object} b Poslije.
757	 * @return {{cats: string[], any: boolean}}
758	 */
759	function revoked(a, b) {
760		var cats = ['p', 's', 'm'].filter(function (k) {
761			return !!a.c[k] && !b.c[k];
762		});
763		var svcGone = SERVICES.some(function (k) {
764			return !!(a.c.m || a.svc[k]) && !(b.c.m || b.svc[k]);
765		});
766		return { cats: cats, any: cats.length > 0 || svcGone };
767	}
768
769	// Naši, nužni i namjerni kolačići se nikad ne brišu.
770	var KEEP = /^(bl_consent|cmplz_|wp_consent_|wordpress_|wp-settings|woocommerce_|wp_woocommerce_|rpsm_attr$)/;
771
772	function clearCookies(cats) {
773		var res = [];
774		cats.forEach(function (k) {
775			((CFG.clean && CFG.clean[k]) || []).forEach(function (p) {
776				res.push(new RegExp('^' + String(p).replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$'));
777			});
778		});
779		if (!res.length) {
780			return;
781		}
782		var host = w.location.hostname;
783		var parts = host.split('.');
784		var domains = ['', host, '.' + host];
785		for (var i = 1; i < parts.length - 1; i++) {
786			domains.push('.' + parts.slice(i).join('.'));
787		}
788		d.cookie.split(';').forEach(function (pair) {
789			var name = pair.split('=')[0].trim();
790			if (!name || KEEP.test(name) || !res.some(function (r) { return r.test(name); })) {
791				return;
792			}
793			domains.forEach(function (dom) {
794				d.cookie = name + '=; Max-Age=0; Path=/' + (dom ? '; Domain=' + dom : '');
795			});
796			log('Obrisan kolačić nakon povlačenja privole.', name);
797		});
798	}
799
800	// Ponovno učitavanje tek kad je odluka zapisana (najdulje 2 s).
801	function reloadAfterSave() {
802		var done = false;
803		function go() {
804			if (!done) {
805				done = true;
806				w.location.reload();
807			}
808		}
809		queue.then(go, go);
810		setTimeout(go, 2000);
811	}
812
813	function allOn() {
814		return { p: 1, s: 1, m: 1 };
815	}
816
817	function allOff() {
818		return { p: 0, s: 0, m: 0 };
819	}
820
821	function noServices() {
822		var o = {};
823		SERVICES.forEach(function (k) {
824			o[k] = 0;
825		});
826		return o;
827	}
828
829	function fromSwitches() {
830		var c = { p: state.c.p, s: state.c.s, m: state.c.m };
831		if (settings) {
832			Array.prototype.forEach.call(settings.querySelectorAll('[data-blc-switch]'), function (sw) {
833				var k = KEY[sw.getAttribute('data-blc-switch')];
834				if (k) {
835					c[k] = sw.getAttribute('aria-checked') === 'true' ? 1 : 0;
836				}
837			});
838		}
839		return c;
840	}
841
842	function act(action, el) {
843		switch (action) {
844			case 'accept_all':
845				decide('accept_all', allOn(), state.svc);
846				break;
847			case 'reject_all':
848				decide('reject_all', allOff(), noServices());
849				break;
850			case 'x':
851				decide('x', allOff(), noServices());
852				break;
853			case 'save':
854				decide('save', fromSwitches(), state.svc);
855				break;
856			case 'settings':
857				openSettings(invoker || el);
858				break;
859		}
860	}
861
862	function allowEmbed(svc, btn) {
863		if (SERVICES.indexOf(svc) === -1) {
864			return;
865		}
866		var ph = btn.closest('.blc-embed');
867		var nextSvc = Object.assign({}, state.svc);
868		nextSvc[svc] = 1;
869		var c = state.decided ? state.c : allOff();
870		var next = normalize({ decided: true, c: c, svc: nextSvc, id: state.id, rev: CFG.rev });
871		state = next;
872		w.blConsentState = copy();
873		html.classList.remove('blc-undecided');
874		html.classList.add('blc-decided');
875		sendConsent('embed', next, svc);
876		applyLocal(next, 'embed');
877		notify();
878		if (currentDialog()) {
879			invoker = null;
880			html.classList.remove('blc-banner-open', 'blc-settings-open');
881			setUiOpen(false);
882		}
883		showService(svc, ph);
884		layoutAlly();
885	}
886
887	// ---------------------------------------------------------------- Ally i položaji
888
889	// Ally (Elementor) gumb je u otvorenom shadowRootu #ea11y-root; rezerva su njegove postavke.
890	function allyFromDom() {
891		var host = d.getElementById('ea11y-root');
892		var sr = host && host.shadowRoot;
893		var btn = sr && sr.querySelector('.ea11y-widget-open-button');
894		if (!btn) {
895			return null;
896		}
897		var r = btn.getBoundingClientRect();
898		if (!r.width || !r.height) {
899			return { hidden: true };
900		}
901		var cw = html.clientWidth;
902		var ch = w.innerHeight;
903		return {
904			side: r.left + r.width / 2 > cw / 2 ? 'right' : 'left',
905			v: r.top + r.height / 2 > ch / 2 ? 'bottom' : 'top',
906			right: Math.max(0, cw - r.right),
907			left: Math.max(0, r.left),
908			bottom: Math.max(0, ch - r.bottom),
909			w: r.width,
910			h: r.height
911		};
912	}
913
914	function toPx(val, unit, base) {
915		var n = parseFloat(val);
916		if (!(n >= 0)) {
917			return 24;
918		}
919		if (unit === '%') {
920			return (n / 100) * base;
921		}
922		if (unit === 'em' || unit === 'rem') {
923			return n * 16;
924		}
925		if (unit === 'vw') {
926			return (n / 100) * html.clientWidth;
927		}
928		if (unit === 'vh') {
929			return (n / 100) * w.innerHeight;
930		}
931		return n;
932	}
933
934	function allyFromSettings() {
935		var wd = w.ea11yWidget;
936		var ic = wd && wd.iconSettings;
937		if (!ic || !ic.position) {
938			return null;
939		}
940		var dev = isMobile() ? ic.position.mobile : ic.position.desktop;
941		if (!dev || dev.hidden) {
942			return { hidden: true };
943		}
944		var size = { small: 36, medium: 44, large: 64 }[(ic.style && ic.style.size) || 'medium'] || 44;
945		var p = String(dev.position || 'bottom-right');
946		var side = /left/.test(p) ? 'left' : 'right';
947		var v = /^top/.test(p) ? 'top' : (/^bottom/.test(p) ? 'bottom' : 'center');
948		var hx = 24;
949		var vy = 24;
950		if (dev.enableExactPosition && dev.exactPosition) {
951			var hz = dev.exactPosition.horizontal || {};
952			var vt = dev.exactPosition.vertical || {};
953			side = hz.direction === 'left' ? 'left' : 'right';
954			v = vt.direction === 'top' ? 'top' : 'bottom';
955			hx = toPx(hz.value, hz.unit, html.clientWidth);
956			vy = toPx(vt.value, vt.unit, w.innerHeight);
957		}
958		return { side: side, v: v, right: hx, left: hx, bottom: vy, w: size, h: size };
959	}
960
961	function allyInfo() {
962		var a = allyFromDom() || allyFromSettings();
963		return a && !a.hidden ? a : null;
964	}
965
966	var layoutRaf = 0;
967
968	function layoutAlly() {
969		if (!root) {
970			return;
971		}
972		var a = allyInfo();
973		var bottomRight = !!(a && a.v === 'bottom' && a.side === 'right');
974		var bottomLeft = !!(a && a.v === 'bottom' && a.side === 'left');
975		var pos = (CFG.float && CFG.float.pos) || 'auto';
976
977		// Plutajuća tipka: 12 px iznad Allyja kad je on dolje desno, inače 24 px od ruba.
978		var fx = 24;
979		var fy = 24;
980		if (pos === 'auto' && bottomRight) {
981			fx = Math.round(a.right + (a.w - 44) / 2);
982			fy = Math.round(a.bottom + a.h + 12);
983		}
984		root.style.setProperty('--blc-fab-x', fx + 'px');
985		root.style.setProperty('--blc-fab-y', fy + 'px');
986
987		// Donji list (mobitel): gumbi završavaju 12 px iznad Allyja (Ally je uvijek iznad svega).
988		var sheet = a && a.v === 'bottom' ? Math.round(a.bottom + a.h + 12) : 28;
989		root.style.setProperty('--blc-sheet-pad', Math.max(28, sheet) + 'px');
990
991		// Traka na dnu (desktop): gumbi ne idu ispod Allyja.
992		root.style.setProperty('--blc-bar-pad-r', (bottomRight ? Math.round(a.right + a.w + 24) : 64) + 'px');
993		root.style.setProperty('--blc-bar-pad-l', (bottomLeft ? Math.round(a.left + a.w + 24) : 64) + 'px');
994	}
995
996	function scheduleLayout() {
997		if (layoutRaf) {
998			return;
999		}
1000		var run = function () {
1001			layoutRaf = 0;
1002			layoutAlly();
1003		};
1004		layoutRaf = w.requestAnimationFrame ? w.requestAnimationFrame(run) : setTimeout(run, 16);
1005	}
1006
1007	// ---------------------------------------------------------------- događaji
1008
1009	function onClick(e) {
1010		var t = e.target;
1011		if (!t || !t.closest) {
1012			return;
1013		}
1014
1015		var emb = t.closest('[data-blc-embed-allow]');
1016		if (emb) {
1017			e.preventDefault();
1018			allowEmbed(emb.getAttribute('data-blc-embed-allow'), emb);
1019			return;
1020		}
1021
1022		var btn = root ? t.closest('#blc-root [data-blc-action]') : null;
1023		if (btn) {
1024			e.preventDefault();
1025			act(btn.getAttribute('data-blc-action'), btn);
1026			return;
1027		}
1028
1029		var sw = settings ? t.closest('#blc-settings [data-blc-switch]') : null;
1030		if (sw) {
1031			e.preventDefault();
1032			sw.setAttribute('aria-checked', sw.getAttribute('aria-checked') === 'true' ? 'false' : 'true');
1033			return;
1034		}
1035
1036		var more = settings ? t.closest('#blc-settings .blc-cat__more') : null;
1037		if (more) {
1038			e.preventDefault();
1039			var listEl = d.getElementById(more.getAttribute('aria-controls'));
1040			var open = more.getAttribute('aria-expanded') !== 'true';
1041			more.setAttribute('aria-expanded', open ? 'true' : 'false');
1042			more.textContent = more.getAttribute(open ? 'data-hide' : 'data-show') || more.textContent;
1043			if (listEl) {
1044				listEl.hidden = !open;
1045			}
1046			return;
1047		}
1048
1049		var opener = t.closest('.blc-open-settings, a[href="#blc-postavke"], a[href$="/#blc-p
1049ostavke"]');
1050		if (opener) {
1051			e.preventDefault();
1052			// Iz bannera se Postavke otvaraju preko data-blc-action; ovdje su podnožje, tipka i okviri.
1053			openSettings(opener);
1054		}
1055	}
1056
1057	function onKey(e) {
1058		var dlg = currentDialog();
1059		if (!dlg) {
1060			return;
1061		}
1062		var modal = isModal();
1063		if (e.key === 'Escape' || e.key === 'Esc') {
1064			// ESC = X. Na stranici bez zatamnjenja samo kad je fokus u prozoru.
1065			if (CFG.xButton !== false && (modal || dlg.contains(d.activeElement))) {
1066				e.preventDefault();
1067				act('x', null);
1068			}
1069			return;
1070		}
1071		if (e.key !== 'Tab' || !modal) {
1072			return;
1073		}
1074		var f = focusables(dlg);
1075		if (!f.length) {
1076			e.preventDefault();
1077			focusDialog(dlg);
1078			return;
1079		}
1080		var first = f[0];
1081		var last = f[f.length - 1];
1082		var a = d.activeElement;
1083		if (e.shiftKey && (a === first || a === dlg || !dlg.contains(a))) {
1084			e.preventDefault();
1085			last.focus();
1086		} else if (!e.shiftKey && (a === last || !dlg.contains(a))) {
1087			e.preventDefault();
1088			first.focus();
1089		}
1090	}
1091
1092	function onFocusIn(e) {
1093		var dlg = currentDialog();
1094		if (!dlg || !isModal()) {
1095			return;
1096		}
1097		var t = e.target;
1098		if (t === w || t === d || dlg.contains(t)) {
1099			return;
1100		}
1101		// Ally ostaje dostupan iznad zatamnjenja (namjerno).
1102		if (t.closest && t.closest('#ea11y-root')) {
1103			return;
1104		}
1105		var f = focusables(dlg);
1106		(f[0] || dlg).focus({ preventScroll: true });
1107	}
1108
1109	// ---------------------------------------------------------------- javni API
1110
1111	var api = {
1112		__full: true,
1113		has: has,
1114		get: copy,
1115		open: function (what) {
1116			if (what === 'banner') {
1117				openBanner(d.activeElement);
1118			} else {
1119				openSettings(d.activeElement);
1120			}
1121		},
1122		on: function (fn) {
1123			if (typeof fn !== 'function') {
1124				return function () {};
1125			}
1126			listeners.push(fn);
1127			return function () {
1128				var i = listeners.indexOf(fn);
1129				if (i !== -1) {
1130					listeners.splice(i, 1);
1131				}
1132			};
1133		}
1134	};
1135	w.blConsent = api;
1136
1137	// ---------------------------------------------------------------- pokretanje
1138
1139	function start() {
1140		neutralizeElementor();
1141		syncModal();
1142		layoutAlly();
1143
1144		d.addEventListener('click', onClick);
1145		d.addEventListener('keydown', onKey);
1146		d.addEventListener('focusin', onFocusIn);
1147		w.addEventListener('resize', scheduleLayout);
1148		w.addEventListener('orientationchange', scheduleLayout);
1149		// Ally se učitava s CDN-a nakon nas: položaj se računa još nekoliko puta.
1150		[400, 1200, 2500, 5000, 9000].forEach(function (ms) {
1151			setTimeout(layoutAlly, ms);
1152		});
1153
1154		if (state.decided) {
1155			runScripts();
1156			showAllowedEmbeds();
1157		}
1158		watchEmbeds();
1159
1160		if (bannerShown() && root) {
1161			setUiOpen(true);
1162			onBannerVisible();
1163			if (isModal()) {
1164				focusWhenVisible(banner);
1165			}
1166		}
1167
1168		if (w.location.hash === '#blc-postavke') {
1169			openSettings(null);
1170		}
1171
1172		log('Pokrenuto.', copy());
1173	}
1174
1175	if (d.readyState === 'loading') {
1176		d.addEventListener('DOMContentLoaded', start);
1177	} else {
1178		start();
1179	}
1180})();

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.