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.