PageSourceSearch

https://www.radimposvom.com.hr/wp-content/plugins/bl-social-proof/assets/js/bubble.js?ver=1.10.0.0

js radimposvom.com.hr collected 2026-10-09 05:14:26 UTC 13,653 bytes, 456 lines download raw bytes

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 = '&times;';
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.