/** * Custom Code Manager - Admin JavaScript */ (function($) { 'use strict'; // Global state let config = {}; let editor = null; let currentType = 'css'; let rules = []; let ruleIndex = 0; let importData = null; /** * Initialize */ function init() { // Load initial config if available if (typeof window.kngCCInitialConfig !== 'undefined') { config = window.kngCCInitialConfig; rules = config.rules || []; currentType = config.type || 'css'; } // Initialize components based on page initListPage(); initEditorPage(); initSettingsPage(); initImportExportPage(); } // ========================================================================= // List Page // ========================================================================= function initListPage() { const $list = $('#kng-cc-snippets-list'); if (!$list.length) return; // Search filter $('#kng-cc-search').on('input', debounce(filterSnippets, 300)); // Dropdown filters $('.kng-cc-filter').on('change', filterSnippets); // Select all checkbox $('.kng-cc-select-all').on('change', function() { const checked = $(this).prop('checked'); $('.kng-cc-row-check:visible').prop('checked', checked); }); // Bulk actions $('#kng-cc-bulk-apply').on('click', handleBulkAction); // Status toggle $(document).on('click', '.kng-cc-status-toggle', handleStatusToggle); // Duplicate button $(document).on('click', '.kng-cc-duplicate-btn', handleDuplicate); // Export single button $(document).on('click', '.kng-cc-export-btn', handleExportSingle); // Delete button $(document).on('click', '.kng-cc-delete-btn', handleDelete); // Pro upgrade popup $('#kng-cc-add-new-limit').on('click', showProPopup); $('#kng-cc-pro-popup-close, .kng-cc-pro-popup-dismiss').on('click', hideProPopup); $('#kng-cc-pro-popup').on('click', function(e) { if (e.target === this) hideProPopup(); }); } function filterSnippets() { const search = $('#kng-cc-search').val().toLowerCase(); const typeFilter = $('[data-filter="type"]').val(); const statusFilter = $('[data-filter="status"]').val(); const locationFilter = $('[data-filter="location"]').val(); $('.kng-cc-row').each(function() { const $row = $(this); const name = $row.find('.kng-cc-name-link').text().toLowerCase(); const type = $row.data('type'); const status = $row.data('status'); const location = $row.data('location'); let visible = true; if (search && !name.includes(search)) { visible = false; } if (typeFilter && type !== typeFilter) { visible = false; } if (statusFilter && status !== statusFilter) { visible = false; } if (locationFilter && location !== locationFilter) { visible = false; } $row.toggle(visible); }); } function handleBulkAction() { const action = $('#kng-cc-bulk-action').val(); if (!action) return; const ids = []; $('.kng-cc-row-check:checked').each(function() { ids.push($(this).val()); }); if (!ids.length) { showNotice(kngCCAdmin.strings.selectSnippets, 'warning'); return; } if (action === 'delete' && !confirm(kngCCAdmin.strings.confirmBulkDelete)) { return; } $.ajax({ url: kngCCAdmin.ajaxUrl, method: 'POST', data: { action: 'kng_cc_bulk_action', nonce: kngCCAdmin.nonce, bulk_action: action, ids: ids }, success: function(response) { if (response.success) { showNotice(response.data.message, 'success'); location.reload(); } else { showNotice(response.data.message || kngCCAdmin.strings.error, 'error'); } }, error: function() { showNotice(kngCCAdmin.strings.error, 'error'); } }); } function handleStatusToggle(e) { e.preventDefault(); const $toggle = $(this); const id = $toggle.data('id'); $.ajax({ url: kngCCAdmin.ajaxUrl, method: 'POST', data: { action: 'kng_cc_toggle_snippet', nonce: kngCCAdmin.nonce, id: id }, success: function(response) { if (response.success) { const $row = $toggle.closest('.kng-cc-row'); $row.data('status', response.data.status); $toggle.prop('checked', response.data.status === 'enabled'); } else { showNotice(response.data.message || kngCCAdmin.strings.error, 'error'); $toggle.prop('checked', !$toggle.prop('checked')); } }, error: function() { showNotice(kngCCAdmin.strings.error, 'error'); $toggle.prop('checked', !$toggle.prop('checked')); } }); } function handleDuplicate(e) { e.preventDefault(); const id = $(this).data('id'); // Check limit client-side first if (!kngCCAdmin.hasPro) { const currentCount = $('.kng-cc-row').length; if (currentCount >= kngCCAdmin.freeLimit) { showProPopup(); return; } } $.ajax({ url: kngCCAdmin.ajaxUrl, method: 'POST', data: { action: 'kng_cc_duplicate_snippet', nonce: kngCCAdmin.nonce, id: id }, success: function(response) { if (response.success) { showNotice(kngCCAdmin.strings.duplicated, 'success'); location.reload(); } else { // If server returns limit error, show popup instead of error if (!kngCCAdmin.hasPro && response.data && (response.data.limit || (response.data.message && response.data.message.indexOf('limit') !== -1))) { showProPopup(); } else { showNotice(response.data.message || kngCCAdmin.strings.error, 'error'); } } }, error: function() { showNotice(kngCCAdmin.strings.error, 'error'); } }); } function handleExportSingle(e) { e.preventDefault(); const id = $(this).data('id'); $.ajax({ url: kngCCAdmin.ajaxUrl, method: 'POST', data: { action: 'kng_cc_export_snippet', nonce: kngCCAdmin.nonce, id: id }, success: function(response) { if (response.success && response.data.export.snippets && response.data.export.snippets.length) { var snippet = response.data.export.snippets[0]; var code = snippet.code || ''; var type = snippet.type || 'txt'; var title = (snippet.title || 'snippet').replace(/[^a-zA-Z0-9_-]/g, '_'); var extMap = { css: '.css', js: '.js', html: '.html' }; var mimeMap = { css: 'text/css', js: 'application/javascript', html: 'text/html' }; var ext = extMap[type] || '.txt'; var mime = mimeMap[type] || 'text/plain'; var blob = new Blob([code], { type: mime }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = title + ext; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showNotice(kngCCAdmin.strings.exportReady, 'success'); } else { showNotice(response.data.message || kngCCAdmin.strings.error, 'error'); } }, error: function() { showNotice(kngCCAdmin.strings.error, 'error'); } }); } function handleDelete(e) { e.preventDefault(); if (!confirm(kngCCAdmin.strings.confirmDelete)) { return; } const id = $(this).data('id'); $.ajax({ url: kngCCAdmin.ajaxUrl, method: 'POST', data: { action: 'kng_cc_delete_snippet', nonce: kngCCAdmin.nonce, id: id }, success: function(response) { if (response.success) { $('[data-id="' + id + '"]').closest('.kng-cc-row').fadeOut(300, function() { $(this).remove(); }); } else { showNotice(response.data.message || kngCCAdmin.strings.error, 'error'); } }, error: function() { showNotice(kngCCAdmin.strings.error, 'error'); } }); } // ========================================================================= // Editor Page // ========================================================================= function initEditorPage() { const $form = $('#kng-cc-editor-form'); if (!$form.length) return; // Initialize CodeMirror initCodeMirror(); // Type tabs $('.kng-cc-type-tab').on('click', handleTypeChange); // Status toggle label $('#kng-cc-status').on('change', updateStatusLabel); // Location change $('#kng-cc-location').on('change', handleLocationChange); // Scope mode change $('input[name="scope_mode"]').on('change', handleScopeModeChange); // Rules builder initRulesBuilder(); // Form submit $form.on('submit', handleFormSubmit); // Fullscreen toggle $('#kng-cc-fullscreen').on('click', toggleFullscreen); // Collapsible sections $('[data-collapsible]').on('click', function() { $(this).toggleClass('is-collapsed'); $(this).next('.kng-cc-section-content').slideToggle(200); }); // Initialize state updateStatusLabel(); handleLocationChange(); handleScopeModeChange(); renderRules(); } function initCodeMirror() { const $textarea = $('#kng-cc-code'); if (!$textarea.length) return; const modeMap = { 'css': 'text/css', 'js': 'application/javascript', 'html': 'text/html' }; const settings = wp.codeEditor.defaultSettings ? _.clone(wp.codeEditor.defaultSettings) : {}; settings.codemirror = _.extend({}, settings.codemirror, { mode: modeMap[currentType] || 'text/css', lineNumbers: true, lineWrapping: true, indentUnit: 4, tabSize: 4, indentWithTabs: false, autoCloseBrackets: true, autoCloseTags: true, matchBrackets: true, matchTags: {bothTags: true}, highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: true }, foldGutter: true, styleActiveLine: true, gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], extraKeys: { 'Ctrl-S': function() { saveSnippet(); }, 'Cmd-S': function() { saveSnippet(); }, 'Ctrl-Space': 'autocomplete', 'F11': function(cm) { cm.setOption('fullScreen', !cm.getOption('fullScreen')); }, 'Esc': function(cm) { if (cm.getOption('fullScreen')) cm.setOption('fullScreen', false); } } }); // Add autocomplete for CSS properties if (currentType === 'css' && window.CodeMirror && CodeMirror.hint && CodeMirror.hint.css) { settings.codemirror.hintOptions = { completeSingle: false }; } editor = wp.codeEditor.initialize($textarea, settings); // Enhance editor instance if (editor && editor.codemirror) { const cm = editor.codemirror; // Auto-format on paste cm.on('paste', function() { setTimeout(function() { if (currentType === 'css' || currentType === 'js') { // Simple auto-indent const totalLines = cm.lineCount(); cm.operation(function() { for (let i = 0; i < totalLines; i++) { cm.indentLine(i); } }); } }, 10); }); // Show autocomplete on input for CSS if (currentType === 'css') { cm.on('inputRead', function(cm, change) { if (change.text[0].match(/[a-z]/i)) { CodeMirror.commands.autocomplete(cm, null, {completeSingle: false}); } }); } } // Refresh on tab switch setTimeout(function() { if (editor && editor.codemirror) { editor.codemirror.refresh(); } }, 100); } function handleTypeChange(e) { e.preventDefault(); const $tab = $(this); if ($tab.is(':disabled') || $tab.hasClass('is-pro')) { if (!kngCCAdmin.hasPro) { showNotice(kngCCAdmin.strings.proRequired, 'warning'); } return; } const type = $tab.data('type'); currentType = type; // Update tabs $('.kng-cc-type-tab').removeClass('is-active').attr('aria-selected', 'false'); $tab.addClass('is-active').attr('aria-selected', 'true'); // Update hidden input $('#kng-cc-type').val(type); // Update CodeMirror mode if (editor && editor.codemirror) { const modeMap = { 'css': 'text/css', 'js': 'application/javascript', 'html': 'text/html' }; editor.codemirror.setOption('mode', modeMap[type] || 'text/css'); } // Show/hide JS options $('#kng-cc-js-options').toggle(type === 'js'); } function updateStatusLabel() { const enabled = $('#kng-cc-status').prop('checked'); $('#kng-cc-status-label').text(enabled ? 'Enabled' : 'Disabled'); } function handleLocationChange() { const location = $('#kng-cc-location').val(); $('#kng-cc-custom-hook-wrap').toggle(location === 'custom_hook'); } function handleScopeModeChange() { const mode = $('input[name="scope_mode"]:checked').val(); // Update radio card visual $('.kng-cc-radio-card').removeClass('is-selected'); $('input[name="scope_mode"]:checked').closest('.kng-cc-radio-card').addClass('is-selected'); // Show/hide rules builder $('#kng-cc-rules-builder').toggle(mode !== 'global'); } function handleFormSubmit(e) { e.preventDefault(); saveSnippet(); } function saveSnippet() { const $btn = $('#kng-cc-save-btn'); const $text = $btn.find('.kng-cc-save-text'); // Collect form data collectRulesFromForm(); const snippet = { id: parseInt($('#kng-cc-id').val()) || 0, title: $('#kng-cc-title').val(), code: editor && editor.codemirror ? editor.codemirror.getValue() : $('#kng-cc-code').val(), type: $('#kng-cc-type').val(), status: $('#kng-cc-status').prop('checked') ? 'enabled' : 'disabled', location: $('#kng-cc-location').val(), custom_hook: $('#kng-cc-custom-hook').val(), priority: parseInt($('#kng-cc-priority').val()) || 10, js_dom_ready: $('#kng-cc-js-dom-ready').prop('checked'), js_defer: $('#kng-cc-js-defer').prop('checked'), js_async: $('#kng-cc-js-async').prop('checked'), js_module: $('#kng-cc-js-module').prop('checked'), scope_mode: $('input[name="scope_mode"]:checked').val(), rules: rules, match_mode: $('#kng-cc-match-mode').val() || 'any', notes: $('#kng-cc-notes').val() }; // Validate if (!snippet.title.trim()) { showNotice('Please enter a snippet name', 'error'); $('#kng-cc-title').focus(); return; } // Show saving state $btn.removeClass('is-saved').addClass('is-saving'); $text.text(kngCCAdmin.strings.saving); $btn.prop('disabled', true); $.ajax({ url: kngCCAdmin.ajaxUrl, method: 'POST', data: { action: 'kng_cc_save_snippet', nonce: kngCCAdmin.nonce, snippet: JSON.stringify(snippet) }, success: function(response) { if (response.success) { // Show success state: green + checkmark $btn.removeClass('is-saving').addClass('is-saved'); $text.text(kngCCAdmin.strings.saved); // Update ID if new snippet if (!snippet.id && response.data.id) { $('#kng-cc-id').val(response.data.id); // Update URL without reload const newUrl = window.location.href.replace('view=new', 'view=edit&id=' + response.data.id); window.history.replaceState({}, '', newUrl); } // Return to normal after delay setTimeout(function() { $btn.removeClass('is-saved'); $text.text('Save Snippet'); $btn.prop('disabled', false); }, 2000); } else { showNotice(response.data.message || kngCCAdmin.strings.error, 'error'); resetSaveButton(); } }, error: function() { showNotice(kngCCAdmin.strings.error, 'error'); resetSaveButton(); } }); function resetSaveButton() { $btn.removeClass('is-saving is-saved'); $text.text('Save Snippet'); $btn.prop('disabled', false); } } function toggleFullscreen() { const $section = $('.kng-cc-code-section'); $section.toggleClass('is-fullscreen'); if (editor && editor.codemirror) { setTimeout(function() { editor.codemirror.refresh(); }, 100); } } // ========================================================================= // Rules Builder // ========================================================================= function initRulesBuilder() { // Add rule button $('#kng-cc-add-rule').on('click', addRule); // Rule type change $(document).on('change', '.kng-cc-rule-type', handleRuleTypeChange); // Remove rule $(document).on('click', '.kng-cc-rule-remove', removeRule); // Set initial rule index ruleIndex = rules.length; } function renderRules() { const $list = $('#kng-cc-rules-list'); $list.empty(); rules.forEach(function(rule, index) { const html = createRuleHTML(index, rule); $list.append(html); renderRuleValue(index, rule); }); } function createRuleHTML(index, rule) { const template = $('#kng-cc-rule-template').html(); let html = template.replace(/\{\{index\}\}/g, index); const $rule = $(html); $rule.find('.kng-cc-rule-type').val(rule.type || 'page'); return $rule; } function addRule() { const index = ruleIndex++; const rule = { type: 'page', value: '' }; rules.push(rule); const html = createRuleHTML(index, rule); $('#kng-cc-rules-list').append(html); renderRuleValue(index, rule); } function removeRule() { const $rule = $(this).closest('.kng-cc-rule'); const index = $rule.data('index'); // Find and remove from rules array const ruleIndex = rules.findIndex((r, i) => { return $('[data-index="' + i + '"]').is($rule); }); if (ruleIndex > -1) { rules.splice(ruleIndex, 1); } $rule.fadeOut(200, function() { $(this).remove(); }); } function handleRuleTypeChange() { const $rule = $(this).closest('.kng-cc-rule'); const index = $rule.data('index'); const type = $(this).val(); // Update rule type const ruleData = getRuleByElement($rule); if (ruleData) { ruleData.type = type; ruleData.value = ''; } renderRuleValue(index, { type: type, value: '' }); } function getRuleByElement($rule) { const index = $rule.data('index'); // Simple approach: use DOM order const domIndex = $rule.index(); return rules[domIndex]; } function renderRuleValue(index, rule) { const $rule = $('[data-index="' + index + '"]'); const $valueContainer = $rule.find('.kng-cc-rule-value'); $valueContainer.empty(); let html = ''; const type = rule.type || 'page'; switch (type) { case 'page': case 'post': html = '