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

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

1,291 lines 43.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Custom Code Manager - Admin JavaScript
3 */
4
5 (function($) {
6 'use strict';
7
8 // Global state
9 let config = {};
10 let editor = null;
11 let currentType = 'css';
12 let rules = [];
13 let ruleIndex = 0;
14 let importData = null;
15
16 /**
17 * Initialize
18 */
19 function init() {
20 // Load initial config if available
21 if (typeof window.kngCCInitialConfig !== 'undefined') {
22 config = window.kngCCInitialConfig;
23 rules = config.rules || [];
24 currentType = config.type || 'css';
25 }
26
27 // Initialize components based on page
28 initListPage();
29 initEditorPage();
30 initSettingsPage();
31 initImportExportPage();
32 }
33
34 // =========================================================================
35 // List Page
36 // =========================================================================
37
38 function initListPage() {
39 const $list = $('#kng-cc-snippets-list');
40 if (!$list.length) return;
41
42 // Search filter
43 $('#kng-cc-search').on('input', debounce(filterSnippets, 300));
44
45 // Dropdown filters
46 $('.kng-cc-filter').on('change', filterSnippets);
47
48 // Select all checkbox
49 $('.kng-cc-select-all').on('change', function() {
50 const checked = $(this).prop('checked');
51 $('.kng-cc-row-check:visible').prop('checked', checked);
52 });
53
54 // Bulk actions
55 $('#kng-cc-bulk-apply').on('click', handleBulkAction);
56
57 // Status toggle
58 $(document).on('click', '.kng-cc-status-toggle', handleStatusToggle);
59
60 // Duplicate button
61 $(document).on('click', '.kng-cc-duplicate-btn', handleDuplicate);
62
63 // Export single button
64 $(document).on('click', '.kng-cc-export-btn', handleExportSingle);
65
66 // Delete button
67 $(document).on('click', '.kng-cc-delete-btn', handleDelete);
68
69 // Pro upgrade popup
70 $('#kng-cc-add-new-limit').on('click', showProPopup);
71 $('#kng-cc-pro-popup-close, .kng-cc-pro-popup-dismiss').on('click', hideProPopup);
72 $('#kng-cc-pro-popup').on('click', function(e) {
73 if (e.target === this) hideProPopup();
74 });
75 }
76
77 function filterSnippets() {
78 const search = $('#kng-cc-search').val().toLowerCase();
79 const typeFilter = $('[data-filter="type"]').val();
80 const statusFilter = $('[data-filter="status"]').val();
81 const locationFilter = $('[data-filter="location"]').val();
82
83 $('.kng-cc-row').each(function() {
84 const $row = $(this);
85 const name = $row.find('.kng-cc-name-link').text().toLowerCase();
86 const type = $row.data('type');
87 const status = $row.data('status');
88 const location = $row.data('location');
89
90 let visible = true;
91
92 if (search && !name.includes(search)) {
93 visible = false;
94 }
95 if (typeFilter && type !== typeFilter) {
96 visible = false;
97 }
98 if (statusFilter && status !== statusFilter) {
99 visible = false;
100 }
101 if (locationFilter && location !== locationFilter) {
102 visible = false;
103 }
104
105 $row.toggle(visible);
106 });
107 }
108
109 function handleBulkAction() {
110 const action = $('#kng-cc-bulk-action').val();
111 if (!action) return;
112
113 const ids = [];
114 $('.kng-cc-row-check:checked').each(function() {
115 ids.push($(this).val());
116 });
117
118 if (!ids.length) {
119 showNotice(kngCCAdmin.strings.selectSnippets, 'warning');
120 return;
121 }
122
123 if (action === 'delete' && !confirm(kngCCAdmin.strings.confirmBulkDelete)) {
124 return;
125 }
126
127 $.ajax({
128 url: kngCCAdmin.ajaxUrl,
129 method: 'POST',
130 data: {
131 action: 'kng_cc_bulk_action',
132 nonce: kngCCAdmin.nonce,
133 bulk_action: action,
134 ids: ids
135 },
136 success: function(response) {
137 if (response.success) {
138 showNotice(response.data.message, 'success');
139 location.reload();
140 } else {
141 showNotice(response.data.message || kngCCAdmin.strings.error, 'error');
142 }
143 },
144 error: function() {
145 showNotice(kngCCAdmin.strings.error, 'error');
146 }
147 });
148 }
149
150 function handleStatusToggle(e) {
151 e.preventDefault();
152 const $toggle = $(this);
153 const id = $toggle.data('id');
154
155 $.ajax({
156 url: kngCCAdmin.ajaxUrl,
157 method: 'POST',
158 data: {
159 action: 'kng_cc_toggle_snippet',
160 nonce: kngCCAdmin.nonce,
161 id: id
162 },
163 success: function(response) {
164 if (response.success) {
165 const $row = $toggle.closest('.kng-cc-row');
166 $row.data('status', response.data.status);
167 $toggle.prop('checked', response.data.status === 'enabled');
168 } else {
169 showNotice(response.data.message || kngCCAdmin.strings.error, 'error');
170 $toggle.prop('checked', !$toggle.prop('checked'));
171 }
172 },
173 error: function() {
174 showNotice(kngCCAdmin.strings.error, 'error');
175 $toggle.prop('checked', !$toggle.prop('checked'));
176 }
177 });
178 }
179
180 function handleDuplicate(e) {
181 e.preventDefault();
182 const id = $(this).data('id');
183
184 // Check limit client-side first
185 if (!kngCCAdmin.hasPro) {
186 const currentCount = $('.kng-cc-row').length;
187 if (currentCount >= kngCCAdmin.freeLimit) {
188 showProPopup();
189 return;
190 }
191 }
192
193 $.ajax({
194 url: kngCCAdmin.ajaxUrl,
195 method: 'POST',
196 data: {
197 action: 'kng_cc_duplicate_snippet',
198 nonce: kngCCAdmin.nonce,
199 id: id
200 },
201 success: function(response) {
202 if (response.success) {
203 showNotice(kngCCAdmin.strings.duplicated, 'success');
204 location.reload();
205 } else {
206 // If server returns limit error, show popup instead of error
207 if (!kngCCAdmin.hasPro && response.data && (response.data.limit || (response.data.message && response.data.message.indexOf('limit') !== -1))) {
208 showProPopup();
209 } else {
210 showNotice(response.data.message || kngCCAdmin.strings.error, 'error');
211 }
212 }
213 },
214 error: function() {
215 showNotice(kngCCAdmin.strings.error, 'error');
216 }
217 });
218 }
219
220 function handleExportSingle(e) {
221 e.preventDefault();
222 const id = $(this).data('id');
223
224 $.ajax({
225 url: kngCCAdmin.ajaxUrl,
226 method: 'POST',
227 data: {
228 action: 'kng_cc_export_snippet',
229 nonce: kngCCAdmin.nonce,
230 id: id
231 },
232 success: function(response) {
233 if (response.success && response.data.export.snippets && response.data.export.snippets.length) {
234 var snippet = response.data.export.snippets[0];
235 var code = snippet.code || '';
236 var type = snippet.type || 'txt';
237 var title = (snippet.title || 'snippet').replace(/[^a-zA-Z0-9_-]/g, '_');
238
239 var extMap = { css: '.css', js: '.js', html: '.html' };
240 var mimeMap = { css: 'text/css', js: 'application/javascript', html: 'text/html' };
241 var ext = extMap[type] || '.txt';
242 var mime = mimeMap[type] || 'text/plain';
243
244 var blob = new Blob([code], { type: mime });
245 var url = URL.createObjectURL(blob);
246 var a = document.createElement('a');
247 a.href = url;
248 a.download = title + ext;
249 document.body.appendChild(a);
250 a.click();
251 document.body.removeChild(a);
252 URL.revokeObjectURL(url);
253
254 showNotice(kngCCAdmin.strings.exportReady, 'success');
255 } else {
256 showNotice(response.data.message || kngCCAdmin.strings.error, 'error');
257 }
258 },
259 error: function() {
260 showNotice(kngCCAdmin.strings.error, 'error');
261 }
262 });
263 }
264
265 function handleDelete(e) {
266 e.preventDefault();
267
268 if (!confirm(kngCCAdmin.strings.confirmDelete)) {
269 return;
270 }
271
272 const id = $(this).data('id');
273
274 $.ajax({
275 url: kngCCAdmin.ajaxUrl,
276 method: 'POST',
277 data: {
278 action: 'kng_cc_delete_snippet',
279 nonce: kngCCAdmin.nonce,
280 id: id
281 },
282 success: function(response) {
283 if (response.success) {
284 $('[data-id="' + id + '"]').closest('.kng-cc-row').fadeOut(300, function() {
285 $(this).remove();
286 });
287 } else {
288 showNotice(response.data.message || kngCCAdmin.strings.error, 'error');
289 }
290 },
291 error: function() {
292 showNotice(kngCCAdmin.strings.error, 'error');
293 }
294 });
295 }
296
297 // =========================================================================
298 // Editor Page
299 // =========================================================================
300
301 function initEditorPage() {
302 const $form = $('#kng-cc-editor-form');
303 if (!$form.length) return;
304
305 // Initialize CodeMirror
306 initCodeMirror();
307
308 // Type tabs
309 $('.kng-cc-type-tab').on('click', handleTypeChange);
310
311 // Status toggle label
312 $('#kng-cc-status').on('change', updateStatusLabel);
313
314 // Location change
315 $('#kng-cc-location').on('change', handleLocationChange);
316
317 // Scope mode change
318 $('input[name="scope_mode"]').on('change', handleScopeModeChange);
319
320 // Rules builder
321 initRulesBuilder();
322
323 // Form submit
324 $form.on('submit', handleFormSubmit);
325
326 // Fullscreen toggle
327 $('#kng-cc-fullscreen').on('click', toggleFullscreen);
328
329 // Collapsible sections
330 $('[data-collapsible]').on('click', function() {
331 $(this).toggleClass('is-collapsed');
332 $(this).next('.kng-cc-section-content').slideToggle(200);
333 });
334
335 // Initialize state
336 updateStatusLabel();
337 handleLocationChange();
338 handleScopeModeChange();
339 renderRules();
340 }
341
342 function initCodeMirror() {
343 const $textarea = $('#kng-cc-code');
344 if (!$textarea.length) return;
345
346 const modeMap = {
347 'css': 'text/css',
348 'js': 'application/javascript',
349 'html': 'text/html'
350 };
351
352 const settings = wp.codeEditor.defaultSettings ? _.clone(wp.codeEditor.defaultSettings) : {};
353 settings.codemirror = _.extend({}, settings.codemirror, {
354 mode: modeMap[currentType] || 'text/css',
355 lineNumbers: true,
356 lineWrapping: true,
357 indentUnit: 4,
358 tabSize: 4,
359 indentWithTabs: false,
360 autoCloseBrackets: true,
361 autoCloseTags: true,
362 matchBrackets: true,
363 matchTags: {bothTags: true},
364 highlightSelectionMatches: {
365 showToken: /\w/,
366 annotateScrollbar: true
367 },
368 foldGutter: true,
369 styleActiveLine: true,
370 gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
371 extraKeys: {
372 'Ctrl-S': function() { saveSnippet(); },
373 'Cmd-S': function() { saveSnippet(); },
374 'Ctrl-Space': 'autocomplete',
375 'F11': function(cm) {
376 cm.setOption('fullScreen', !cm.getOption('fullScreen'));
377 },
378 'Esc': function(cm) {
379 if (cm.getOption('fullScreen')) cm.setOption('fullScreen', false);
380 }
381 }
382 });
383
384 // Add autocomplete for CSS properties
385 if (currentType === 'css' && window.CodeMirror && CodeMirror.hint && CodeMirror.hint.css) {
386 settings.codemirror.hintOptions = {
387 completeSingle: false
388 };
389 }
390
391 editor = wp.codeEditor.initialize($textarea, settings);
392
393 // Enhance editor instance
394 if (editor && editor.codemirror) {
395 const cm = editor.codemirror;
396
397 // Auto-format on paste
398 cm.on('paste', function() {
399 setTimeout(function() {
400 if (currentType === 'css' || currentType === 'js') {
401 // Simple auto-indent
402 const totalLines = cm.lineCount();
403 cm.operation(function() {
404 for (let i = 0; i < totalLines; i++) {
405 cm.indentLine(i);
406 }
407 });
408 }
409 }, 10);
410 });
411
412 // Show autocomplete on input for CSS
413 if (currentType === 'css') {
414 cm.on('inputRead', function(cm, change) {
415 if (change.text[0].match(/[a-z]/i)) {
416 CodeMirror.commands.autocomplete(cm, null, {completeSingle: false});
417 }
418 });
419 }
420 }
421
422 // Refresh on tab switch
423 setTimeout(function() {
424 if (editor && editor.codemirror) {
425 editor.codemirror.refresh();
426 }
427 }, 100);
428 }
429
430 function handleTypeChange(e) {
431 e.preventDefault();
432
433 const $tab = $(this);
434 if ($tab.is(':disabled') || $tab.hasClass('is-pro')) {
435 if (!kngCCAdmin.hasPro) {
436 showNotice(kngCCAdmin.strings.proRequired, 'warning');
437 }
438 return;
439 }
440
441 const type = $tab.data('type');
442 currentType = type;
443
444 // Update tabs
445 $('.kng-cc-type-tab').removeClass('is-active').attr('aria-selected', 'false');
446 $tab.addClass('is-active').attr('aria-selected', 'true');
447
448 // Update hidden input
449 $('#kng-cc-type').val(type);
450
451 // Update CodeMirror mode
452 if (editor && editor.codemirror) {
453 const modeMap = {
454 'css': 'text/css',
455 'js': 'application/javascript',
456 'html': 'text/html'
457 };
458 editor.codemirror.setOption('mode', modeMap[type] || 'text/css');
459 }
460
461 // Show/hide JS options
462 $('#kng-cc-js-options').toggle(type === 'js');
463 }
464
465 function updateStatusLabel() {
466 const enabled = $('#kng-cc-status').prop('checked');
467 $('#kng-cc-status-label').text(enabled ? 'Enabled' : 'Disabled');
468 }
469
470 function handleLocationChange() {
471 const location = $('#kng-cc-location').val();
472 $('#kng-cc-custom-hook-wrap').toggle(location === 'custom_hook');
473 }
474
475 function handleScopeModeChange() {
476 const mode = $('input[name="scope_mode"]:checked').val();
477
478 // Update radio card visual
479 $('.kng-cc-radio-card').removeClass('is-selected');
480 $('input[name="scope_mode"]:checked').closest('.kng-cc-radio-card').addClass('is-selected');
481
482 // Show/hide rules builder
483 $('#kng-cc-rules-builder').toggle(mode !== 'global');
484 }
485
486 function handleFormSubmit(e) {
487 e.preventDefault();
488 saveSnippet();
489 }
490
491 function saveSnippet() {
492 const $btn = $('#kng-cc-save-btn');
493 const $text = $btn.find('.kng-cc-save-text');
494
495 // Collect form data
496 collectRulesFromForm();
497
498 const snippet = {
499 id: parseInt($('#kng-cc-id').val()) || 0,
500 title: $('#kng-cc-title').val(),
501 code: editor && editor.codemirror ? editor.codemirror.getValue() : $('#kng-cc-code').val(),
502 type: $('#kng-cc-type').val(),
503 status: $('#kng-cc-status').prop('checked') ? 'enabled' : 'disabled',
504 location: $('#kng-cc-location').val(),
505 custom_hook: $('#kng-cc-custom-hook').val(),
506 priority: parseInt($('#kng-cc-priority').val()) || 10,
507 js_dom_ready: $('#kng-cc-js-dom-ready').prop('checked'),
508 js_defer: $('#kng-cc-js-defer').prop('checked'),
509 js_async: $('#kng-cc-js-async').prop('checked'),
510 js_module: $('#kng-cc-js-module').prop('checked'),
511 scope_mode: $('input[name="scope_mode"]:checked').val(),
512 rules: rules,
513 match_mode: $('#kng-cc-match-mode').val() || 'any',
514 notes: $('#kng-cc-notes').val()
515 };
516
517 // Validate
518 if (!snippet.title.trim()) {
519 showNotice('Please enter a snippet name', 'error');
520 $('#kng-cc-title').focus();
521 return;
522 }
523
524 // Show saving state
525 $btn.removeClass('is-saved').addClass('is-saving');
526 $text.text(kngCCAdmin.strings.saving);
527 $btn.prop('disabled', true);
528
529 $.ajax({
530 url: kngCCAdmin.ajaxUrl,
531 method: 'POST',
532 data: {
533 action: 'kng_cc_save_snippet',
534 nonce: kngCCAdmin.nonce,
535 snippet: JSON.stringify(snippet)
536 },
537 success: function(response) {
538 if (response.success) {
539 // Show success state: green + checkmark
540 $btn.removeClass('is-saving').addClass('is-saved');
541 $text.text(kngCCAdmin.strings.saved);
542
543 // Update ID if new snippet
544 if (!snippet.id && response.data.id) {
545 $('#kng-cc-id').val(response.data.id);
546
547 // Update URL without reload
548 const newUrl = window.location.href.replace('view=new', 'view=edit&id=' + response.data.id);
549 window.history.replaceState({}, '', newUrl);
550 }
551
552 // Return to normal after delay
553 setTimeout(function() {
554 $btn.removeClass('is-saved');
555 $text.text('Save Snippet');
556 $btn.prop('disabled', false);
557 }, 2000);
558 } else {
559 showNotice(response.data.message || kngCCAdmin.strings.error, 'error');
560 resetSaveButton();
561 }
562 },
563 error: function() {
564 showNotice(kngCCAdmin.strings.error, 'error');
565 resetSaveButton();
566 }
567 });
568
569 function resetSaveButton() {
570 $btn.removeClass('is-saving is-saved');
571 $text.text('Save Snippet');
572 $btn.prop('disabled', false);
573 }
574 }
575
576 function toggleFullscreen() {
577 const $section = $('.kng-cc-code-section');
578 $section.toggleClass('is-fullscreen');
579
580 if (editor && editor.codemirror) {
581 setTimeout(function() {
582 editor.codemirror.refresh();
583 }, 100);
584 }
585 }
586
587 // =========================================================================
588 // Rules Builder
589 // =========================================================================
590
591 function initRulesBuilder() {
592 // Add rule button
593 $('#kng-cc-add-rule').on('click', addRule);
594
595 // Rule type change
596 $(document).on('change', '.kng-cc-rule-type', handleRuleTypeChange);
597
598 // Remove rule
599 $(document).on('click', '.kng-cc-rule-remove', removeRule);
600
601 // Set initial rule index
602 ruleIndex = rules.length;
603 }
604
605 function renderRules() {
606 const $list = $('#kng-cc-rules-list');
607 $list.empty();
608
609 rules.forEach(function(rule, index) {
610 const html = createRuleHTML(index, rule);
611 $list.append(html);
612 renderRuleValue(index, rule);
613 });
614 }
615
616 function createRuleHTML(index, rule) {
617 const template = $('#kng-cc-rule-template').html();
618 let html = template.replace(/\{\{index\}\}/g, index);
619
620 const $rule = $(html);
621 $rule.find('.kng-cc-rule-type').val(rule.type || 'page');
622
623 return $rule;
624 }
625
626 function addRule() {
627 const index = ruleIndex++;
628 const rule = { type: 'page', value: '' };
629 rules.push(rule);
630
631 const html = createRuleHTML(index, rule);
632 $('#kng-cc-rules-list').append(html);
633 renderRuleValue(index, rule);
634 }
635
636 function removeRule() {
637 const $rule = $(this).closest('.kng-cc-rule');
638 const index = $rule.data('index');
639
640 // Find and remove from rules array
641 const ruleIndex = rules.findIndex((r, i) => {
642 return $('[data-index="' + i + '"]').is($rule);
643 });
644
645 if (ruleIndex > -1) {
646 rules.splice(ruleIndex, 1);
647 }
648
649 $rule.fadeOut(200, function() {
650 $(this).remove();
651 });
652 }
653
654 function handleRuleTypeChange() {
655 const $rule = $(this).closest('.kng-cc-rule');
656 const index = $rule.data('index');
657 const type = $(this).val();
658
659 // Update rule type
660 const ruleData = getRuleByElement($rule);
661 if (ruleData) {
662 ruleData.type = type;
663 ruleData.value = '';
664 }
665
666 renderRuleValue(index, { type: type, value: '' });
667 }
668
669 function getRuleByElement($rule) {
670 const index = $rule.data('index');
671 // Simple approach: use DOM order
672 const domIndex = $rule.index();
673 return rules[domIndex];
674 }
675
676 function renderRuleValue(index, rule) {
677 const $rule = $('[data-index="' + index + '"]');
678 const $valueContainer = $rule.find('.kng-cc-rule-value');
679 $valueContainer.empty();
680
681 let html = '';
682 const type = rule.type || 'page';
683
684 switch (type) {
685 case 'page':
686 case 'post':
687 html = '<div class="kng-cc-rule-search-wrap">' +
688 '<input type="text" class="kng-v3-input kng-cc-rule-search" placeholder="Search by title or ID..." data-type="' + type + '" />' +
689 '<input type="hidden" class="kng-cc-rule-value-input" name="rules[' + index + '][value]" value="' + (rule.value || '') + '" />' +
690 '<div class="kng-cc-rule-search-results"></div>' +
691 '</div>';
692 break;
693
694 case 'post_type':
695 html = '<select class="kng-v3-select kng-cc-rule-value-input" name="rules[' + index + '][value]">' +
696 '<option value="post">Posts</option>' +
697 '<option value="page">Pages</option>' +
698 '<option value="product">Products</option>' +
699 '</select>';
700 break;
701
702 case 'url_contains':
703 case 'url_starts':
704 case 'url_ends':
705 case 'url_regex':
706 html = '<input type="text" class="kng-v3-input kng-cc-rule-value-input" name="rules[' + index + '][value]" value="' + escapeHtml(rule.value || '') + '" placeholder="' + getPlaceholder(type) + '" />';
707 break;
708
709 case 'user_logged_in':
710 html = '<select class="kng-v3-select kng-cc-rule-value-input" name="rules[' + index + '][value]">' +
711 '<option value="yes"' + (rule.value === 'yes' ? ' selected' : '') + '>Logged In</option>' +
712 '<option value="no"' + (rule.value === 'no' ? ' selected' : '') + '>Logged Out</option>' +
713 '</select>';
714 break;
715
716 case 'user_role':
717 html = '<select class="kng-v3-select kng-cc-rule-value-input" name="rules[' + index + '][value]">' +
718 '<option value="administrator">Administrator</option>' +
719 '<option value="editor">Editor</option>' +
720 '<option value="author">Author</option>' +
721 '<option value="contributor">Contributor</option>' +
722 '<option value="subscriber">Subscriber</option>' +
723 '</select>';
724 break;
725
726 case 'device':
727 html = '<select class="kng-v3-select kng-cc-rule-value-input" name="rules[' + index + '][value]">' +
728 '<option value="desktop"' + (rule.value === 'desktop' ? ' selected' : '') + '>Desktop</option>' +
729 '<option value="tablet"' + (rule.value === 'tablet' ? ' selected' : '') + '>Tablet</option>' +
730 '<option value="mobile"' + (rule.value === 'mobile' ? ' selected' : '') + '>Mobile</option>' +
731 '</select>';
732 break;
733
734 case 'front_page':
735 case 'blog_page':
736 case 'archive':
737 case 'search':
738 case '404':
739 // No value needed
740 html = '<div class="kng-cc-rule-no-value">No additional configuration needed</div>';
741 break;
742
743 default:
744 html = '<input type="text" class="kng-v3-input kng-cc-rule-value-input" name="rules[' + index + '][value]" value="' + escapeHtml(rule.value || '') + '" />';
745 }
746
747 $valueContainer.html(html);
748
749 // Set initial value for selects
750 if (rule.value) {
751 $valueContainer.find('select.kng-cc-rule-value-input').val(rule.value);
752 }
753
754 // Initialize search if needed
755 if (type === 'page' || type === 'post') {
756 initRuleSearch($rule.find('.kng-cc-rule-search'));
757 }
758 }
759
760 function getPlaceholder(type) {
761 const placeholders = {
762 'url_contains': '/shop/',
763 'url_starts': '/products',
764 'url_ends': '/checkout/',
765 'url_regex': '/\\/product\\/[0-9]+/'
766 };
767 return placeholders[type] || '';
768 }
769
770 function initRuleSearch($input) {
771 let searchTimeout;
772
773 $input.on('input', function() {
774 const $wrap = $(this).closest('.kng-cc-rule-search-wrap');
775 const $results = $wrap.find('.kng-cc-rule-search-results');
776 const search = $(this).val();
777 const type = $(this).data('type');
778
779 clearTimeout(searchTimeout);
780
781 if (search.length < 2) {
782 $results.empty().hide();
783 return;
784 }
785
786 searchTimeout = setTimeout(function() {
787 $.ajax({
788 url: kngCCAdmin.ajaxUrl,
789 method: 'POST',
790 data: {
791 action: 'kng_cc_search_content',
792 nonce: kngCCAdmin.nonce,
793 search: search,
794 content_type: type
795 },
796 success: function(response) {
797 if (response.success && response.data.length) {
798 let html = '';
799 response.data.forEach(function(item) {
800 html += '<div class="kng-cc-search-result" data-id="' + item.id + '" data-title="' + escapeHtml(item.title) + '">' +
801 escapeHtml(item.title) + ' <span style="color:var(--kng-v3-muted);font-size:12px;">(ID: ' + item.id + ')</span>' +
802 '</div>';
803 });
804 $results.html(html).show();
805 } else {
806 $results.html('<div class="kng-cc-search-no-results">' + kngCCAdmin.strings.noResults + '</div>').show();
807 }
808 }
809 });
810 }, 300);
811 });
812
813 // Select result
814 $(document).on('click', '.kng-cc-search-result', function() {
815 const $wrap = $(this).closest('.kng-cc-rule-search-wrap');
816 const id = $(this).data('id');
817 const title = $(this).data('title');
818
819 $wrap.find('.kng-cc-rule-search').val(title);
820 $wrap.find('.kng-cc-rule-value-input').val(id);
821 $wrap.find('.kng-cc-rule-search-results').empty().hide();
822 });
823
824 // Hide results on outside click
825 $(document).on('click', function(e) {
826 if (!$(e.target).closest('.kng-cc-rule-search-wrap').length) {
827 $('.kng-cc-rule-search-results').empty().hide();
828 }
829 });
830 }
831
832 function collectRulesFromForm() {
833 rules = [];
834
835 $('.kng-cc-rule').each(function() {
836 const $rule = $(this);
837 const type = $rule.find('.kng-cc-rule-type').val();
838 const value = $rule.find('.kng-cc-rule-value-input').val() || '';
839
840 rules.push({
841 type: type,
842 value: value
843 });
844 });
845 }
846
847 // =========================================================================
848 // Settings Page
849 // =========================================================================
850
851 function initSettingsPage() {
852 const $form = $('#kng-cc-settings-form');
853 if (!$form.length) return;
854
855 $form.on('submit', function(e) {
856 e.preventDefault();
857
858 const settings = {
859 enabled: $form.find('[name="enabled"]').prop('checked'),
860 default_location_css: $form.find('[name="default_location_css"]').val(),
861 default_location_js: $form.find('[name="default_location_js"]').val(),
862 default_priority: parseInt($form.find('[name="default_priority"]').val()) || 10,
863 debug_mode: $form.find('[name="debug_mode"]').prop('checked')
864 };
865
866 $.ajax({
867 url: kngCCAdmin.ajaxUrl,
868 method: 'POST',
869 data: {
870 action: 'kng_cc_save_settings',
871 nonce: kngCCAdmin.nonce,
872 settings: JSON.stringify(settings)
873 },
874 success: function(response) {
875 if (response.success) {
876 showNotice(kngCCAdmin.strings.saved, 'success');
877 } else {
878 showNotice(response.data.message || kngCCAdmin.strings.error, 'error');
879 }
880 },
881 error: function() {
882 showNotice(kngCCAdmin.strings.error, 'error');
883 }
884 });
885 });
886 }
887
888 // =========================================================================
889 // Import/Export Page
890 // =========================================================================
891
892 function initImportExportPage() {
893 const $exportBtn = $('#kng-cc-export-btn');
894 const $importBtn = $('#kng-cc-import-btn');
895
896 if (!$exportBtn.length && !$importBtn.length) return;
897
898 // Export all
899 $exportBtn.on('click', handleExportAll);
900
901 // Import dropzone
902 const $dropzone = $('#kng-cc-import-dropzone');
903 const $fileInput = $('#kng-cc-import-file');
904
905 $dropzone.on('click', function() {
906 $fileInput.click();
907 });
908
909 $dropzone.on('dragover dragenter', function(e) {
910 e.preventDefault();
911 $(this).addClass('is-dragover');
912 });
913
914 $dropzone.on('dragleave dragend drop', function(e) {
915 e.preventDefault();
916 $(this).removeClass('is-dragover');
917 });
918
919 $dropzone.on('drop', function(e) {
920 const files = e.originalEvent.dataTransfer.files;
921 if (files.length) {
922 handleFileSelect(files[0]);
923 }
924 });
925
926 $fileInput.on('change', function() {
927 if (this.files.length) {
928 handleFileSelect(this.files[0]);
929 }
930 });
931
932 // Remove file
933 $(document).on('click', '.kng-cc-file-remove', function(e) {
934 e.stopPropagation();
935 importData = null;
936 $dropzone.find('.kng-cc-dropzone-content').show();
937 $dropzone.find('.kng-cc-dropzone-file').hide();
938 $importBtn.prop('disabled', true);
939 $fileInput.val('');
940 });
941
942 // Import button
943 $importBtn.on('click', handleImport);
944 }
945
946 function handleExportAll() {
947 $.ajax({
948 url: kngCCAdmin.ajaxUrl,
949 method: 'POST',
950 data: {
951 action: 'kng_cc_export_all',
952 nonce: kngCCAdmin.nonce
953 },
954 success: function(response) {
955 if (response.success) {
956 const filename = 'king-addons-snippets-' + new Date().toISOString().slice(0, 10) + '.json';
957 downloadJSON(response.data.export, filename);
958 showNotice(kngCCAdmin.strings.exportReady, 'success');
959 } else {
960 showNotice(response.data.message || kngCCAdmin.strings.error, 'error');
961 }
962 },
963 error: function() {
964 showNotice(kngCCAdmin.strings.error, 'error');
965 }
966 });
967 }
968
969 function handleFileSelect(file) {
970 if (!file.name.endsWith('.json')) {
971 showNotice(kngCCAdmin.strings.invalidFile, 'error');
972 return;
973 }
974
975 const reader = new FileReader();
976 reader.onload = function(e) {
977 try {
978 importData = JSON.parse(e.target.result);
979
980 if (!importData.snippets || !Array.isArray(importData.snippets)) {
981 throw new Error('Invalid format');
982 }
983
984 const $dropzone = $('#kng-cc-import-dropzone');
985 $dropzone.find('.kng-cc-dropzone-content').hide();
986 $dropzone.find('.kng-cc-dropzone-file').show();
987 $dropzone.find('.kng-cc-file-name').text(file.name + ' (' + importData.snippets.length + ' snippets)');
988
989 $('#kng-cc-import-btn').prop('disabled', false);
990 } catch (err) {
991 showNotice(kngCCAdmin.strings.invalidFile, 'error');
992 importData = null;
993 }
994 };
995 reader.readAsText(file);
996 }
997
998 function handleImport() {
999 if (!importData) return;
1000
1001 const mode = $('#kng-cc-import-mode').val();
1002 const $btn = $('#kng-cc-import-btn');
1003
1004 $btn.prop('disabled', true).text('Importing...');
1005
1006 $.ajax({
1007 url: kngCCAdmin.ajaxUrl,
1008 method: 'POST',
1009 data: {
1010 action: 'kng_cc_import',
1011 nonce: kngCCAdmin.nonce,
1012 import: JSON.stringify(importData),
1013 mode: mode
1014 },
1015 success: function(response) {
1016 if (response.success) {
1017 $('#kng-cc-imported-count').text(response.data.imported);
1018 $('#kng-cc-skipped-count').text(response.data.skipped);
1019 $('#kng-cc-errors-count').text(response.data.errors);
1020 $('#kng-cc-import-results').show();
1021
1022 showNotice(kngCCAdmin.strings.importSuccess, 'success');
1023 } else {
1024 showNotice(response.data.message || kngCCAdmin.strings.error, 'error');
1025 }
1026
1027 $btn.prop('disabled', false).html(
1028 '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">' +
1029 '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>' +
1030 '<polyline points="17 8 12 3 7 8"/>' +
1031 '<line x1="12" y1="3" x2="12" y2="15"/>' +
1032 '</svg> Import Snippets'
1033 );
1034 },
1035 error: function() {
1036 showNotice(kngCCAdmin.strings.error, 'error');
1037 $btn.prop('disabled', false);
1038 }
1039 });
1040 }
1041
1042 // =========================================================================
1043 // Utilities
1044 // =========================================================================
1045
1046 function showProPopup() {
1047 $('#kng-cc-pro-popup').fadeIn(200);
1048 }
1049
1050 function hideProPopup() {
1051 $('#kng-cc-pro-popup').fadeOut(150);
1052 }
1053
1054 function showNotice(message, type) {
1055 // Remove existing notices
1056 $('.kng-cc-notice').remove();
1057
1058 const typeClass = type === 'success' ? 'kng-cc-notice--success' :
1059 type === 'warning' ? 'kng-cc-notice--warning' :
1060 'kng-cc-notice--error';
1061
1062 const $notice = $('<div class="kng-cc-notice ' + typeClass + '">' +
1063 '<span>' + escapeHtml(message) + '</span>' +
1064 '<button type="button" class="kng-cc-notice-close">&times;</button>' +
1065 '</div>');
1066
1067 $('body').append($notice);
1068
1069 setTimeout(function() {
1070 $notice.addClass('is-visible');
1071 }, 10);
1072
1073 // Auto hide after 5 seconds
1074 setTimeout(function() {
1075 $notice.removeClass('is-visible');
1076 setTimeout(function() {
1077 $notice.remove();
1078 }, 300);
1079 }, 5000);
1080
1081 // Close on click
1082 $notice.find('.kng-cc-notice-close').on('click', function() {
1083 $notice.removeClass('is-visible');
1084 setTimeout(function() {
1085 $notice.remove();
1086 }, 300);
1087 });
1088 }
1089
1090 function downloadJSON(data, filename) {
1091 const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
1092 const url = URL.createObjectURL(blob);
1093 const a = document.createElement('a');
1094 a.href = url;
1095 a.download = filename;
1096 document.body.appendChild(a);
1097 a.click();
1098 document.body.removeChild(a);
1099 URL.revokeObjectURL(url);
1100 }
1101
1102 function escapeHtml(str) {
1103 if (!str) return '';
1104 const div = document.createElement('div');
1105 div.appendChild(document.createTextNode(str));
1106 return div.innerHTML;
1107 }
1108
1109 function debounce(func, wait) {
1110 let timeout;
1111 return function(...args) {
1112 clearTimeout(timeout);
1113 timeout = setTimeout(() => func.apply(this, args), wait);
1114 };
1115 }
1116
1117 // Initialize when DOM is ready
1118 $(document).ready(init);
1119
1120 })(jQuery);
1121
1122 // Add notice styles dynamically
1123 (function() {
1124 const style = document.createElement('style');
1125 style.textContent = `
1126 .kng-cc-notice {
1127 position: fixed;
1128 top: 50px;
1129 right: 20px;
1130 padding: 14px 20px;
1131 background: #1d1d1f;
1132 color: #ffffff;
1133 border-radius: 10px;
1134 box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
1135 z-index: 999999;
1136 display: flex;
1137 align-items: center;
1138 gap: 12px;
1139 font-size: 14px;
1140 font-family: -apple-system, BlinkMacSystemFont, sans-serif;
1141 transform: translateX(120%);
1142 transition: transform 0.3s ease;
1143 }
1144 .kng-cc-notice.is-visible {
1145 transform: translateX(0);
1146 }
1147 .kng-cc-notice--success {
1148 background: #30d158;
1149 }
1150 .kng-cc-notice--warning {
1151 background: #ff9f0a;
1152 color: #1d1d1f;
1153 }
1154 .kng-cc-notice--error {
1155 background: #ff453a;
1156 }
1157 .kng-cc-notice-close {
1158 background: transparent;
1159 border: none;
1160 color: inherit;
1161 font-size: 20px;
1162 cursor: pointer;
1163 opacity: 0.7;
1164 line-height: 1;
1165 }
1166 .kng-cc-notice-close:hover {
1167 opacity: 1;
1168 }
1169 .kng-cc-rule-search-results {
1170 position: absolute;
1171 top: 100%;
1172 left: 0;
1173 right: 0;
1174 background: #ffffff;
1175 border: 1px solid #d2d2d7;
1176 border-radius: 8px;
1177 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
1178 z-index: 100;
1179 max-height: 200px;
1180 overflow-y: auto;
1181 display: none;
1182 }
1183 .kng-cc-search-result {
1184 padding: 10px 14px;
1185 cursor: pointer;
1186 border-bottom: 1px solid #e5e5e7;
1187 }
1188 .kng-cc-search-result:last-child {
1189 border-bottom: none;
1190 }
1191 .kng-cc-search-result:hover {
1192 background: #f5f5f7;
1193 }
1194 .kng-cc-search-no-results {
1195 padding: 12px 14px;
1196 color: #86868b;
1197 text-align: center;
1198 }
1199 .kng-cc-rule-search-wrap {
1200 position: relative;
1201 }
1202 .kng-cc-rule-no-value {
1203 font-size: 12px;
1204 color: #86868b;
1205 font-style: italic;
1206 }
1207 `;
1208 document.head.appendChild(style);
1209 })();
1210
1211 // =========================================================================
1212 // Theme Switcher (ka-v3-theme-segment)
1213 // =========================================================================
1214
1215 (function initThemeSwitcher($) {
1216 'use strict';
1217
1218 var $segment = $('#ka-v3-theme-segment');
1219 if (!$segment.length) return;
1220
1221 var $buttons = $segment.find('.ka-v3-segmented-btn');
1222 var mql = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
1223 var mode = ($segment.attr('data-active') || 'dark').toString();
1224 var mqlHandler = null;
1225
1226 function saveUISetting(value) {
1227 if (!window.kngCCAdmin) return;
1228 $.post(kngCCAdmin.ajaxUrl, {
1229 action: 'king_addons_save_dashboard_ui',
1230 nonce: kngCCAdmin.themeNonce,
1231 key: 'theme_mode',
1232 value: value
1233 });
1234 }
1235
1236 function updateSegment(activeMode) {
1237 $segment.attr('data-active', activeMode);
1238 $buttons.each(function() {
1239 var theme = ($(this).data('theme') || 'dark').toString();
1240 $(this).attr('aria-pressed', theme === activeMode ? 'true' : 'false');
1241 });
1242 }
1243
1244 function applyTheme(isDark) {
1245 $('body').toggleClass('ka-v3-dark', isDark);
1246 document.documentElement.classList.toggle('ka-v3-dark', isDark);
1247 }
1248
1249 function setThemeMode(nextMode, save) {
1250 mode = (nextMode || 'dark').toString();
1251 updateSegment(mode);
1252
1253 if (mqlHandler && mql) {
1254 if (mql.removeEventListener) {
1255 mql.removeEventListener('change', mqlHandler);
1256 } else if (mql.removeListener) {
1257 mql.removeListener(mqlHandler);
1258 }
1259 mqlHandler = null;
1260 }
1261
1262 if (mode === 'auto') {
1263 applyTheme(!!(mql && mql.matches));
1264 mqlHandler = function(e) {
1265 if (mode !== 'auto') return;
1266 applyTheme(!!e.matches);
1267 };
1268 if (mql) {
1269 if (mql.addEventListener) {
1270 mql.addEventListener('change', mqlHandler);
1271 } else if (mql.addListener) {
1272 mql.addListener(mqlHandler);
1273 }
1274 }
1275 } else {
1276 applyTheme(mode === 'dark');
1277 }
1278
1279 if (save) {
1280 saveUISetting(mode);
1281 }
1282 }
1283
1284 $segment.on('click', '.ka-v3-segmented-btn', function(e) {
1285 e.preventDefault();
1286 setThemeMode(($(this).data('theme') || 'dark').toString(), true);
1287 });
1288
1289 setThemeMode(mode, false);
1290 })(jQuery);
1291