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

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