PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.83
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.83
51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 51.1.46 51.1.47 51.1.49 All 37 releases
king-addons / includes / extensions / Fomo_Notifications / assets / frontend.js

frontend.js in King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder 51.1.83, at includes/extensions/Fomo_Notifications/assets/frontend.js

812 lines 34.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Fomo Notifications - Frontend Engine
3 *
4 * Full notification display engine with entry cycling, notification bar,
5 * FOMO popups, progress bars, and analytics tracking.
6 *
7 * Each notification post can have multiple entries (items).
8 * Entries cycle through with configurable timing (like NotificationX).
9 * Notification bars are persistent (no cycling).
10 * Popup notifications cycle through entries with show/hide animations.
11 *
12 * @package King_Addons
13 */
14
15 (function () {
16 'use strict';
17
18 if (window.KngFomo) return;
19
20 /* =============================================
21 Utility helpers
22 ============================================= */
23
24 function esc(str) {
25 if (!str) return '';
26 var d = document.createElement('div');
27 d.textContent = str;
28 return d.innerHTML;
29 }
30
31 function timeAgo(timestamp) {
32 if (!timestamp) return '';
33 var seconds = Math.floor((Date.now() / 1000) - timestamp);
34 if (seconds < 60) return 'just now';
35 var minutes = Math.floor(seconds / 60);
36 if (minutes < 60) return minutes + (minutes === 1 ? ' min' : ' mins') + ' ago';
37 var hours = Math.floor(minutes / 60);
38 if (hours < 24) return hours + (hours === 1 ? ' hour' : ' hours') + ' ago';
39 var days = Math.floor(hours / 24);
40 if (days < 7) return days + (days === 1 ? ' day' : ' days') + ' ago';
41 var weeks = Math.floor(days / 7);
42 return weeks + (weeks === 1 ? ' week' : ' weeks') + ' ago';
43 }
44
45 /**
46 * Process template string, replacing {{placeholders}} with item data.
47 * Returns empty string if template is empty/undefined.
48 */
49 function processTemplate(template, item) {
50 if (!template) return '';
51 return template.replace(/\{\{(\w+)\}\}/g, function (match, key) {
52 if (key === 'name' || key === 'username') return item.username || item.name || '';
53 if (key === 'product') return item.product || item.post_title || '';
54 if (key === 'location') return item.location || '';
55 if (key === 'time' || key === 'time_ago') return item.time_ago || (item.time ? timeAgo(item.time) : '');
56 if (key === 'email') return item.email || '';
57 if (key === 'content') return item.content || '';
58 if (key === 'url' || key === 'link') return item.product_url || item.post_url || item.url || '';
59 return item[key] || '';
60 });
61 }
62
63 /* =============================================
64 Main engine
65 ============================================= */
66
67 var KngFomo = {
68 /** All notification configs keyed by id */
69 notifications: {},
70 /** Global settings */
71 settings: {},
72 /** Popup containers keyed by position */
73 containers: {},
74 /** Active cycling timers keyed by notif id */
75 timers: {},
76 /** Session view count */
77 sessionViews: 0,
78 /** Initialized flag */
79 ready: false,
80
81 /* ----- Bootstrap ----- */
82
83 init: function () {
84 if (this.ready) return;
85 if (!window.kngFomoData || !window.kngFomoData.notifications) return;
86
87 this.ready = true;
88 this.settings = window.kngFomoData.settings || {};
89 this.sessionViews = this.getSessionViews();
90
91 var notifs = window.kngFomoData.notifications;
92 if (!notifs.length) return;
93
94 var bars = [];
95 var popups = [];
96
97 for (var i = 0; i < notifs.length; i++) {
98 var n = notifs[i];
99 if (this.isDismissed(n.id)) continue;
100 if (!this.isDeviceAllowed(n)) continue;
101 if (!this.isPageAllowed(n)) continue;
102 this.notifications[n.id] = n;
103
104 if (n.type === 'notification_bar') {
105 bars.push(n);
106 } else {
107 popups.push(n);
108 }
109 }
110
111 // Bars: render immediately (static, no cycling)
112 for (var b = 0; b < bars.length; b++) {
113 this.renderBar(bars[b]);
114 }
115
116 // Popups: start independent cycling for each
117 for (var p = 0; p < popups.length; p++) {
118 this.startPopupCycle(popups[p]);
119 }
120 },
121
122 getCurrentDevice: function () {
123 var width = window.innerWidth || document.documentElement.clientWidth || 1024;
124 if (width < 768) return 'mobile';
125 if (width < 1024) return 'tablet';
126 return 'desktop';
127 },
128
129 isDeviceAllowed: function (notif) {
130 var currentDevice = this.getCurrentDevice();
131
132 // New format: display.devices = ['desktop', 'tablet', 'mobile']
133 var devices = notif.display && Array.isArray(notif.display.devices) ? notif.display.devices : null;
134 if (devices && devices.length) {
135 return devices.indexOf(currentDevice) !== -1;
136 }
137
138 // Legacy format: customize.visibility = { desktop: true, tablet: true, mobile: true }
139 var visibility = notif.customize && notif.customize.visibility ? notif.customize.visibility : null;
140 if (visibility && typeof visibility === 'object') {
141 return visibility[currentDevice] !== false;
142 }
143
144 return true;
145 },
146
147 isPageAllowed: function (notif) {
148 var display = notif.display || {};
149
150 // Legacy shape from old settings
151 if (display.show_on === 'include' && Array.isArray(display.include_pages) && display.include_pages.length) {
152 return this.matchesIdRule(display.include_pages);
153 }
154 if (display.show_on === 'exclude' && Array.isArray(display.exclude_pages) && display.exclude_pages.length) {
155 return !this.matchesIdRule(display.exclude_pages);
156 }
157
158 // Wizard shape
159 var pagesMode = display.pages || 'all';
160 if (pagesMode === 'all') return true;
161
162 var rules = Array.isArray(display.page_rules) ? display.page_rules : [];
163 if (!rules.length) return true;
164
165 var includeMatched = false;
166 var hasIncludeRules = false;
167
168 for (var i = 0; i < rules.length; i++) {
169 var rule = rules[i] || {};
170 var type = rule.type || 'include';
171 var matched = this.matchesPageRule(rule);
172
173 if (type === 'exclude' && matched) {
174 return false;
175 }
176
177 if (type === 'include') {
178 hasIncludeRules = true;
179 if (matched) includeMatched = true;
180 }
181 }
182
183 return hasIncludeRules ? includeMatched : true;
184 },
185
186 matchesIdRule: function (ids) {
187 if (!Array.isArray(ids) || !ids.length) return false;
188 var bodyClass = document.body && document.body.className ? document.body.className : '';
189 for (var i = 0; i < ids.length; i++) {
190 var id = String(ids[i]);
191 if (bodyClass.indexOf('page-id-' + id) !== -1 || bodyClass.indexOf('postid-' + id) !== -1) {
192 return true;
193 }
194 }
195 return false;
196 },
197
198 matchesPageRule: function (rule) {
199 var condition = rule.condition || 'url_contains';
200 var rawValue = rule.value === undefined || rule.value === null ? '' : String(rule.value).trim();
201 if (!rawValue) return false;
202
203 var currentPath = (window.location.pathname || '').toLowerCase();
204 var currentUrl = (window.location.href || '').toLowerCase();
205 var value = rawValue.toLowerCase();
206
207 if (condition === 'url_contains') {
208 return currentUrl.indexOf(value) !== -1;
209 }
210
211 if (condition === 'url_is') {
212 if (currentUrl === value) return true;
213 // Also support path-only values in UI
214 var normalized = value.charAt(0) === '/' ? value : ('/' + value);
215 return currentPath === normalized;
216 }
217
218 if (condition === 'page' || condition === 'post') {
219 var bodyClass = document.body && document.body.className ? document.body.className : '';
220 return bodyClass.indexOf('page-id-' + rawValue) !== -1 || bodyClass.indexOf('postid-' + rawValue) !== -1;
221 }
222
223 return false;
224 },
225
226 /* =============================================
227 NOTIFICATION BAR
228 ============================================= */
229
230 renderBar: function (notif) {
231 var position = notif.bar_position || 'top';
232 var el = document.createElement('div');
233 el.className = 'kng-fomo-bar kng-fomo-bar--' + position;
234 el.id = 'kng-fomo-bar-' + notif.id;
235 el.setAttribute('data-notif-id', notif.id);
236
237 if (notif.bg_color) el.style.setProperty('--kng-fomo-bg', notif.bg_color);
238 if (notif.text_color) el.style.setProperty('--kng-fomo-text', notif.text_color);
239 if (notif.accent_color) el.style.setProperty('--kng-fomo-accent', notif.accent_color);
240
241 var html = '';
242
243 if (notif.message) {
244 html += '<p class="kng-fomo-bar__message">' + esc(notif.message) + '</p>';
245 } else if (notif.title) {
246 html += '<p class="kng-fomo-bar__message">' + esc(notif.title) + '</p>';
247 }
248
249 if (notif.cta_text && notif.cta_url) {
250 html += '<a href="' + esc(notif.cta_url) + '" class="kng-fomo-bar__cta" target="_blank" rel="noopener">';
251 html += esc(notif.cta_text);
252 html += '</a>';
253 }
254
255 html += '<button type="button" class="kng-fomo-bar__close" aria-label="Close">';
256 html += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>';
257 html += '</button>';
258
259 el.innerHTML = html;
260
261 if (position === 'top') {
262 document.body.insertBefore(el, document.body.firstChild);
263 } else {
264 document.body.appendChild(el);
265 }
266
267 this.trackEvent(notif.id, 'view');
268
269 var self = this;
270 requestAnimationFrame(function () {
271 el.classList.add('is-visible');
272 self.adjustBodyPadding(position, el.offsetHeight);
273 });
274
275 el.querySelector('.kng-fomo-bar__close').addEventListener('click', function () {
276 self.dismiss(notif.id);
277 el.classList.remove('is-visible');
278 self.adjustBodyPadding(position, 0);
279 setTimeout(function () { el.remove(); }, 400);
280 });
281
282 var cta = el.querySelector('.kng-fomo-bar__cta');
283 if (cta) {
284 cta.addEventListener('click', function () {
285 self.trackEvent(notif.id, 'click');
286 });
287 }
288
289 // Auto-hide support
290 var cust = notif.customize || {};
291 if (cust.auto_hide && cust.hide_after > 0) {
292 setTimeout(function () {
293 el.classList.remove('is-visible');
294 self.adjustBodyPadding(position, 0);
295 setTimeout(function () { el.remove(); }, 400);
296 }, cust.hide_after * 1000);
297 }
298 },
299
300 adjustBodyPadding: function (position, height) {
301 if (position === 'top') {
302 document.body.style.paddingTop = height ? (height + 'px') : '';
303 document.body.classList.toggle('has-kng-fomo-bar-top', height > 0);
304 } else {
305 document.body.style.paddingBottom = height ? (height + 'px') : '';
306 document.body.classList.toggle('has-kng-fomo-bar-bottom', height > 0);
307 }
308 },
309
310 /* =============================================
311 POPUP NOTIFICATIONS — entry cycling engine
312 ============================================= */
313
314 startPopupCycle: function (notif) {
315 var self = this;
316 var disp = notif.display || {};
317
318 // Build entries and optionally randomize
319 var entries = this.buildEntries(notif);
320 if (!entries.length) return;
321
322 if (disp.random) {
323 // Fisher-Yates shuffle
324 for (var si = entries.length - 1; si > 0; si--) {
325 var ri = Math.floor(Math.random() * (si + 1));
326 var tmp = entries[si];
327 entries[si] = entries[ri];
328 entries[ri] = tmp;
329 }
330 }
331
332 var delayBefore = (parseInt(disp.delay, 10) || this.settings.delayBefore || 5) * 1000;
333 var displayFor = (parseInt(disp.duration, 10) || this.settings.displayFor || 5) * 1000;
334 var delayBetween = (parseInt(disp.interval, 10) || this.settings.delayBetween || 5) * 1000;
335 var loop = disp.loop !== undefined ? !!disp.loop : (this.settings.loop !== undefined ? !!this.settings.loop : true);
336 var maxPerSession = parseInt(disp.max_per_session, 10) || 0;
337
338 var position = (notif.design && notif.design.position) || this.settings.position || 'bottom-left';
339 var container = this.getContainer(position);
340
341 var state = {
342 entries: entries,
343 index: 0,
344 notif: notif,
345 container: container,
346 displayFor: displayFor,
347 delayBetween: delayBetween,
348 loop: loop,
349 maxPerSession: maxPerSession,
350 shown: 0,
351 intervalId: null,
352 timeoutId: null,
353 active: true
354 };
355
356 this.timers[notif.id] = state;
357
358 // Initial delay then show first entry
359 state.timeoutId = setTimeout(function () {
360 self.showEntry(state);
361
362 // Cycling: use setTimeout chain instead of setInterval for reliability
363 function scheduleNext() {
364 if (!state.active) return;
365 state.timeoutId = setTimeout(function () {
366 if (!state.active) return;
367
368 state.index++;
369 if (state.index >= state.entries.length) {
370 if (!state.loop) {
371 state.active = false;
372 return;
373 }
374 state.index = 0;
375 }
376 self.showEntry(state);
377 scheduleNext();
378 }, displayFor + delayBetween);
379 }
380
381 if (entries.length > 1 || loop) {
382 scheduleNext();
383 }
384 }, delayBefore);
385 },
386
387 /**
388 * Build entries from notification data.
389 * Dynamic sources (WooCommerce, comments) produce multiple entries.
390 * Manual/static notifications produce a single entry.
391 */
392 buildEntries: function (notif) {
393 var items = notif.items || [];
394 var entries = [];
395
396 if (items.length > 0) {
397 for (var i = 0; i < items.length; i++) {
398 entries.push(this.buildEntryFromItem(notif, items[i]));
399 }
400 } else {
401 // Manual: single entry from the notification content
402 entries.push({
403 title: notif.title || '',
404 message: notif.message || '',
405 image: notif.image || '',
406 image_style: notif.image_style || 'product',
407 time_text: notif.time_text || '',
408 cta_text: notif.cta_text || '',
409 cta_url: notif.cta_url || '',
410 click_url: notif.click_url || '',
411 click_target: notif.click_target || '_self'
412 });
413 }
414
415 return entries;
416 },
417
418 /**
419 * Build one entry from a dynamic item.
420 * Maps item data to display fields based on notification type.
421 */
422 buildEntryFromItem: function (notif, item) {
423 var type = notif.type;
424 var ct = notif.content || {};
425 var clickAction = (notif.customize && notif.customize.click_action) || 'link';
426 var entry = {
427 image: '', image_style: 'product',
428 title: '', message: '', time_text: '',
429 cta_text: notif.cta_text || '', cta_url: '',
430 click_url: '', click_target: '_self', click_action: clickAction
431 };
432
433 // If user provided templates with {{placeholders}}, use them
434 var hasTemplateTitle = ct.title && ct.title.indexOf('{{') !== -1;
435 var hasTemplateMessage = ct.message && ct.message.indexOf('{{') !== -1;
436
437 if (type === 'sales_notification' || type === 'woocommerce_sales') {
438 entry.title = hasTemplateTitle ? processTemplate(ct.title, item) : (item.username || 'Someone');
439 entry.message = hasTemplateMessage ? processTemplate(ct.message, item) : ((ct.action_text || 'just purchased') + ' ' + (item.product || ''));
440 entry.image = item.product_image || item.avatar || '';
441 entry.image_style = 'product';
442 entry.time_text = item.time_ago || (item.time ? timeAgo(item.time) : '');
443 entry.click_url = item.product_url || '';
444 entry.cta_url = item.product_url || '';
445 if (item.location) {
446 entry.time_text += (entry.time_text ? ' \u00b7 ' : '') + item.location;
447 }
448 } else if (type === 'review_notification' || type === 'reviews') {
449 entry.title = hasTemplateTitle ? processTemplate(ct.title, item) : (item.username || 'Someone');
450 entry.message = hasTemplateMessage ? processTemplate(ct.message, item) : ((ct.action_text || 'left a review on') + ' ' + (item.product || item.post_title || ''));
451 entry.image = item.avatar || item.product_image || '';
452 entry.image_style = 'avatar';
453 entry.time_text = item.time_ago || (item.time ? timeAgo(item.time) : '');
454 entry.click_url = item.product_url || item.post_url || '';
455 entry.cta_url = entry.click_url;
456 } else if (type === 'comment_notification' || type === 'comments' || type === 'wordpress_comments') {
457 entry.title = hasTemplateTitle ? processTemplate(ct.title, item) : (item.username || 'Someone');
458 entry.message = hasTemplateMessage ? processTemplate(ct.message, item) : ((ct.action_text || 'commented on') + ' ' + (item.post_title || ''));
459 entry.image = item.avatar || '';
460 entry.image_style = 'avatar';
461 entry.time_text = item.time_ago || (item.time ? timeAgo(item.time) : '');
462 entry.click_url = item.post_url || '';
463 entry.cta_url = entry.click_url;
464 } else if (type === 'download_stats' || type === 'wporg_downloads') {
465 entry.title = hasTemplateTitle ? processTemplate(ct.title, item) : (item.name || '');
466 entry.message = hasTemplateMessage ? processTemplate(ct.message, item) : ((item.active_installs || '0') + ' active installs');
467 entry.image = notif.image || '';
468 entry.time_text = (item.downloads || '0') + ' total downloads';
469 } else if (type === 'email_subscription') {
470 entry.title = hasTemplateTitle ? processTemplate(ct.title, item) : (item.username || item.email || 'Someone');
471 entry.message = hasTemplateMessage ? processTemplate(ct.message, item) : (ct.action_text || 'just subscribed');
472 entry.image = item.avatar || '';
473 entry.image_style = 'avatar';
474 entry.time_text = item.time_ago || (item.time ? timeAgo(item.time) : '');
475 } else if (type === 'custom_notification' || type === 'custom_csv') {
476 entry.title = hasTemplateTitle ? processTemplate(ct.title, item) : (item.title || item.username || '');
477 entry.message = hasTemplateMessage ? processTemplate(ct.message, item) : (item.message || item.content || '');
478 entry.image = item.image || item.avatar || '';
479 entry.time_text = item.time_text || item.time_ago || (item.time ? timeAgo(item.time) : '');
480 entry.click_url = item.url || item.link || '';
481 entry.cta_url = entry.click_url;
482 } else {
483 // Fallback generic
484 entry.title = hasTemplateTitle ? processTemplate(ct.title, item) : (item.username || item.title || item.name || '');
485 entry.message = hasTemplateMessage ? processTemplate(ct.message, item) : (item.message || item.content || item.product || item.post_title || '');
486 entry.image = item.image || item.avatar || item.product_image || '';
487 entry.time_text = item.time_ago || (item.time ? timeAgo(item.time) : '');
488 entry.click_url = item.url || item.link || item.product_url || item.post_url || '';
489 entry.cta_url = entry.click_url;
490 }
491
492 // Respect click action from wizard/customize settings.
493 if (clickAction !== 'link') {
494 entry.click_url = '';
495 }
496
497 return entry;
498 },
499
500 /**
501 * Show a single entry (one step of the cycle)
502 */
503 showEntry: function (state) {
504 var self = this;
505 var notif = state.notif;
506 var entry = state.entries[state.index];
507
508 // Session limit checks
509 if (state.maxPerSession && state.shown >= state.maxPerSession) {
510 state.active = false;
511 return;
512 }
513 if (this.settings.sessionLimit && this.sessionViews >= this.settings.sessionLimit) {
514 state.active = false;
515 return;
516 }
517
518 // Remove current visible notification in the container
519 var existing = state.container.querySelector('.kng-fomo-notification');
520 if (existing) {
521 existing.classList.remove('is-visible');
522 setTimeout(function () { existing.remove(); }, 300);
523 }
524
525 var showDelay = existing ? 350 : 0;
526
527 setTimeout(function () {
528 var el = self.buildPopupHTML(notif, entry, state);
529 state.container.appendChild(el);
530
531 if (notif.sound) self.playSound(notif.sound);
532
533 self.trackEvent(notif.id, 'view');
534 self.incrementSessionViews();
535 state.shown++;
536
537 // Double rAF for reliable CSS transition trigger
538 requestAnimationFrame(function () {
539 requestAnimationFrame(function () {
540 el.classList.add('is-visible');
541 });
542 });
543
544 self.startProgressBar(el, state.displayFor);
545
546 // Auto-hide
547 setTimeout(function () {
548 el.classList.remove('is-visible');
549 setTimeout(function () { el.remove(); }, 400);
550 }, state.displayFor);
551 }, showDelay);
552 },
553
554 /**
555 * Build popup HTML element
556 */
557 buildPopupHTML: function (notif, entry, state) {
558 var el = document.createElement('div');
559 var animation = notif.animation || 'slide';
560 var clickAction = entry.click_action || ((notif.customize && notif.customize.click_action) || 'link');
561 var hasClick = (clickAction === 'link' && entry.click_url) || clickAction === 'dismiss';
562
563 el.className = 'kng-fomo-notification kng-fomo-notification--shadow kng-fomo-notification--' + animation;
564 el.setAttribute('data-notif-id', notif.id);
565 if (hasClick) el.classList.add('kng-fomo-notification--clickable');
566 el.classList.add('kng-fomo-notification--type-' + (notif.type || 'generic'));
567
568 if (notif.bg_color) el.style.setProperty('--kng-fomo-bg', notif.bg_color);
569 if (notif.text_color) el.style.setProperty('--kng-fomo-text', notif.text_color);
570 if (notif.accent_color) el.style.setProperty('--kng-fomo-accent', notif.accent_color);
571
572 var html = '';
573
574 // Image
575 if (entry.image) {
576 var imgCls = (entry.image_style === 'avatar') ? ' kng-fomo-notification__image--avatar' : '';
577 html += '<div class="kng-fomo-notification__image' + imgCls + '">';
578 html += '<img src="' + esc(entry.image) + '" alt="" loading="lazy">';
579 html += '</div>';
580 }
581
582 // Content
583 html += '<div class="kng-fomo-notification__content">';
584
585 if (entry.title) {
586 html += '<p class="kng-fomo-notification__title">' + esc(entry.title) + '</p>';
587 }
588 if (entry.message) {
589 html += '<p class="kng-fomo-notification__message">' + esc(entry.message) + '</p>';
590 }
591 if (entry.time_text) {
592 html += '<div class="kng-fomo-notification__time">';
593 html += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>';
594 html += '<span>' + esc(entry.time_text) + '</span>';
595 html += '</div>';
596 }
597 if (entry.cta_text && entry.cta_url) {
598 html += '<a href="' + esc(entry.cta_url) + '" class="kng-fomo-notification__cta" target="_blank" rel="noopener">';
599 html += esc(entry.cta_text);
600 html += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>';
601 html += '</a>';
602 }
603
604 html += '</div>'; // .content
605
606 // Close button
607 html += '<button type="button" class="kng-fomo-notification__close" aria-label="Close">';
608 html += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>';
609 html += '</button>';
610
611 // Progress bar
612 html += '<div class="kng-fomo-notification__progress"><div class="kng-fomo-notification__progress-bar"></div></div>';
613
614 el.innerHTML = html;
615
616 // --- Event listeners ---
617 var self = this;
618
619 // Close dismisses and stops cycling for this notification
620 el.querySelector('.kng-fomo-notification__close').addEventListener('click', function (e) {
621 e.stopPropagation();
622 self.dismiss(notif.id);
623 state.active = false;
624 if (state.intervalId) clearInterval(state.intervalId);
625 if (state.timeoutId) clearTimeout(state.timeoutId);
626 el.classList.remove('is-visible');
627 setTimeout(function () { el.remove(); }, 400);
628 });
629
630 // Click on body navigates
631 if (entry.click_url) {
632 el.addEventListener('click', function (e) {
633 if (e.target.closest('.kng-fomo-notification__close')) return;
634 if (e.target.closest('.kng-fomo-notification__cta')) return;
635
636 if (clickAction === 'dismiss') {
637 self.dismiss(notif.id);
638 state.active = false;
639 if (state.intervalId) clearInterval(state.intervalId);
640 if (state.timeoutId) clearTimeout(state.timeoutId);
641 el.classList.remove('is-visible');
642 setTimeout(function () { el.remove(); }, 400);
643 return;
644 }
645
646 if (clickAction === 'link' && entry.click_url) {
647 self.trackEvent(notif.id, 'click');
648 window.open(entry.click_url, entry.click_target || '_self');
649 }
650 });
651 } else if (clickAction === 'dismiss') {
652 el.addEventListener('click', function (e) {
653 if (e.target.closest('.kng-fomo-notification__close')) return;
654 if (e.target.closest('.kng-fomo-notification__cta')) return;
655 self.dismiss(notif.id);
656 state.active = false;
657 if (state.intervalId) clearInterval(state.intervalId);
658 if (state.timeoutId) clearTimeout(state.timeoutId);
659 el.classList.remove('is-visible');
660 setTimeout(function () { el.remove(); }, 400);
661 });
662 }
663
664 // CTA
665 var ctaBtn = el.querySelector('.kng-fomo-notification__cta');
666 if (ctaBtn) {
667 ctaBtn.addEventListener('click', function (e) {
668 e.stopPropagation();
669 self.trackEvent(notif.id, 'click');
670 });
671 }
672
673 return el;
674 },
675
676 /**
677 * Progress bar animation: 100% → 0% over duration
678 */
679 startProgressBar: function (el, duration) {
680 var bar = el.querySelector('.kng-fomo-notification__progress-bar');
681 if (!bar) return;
682
683 bar.style.transition = 'none';
684 bar.style.width = '100%';
685
686 requestAnimationFrame(function () {
687 requestAnimationFrame(function () {
688 bar.style.transition = 'width ' + duration + 'ms linear';
689 bar.style.width = '0%';
690 });
691 });
692 },
693
694 /* =============================================
695 Container management
696 ============================================= */
697
698 getContainer: function (position) {
699 if (this.containers[position]) return this.containers[position];
700 var el = document.createElement('div');
701 el.className = 'kng-fomo-notification-wrap kng-fomo-notification-wrap--' + position;
702 document.body.appendChild(el);
703 this.containers[position] = el;
704 return el;
705 },
706
707 /* =============================================
708 Sound
709 ============================================= */
710
711 playSound: function (soundType) {
712 // Try pre-loaded audio files
713 var src = this.getSoundUrl(soundType);
714 if (src) {
715 try {
716 var audio = new Audio(src);
717 audio.volume = (this.settings.soundVolume || 50) / 100;
718 audio.play().catch(function () {});
719 return;
720 } catch (e) {}
721 }
722
723 // Fallback: Web Audio API oscillator
724 try {
725 var ctx = new (window.AudioContext || window.webkitAudioContext)();
726 var osc = ctx.createOscillator();
727 var gain = ctx.createGain();
728 osc.connect(gain);
729 gain.connect(ctx.destination);
730 var vol = ((this.settings.soundVolume || 50) / 100) * 0.3;
731 gain.gain.value = vol;
732 switch (soundType) {
733 case 'pop': osc.frequency.value = 600; osc.type = 'sine'; break;
734 case 'ding': osc.frequency.value = 800; osc.type = 'triangle'; break;
735 case 'chime': osc.frequency.value = 523; osc.type = 'sine'; break;
736 default: osc.frequency.value = 600; osc.type = 'sine';
737 }
738 gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.2);
739 osc.start();
740 osc.stop(ctx.currentTime + 0.2);
741 } catch (e) {}
742 },
743
744 getSoundUrl: function (soundType) {
745 if (window.kngFomoData && window.kngFomoData.sounds && window.kngFomoData.sounds[soundType]) {
746 return window.kngFomoData.sounds[soundType];
747 }
748 return null;
749 },
750
751 /* =============================================
752 Analytics
753 ============================================= */
754
755 trackEvent: function (notifId, eventType) {
756 if (!window.kngFomoData || !window.kngFomoData.ajaxUrl) return;
757 var fd = new FormData();
758 fd.append('action', 'kng_fomo_track_event');
759 fd.append('notification_id', notifId);
760 fd.append('event_type', eventType);
761 fd.append('nonce', window.kngFomoData.nonce);
762 fetch(window.kngFomoData.ajaxUrl, {
763 method: 'POST', body: fd, credentials: 'same-origin'
764 }).catch(function () {});
765 },
766
767 /* =============================================
768 Dismissal & session
769 ============================================= */
770
771 dismiss: function (id) {
772 var d = this.getDismissed();
773 if (d.indexOf(id) === -1) d.push(id);
774 try { sessionStorage.setItem('kng_fomo_dismissed', JSON.stringify(d)); } catch (e) {}
775 },
776 isDismissed: function (id) { return this.getDismissed().indexOf(id) > -1; },
777 getDismissed: function () {
778 try { return JSON.parse(sessionStorage.getItem('kng_fomo_dismissed') || '[]'); } catch (e) { return []; }
779 },
780 getSessionViews: function () {
781 try { return parseInt(sessionStorage.getItem('kng_fomo_views') || '0', 10); } catch (e) { return 0; }
782 },
783 incrementSessionViews: function () {
784 this.sessionViews++;
785 try { sessionStorage.setItem('kng_fomo_views', this.sessionViews.toString()); } catch (e) {}
786 },
787
788 /* =============================================
789 Public API
790 ============================================= */
791
792 stop: function (id) {
793 var s = this.timers[id];
794 if (s) { s.active = false; if (s.intervalId) clearInterval(s.intervalId); if (s.timeoutId) clearTimeout(s.timeoutId); }
795 },
796 stopAll: function () {
797 var ids = Object.keys(this.timers);
798 for (var i = 0; i < ids.length; i++) this.stop(ids[i]);
799 }
800 };
801
802 // Init on DOM ready
803 if (document.readyState === 'loading') {
804 document.addEventListener('DOMContentLoaded', function () { KngFomo.init(); });
805 } else {
806 KngFomo.init();
807 }
808
809 window.KngFomo = KngFomo;
810
811 })();
812