PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.44
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.44
51.1.86 51.1.84 51.1.85 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 All 40 releases
king-addons / includes / extensions / Fomo_Notifications / assets / frontend.js

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

467 lines 16.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Fomo Notifications - Frontend JavaScript
3 * Lightweight notification display engine
4 *
5 * @package King_Addons
6 */
7
8 (function() {
9 'use strict';
10
11 // Bail if already initialized
12 if (window.KngFomo) return;
13
14 const KngFomo = {
15 queue: [],
16 current: null,
17 container: null,
18 settings: {},
19 sessionViews: 0,
20 init: function() {
21 if (!window.kngFomoData || !window.kngFomoData.notifications) return;
22
23 this.settings = window.kngFomoData.settings || {};
24 this.sessionViews = this.getSessionViews();
25
26 // Check session limit
27 if (this.settings.sessionLimit && this.sessionViews >= this.settings.sessionLimit) {
28 return;
29 }
30
31 // Create container
32 this.createContainer();
33
34 // Filter and prepare notifications
35 this.prepareQueue();
36
37 // Start display cycle
38 if (this.queue.length > 0) {
39 this.startCycle();
40 }
41 },
42
43 createContainer: function() {
44 const position = this.settings.position || 'bottom-left';
45
46 // Check for bar notifications
47 const barNotifs = window.kngFomoData.notifications.filter(n => n.type === 'notification_bar');
48 const popupNotifs = window.kngFomoData.notifications.filter(n => n.type !== 'notification_bar');
49
50 // Create popup container
51 if (popupNotifs.length > 0) {
52 this.container = document.createElement('div');
53 this.container.className = 'kng-fomo-notification-wrap kng-fomo-notification-wrap--' + position;
54 document.body.appendChild(this.container);
55 }
56 },
57
58 prepareQueue: function() {
59 const notifications = window.kngFomoData.notifications;
60 const now = Date.now();
61
62 notifications.forEach(notif => {
63 // Check if already dismissed
64 if (this.isDismissed(notif.id)) return;
65
66 // Check device
67 if (!this.checkDevice(notif.device)) return;
68
69 // Check page rules
70 if (!this.checkPageRules(notif.page_rules)) return;
71
72 // Check date range
73 if (notif.date_start && new Date(notif.date_start) > now) return;
74 if (notif.date_end && new Date(notif.date_end) < now) return;
75
76 // Add to queue
77 if (notif.type === 'notification_bar') {
78 // Show bars immediately
79 this.showBar(notif);
80 } else {
81 this.queue.push(notif);
82 }
83 });
84
85 // Shuffle or sort queue based on settings
86 if (this.settings.randomOrder) {
87 this.shuffleQueue();
88 }
89 },
90
91 checkDevice: function(device) {
92 if (!device || device === 'all') return true;
93
94 const width = window.innerWidth;
95 const isMobile = width < 768;
96 const isTablet = width >= 768 && width < 1024;
97 const isDesktop = width >= 1024;
98
99 switch (device) {
100 case 'desktop': return isDesktop;
101 case 'tablet': return isTablet;
102 case 'mobile': return isMobile;
103 case 'desktop_tablet': return isDesktop || isTablet;
104 case 'mobile_tablet': return isMobile || isTablet;
105 default: return true;
106 }
107 },
108
109 checkPageRules: function(rules) {
110 if (!rules || !rules.type || rules.type === 'all') return true;
111
112 const currentUrl = window.location.href;
113 const currentPath = window.location.pathname;
114 const pages = rules.pages || [];
115
116 switch (rules.type) {
117 case 'include':
118 return pages.some(page => this.matchPage(page, currentUrl, currentPath));
119 case 'exclude':
120 return !pages.some(page => this.matchPage(page, currentUrl, currentPath));
121 default:
122 return true;
123 }
124 },
125
126 matchPage: function(page, url, path) {
127 // Simple matching - can be enhanced
128 if (page.indexOf('*') > -1) {
129 const regex = new RegExp('^' + page.replace(/\*/g, '.*') + '$');
130 return regex.test(path);
131 }
132 return url.indexOf(page) > -1 || path === page;
133 },
134
135 shuffleQueue: function() {
136 for (let i = this.queue.length - 1; i > 0; i--) {
137 const j = Math.floor(Math.random() * (i + 1));
138 [this.queue[i], this.queue[j]] = [this.queue[j], this.queue[i]];
139 }
140 },
141
142 startCycle: function() {
143 const initialDelay = parseInt(this.settings.initialDelay) || 3000;
144
145 setTimeout(() => {
146 this.showNext();
147 }, initialDelay);
148 },
149
150 showNext: function() {
151 if (this.queue.length === 0) {
152 // Check if loop
153 if (this.settings.loop) {
154 this.prepareQueue();
155 }
156 return;
157 }
158
159 // Check session limit
160 if (this.settings.sessionLimit && this.sessionViews >= this.settings.sessionLimit) {
161 return;
162 }
163
164 const notif = this.queue.shift();
165 this.showNotification(notif);
166 },
167
168 showNotification: function(notif) {
169 this.current = notif;
170
171 // Build HTML
172 const el = this.buildNotificationHTML(notif);
173 this.container.appendChild(el);
174
175 // Track view
176 this.trackEvent(notif.id, 'view');
177 this.incrementSessionViews();
178
179 // Play sound if enabled
180 if (notif.sound) {
181 this.playSound(notif.sound);
182 }
183
184 // Show with animation
185 requestAnimationFrame(() => {
186 el.classList.add('is-visible');
187 });
188
189 // Auto-hide
190 const displayTime = parseInt(notif.display_time) || 5000;
191 setTimeout(() => {
192 this.hideNotification(el, notif);
193 }, displayTime);
194 },
195
196 buildNotificationHTML: function(notif) {
197 const el = document.createElement('div');
198 const animation = notif.animation || 'slide';
199 const hasClick = notif.click_url || notif.cta_url;
200
201 el.className = 'kng-fomo-notification kng-fomo-notification--shadow kng-fomo-notification--' + animation;
202 if (hasClick) {
203 el.classList.add('kng-fomo-notification--clickable');
204 }
205
206 // Apply custom colors
207 if (notif.bg_color) el.style.setProperty('--kng-fomo-bg', notif.bg_color);
208 if (notif.text_color) el.style.setProperty('--kng-fomo-text', notif.text_color);
209 if (notif.accent_color) el.style.setProperty('--kng-fomo-accent', notif.accent_color);
210
211 // Build inner HTML
212 let html = '';
213
214 // Image
215 if (notif.image) {
216 const imgClass = notif.image_style === 'avatar' ? 'kng-fomo-notification__image--avatar' : '';
217 html += '<div class="kng-fomo-notification__image ' + imgClass + '"><img src="' + this.escapeHtml(notif.image) + '" alt="" loading="lazy"></div>';
218 }
219
220 // Content
221 html += '<div class="kng-fomo-notification__content">';
222
223 if (notif.title) {
224 html += '<p class="kng-fomo-notification__title">' + this.escapeHtml(notif.title) + '</p>';
225 }
226
227 if (notif.message) {
228 html += '<p class="kng-fomo-notification__message">' + this.escapeHtml(notif.message) + '</p>';
229 }
230
231 if (notif.time_text) {
232 html += '<div class="kng-fomo-notification__time">';
233 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>';
234 html += '<span>' + this.escapeHtml(notif.time_text) + '</span>';
235 html += '</div>';
236 }
237
238 if (notif.cta_text && notif.cta_url) {
239 html += '<a href="' + this.escapeHtml(notif.cta_url) + '" class="kng-fomo-notification__cta" data-notif-id="' + notif.id + '">';
240 html += this.escapeHtml(notif.cta_text);
241 html += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>';
242 html += '</a>';
243 }
244
245 html += '</div>';
246
247 // Close button
248 html += '<button type="button" class="kng-fomo-notification__close" aria-label="Close">';
249 html += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>';
250 html += '</button>';
251
252 el.innerHTML = html;
253
254 // Event listeners
255 const closeBtn = el.querySelector('.kng-fomo-notification__close');
256 closeBtn.addEventListener('click', (e) => {
257 e.stopPropagation();
258 this.dismiss(notif.id);
259 this.hideNotification(el, notif);
260 });
261
262 // Click handler for whole notification or CTA
263 if (notif.click_url) {
264 el.addEventListener('click', (e) => {
265 if (e.target.closest('.kng-fomo-notification__close')) return;
266 this.trackEvent(notif.id, 'click');
267 window.open(notif.click_url, notif.click_target || '_self');
268 });
269 }
270
271 // CTA click
272 const ctaBtn = el.querySelector('.kng-fomo-notification__cta');
273 if (ctaBtn) {
274 ctaBtn.addEventListener('click', (e) => {
275 this.trackEvent(notif.id, 'click');
276 });
277 }
278
279 return el;
280 },
281
282 hideNotification: function(el, notif) {
283 el.classList.remove('is-visible');
284
285 setTimeout(() => {
286 el.remove();
287 this.current = null;
288
289 // Show next
290 const betweenDelay = parseInt(this.settings.betweenDelay) || 3000;
291 setTimeout(() => {
292 this.showNext();
293 }, betweenDelay);
294 }, 400);
295 },
296
297 showBar: function(notif) {
298 // Check if already dismissed
299 if (this.isDismissed(notif.id)) return;
300
301 const position = notif.bar_position || 'top';
302
303 const el = document.createElement('div');
304 el.className = 'kng-fomo-bar kng-fomo-bar--' + position;
305 el.id = 'kng-fomo-bar-' + notif.id;
306
307 // Apply custom colors
308 if (notif.bg_color) el.style.setProperty('--kng-fomo-bg', notif.bg_color);
309 if (notif.text_color) el.style.setProperty('--kng-fomo-text', notif.text_color);
310
311 let html = '';
312
313 if (notif.message) {
314 html += '<p class="kng-fomo-bar__message">' + this.escapeHtml(notif.message) + '</p>';
315 }
316
317 if (notif.cta_text && notif.cta_url) {
318 html += '<a href="' + this.escapeHtml(notif.cta_url) + '" class="kng-fomo-bar__cta">' + this.escapeHtml(notif.cta_text) + '</a>';
319 }
320
321 html += '<button type="button" class="kng-fomo-bar__close" aria-label="Close">';
322 html += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>';
323 html += '</button>';
324
325 el.innerHTML = html;
326
327 document.body.appendChild(el);
328
329 // Track view
330 this.trackEvent(notif.id, 'view');
331
332 // Show with animation
333 requestAnimationFrame(() => {
334 el.classList.add('is-visible');
335 });
336
337 // Close button
338 const closeBtn = el.querySelector('.kng-fomo-bar__close');
339 closeBtn.addEventListener('click', () => {
340 this.dismiss(notif.id);
341 el.classList.remove('is-visible');
342 setTimeout(() => el.remove(), 400);
343 });
344
345 // CTA click tracking
346 const ctaBtn = el.querySelector('.kng-fomo-bar__cta');
347 if (ctaBtn) {
348 ctaBtn.addEventListener('click', () => {
349 this.trackEvent(notif.id, 'click');
350 });
351 }
352 },
353
354 trackEvent: function(notifId, eventType) {
355 if (!window.kngFomoData.ajaxUrl) return;
356
357 const formData = new FormData();
358 formData.append('action', 'kng_fomo_track_event');
359 formData.append('notification_id', notifId);
360 formData.append('event_type', eventType);
361 formData.append('nonce', window.kngFomoData.nonce);
362
363 fetch(window.kngFomoData.ajaxUrl, {
364 method: 'POST',
365 body: formData,
366 credentials: 'same-origin'
367 }).catch(() => {});
368 },
369
370 playSound: function(soundType) {
371 const volume = parseFloat(this.settings.soundVolume) || 0.5;
372
373 // Create audio context
374 try {
375 const ctx = new (window.AudioContext || window.webkitAudioContext)();
376 const oscillator = ctx.createOscillator();
377 const gainNode = ctx.createGain();
378
379 oscillator.connect(gainNode);
380 gainNode.connect(ctx.destination);
381
382 gainNode.gain.value = volume * 0.3;
383
384 switch (soundType) {
385 case 'pop':
386 oscillator.frequency.value = 600;
387 oscillator.type = 'sine';
388 break;
389 case 'ding':
390 oscillator.frequency.value = 800;
391 oscillator.type = 'triangle';
392 break;
393 case 'chime':
394 oscillator.frequency.value = 523;
395 oscillator.type = 'sine';
396 break;
397 default:
398 oscillator.frequency.value = 600;
399 oscillator.type = 'sine';
400 }
401
402 gainNode.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.2);
403
404 oscillator.start();
405 oscillator.stop(ctx.currentTime + 0.2);
406 } catch (e) {
407 // Audio not supported
408 }
409 },
410
411 dismiss: function(notifId) {
412 const dismissed = this.getDismissed();
413 dismissed.push(notifId);
414
415 try {
416 sessionStorage.setItem('kng_fomo_dismissed', JSON.stringify(dismissed));
417 } catch (e) {}
418 },
419
420 isDismissed: function(notifId) {
421 const dismissed = this.getDismissed();
422 return dismissed.indexOf(notifId) > -1;
423 },
424
425 getDismissed: function() {
426 try {
427 return JSON.parse(sessionStorage.getItem('kng_fomo_dismissed') || '[]');
428 } catch (e) {
429 return [];
430 }
431 },
432
433 getSessionViews: function() {
434 try {
435 return parseInt(sessionStorage.getItem('kng_fomo_views') || '0');
436 } catch (e) {
437 return 0;
438 }
439 },
440
441 incrementSessionViews: function() {
442 this.sessionViews++;
443 try {
444 sessionStorage.setItem('kng_fomo_views', this.sessionViews.toString());
445 } catch (e) {}
446 },
447
448 escapeHtml: function(str) {
449 if (!str) return '';
450 const div = document.createElement('div');
451 div.textContent = str;
452 return div.innerHTML;
453 }
454 };
455
456 // Initialize
457 if (document.readyState === 'loading') {
458 document.addEventListener('DOMContentLoaded', () => KngFomo.init());
459 } else {
460 KngFomo.init();
461 }
462
463 // Export
464 window.KngFomo = KngFomo;
465
466 })();
467