1/** 2 * BL Social Proof - bubble notifikacije (frontend render). 3 * 4 * PovlaÄi same-origin proxy feed (/bl-sp/v1/feed), prikazuje bubble u kutu, 5 * cikla kroz evente. Rodno neutralno (oznaka + ime + grad + proizvod, bez 6 * "kupio/kupila"). Vrijeme na razini sata; ako nije danas, jasno se oznaÄi. 7 * Padež grada (genitiv) stiže gotov sa servera. 8 * 9 * Suradnja s BL KolaÄiÄima (bl-consent): dok html nosi klasu blc-ui-open 10 * (banner ili Postavke kolaÄiÄa otvoreni) nema oblaÄiÄa; Äekaju u redu i ne 11 * gube se. Kad se prozor zatvori (dogaÄaj bl:consent-ui), prvi kreÄe nakon 12 * 4 s. OblaÄiÄ se diže iznad Allyja i plutajuÄe tipke kolaÄiÄa kad bi ih 13 * inaÄe prekrio. Sajt bez BL KolaÄiÄa i bez Allyja radi kao prije. 14 */ 15( function () { 16 'use strict'; 17 18 var cfg = window.BL_SP || {}; 19 if ( ! cfg.feed ) { 20 return; 21 } 22 23 var L = cfg.labels || {}; 24 var DAYS = cfg.days || []; 25 var CHIP = { buy: L.buy || 'Nova kupnja', ren: L.ren || 'Obnova pretplate', mem: L.mem || 'Novi Älan' }; 26 27 var events = []; 28 var idx = 0; 29 var shown = 0; 30 var root = null; 31 32 // Broj prikazanih bubbleova. Kad je sessionPersist, pamti se kroz sve stranice 33 // (sessionStorage) - pa korisnik koji je vidio max ne dobiva ponovno na drugoj 34 // landing stranici. Bez toga: resetira se po stranici. 35 var SESS_KEY = 'bl_sp_shown'; 36 function shownCount() { 37 if ( cfg.sessionPersist ) { 38 try { return parseInt( sessionStorage.getItem( SESS_KEY ) || '0', 10 ) || 0; } catch ( e ) {} 39 } 40 return shown; 41 } 42 function bumpShown() { 43 shown = shownCount() + 1; 44 if ( cfg.sessionPersist ) {
45 try { sessionStorage.setItem( SESS_KEY, String( shown ) ); } catch ( e ) {} 46 } 47 } 48 49 function h( n ) { 50 return String( n ); 51 } 52 53 function fmtTime( ts ) { 54 // ts u sekundama; vrijeme na razini sata, jasna oznaka kad nije danas. 55 var d = new Date( ts * 1000 ); 56 var now = new Date(); 57 var hh = d.getHours(); 58 var sameDay = d.toDateString() === now.toDateString(); 59 var yest = new Date( now.getTime() - 86400000 ).toDateString() === d.toDateString(); 60 var txt, old = false; 61 if ( sameDay ) { 62 txt = 'danas u ' + hh + 'h'; 63 } else if ( yest ) { 64 txt = 'juÄer u ' + hh + 'h'; 65 old = true; 66 } else if ( ( now.getTime() - d.getTime() ) < 7 * 86400000 ) { 67 txt = ( DAYS[ d.getDay() ] || '' ) + ' u ' + hh + 'h'; 68 old = true; 69 } else { 70 txt = d.getDate() + '.' + ( d.getMonth() + 1 ) + '. u ' + hh + 'h'; 71 old = true; 72 } 73 return { txt: txt, old: old }; 74 } 75 76 function el( tag, cls ) { 77 var e = document.createElement( tag ); 78 if ( cls ) { 79 e.className = cls; 80 } 81 return e; 82 } 83 84 function buildBubble( ev, fresh ) { 85 var t = fmtTime( ev.ts ); 86 var b = el( 'div', 'bl-sp-bubble bl-sp-' + ev.type + ( fresh ? ' bl-sp-fresh' : '' ) ); 87 88 var x = el( 'span', 'bl-sp-x' ); 89 x.innerHTML = '×'; 90 x.onclick = function () { 91 current = null; 92 hide(); 93 }; 94 b.appendChild( x ); 95 96 var av = el( 'div', 'bl-sp-av' ); 97 av.textContent = ( ev.name || '?' ).charAt( 0 ); 98 b.appendChild( av ); 99 100 var body = el( 'div', 'bl-sp-body' ); 101 102 var chip = el( 'span', 'bl-sp-chip' ); 103 chip.appendChild( el( 'span', 'bl-sp-dot' ) ); 104 chip.appendChild( document.createTextNode( CHIP[ ev.type ] || '' ) ); 105 body.appendChild( chip ); 106 107 // Svjeza kupnja (dogodila se dok je korisnik na sajtu) -> zeleni "Upravo" pill. 108 if ( fresh ) { 109 var pill = el( 'span', 'bl-sp-upravo' ); 110 pill.appendChild( el( 'span', 'bl-sp-upravo-dot' ) ); 111 pill.appendChild( document.createTextNode( L.now || 'Upravo' ) ); 112 body.appendChild( pill ); 113 } 114 115 var who = el( 'div', 'bl-sp-who' ); 116 who.textContent = ev.name + ' ' + ( L.from || 'iz' ) + ' ' + ev.place; 117 body.appendChild( who ); 118 119 if ( ev.product ) { 120 var prod = el( 'div', 'bl-sp-product' ); 121 prod.textContent = ev.product; 122 body.appendChild( prod ); 123 } 124 125 var time = el( 'div', 'bl-sp-time' + ( fresh ? ' bl-sp-time-fresh' : '' ) ); 126 if ( fresh ) { 127 time.appendChild( el( 'span', 'bl-sp-pulse' ) ); 128 time.appendChild( document.createTextNode( L.justNow || 'upravo' ) ); 129 } else { 130 time.textContent = t.txt; 131 } 132 body.appendChild( time ); 133 134 b.appendChild( body ); 135 return b; 136 } 137 138 function ensureRoot() { 139 if ( root ) { 140 return root; 141 } 142 root = el( 'div', 'bl-sp-root bl-sp-' + ( cfg.position === 'br' ? 'br' : 'bl' ) ); 143 root.setAttribute( 'data-brand', cfg.brand || '' ); 144 document.body.appendChild( root ); 145 return root; 146 } 147 148 function hide() { 149 if ( root ) { 150 root.innerHTML = ''; 151 } 152 } 153 154 var queue = []; // { ev, fresh } za prikaz 155 var started = false; // inicijalni backlog napravljen NA OVOJ stranici 156 var ticking = false; 157 var current = null; // oblaÄiÄ koji je upravo na ekranu 158 var timer = 0; // jedini zakazani korak (skrivanje, pauza ili nastavak) 159 160 // ---------------------------------------------------------------- BL KolaÄiÄi i Ally 161 162 var html = document.documentElement; 163 var AFTER_CONSENT = 4000; // prvi oblaÄiÄ nakon zatvaranja bannera ili Postavki (ms) 164 var GAP = 12; // razmak iznad Allyja i tipke kolaÄiÄa (px) 165 var MOBILE_MAX = 767; // isto kao u BL KolaÄiÄima 166 167 function consentOpen() { 168 return !! ( html.classList && html.classList.contains( 'blc-ui-open' ) ); 169 } 170 171 function toPx( val, unit, base ) { 172 var n = parseFloat( val ); 173 if ( ! ( n >= 0 ) ) { 174 return 24; 175 } 176 if ( '%' === unit ) { 177 return ( n / 100 ) * base; 178 } 179 if ( 'em' === unit || 'rem' === unit ) { 180 return n * 16; 181 } 182 if ( 'vw' === unit ) { 183 return ( n / 100 ) * html.clientWidth; 184 } 185 if ( 'vh' === unit ) { 186 return ( n / 100 ) * window.innerHeight; 187 } 188 return n; 189 } 190 191 // Ally (Elementor) gumb je u otvorenom shadowRootu #ea11y-root. Dok se joÅ¡ nije 192 // uÄitao, položaj se raÄuna iz window.ea11yWidget.iconSettings. 193 function allyRect() { 194 var host = document.getElementById( 'ea11y-root' ); 195 var sr = host && host.shadowRoot; 196 var btn = sr && sr.querySelector( '.ea11y-widget-open-button' ); 197 if ( btn ) { 198 var r = btn.getBoundingClientRect(); 199 return r.width && r.height ? r : null; 200 } 201 var ic = window.ea11yWidget && window.ea11yWidget.iconSettings; 202 if ( ! ic || ! ic.position ) { 203 return null; 204 } 205 var vw = html.clientWidth; 206 var vh = window.innerHeight; 207 var dev = ( window.innerWidth || vw ) <= MOBILE_MAX ? ic.position.mobile : ic.position.desktop; 208 if ( ! dev || dev.hidden ) { 209 return null; 210 } 211 var size = { small: 36, medium: 44, large: 64 }[ ( ic.style && ic.style.size ) || 'medium' ] || 44; 212 var p = String( dev.position || 'bottom-right' ); 213 var side = /left/.test( p ) ? 'left' : 'right'; 214 var v = /^top/.test( p ) ? 'top' : ( /^bottom/.test( p ) ? 'bottom' : 'center' ); 215 var hx = 24; 216 var vy = 24; 217 if ( dev.enableExactPosition && dev.exactPosition ) { 218 var hz = dev.exactPosition.horizontal || {}; 219 var vt = dev.exactPosition.vertical || {}; 220 side = 'left' === hz.direction ? 'left' : 'right'; 221 v = 'top' === vt.direction ? 'top' : 'bottom'; 222 hx = toPx( hz.value, hz.unit, vw ); 223 vy = toPx( vt.value, vt.unit, vh ); 224 } 225 if ( 'bottom' !== v ) { 226 return null; 227 } 228 var left = 'left' === side ? hx : vw - hx - size; 229 return { left: left, right: left + size, top: vh - vy - size, bottom: vh - vy }; 230 } 231 232 // PlutajuÄa tipka BL KolaÄiÄa; kad je skrivena, nema mjesta na ekranu. 233 function fabRect() { 234 var f = document.querySelector( '.blc-root .blc-fab' ); 235 if ( ! f ) { 236 return null; 237 } 238 var r = f.getBoundingClientRect(); 239 return r.width && r.height ? r : null; 240 } 241 242 // Diže oblaÄiÄ iznad Allyja i tipke kolaÄiÄa kad bi ih prekrio (mobitel uz 243 // Ally, ili položaj "Dolje desno"). Mjeri bez transforma animacije. 244 function place() { 245 if ( ! root ) { 246 return; 247 } 248 root.style.bottom = ''; 249 var cs = window.getComputedStyle( root ); 250 var base = parseFloat( cs.bottom ) || 0; 251 var vw = html.clientWidth; 252 var vh = window.innerHeight; 253 var w = root.offsetWidth; 254 var x1, x2; 255 if ( 'br' === cfg.position ) { 256 x2 = vw - ( parseFloat( cs.right ) || 0 ); 257 x1 = x2 - w; 258 } else { 259 x1 = parseFloat( cs.left ) || 0; 260 x2 = x1 + w; 261 } 262 var need = base;
263 var obstacles = [ allyRect(), fabRect() ]; 264 for ( var i = 0; i < obstacles.length; i++ ) { 265 var r = obstacles[ i ]; 266 if ( ! r || r.top < vh / 2 || r.right <= x1 || r.left >= x2 ) { 267 continue; 268 } 269 need = Math.max( need, Math.ceil( vh - r.top + GAP ) ); 270 } 271 if ( need > base ) { 272 root.style.bottom = need + 'px'; 273 } 274 } 275 276 var placeRaf = 0; 277 window.addEventListener( 'resize', function () { 278 if ( ! current || placeRaf ) { 279 return; 280 } 281 placeRaf = window.requestAnimationFrame( function () { 282 placeRaf = 0; 283 place(); 284 } ); 285 } ); 286 287 // Banner ili Postavke kolaÄiÄa: otvaranje sakrije oblaÄiÄ i vrati ga na poÄetak 288 // reda, zatvaranje pokrene red nakon AFTER_CONSENT. 289 document.addEventListener( 'bl:consent-ui', function ( e ) { 290 var open = !! ( e && e.detail && e.detail.open ); 291 if ( timer ) { 292 window.clearTimeout( timer ); 293 timer = 0; 294 } 295 ticking = false; 296 if ( open ) { 297 if ( current ) { 298 queue.unshift( current ); 299 current = null; 300 hide(); 301 } 302 return; 303 } 304 timer = window.setTimeout( tick, AFTER_CONSENT ); 305 } ); 306 307 // Baseline "svjeze" = PRVO ucitavanje u sesiji (sessionStorage). Tako je svaka 308 // kupnja nakon prvog dolaska na sajt svjeza, bez obzira na prelaske stranica. 309 // Dedup (seen) takodjer u sessionStorage da se ista ne pokaze dvaput kroz stranice. 310 var BASE_KEY = 'bl_sp_base'; 311 var SEEN_KEY = 'bl_sp_seen'; 312 function ssGet( k, d ) { try { var v = sessionStorage.getItem( k ); return null === v ? d : v; } catch ( e ) { return d; } } 313 function ssSet( k, v ) { try { sessionStorage.setItem( k, v ); } catch ( e ) {} } 314 315 var baseTs = parseInt( ssGet( BASE_KEY, '0' ), 10 ) || 0; 316 var seenKeys = {}; 317 ( function () { 318 try { JSON.parse( ssGet( SEEN_KEY, '[]' ) ).forEach( function ( k ) { seenKeys[ k ] = true; } ); } catch ( e ) {} 319 } )(); 320 function markSeen( k ) { 321 seenKeys[ k ] = true; 322 var arr = Object.keys( seenKeys ); 323 if ( arr.length > 200 ) { arr = arr.slice( -200 ); } 324 ssSet( SEEN_KEY, JSON.stringify( arr ) ); 325 } 326 327 function keyOf( ev ) { 328 return ev.type + '|' + ev.name + '|' + ( ev.product || '' ) + '|' + ev.ts; 329 } 330 331 // Ubaci evente. Svjeze (ts > baseTs, nakon prvog dolaska) PROBIJAJU cap i dobivaju 332 // "Upravo". Staro (<= baseTs) je backlog, ograniceno session cap-om. 333 function ingest( data, isPoll ) { 334 if ( data && data.counters ) { 335 updateCounters( data.counters.online ); 336 } 337 var list = ( data && data.events ) || []; 338 if ( ! list.length ) { 339 return; 340 } 341 var maxTs = list.reduce( function ( m, e ) { return Math.max( m, e.ts || 0 ); }, 0 ); 342 343 // Prvo ucitavanje u sesiji: postavi baseline (sve postojece = staro). 344 if ( 0 === baseTs ) { 345 baseTs = maxTs; 346 ssSet( BASE_KEY, String( baseTs ) ); 347 } 348 349 if ( ! isPoll && ! started ) { 350 // Inicijalno ucitavanje OVE stranice: svjezi (ts>base) + backlog (do cap-a). 351 started = true; 352 var cap = Math.max( 0, ( cfg.maxSess || 6 ) - shownCount() ); 353 var used = 0; 354 for ( var i = 0; i < list.length; i++ ) { 355 var ev = list[ i ], k = keyOf( ev ); 356 if ( seenKeys[ k ] ) { continue; } 357 if ( ( ev.ts || 0 ) > baseTs ) { 358 markSeen( k ); 359 queue.push( { ev: ev, fresh: true } ); 360 } else if ( used < cap ) { 361 markSeen( k ); 362 queue.push( { ev: ev, fresh: false } ); 363 used++; 364 } 365 } 366 } else { 367 // Poll: samo svjezi eventi (nakon prvog dolaska), probijaju cap. 368 var f = []; 369 for ( var j = 0; j < list.length; j++ ) { 370 var key = keyOf( list[ j ] ); 371 if ( ( list[ j ].ts || 0 ) > baseTs && ! seenKeys[ key ] ) { 372 markSeen( key ); 373 f.push( list[ j ] ); 374 } 375 } 376 f.sort( function ( a, b ) { return ( a.ts || 0 ) - ( b.ts || 0 ); } ); 377 for ( var x = 0; x < f.length; x++ ) { 378 queue.push( { ev: f[ x ], fresh: true } ); 379 } 380 } 381 } 382 383 function tick() { 384 timer = 0; 385 // Banner ili Postavke kolaÄiÄa su otvoreni: red Äeka dogaÄaj zatvaranja. 386 if ( consentOpen() ) { 387 ticking = false; 388 return; 389 } 390 if ( ! queue.length ) { 391 ticking = false; 392 return; 393 } 394 ticking = true; 395 var item = queue.shift(); 396 // Samo backlog se broji u session cap; svjezi (fresh) eventi probijaju. 397 // OblaÄiÄ vraÄen u red zbog bannera ne broji se dvaput. 398 if ( ! item.fresh && ! item.counted ) { 399 bumpShown(); 400 item.counted = true; 401 } 402 current = item; 403 var r = ensureRoot(); 404 r.innerHTML = ''; 405 r.appendChild( buildBubble( item.ev, item.fresh ) ); 406 place(); 407 // Vidljiv `duration`, sakrij, PAUZA `interval`, pa sljedeci. 408 timer = window.setTimeout( function () { 409 current = null; 410 hide(); 411 timer = window.setTimeout( tick, cfg.interval || 9000 ); 412 }, cfg.duration || 6000 ); 413 } 414 415 function ensureTick() { 416 if ( ! ticking && ! timer ) { 417 tick(); 418 } 419 } 420 421 function updateCounters( online ) { 422 if ( typeof online !== 'number' ) { 423 return; 424 } 425 var nodes = document.querySelectorAll( '[data-bl-sp-counter="online"] strong' ); 426 for ( var i = 0; i < nodes.length; i++ ) {
427 nodes[ i ].textContent = online.toLocaleString( 'hr-HR' ); 428 } 429 } 430 431 function load( isPoll ) { 432 fetch( cfg.feed, { credentials: 'same-origin' } ) 433 .then( function ( r ) { 434 return r.ok ? r.json() : null; 435 } ) 436 .then( function ( data ) { 437 if ( data ) { 438 ingest( data, isPoll ); 439 ensureTick(); 440 } 441 } ) 442 .catch( function () {} ); 443 } 444 445 function start() { 446 load( false ); 447 // Periodicno provjeri feed za NOVE kupnje (i osvjezi online broj). 448 window.setInterval( function () { load( true ); }, cfg.poll || 60000 ); 449 } 450 451 if ( document.readyState === 'loading' ) { 452 document.addEventListener( 'DOMContentLoaded', start ); 453 } else { 454 start(); 455 } 456} )();
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.