PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.78
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.78
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 / admin.js

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

1,421 lines 51.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Fomo Notifications - Admin JavaScript
3 *
4 * @package King_Addons
5 */
6
7 (function($) {
8 'use strict';
9
10 /**
11 * Main Fomo Notifications Admin Class
12 */
13 const KngFomoAdmin = {
14 /**
15 * Current wizard data
16 */
17 wizardData: {
18 step: 1,
19 notification_id: 0,
20 name: '',
21 type: '',
22 source: '',
23 source_config: {},
24 design: {
25 template: 'default',
26 position: 'bottom-left',
27 animation: 'slide',
28 bg_color: '#ffffff',
29 text_color: '#1d1d1f',
30 accent_color: '#0071e3',
31 border_radius: 16,
32 shadow: true
33 },
34 content: {
35 title: '',
36 message: '',
37 image_type: 'product',
38 custom_image: '',
39 show_time: true,
40 time_format: 'relative',
41 cta_text: '',
42 cta_url: ''
43 },
44 display: {
45 delay: 3,
46 duration: 5,
47 interval: 10,
48 max_per_session: 5,
49 devices: ['desktop', 'tablet', 'mobile'],
50 pages: 'all',
51 page_rules: [],
52 audience: 'all',
53 exclude_logged_in: false
54 },
55 customize: {
56 z_index: 99999,
57 close_button: true,
58 click_action: 'link',
59 sound: false,
60 analytics: true
61 }
62 },
63
64 /**
65 * Initialize
66 */
67 init: function() {
68 this.bindEvents();
69 this.initWizard();
70 this.initCharts();
71 this.initToggles();
72 },
73
74 /**
75 * Bind events
76 */
77 bindEvents: function() {
78 const self = this;
79
80 // Navigation
81 $(document).on('click', '.kng-fomo-nav-item', function(e) {
82 const href = $(this).attr('href');
83 if (href && href.indexOf('#') !== 0) {
84 return;
85 }
86 e.preventDefault();
87 const tab = $(this).data('tab');
88 self.switchTab(tab);
89 });
90
91 // Wizard steps
92 $(document).on('click', '.kng-fomo-wizard-step', function() {
93 const step = parseInt($(this).data('step'));
94 if (step <= self.wizardData.step || self.canGoToStep(step)) {
95 self.goToStep(step);
96 }
97 });
98
99 // Wizard navigation buttons
100 $(document).on('click', '.kng-fomo-wizard-prev', function() {
101 self.prevStep();
102 });
103
104 $(document).on('click', '.kng-fomo-wizard-next', function() {
105 self.nextStep();
106 });
107
108 // Notification name (separate from content title)
109 $(document).on('change input', '#kng-fomo-notif-name', function() {
110 self.wizardData.name = $(this).val();
111 });
112
113 // Notification type selection — type also determines the source
114 $(document).on('change', 'input[name="notification_type"]', function() {
115 var type = $(this).val();
116 var isNewType = (self.wizardData.type !== type);
117 self.wizardData.type = type;
118 self.wizardData.source = type; // type IS the source
119 self.updateSourceOptions();
120 self.updateSourceConfig();
121
122 // Auto-populate content defaults and source_config for the selected type
123 if (isNewType) {
124 self.applyTypeDefaults(type);
125 }
126 });
127
128 // Design template selection
129 $(document).on('click', '.kng-fomo-template-card', function() {
130 $('.kng-fomo-template-card').removeClass('is-selected');
131 $(this).addClass('is-selected');
132 self.wizardData.design.template = $(this).data('template');
133 self.updatePreview();
134 });
135
136 // Position selection - buttons
137 $(document).on('click', '.kng-fomo-position-btn', function() {
138 const position = $(this).data('position');
139 $('.kng-fomo-position-btn').removeClass('is-active');
140 $(this).addClass('is-active');
141 $('.kng-fomo-position-dot').removeClass('is-active');
142 $('.kng-fomo-position-dot[data-pos="' + position + '"]').addClass('is-active');
143 self.wizardData.design.position = position;
144 self.updatePreview();
145 });
146
147 // Position selection - dots (visual preview)
148 $(document).on('click', '.kng-fomo-position-dot', function() {
149 const position = $(this).data('pos');
150 $('.kng-fomo-position-dot').removeClass('is-active');
151 $(this).addClass('is-active');
152 $('.kng-fomo-position-btn').removeClass('is-active');
153 $('.kng-fomo-position-btn[data-position="' + position + '"]').addClass('is-active');
154 self.wizardData.design.position = position;
155 self.updatePreview();
156 });
157
158 // Form inputs
159 $(document).on('change input', '.kng-fomo-wizard [data-field]', function() {
160 const field = $(this).data('field');
161 const section = $(this).data('section');
162 const value = $(this).is(':checkbox') ? $(this).is(':checked') : $(this).val();
163
164 if (section && self.wizardData[section]) {
165 self.wizardData[section][field] = value;
166 } else {
167 self.wizardData[field] = value;
168 }
169
170 self.updatePreview();
171 });
172
173 // Color pickers
174 $(document).on('input', '.kng-fomo-color-picker', function() {
175 const field = $(this).data('field');
176 const value = $(this).val();
177 $(this).siblings('.kng-fomo-color-input').val(value);
178 self.wizardData.design[field] = value;
179 self.updatePreview();
180 });
181
182 $(document).on('input', '.kng-fomo-color-input', function() {
183 const field = $(this).data('field');
184 const value = $(this).val();
185 $(this).siblings('.kng-fomo-color-picker').val(value);
186 self.wizardData.design[field] = value;
187 self.updatePreview();
188 });
189
190 // Save notification
191 $(document).on('click', '.kng-fomo-save-notification', function() {
192 self.saveNotification();
193 });
194
195 // Toggle notification status
196 $(document).on('change', '.kng-fomo-toggle input', function() {
197 const notificationId = $(this).closest('tr').data('id') || $(this).data('id');
198 const status = $(this).is(':checked') ? 'enabled' : 'disabled';
199 self.toggleNotification(notificationId, status);
200 });
201
202 // Delete notification
203 $(document).on('click', '.kng-fomo-delete', function(e) {
204 e.preventDefault();
205 const notificationId = $(this).closest('tr').data('id') || $(this).data('id');
206 self.deleteNotification(notificationId);
207 });
208
209 // Duplicate notification
210 $(document).on('click', '.kng-fomo-duplicate', function(e) {
211 e.preventDefault();
212 const notificationId = $(this).closest('tr').data('id') || $(this).data('id');
213 self.duplicateNotification(notificationId);
214 });
215
216 // Edit notification
217 $(document).on('click', '.kng-fomo-edit', function(e) {
218 e.preventDefault();
219 const notificationId = $(this).closest('tr').data('id') || $(this).data('id');
220 self.loadNotification(notificationId);
221 });
222
223 // Import/Export
224 $(document).on('click', '.kng-fomo-export', function() {
225 self.exportNotifications();
226 });
227
228 $(document).on('click', '.kng-fomo-import', function() {
229 self.importNotifications();
230 });
231
232 // Settings save
233 $(document).on('click', '.kng-fomo-save-settings', function() {
234 self.saveSettings();
235 });
236
237 // Modal
238 $(document).on('click', '.kng-fomo-modal-close, .kng-fomo-modal-overlay', function(e) {
239 if (e.target === this) {
240 self.closeModal();
241 }
242 });
243
244 // Analytics date range
245 $(document).on('change', '.kng-fomo-date-range', function() {
246 self.loadAnalytics($(this).val());
247 });
248
249 // Tab switching
250 $(document).on('click', '.kng-fomo-tab', function() {
251 const tabId = $(this).data('tab');
252 self.switchContentTab(tabId);
253 });
254
255 // Device checkboxes
256 $(document).on('change', '.kng-fomo-device-check', function() {
257 const device = $(this).val();
258 const checked = $(this).is(':checked');
259
260 if (checked) {
261 if (!self.wizardData.display.devices.includes(device)) {
262 self.wizardData.display.devices.push(device);
263 }
264 } else {
265 self.wizardData.display.devices = self.wizardData.display.devices.filter(d => d !== device);
266 }
267 });
268
269 // Page rules
270 $(document).on('click', '.kng-fomo-add-rule', function() {
271 self.addPageRule();
272 });
273
274 $(document).on('click', '.kng-fomo-remove-rule', function() {
275 $(this).closest('.kng-fomo-page-rule').remove();
276 self.collectPageRules();
277 });
278
279 $(document).on('change', '.kng-fomo-page-rule select, .kng-fomo-page-rule input', function() {
280 self.collectPageRules();
281 });
282 },
283
284 /**
285 * Initialize wizard
286 */
287 initWizard: function() {
288 if (!$('.kng-fomo-wizard').length) {
289 return;
290 }
291
292 // Check if editing existing notification
293 const editId = this.getUrlParam('edit');
294 if (editId) {
295 this.loadNotification(editId);
296 } else {
297 this.goToStep(1);
298 }
299 },
300
301 /**
302 * Go to step
303 */
304 goToStep: function(step) {
305 this.wizardData.step = step;
306
307 // Update steps UI
308 $('.kng-fomo-wizard-step').each(function() {
309 const stepNum = parseInt($(this).data('step'));
310 $(this).removeClass('is-active is-completed');
311
312 if (stepNum === step) {
313 $(this).addClass('is-active');
314 } else if (stepNum < step) {
315 $(this).addClass('is-completed');
316 }
317 });
318
319 // Show/hide content panels
320 $('.kng-fomo-wizard-panel').removeClass('is-active');
321 $('.kng-fomo-wizard-panel[data-step="' + step + '"]').addClass('is-active');
322
323 // Update buttons
324 if (step === 1) {
325 $('.kng-fomo-wizard-prev').hide();
326 } else {
327 $('.kng-fomo-wizard-prev').show();
328 }
329
330 if (step === 5) {
331 $('.kng-fomo-wizard-next').hide();
332 $('.kng-fomo-save-notification').show();
333 } else {
334 $('.kng-fomo-wizard-next').show();
335 $('.kng-fomo-save-notification').hide();
336 }
337
338 // Update preview
339 this.updatePreview();
340
341 // Scroll to top
342 const wizardEl = $('.kng-fomo-wizard').get(0);
343 if (wizardEl) {
344 wizardEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
345 }
346 },
347
348 /**
349 * Previous step
350 */
351 prevStep: function() {
352 if (this.wizardData.step > 1) {
353 this.goToStep(this.wizardData.step - 1);
354 }
355 },
356
357 /**
358 * Next step
359 */
360 nextStep: function() {
361 if (this.validateStep(this.wizardData.step)) {
362 if (this.wizardData.step < 5) {
363 this.goToStep(this.wizardData.step + 1);
364 }
365 }
366 },
367
368 /**
369 * Validate step
370 */
371 validateStep: function(step) {
372 let isValid = true;
373 let message = '';
374
375 switch (step) {
376 case 1:
377 if (!this.wizardData.type) {
378 message = kngFomoAdmin.i18n.select_type || 'Please select a notification type.';
379 isValid = false;
380 }
381 break;
382 case 2:
383 // Template has default value, so this is always valid
384 break;
385 case 3:
386 // Content is optional for some notification types
387 break;
388 }
389
390 if (!isValid && message) {
391 this.showToast(message, 'error');
392 }
393
394 return isValid;
395 },
396
397 /**
398 * Can go to step
399 */
400 canGoToStep: function(step) {
401 for (let i = 1; i < step; i++) {
402 if (!this.validateStep(i)) {
403 return false;
404 }
405 }
406 return true;
407 },
408
409 /**
410 * Update source options based on type
411 */
412 updateSourceOptions: function() {
413 const type = this.wizardData.type;
414 const $sources = $('.kng-fomo-source-options');
415
416 if (!$sources.length) {
417 return;
418 }
419
420 // Show relevant sources
421 $sources.find('.kng-fomo-radio-card').hide();
422 $sources.find('.kng-fomo-radio-card[data-type="' + type + '"], .kng-fomo-radio-card[data-type="all"]').show();
423 },
424
425 /**
426 * Update source configuration
427 */
428 updateSourceConfig: function() {
429 const source = this.wizardData.source;
430 const $config = $('.kng-fomo-source-config');
431
432 if (!$config.length) {
433 return;
434 }
435
436 // Hide all config panels
437 $config.find('.kng-fomo-source-config-panel').hide();
438
439 // Show relevant config
440 $config.find('.kng-fomo-source-config-panel[data-source="' + source + '"]').fadeIn(200);
441 },
442
443 /**
444 * Update live preview
445 */
446 updatePreview: function() {
447 const $preview = $('.kng-fomo-preview-notification');
448
449 if (!$preview.length) {
450 return;
451 }
452
453 const data = this.wizardData;
454
455 // Update position class
456 $preview.removeClass('pos-top-left pos-top-right pos-bottom-left pos-bottom-right pos-top-center pos-bottom-center');
457 $preview.addClass('pos-' + data.design.position);
458
459 // Update styles
460 $preview.css({
461 '--bg-color': data.design.bg_color,
462 '--text-color': data.design.text_color,
463 '--accent-color': data.design.accent_color,
464 '--border-radius': data.design.border_radius + 'px'
465 });
466
467 // Update content
468 if (data.content.title) {
469 $preview.find('.kng-fomo-preview-title').text(data.content.title);
470 }
471 if (data.content.message) {
472 $preview.find('.kng-fomo-preview-message').text(data.content.message);
473 }
474
475 // Toggle shadow
476 if (data.design.shadow) {
477 $preview.addClass('has-shadow');
478 } else {
479 $preview.removeClass('has-shadow');
480 }
481
482 // Toggle close button
483 if (data.customize.close_button) {
484 $preview.find('.kng-fomo-preview-close').show();
485 } else {
486 $preview.find('.kng-fomo-preview-close').hide();
487 }
488 },
489
490 /**
491 * Save notification
492 */
493 saveNotification: function() {
494 const self = this;
495 const $btn = $('.kng-fomo-save-notification');
496 const normalizedDisplay = this.buildDisplayPayload(this.wizardData.display || {});
497
498 $btn.prop('disabled', true).addClass('is-loading');
499
500 $.ajax({
501 url: kngFomoAdmin.ajaxUrl,
502 type: 'POST',
503 data: {
504 action: 'kng_fomo_save_notification',
505 nonce: kngFomoAdmin.nonce,
506 id: this.wizardData.notification_id,
507 name: this.wizardData.name || this.wizardData.content.title || 'Untitled Notification',
508 title: this.wizardData.content.title || '',
509 status: 'disabled',
510 type: this.wizardData.type,
511 source: this.wizardData.source,
512 source_config: JSON.stringify(this.wizardData.source_config),
513 design: JSON.stringify(this.wizardData.design),
514 content: JSON.stringify(this.wizardData.content),
515 display: JSON.stringify(normalizedDisplay),
516 customize: JSON.stringify(this.wizardData.customize)
517 },
518 success: function(response) {
519 if (response.success) {
520 self.showToast(kngFomoAdmin.i18n.saved, 'success');
521
522 // Redirect to list
523 setTimeout(function() {
524 window.location.href = kngFomoAdmin.listUrl;
525 }, 1000);
526 } else {
527 self.showToast(response.data.message || kngFomoAdmin.i18n.error, 'error');
528 }
529 },
530 error: function() {
531 self.showToast(kngFomoAdmin.i18n.error, 'error');
532 },
533 complete: function() {
534 $btn.prop('disabled', false).removeClass('is-loading');
535 }
536 });
537 },
538
539 /**
540 * Load notification for editing
541 */
542 loadNotification: function(id) {
543 const self = this;
544
545 $.ajax({
546 url: kngFomoAdmin.ajaxUrl,
547 type: 'POST',
548 data: {
549 action: 'kng_fomo_get_notification',
550 nonce: kngFomoAdmin.nonce,
551 notification_id: id
552 },
553 success: function(response) {
554 if (response.success) {
555 if (response.data && response.data.display) {
556 response.data.display = self.normalizeDisplayFromServer(response.data.display);
557 }
558
559 self.wizardData = $.extend(true, self.wizardData, response.data);
560 self.populateWizardFields();
561 self.goToStep(1);
562 } else {
563 self.showToast(response.data.message || kngFomoAdmin.i18n.error, 'error');
564 }
565 },
566 error: function() {
567 self.showToast(kngFomoAdmin.i18n.error, 'error');
568 }
569 });
570 },
571
572 /**
573 * Populate wizard fields from loaded data
574 */
575 populateWizardFields: function() {
576 const data = this.wizardData;
577 data.display = this.normalizeDisplayFromServer(data.display || {});
578
579 // Notification name (separate from content title)
580 if (data.name) {
581 $('#kng-fomo-notif-name').val(data.name);
582 }
583
584 // Type
585 if (data.type) {
586 $('input[name="notification_type"][value="' + data.type + '"]').prop('checked', true);
587 // Source = type (wizard has no separate source selector)
588 if (!data.source || data.source === 'manual') {
589 data.source = data.type;
590 }
591 this.updateSourceOptions();
592 this.updateSourceConfig();
593 }
594
595 // Source config (populate form fields FROM saved data)
596 if (data.source_config && typeof data.source_config === 'object') {
597 var sc = data.source_config;
598 Object.keys(sc).forEach(function(key) {
599 var $el = $('[data-field="' + key + '"][data-section="source_config"]');
600 if ($el.length) {
601 if ($el.is('select[multiple]') && Array.isArray(sc[key])) {
602 $el.val(sc[key]);
603 } else {
604 $el.val(sc[key]);
605 }
606 }
607 });
608 }
609
610 // Design - Template
611 if (data.design.template) {
612 $('.kng-fomo-template-card').removeClass('is-selected');
613 $('.kng-fomo-template-card[data-template="' + data.design.template + '"]').addClass('is-selected');
614 }
615
616 // Design - Position
617 if (data.design.position) {
618 $('.kng-fomo-position-btn').removeClass('is-active');
619 $('.kng-fomo-position-btn[data-position="' + data.design.position + '"]').addClass('is-active');
620 $('.kng-fomo-position-dot').removeClass('is-active');
621 $('.kng-fomo-position-dot[data-pos="' + data.design.position + '"]').addClass('is-active');
622 }
623
624 // Colors
625 $('[data-field="bg_color"]').val(data.design.bg_color);
626 $('[data-field="text_color"]').val(data.design.text_color);
627 $('[data-field="accent_color"]').val(data.design.accent_color);
628 $('[data-field="border_radius"]').val(data.design.border_radius);
629 $('[data-field="shadow"]').prop('checked', data.design.shadow);
630
631 // Content (title template, message template)
632 $('[data-field="title"][data-section="content"]').val(data.content.title);
633 $('[data-field="message"][data-section="content"]').val(data.content.message);
634 $('[data-field="cta_text"]').val(data.content.cta_text);
635 $('[data-field="cta_url"]').val(data.content.cta_url);
636
637 // Show auto-hint if content has {{}} templates
638 if (data.content.title && data.content.title.indexOf('{{') !== -1) {
639 $('.kng-fomo-content-auto-hint').show();
640 }
641
642 // Display
643 $('[data-field="delay"]').val(data.display.delay);
644 $('[data-field="duration"]').val(data.display.duration);
645 $('[data-field="interval"]').val(data.display.interval);
646 $('[data-field="max_per_session"]').val(data.display.max_per_session);
647 $('[data-field="pages"]').val(data.display.pages || 'all');
648 $('[data-field="audience"]').val(data.display.audience || 'all');
649 $('[data-field="loop"]').prop('checked', data.display.loop !== false);
650 $('[data-field="random"]').prop('checked', !!data.display.random);
651
652 // Devices
653 $('.kng-fomo-device-check').prop('checked', false);
654 (data.display.devices || []).forEach(function(device) {
655 $('.kng-fomo-device-check[value="' + device + '"]').prop('checked', true);
656 });
657
658 // Page rules
659 this.renderPageRules(data.display.page_rules || []);
660
661 // Customize
662 $('[data-field="z_index"]').val(data.customize.z_index);
663 $('[data-field="close_button"]').prop('checked', data.customize.close_button);
664 $('[data-field="sound"]').prop('checked', data.customize.sound);
665 $('[data-field="analytics"]').prop('checked', data.customize.analytics);
666
667 this.updatePreview();
668 },
669
670 /**
671 * Apply type-specific defaults when user selects a notification type.
672 * Auto-fills content templates, source_config defaults, and shows hint.
673 */
674 applyTypeDefaults: function(type) {
675 var defaults = (window.kngFomoAdmin && kngFomoAdmin.typeDefaults) ? kngFomoAdmin.typeDefaults[type] : null;
676 if (!defaults) return;
677
678 // Auto-fill content templates if currently empty or non-template
679 var cd = defaults.content_defaults || {};
680 var currentTitle = this.wizardData.content.title || '';
681 var currentMessage = this.wizardData.content.message || '';
682
683 if (!currentTitle || currentTitle.indexOf('{{') === -1) {
684 this.wizardData.content.title = cd.title || '';
685 $('[data-field="title"][data-section="content"]').val(this.wizardData.content.title);
686 }
687 if (!currentMessage || currentMessage.indexOf('{{') === -1) {
688 this.wizardData.content.message = cd.message || '';
689 $('[data-field="message"][data-section="content"]').val(this.wizardData.content.message);
690 }
691
692 // Show auto-hint if we set templates
693 if (this.wizardData.content.title && this.wizardData.content.title.indexOf('{{') !== -1) {
694 $('.kng-fomo-content-auto-hint').show();
695 } else {
696 $('.kng-fomo-content-auto-hint').hide();
697 }
698
699 // Auto-populate source_config with defaults from server
700 var scd = defaults.source_config_defaults || {};
701 Object.keys(scd).forEach(function(key) {
702 if (this.wizardData.source_config[key] === undefined) {
703 this.wizardData.source_config[key] = scd[key];
704 }
705 // Also set in the form
706 var $el = $('[data-field="' + key + '"][data-section="source_config"]');
707 if ($el.length && !$el.val()) {
708 $el.val(scd[key]);
709 }
710 }.bind(this));
711
712 // Also set image type default for certain types
713 if (type === 'wordpress_comments' || type === 'reviews' || type === 'email_subscription') {
714 this.wizardData.content.image_type = 'avatar';
715 $('[data-field="image_type"]').val('avatar');
716 } else if (type === 'woocommerce_sales') {
717 this.wizardData.content.image_type = 'product';
718 $('[data-field="image_type"]').val('product');
719 }
720
721 this.updatePreview();
722 },
723
724 /**
725 * Normalize server display payload to wizard shape
726 */
727 normalizeDisplayFromServer: function(display) {
728 const out = $.extend(true, {
729 delay: 3,
730 duration: 5,
731 interval: 10,
732 max_per_session: 5,
733 devices: ['desktop', 'tablet', 'mobile'],
734 pages: 'all',
735 page_rules: [],
736 audience: 'all',
737 exclude_logged_in: false,
738 loop: true,
739 random: false
740 }, display || {});
741
742 if (!Array.isArray(out.devices) || !out.devices.length) {
743 out.devices = ['desktop', 'tablet', 'mobile'];
744 }
745
746 // Audience legacy -> wizard
747 if (!out.audience) {
748 const displayFor = out.display_for || 'everyone';
749 if (displayFor === 'logged_in') out.audience = 'logged_in';
750 else if (displayFor === 'logged_out' || displayFor === 'guests') out.audience = 'logged_out';
751 else out.audience = 'all';
752 }
753
754 // Pages/rules legacy -> wizard
755 if (!Array.isArray(out.page_rules)) {
756 out.page_rules = [];
757 }
758
759 if (!out.pages) {
760 out.pages = 'all';
761 }
762
763 if (!out.page_rules.length && (out.show_on === 'include' || out.show_on === 'exclude')) {
764 const ids = out.show_on === 'include' ? (out.include_pages || []) : (out.exclude_pages || []);
765 if (Array.isArray(ids) && ids.length) {
766 out.pages = 'specific';
767 out.page_rules = ids.map(function(id) {
768 return {
769 type: out.show_on,
770 condition: 'page',
771 value: String(id)
772 };
773 });
774 }
775 }
776
777 if (out.page_rules.length) {
778 out.pages = 'specific';
779 }
780
781 return out;
782 },
783
784 /**
785 * Build display payload with legacy-compatible keys
786 */
787 buildDisplayPayload: function(display) {
788 const out = $.extend(true, {}, display || {});
789
790 // Audience wizard -> legacy
791 if (out.audience === 'logged_in') out.display_for = 'logged_in';
792 else if (out.audience === 'logged_out') out.display_for = 'logged_out';
793 else out.display_for = 'everyone';
794
795 // Page rules wizard -> legacy
796 const rules = Array.isArray(out.page_rules) ? out.page_rules : [];
797 if (out.pages === 'specific' && rules.length) {
798 const includeIds = [];
799 const excludeIds = [];
800
801 rules.forEach(function(rule) {
802 if (!rule || (rule.condition !== 'page' && rule.condition !== 'post')) return;
803 if (!/^\d+$/.test(String(rule.value || '').trim())) return;
804 const id = parseInt(rule.value, 10);
805 if (rule.type === 'exclude') excludeIds.push(id);
806 else includeIds.push(id);
807 });
808
809 if (includeIds.length) {
810 out.show_on = 'include';
811 out.include_pages = includeIds;
812 out.exclude_pages = [];
813 } else if (excludeIds.length) {
814 out.show_on = 'exclude';
815 out.exclude_pages = excludeIds;
816 out.include_pages = [];
817 } else {
818 out.show_on = 'everywhere';
819 out.include_pages = [];
820 out.exclude_pages = [];
821 }
822 } else {
823 out.show_on = 'everywhere';
824 out.include_pages = [];
825 out.exclude_pages = [];
826 }
827
828 return out;
829 },
830
831 /**
832 * Toggle notification status
833 */
834 toggleNotification: function(id, status) {
835 const self = this;
836
837 $.ajax({
838 url: kngFomoAdmin.ajaxUrl,
839 type: 'POST',
840 data: {
841 action: 'kng_fomo_toggle_status',
842 nonce: kngFomoAdmin.nonce,
843 id: id,
844 status: status
845 },
846 success: function(response) {
847 if (response.success) {
848 self.showToast(kngFomoAdmin.i18n.saved, 'success');
849 } else {
850 self.showToast(response.data.message || kngFomoAdmin.i18n.error, 'error');
851 }
852 },
853 error: function() {
854 self.showToast(kngFomoAdmin.i18n.error, 'error');
855 }
856 });
857 },
858
859 /**
860 * Delete notification
861 */
862 deleteNotification: function(id) {
863 const self = this;
864
865 if (!confirm(kngFomoAdmin.i18n.confirmDelete)) {
866 return;
867 }
868
869 $.ajax({
870 url: kngFomoAdmin.ajaxUrl,
871 type: 'POST',
872 data: {
873 action: 'kng_fomo_delete_notification',
874 nonce: kngFomoAdmin.nonce,
875 id: id
876 },
877 success: function(response) {
878 if (response.success) {
879 self.showToast(kngFomoAdmin.i18n.deleted, 'success');
880 $('tr[data-id="' + id + '"]').fadeOut(300, function() {
881 $(this).remove();
882 self.checkEmptyState();
883 });
884 } else {
885 self.showToast(response.data.message || kngFomoAdmin.i18n.error, 'error');
886 }
887 },
888 error: function() {
889 self.showToast(kngFomoAdmin.i18n.error, 'error');
890 }
891 });
892 },
893
894 /**
895 * Duplicate notification
896 */
897 duplicateNotification: function(id) {
898 const self = this;
899
900 $.ajax({
901 url: kngFomoAdmin.ajaxUrl,
902 type: 'POST',
903 data: {
904 action: 'kng_fomo_duplicate_notification',
905 nonce: kngFomoAdmin.nonce,
906 id: id
907 },
908 success: function(response) {
909 if (response.success) {
910 self.showToast(kngFomoAdmin.i18n.duplicated, 'success');
911 setTimeout(function() {
912 window.location.reload();
913 }, 500);
914 } else {
915 self.showToast(response.data.message || kngFomoAdmin.i18n.error, 'error');
916 }
917 },
918 error: function() {
919 self.showToast(kngFomoAdmin.i18n.error, 'error');
920 }
921 });
922 },
923
924 /**
925 * Check empty state
926 */
927 checkEmptyState: function() {
928 if ($('.kng-fomo-table tbody tr').length === 0) {
929 $('.kng-fomo-table-wrap').hide();
930 $('.kng-fomo-empty').show();
931 }
932 },
933
934 /**
935 * Add page rule
936 */
937 addPageRule: function() {
938 const $container = $('.kng-fomo-page-rules');
939 const template = `
940 <div class="kng-fomo-page-rule">
941 <select class="kng-fomo-input kng-fomo-input--sm kng-fomo-rule-type">
942 <option value="include">Include</option>
943 <option value="exclude">Exclude</option>
944 </select>
945 <select class="kng-fomo-input kng-fomo-input--sm kng-fomo-rule-condition">
946 <option value="page">Page</option>
947 <option value="post">Post</option>
948 <option value="url_contains">URL Contains</option>
949 <option value="url_is">URL Is</option>
950 </select>
951 <input type="text" class="kng-fomo-input kng-fomo-input--sm kng-fomo-rule-value" placeholder="Value">
952 <button type="button" class="kng-fomo-btn kng-fomo-btn--sm kng-fomo-btn--danger kng-fomo-remove-rule">
953 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
954 </button>
955 </div>
956 `;
957 $container.append(template);
958 },
959
960 renderPageRules: function(rules) {
961 const $container = $('.kng-fomo-page-rules');
962 if (!$container.length) return;
963
964 $container.empty();
965 if (!Array.isArray(rules) || !rules.length) {
966 this.collectPageRules();
967 return;
968 }
969
970 const escHtml = function(str) {
971 return String(str === undefined || str === null ? '' : str)
972 .replace(/&/g, '&amp;')
973 .replace(/</g, '&lt;')
974 .replace(/>/g, '&gt;')
975 .replace(/"/g, '&quot;');
976 };
977
978 rules.forEach(function(rule) {
979 const type = rule.type === 'exclude' ? 'exclude' : 'include';
980 const condition = ['page', 'post', 'url_contains', 'url_is'].includes(rule.condition) ? rule.condition : 'url_contains';
981 const value = escHtml(rule.value || '');
982
983 const row = `
984 <div class="kng-fomo-page-rule">
985 <select class="kng-fomo-input kng-fomo-input--sm kng-fomo-rule-type">
986 <option value="include" ${type === 'include' ? 'selected' : ''}>Include</option>
987 <option value="exclude" ${type === 'exclude' ? 'selected' : ''}>Exclude</option>
988 </select>
989 <select class="kng-fomo-input kng-fomo-input--sm kng-fomo-rule-condition">
990 <option value="page" ${condition === 'page' ? 'selected' : ''}>Page</option>
991 <option value="post" ${condition === 'post' ? 'selected' : ''}>Post</option>
992 <option value="url_contains" ${condition === 'url_contains' ? 'selected' : ''}>URL Contains</option>
993 <option value="url_is" ${condition === 'url_is' ? 'selected' : ''}>URL Is</option>
994 </select>
995 <input type="text" class="kng-fomo-input kng-fomo-input--sm kng-fomo-rule-value" placeholder="Value" value="${value}">
996 <button type="button" class="kng-fomo-btn kng-fomo-btn--sm kng-fomo-btn--danger kng-fomo-remove-rule">
997 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
998 </button>
999 </div>
1000 `;
1001
1002 $container.append(row);
1003 });
1004
1005 this.collectPageRules();
1006 },
1007
1008 /**
1009 * Collect page rules
1010 */
1011 collectPageRules: function() {
1012 const rules = [];
1013
1014 $('.kng-fomo-page-rule').each(function() {
1015 const rule = {
1016 type: $(this).find('.kng-fomo-rule-type').val(),
1017 condition: $(this).find('.kng-fomo-rule-condition').val(),
1018 value: $(this).find('.kng-fomo-rule-value').val()
1019 };
1020
1021 if (rule.value) {
1022 rules.push(rule);
1023 }
1024 });
1025
1026 this.wizardData.display.page_rules = rules;
1027 },
1028
1029 /**
1030 * Initialize charts
1031 */
1032 initCharts: function() {
1033 if (!$('#kng-fomo-chart').length) {
1034 return;
1035 }
1036
1037 this.loadAnalytics('7days');
1038 },
1039
1040 /**
1041 * Load analytics data
1042 */
1043 loadAnalytics: function(range) {
1044 const self = this;
1045
1046 $.ajax({
1047 url: kngFomoAdmin.ajaxUrl,
1048 type: 'POST',
1049 data: {
1050 action: 'kng_fomo_get_analytics',
1051 nonce: kngFomoAdmin.nonce,
1052 period: range
1053 },
1054 success: function(response) {
1055 if (response.success) {
1056 self.renderChart(response.data);
1057 self.updateKPIs(response.data);
1058 }
1059 }
1060 });
1061 },
1062
1063 /**
1064 * Render chart
1065 */
1066 renderChart: function(data) {
1067 const ctx = document.getElementById('kng-fomo-chart');
1068
1069 if (!ctx) {
1070 return;
1071 }
1072
1073 // Destroy existing chart
1074 if (this.chart) {
1075 this.chart.destroy();
1076 }
1077
1078 this.chart = new Chart(ctx, {
1079 type: 'line',
1080 data: {
1081 labels: data.chart.labels,
1082 datasets: [
1083 {
1084 label: 'Views',
1085 data: data.chart.views,
1086 borderColor: '#0071e3',
1087 backgroundColor: 'rgba(0, 113, 227, 0.1)',
1088 tension: 0.4,
1089 fill: true
1090 },
1091 {
1092 label: 'Clicks',
1093 data: data.chart.clicks,
1094 borderColor: '#34c759',
1095 backgroundColor: 'rgba(52, 199, 89, 0.1)',
1096 tension: 0.4,
1097 fill: true
1098 }
1099 ]
1100 },
1101 options: {
1102 responsive: true,
1103 maintainAspectRatio: false,
1104 plugins: {
1105 legend: {
1106 display: true,
1107 position: 'top',
1108 labels: {
1109 usePointStyle: true,
1110 padding: 20,
1111 font: {
1112 family: "-apple-system, BlinkMacSystemFont, 'SF Pro Display', sans-serif",
1113 size: 13
1114 }
1115 }
1116 }
1117 },
1118 scales: {
1119 x: {
1120 grid: {
1121 display: false
1122 },
1123 ticks: {
1124 font: {
1125 family: "-apple-system, BlinkMacSystemFont, 'SF Pro Display', sans-serif",
1126 size: 12
1127 }
1128 }
1129 },
1130 y: {
1131 beginAtZero: true,
1132 grid: {
1133 color: 'rgba(0, 0, 0, 0.04)'
1134 },
1135 ticks: {
1136 font: {
1137 family: "-apple-system, BlinkMacSystemFont, 'SF Pro Display', sans-serif",
1138 size: 12
1139 }
1140 }
1141 }
1142 },
1143 interaction: {
1144 intersect: false,
1145 mode: 'index'
1146 }
1147 }
1148 });
1149 },
1150
1151 /**
1152 * Update KPI cards
1153 */
1154 updateKPIs: function(data) {
1155 $('.kng-fomo-kpi-value[data-kpi="views"]').text(this.formatNumber(data.totals.views));
1156 $('.kng-fomo-kpi-value[data-kpi="clicks"]').text(this.formatNumber(data.totals.clicks));
1157 $('.kng-fomo-kpi-value[data-kpi="ctr"]').text(data.totals.ctr + '%');
1158 },
1159
1160 /**
1161 * Format number
1162 */
1163 formatNumber: function(num) {
1164 if (num >= 1000000) {
1165 return (num / 1000000).toFixed(1) + 'M';
1166 }
1167 if (num >= 1000) {
1168 return (num / 1000).toFixed(1) + 'K';
1169 }
1170 return num.toLocaleString();
1171 },
1172
1173 /**
1174 * Initialize toggles
1175 */
1176 initToggles: function() {
1177 // Module toggles
1178 $(document).on('change', '.kng-fomo-module-toggle', function() {
1179 const module = $(this).data('module');
1180 const enabled = $(this).is(':checked');
1181
1182 // Visual feedback
1183 $(this).closest('.kng-fomo-module').toggleClass('is-enabled', enabled);
1184 });
1185 },
1186
1187 /**
1188 * Save settings
1189 */
1190 saveSettings: function() {
1191 const self = this;
1192 const $btn = $('.kng-fomo-save-settings');
1193 const settings = {};
1194
1195 // Collect all settings
1196 $('.kng-fomo-settings-form [name]').each(function() {
1197 const name = $(this).attr('name');
1198 const value = $(this).is(':checkbox') ? $(this).is(':checked') : $(this).val();
1199 settings[name] = value;
1200 });
1201
1202 // Collect modules
1203 const modules = {};
1204 $('.kng-fomo-module-toggle').each(function() {
1205 modules[$(this).data('module')] = $(this).is(':checked');
1206 });
1207 settings.modules = modules;
1208
1209 $btn.prop('disabled', true).addClass('is-loading');
1210
1211 $.ajax({
1212 url: kngFomoAdmin.ajaxUrl,
1213 type: 'POST',
1214 data: {
1215 action: 'kng_fomo_save_settings',
1216 nonce: kngFomoAdmin.nonce,
1217 settings: settings
1218 },
1219 success: function(response) {
1220 if (response.success) {
1221 self.showToast(kngFomoAdmin.i18n.settings_saved, 'success');
1222 } else {
1223 self.showToast(response.data.message || kngFomoAdmin.i18n.error, 'error');
1224 }
1225 },
1226 error: function() {
1227 self.showToast(kngFomoAdmin.i18n.error, 'error');
1228 },
1229 complete: function() {
1230 $btn.prop('disabled', false).removeClass('is-loading');
1231 }
1232 });
1233 },
1234
1235 /**
1236 * Export notifications
1237 */
1238 exportNotifications: function() {
1239 const self = this;
1240
1241 $.ajax({
1242 url: kngFomoAdmin.ajaxUrl,
1243 type: 'POST',
1244 data: {
1245 action: 'kng_fomo_export_notification',
1246 nonce: kngFomoAdmin.nonce
1247 },
1248 success: function(response) {
1249 if (response.success) {
1250 // Download JSON file
1251 const blob = new Blob([JSON.stringify(response.data, null, 2)], { type: 'application/json' });
1252 const url = URL.createObjectURL(blob);
1253 const a = document.createElement('a');
1254 a.href = url;
1255 a.download = 'fomo-notifications-export.json';
1256 a.click();
1257 URL.revokeObjectURL(url);
1258
1259 self.showToast(kngFomoAdmin.i18n.exported, 'success');
1260 } else {
1261 self.showToast(response.data.message || kngFomoAdmin.i18n.error, 'error');
1262 }
1263 },
1264 error: function() {
1265 self.showToast(kngFomoAdmin.i18n.error, 'error');
1266 }
1267 });
1268 },
1269
1270 /**
1271 * Import notifications
1272 */
1273 importNotifications: function() {
1274 const self = this;
1275 const $input = $('<input type="file" accept=".json">');
1276
1277 $input.on('change', function(e) {
1278 const file = e.target.files[0];
1279 if (!file) {
1280 return;
1281 }
1282
1283 const reader = new FileReader();
1284 reader.onload = function(e) {
1285 try {
1286 const data = JSON.parse(e.target.result);
1287 self.processImport(data);
1288 } catch (error) {
1289 self.showToast(kngFomoAdmin.i18n.invalid_file, 'error');
1290 }
1291 };
1292 reader.readAsText(file);
1293 });
1294
1295 $input.click();
1296 },
1297
1298 /**
1299 * Process import
1300 */
1301 processImport: function(data) {
1302 const self = this;
1303
1304 $.ajax({
1305 url: kngFomoAdmin.ajaxUrl,
1306 type: 'POST',
1307 data: {
1308 action: 'kng_fomo_import_notification',
1309 nonce: kngFomoAdmin.nonce,
1310 data: JSON.stringify(data)
1311 },
1312 success: function(response) {
1313 if (response.success) {
1314 self.showToast(kngFomoAdmin.i18n.imported, 'success');
1315 setTimeout(function() {
1316 window.location.reload();
1317 }, 500);
1318 } else {
1319 self.showToast(response.data.message || kngFomoAdmin.i18n.error, 'error');
1320 }
1321 },
1322 error: function() {
1323 self.showToast(kngFomoAdmin.i18n.error, 'error');
1324 }
1325 });
1326 },
1327
1328 /**
1329 * Switch tab
1330 */
1331 switchTab: function(tab) {
1332 $('.kng-fomo-nav-item').removeClass('is-active');
1333 $('.kng-fomo-nav-item[data-tab="' + tab + '"]').addClass('is-active');
1334
1335 // Update URL
1336 const url = new URL(window.location);
1337 url.searchParams.set('tab', tab);
1338 window.history.pushState({}, '', url);
1339 },
1340
1341 /**
1342 * Switch content tab
1343 */
1344 switchContentTab: function(tabId) {
1345 $('.kng-fomo-tab').removeClass('is-active');
1346 $('.kng-fomo-tab[data-tab="' + tabId + '"]').addClass('is-active');
1347
1348 $('.kng-fomo-tab-content').removeClass('is-active');
1349 $('#' + tabId).addClass('is-active');
1350 },
1351
1352 /**
1353 * Open modal
1354 */
1355 openModal: function(content) {
1356 const $overlay = $('.kng-fomo-modal-overlay');
1357
1358 if (!$overlay.length) {
1359 $('body').append('<div class="kng-fomo-modal-overlay"><div class="kng-fomo-modal"></div></div>');
1360 }
1361
1362 $('.kng-fomo-modal').html(content);
1363 $('.kng-fomo-modal-overlay').addClass('is-visible');
1364 $('body').css('overflow', 'hidden');
1365 },
1366
1367 /**
1368 * Close modal
1369 */
1370 closeModal: function() {
1371 $('.kng-fomo-modal-overlay').removeClass('is-visible');
1372 $('body').css('overflow', '');
1373 },
1374
1375 /**
1376 * Show toast notification
1377 */
1378 showToast: function(message, type) {
1379 type = type || 'success';
1380
1381 // Remove existing toasts
1382 $('.kng-fomo-toast').remove();
1383
1384 const icon = type === 'success'
1385 ? '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>'
1386 : '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>';
1387
1388 const $toast = $(`
1389 <div class="kng-fomo-toast kng-fomo-toast--${type}">
1390 <span class="kng-fomo-toast-icon">${icon}</span>
1391 <span class="kng-fomo-toast-message">${message}</span>
1392 </div>
1393 `);
1394
1395 $('body').append($toast);
1396
1397 // Auto remove
1398 setTimeout(function() {
1399 $toast.addClass('is-hiding');
1400 setTimeout(function() {
1401 $toast.remove();
1402 }, 200);
1403 }, 3000);
1404 },
1405
1406 /**
1407 * Get URL parameter
1408 */
1409 getUrlParam: function(param) {
1410 const urlParams = new URLSearchParams(window.location.search);
1411 return urlParams.get(param);
1412 }
1413 };
1414
1415 // Initialize on document ready
1416 $(document).ready(function() {
1417 KngFomoAdmin.init();
1418 });
1419
1420 })(jQuery);
1421