/** * JavaScript para el Dashboard del Panel de Administración ROI Theme * Vanilla JavaScript - No frameworks */ (function() { 'use strict'; /** * Inicializa el dashboard cuando el DOM está listo */ document.addEventListener('DOMContentLoaded', function() { // Nueva navegación por Cards/Grupos initializeCardNavigation(); // Funcionalidad existente (solo si hay tabs visibles) if (document.querySelector('.nav-tabs-admin')) { initializeTabs(); } initializeFormValidation(); initializeButtons(); initializeColorPickers(); }); /** * Inicializa la navegación por Cards/Grupos (App-Style) */ function initializeCardNavigation() { // Verificar que estamos en el panel correcto const adminPanel = document.querySelector('.roi-admin-panel'); if (!adminPanel) { return; } // Delegación de eventos para mini-cards document.addEventListener('click', function(e) { const minicard = e.target.closest('.roi-component-minicard'); if (minicard) { e.preventDefault(); const componentId = minicard.getAttribute('data-component-id'); if (componentId) { navigateToComponent(componentId); } } }); // Botón volver al home document.addEventListener('click', function(e) { if (e.target.closest('.roi-back-to-home')) { e.preventDefault(); navigateToHome(); } }); } /** * Navega a un componente específico * * @param {string} componentId ID del componente en kebab-case */ function navigateToComponent(componentId) { const url = new URL(window.location.href); url.searchParams.set('component', componentId); // Eliminar el parámetro admin-tab si existe (legacy) url.searchParams.delete('admin-tab'); window.location.href = url.toString(); } /** * Navega de vuelta al home (vista de grupos) */ function navigateToHome() { const url = new URL(window.location.href); url.searchParams.delete('component'); url.searchParams.delete('admin-tab'); window.location.href = url.toString(); } /** * Inicializa el sistema de tabs con persistencia en URL */ function initializeTabs() { const tabButtons = document.querySelectorAll('[data-bs-toggle="tab"]'); // Leer parametro admin-tab de la URL al cargar const urlParams = new URLSearchParams(window.location.search); const activeTabParam = urlParams.get('admin-tab'); if (activeTabParam) { // Buscar el boton del tab correspondiente const targetButton = document.querySelector('[data-bs-target="#' + activeTabParam + 'Tab"]'); if (targetButton) { // Activar el tab usando Bootstrap API const tab = new bootstrap.Tab(targetButton); tab.show(); } } // Escuchar cambios de tab para actualizar URL tabButtons.forEach(function(tabButton) { tabButton.addEventListener('shown.bs.tab', function(e) { // Obtener el ID del componente desde data-bs-target const target = e.target.getAttribute('data-bs-target'); const componentId = target.replace('#', '').replace('Tab', ''); // Actualizar URL sin recargar pagina updateUrlWithTab(componentId); }); }); } /** * Actualiza la URL con el parametro admin-tab sin recargar la pagina * * @param {string} tabId ID del tab activo */ function updateUrlWithTab(tabId) { const url = new URL(window.location.href); url.searchParams.set('admin-tab', tabId); window.history.replaceState({}, '', url.toString()); } /** * Obtiene el ID del tab activo actualmente * * @returns {string|null} ID del componente activo o null */ function getActiveTabId() { const activeTab = document.querySelector('.tab-pane.active'); if (activeTab) { return activeTab.id.replace('Tab', ''); } return null; } /** * Inicializa validación de formularios */ function initializeFormValidation() { const forms = document.querySelectorAll('.roi-component-config form'); forms.forEach(function(form) { form.addEventListener('submit', function(e) { if (!validateForm(form)) { e.preventDefault(); showError('Por favor, corrige los errores en el formulario.'); } }); }); } /** * Valida un formulario * * @param {HTMLFormElement} form Formulario a validar * @returns {boolean} True si es válido */ function validateForm(form) { let isValid = true; const requiredFields = form.querySelectorAll('[required]'); requiredFields.forEach(function(field) { if (!field.value.trim()) { field.classList.add('error'); isValid = false; } else { field.classList.remove('error'); } }); return isValid; } /** * Muestra un mensaje de error * * @param {string} message Mensaje a mostrar */ function showError(message) { const notice = document.createElement('div'); notice.className = 'notice notice-error is-dismissible'; notice.innerHTML = '

' + escapeHtml(message) + '

'; const h1 = document.querySelector('.roi-admin-dashboard h1'); if (h1 && h1.nextElementSibling) { h1.nextElementSibling.after(notice); } } /** * Escapa HTML para prevenir XSS * * @param {string} text Texto a escapar * @returns {string} Texto escapado */ function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Inicializa los botones del panel */ function initializeButtons() { // Botón Guardar Cambios const saveButton = document.getElementById('saveSettings'); if (saveButton) { saveButton.addEventListener('click', handleSaveSettings); } // Botón Cancelar const cancelButton = document.getElementById('cancelChanges'); if (cancelButton) { cancelButton.addEventListener('click', handleCancelChanges); } // Botones Restaurar valores por defecto (dinámico para todos los componentes) const resetButtons = document.querySelectorAll('.btn-reset-defaults[data-component]'); resetButtons.forEach(function(button) { button.addEventListener('click', function(e) { const componentId = this.getAttribute('data-component'); handleResetDefaults(e, componentId, this); }); }); } /** * Guarda los cambios del formulario */ function handleSaveSettings(e) { e.preventDefault(); // Obtener el tab activo const activeTab = document.querySelector('.tab-pane.active'); if (!activeTab) { showNotice('error', 'No hay ningún componente seleccionado.'); return; } // Obtener el ID del componente desde el tab const componentId = activeTab.id.replace('Tab', ''); // Recopilar todos los campos del formulario activo const formData = collectFormData(activeTab); // Mostrar loading en el botón const saveButton = document.getElementById('saveSettings'); const originalText = saveButton.innerHTML; saveButton.disabled = true; saveButton.innerHTML = ' Guardando...'; // Enviar por AJAX fetch(ajaxurl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ action: 'roi_save_component_settings', nonce: roiAdminDashboard.nonce, component: componentId, settings: JSON.stringify(formData) }) }) .then(response => response.json()) .then(data => { if (data.success) { showNotice('success', data.data.message || 'Cambios guardados correctamente.'); } else { showNotice('error', data.data.message || 'Error al guardar los cambios.'); } }) .catch(error => { console.error('Error:', error); showNotice('error', 'Error de conexión al guardar los cambios.'); }) .finally(() => { saveButton.disabled = false; saveButton.innerHTML = originalText; }); } /** * Cancela los cambios y recarga la página */ function handleCancelChanges(e) { e.preventDefault(); showConfirmModal( 'Cancelar cambios', '¿Descartar todos los cambios no guardados?', function() { location.reload(); } ); } /** * Restaura los valores por defecto de un componente * * @param {Event} e Evento del click * @param {string} componentId ID del componente a resetear * @param {HTMLElement} resetButton Elemento del botón que disparó el evento */ function handleResetDefaults(e, componentId, resetButton) { e.preventDefault(); showConfirmModal( 'Restaurar valores por defecto', '¿Restaurar todos los valores a los valores por defecto? Esta acción no se puede deshacer.', function() { // Mostrar loading en el botón const originalText = resetButton.innerHTML; resetButton.disabled = true; resetButton.innerHTML = ' Restaurando...'; // Enviar por AJAX fetch(ajaxurl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ action: 'roi_reset_component_defaults', nonce: roiAdminDashboard.nonce, component: componentId }) }) .then(response => response.json()) .then(data => { if (data.success) { showNotice('success', data.data.message || 'Valores restaurados correctamente.'); // Recargar preservando el tab activo setTimeout(() => { const activeTabId = getActiveTabId(); if (activeTabId) { const url = new URL(window.location.href); url.searchParams.set('admin-tab', activeTabId); window.location.href = url.toString(); } else { location.reload(); } }, 1500); } else { showNotice('error', data.data.message || 'Error al restaurar los valores.'); } }) .catch(error => { console.error('Error:', error); showNotice('error', 'Error de conexión al restaurar los valores.'); }) .finally(() => { resetButton.disabled = false; resetButton.innerHTML = originalText; }); } ); } /** * Recopila los datos del formulario del tab activo */ function collectFormData(container) { const formData = {}; // Inputs de texto, textarea, select, color, number, email, password const textInputs = container.querySelectorAll('input[type="text"], input[type="url"], input[type="color"], input[type="number"], input[type="email"], input[type="password"], textarea, select'); textInputs.forEach(input => { if (input.id) { formData[input.id] = input.value; } }); // Checkboxes (switches) const checkboxes = container.querySelectorAll('input[type="checkbox"]'); checkboxes.forEach(checkbox => { if (checkbox.id) { formData[checkbox.id] = checkbox.checked; } }); return formData; } /** * Muestra un toast de Bootstrap */ function showNotice(type, message) { // Mapear tipos const typeMap = { 'success': { bg: 'success', icon: 'bi-check-circle-fill', text: 'Éxito' }, 'error': { bg: 'danger', icon: 'bi-x-circle-fill', text: 'Error' }, 'warning': { bg: 'warning', icon: 'bi-exclamation-triangle-fill', text: 'Advertencia' }, 'info': { bg: 'info', icon: 'bi-info-circle-fill', text: 'Información' } }; const config = typeMap[type] || typeMap['info']; // Crear container de toasts si no existe let toastContainer = document.getElementById('roiToastContainer'); if (!toastContainer) { toastContainer = document.createElement('div'); toastContainer.id = 'roiToastContainer'; toastContainer.className = 'toast-container position-fixed start-50 translate-middle-x'; toastContainer.style.top = '60px'; toastContainer.style.zIndex = '999999'; document.body.appendChild(toastContainer); } // Crear toast const toastId = 'toast-' + Date.now(); const toastHTML = ` `; toastContainer.insertAdjacentHTML('beforeend', toastHTML); // Mostrar toast const toastElement = document.getElementById(toastId); const toast = new bootstrap.Toast(toastElement, { autohide: true, delay: 5000 }); toast.show(); // Eliminar del DOM después de ocultarse toastElement.addEventListener('hidden.bs.toast', function() { toastElement.remove(); }); } /** * Muestra un modal de confirmación de Bootstrap */ function showConfirmModal(title, message, onConfirm) { // Crear modal si no existe let modal = document.getElementById('roiConfirmModal'); if (!modal) { const modalHTML = ` `; document.body.insertAdjacentHTML('beforeend', modalHTML); modal = document.getElementById('roiConfirmModal'); } // Actualizar contenido document.getElementById('roiConfirmModalTitle').textContent = title; document.getElementById('roiConfirmModalBody').textContent = message; // Configurar callback const confirmButton = document.getElementById('roiConfirmModalConfirm'); const newConfirmButton = confirmButton.cloneNode(true); confirmButton.parentNode.replaceChild(newConfirmButton, confirmButton); newConfirmButton.addEventListener('click', function() { const bsModal = bootstrap.Modal.getInstance(modal); bsModal.hide(); if (typeof onConfirm === 'function') { onConfirm(); } }); // Mostrar modal const bsModal = new bootstrap.Modal(modal); bsModal.show(); } /** * Inicializa los color pickers para mostrar el valor HEX */ function initializeColorPickers() { const colorPickers = document.querySelectorAll('input[type="color"]'); colorPickers.forEach(picker => { // Elemento donde se muestra el valor HEX const valueDisplay = document.getElementById(picker.id + 'Value'); if (valueDisplay) { picker.addEventListener('input', function() { valueDisplay.textContent = this.value.toUpperCase(); }); } }); } // ========================================================================= // IN-CONTENT ADS AVANZADO - JavaScript // ========================================================================= document.addEventListener('DOMContentLoaded', function() { initializeInContentAdvanced(); }); /** * Inicializa la funcionalidad de In-Content Ads Avanzado */ function initializeInContentAdvanced() { // Buscar el selector de modo (puede tener prefijo dinámico) const modeSelect = document.querySelector('[id$="IncontentMode"]'); if (!modeSelect) { return; // No estamos en la página de AdSense } // Obtener prefijo del componente desde el ID const componentPrefix = modeSelect.id.replace('IncontentMode', ''); // Definir presets de modos const modePresets = { 'paragraphs_only': null, // Solo inserta despues de parrafos (config basica) 'conservative': { maxAds: '5', minSpacing: '5', h2: { enabled: true, prob: '75' }, h3: { enabled: false, prob: '50' }, paragraphs: { enabled: true, prob: '50' }, images: { enabled: false, prob: '50' }, lists: { enabled: false, prob: '50' }, blockquotes: { enabled: false, prob: '50' }, tables: { enabled: false, prob: '50' } }, 'balanced': { maxAds: '8', minSpacing: '3', h2: { enabled: true, prob: '100' }, h3: { enabled: true, prob: '50' }, paragraphs: { enabled: true, prob: '75' }, images: { enabled: true, prob: '75' }, lists: { enabled: false, prob: '50' }, blockquotes: { enabled: false, prob: '50' }, tables: { enabled: false, prob: '50' } }, 'aggressive': { maxAds: '15', minSpacing: '2', h2: { enabled: true, prob: '100' }, h3: { enabled: true, prob: '100' }, paragraphs: { enabled: true, prob: '100' }, images: { enabled: true, prob: '100' }, lists: { enabled: true, prob: '75' }, blockquotes: { enabled: true, prob: '75' }, tables: { enabled: true, prob: '75' } }, 'custom': null // Configuración manual }; // Elementos del DOM const elements = { mode: modeSelect, paragraphsOnlyBanner: document.getElementById('roiParagraphsOnlyBanner'), densityIndicator: document.getElementById('roiIncontentDensityIndicator'), densityLevel: document.getElementById('roiDensityLevel'), densityBadge: document.getElementById('roiDensityBadge'), highDensityWarning: document.getElementById('roiHighDensityWarning'), locationsDetails: document.getElementById('roiLocationsDetails'), limitsDetails: document.getElementById('roiLimitsDetails'), maxAds: document.querySelector('[id$="IncontentMaxTotalAds"]'), minSpacing: document.querySelector('[id$="IncontentMinSpacing"]'), // Descripciones de modos modeDescriptions: { paragraphs_only: document.getElementById('roiModeDescParagraphsOnly'), conservative: document.getElementById('roiModeDescConservative'), balanced: document.getElementById('roiModeDescBalanced'), aggressive: document.getElementById('roiModeDescAggressive'), custom: document.getElementById('roiModeDescCustom') }, locations: [ { key: 'H2', el: document.querySelector('[id$="IncontentAfterH2Enabled"]'), prob: document.querySelector('[id$="IncontentAfterH2Probability"]') }, { key: 'H3', el: document.querySelector('[id$="IncontentAfterH3Enabled"]'), prob: document.querySelector('[id$="IncontentAfterH3Probability"]') }, { key: 'Paragraphs', el: document.querySelector('[id$="IncontentAfterParagraphsEnabled"]'), prob: document.querySelector('[id$="IncontentAfterParagraphsProbability"]') }, { key: 'Images', el: document.querySelector('[id$="IncontentAfterImagesEnabled"]'), prob: document.querySelector('[id$="IncontentAfterImagesProbability"]') }, { key: 'Lists', el: document.querySelector('[id$="IncontentAfterListsEnabled"]'), prob: document.querySelector('[id$="IncontentAfterListsProbability"]') }, { key: 'Blockquotes', el: document.querySelector('[id$="IncontentAfterBlockquotesEnabled"]'), prob: document.querySelector('[id$="IncontentAfterBlockquotesProbability"]') }, { key: 'Tables', el: document.querySelector('[id$="IncontentAfterTablesEnabled"]'), prob: document.querySelector('[id$="IncontentAfterTablesProbability"]') } ] }; // Estado para detectar cambios manuales let isApplyingPreset = false; /** * Actualiza el indicador de densidad */ function updateDensityIndicator() { const mode = elements.mode.value; if (mode === 'paragraphs_only') { elements.densityLevel.textContent = 'Solo parrafos'; elements.densityBadge.textContent = 'clasico'; elements.densityBadge.className = 'badge bg-secondary ms-1'; elements.densityIndicator.className = 'alert alert-light border small mb-3'; elements.highDensityWarning.classList.add('d-none'); return; } // Calcular densidad estimada const maxAds = parseInt(elements.maxAds.value) || 8; let totalWeight = 0; let enabledCount = 0; elements.locations.forEach(loc => { if (loc.el && loc.el.checked) { const prob = parseInt(loc.prob.value) || 100; totalWeight += prob; enabledCount++; } }); const avgProb = enabledCount > 0 ? totalWeight / enabledCount : 0; const estimatedAds = Math.round((maxAds * avgProb) / 100); // Determinar nivel let level, badgeClass, alertClass; if (estimatedAds <= 3) { level = 'Baja'; badgeClass = 'bg-success'; alertClass = 'alert-success'; } else if (estimatedAds <= 6) { level = 'Media'; badgeClass = 'bg-info'; alertClass = 'alert-info'; } else if (estimatedAds <= 10) { level = 'Alta'; badgeClass = 'bg-warning'; alertClass = 'alert-warning'; } else { level = 'Muy Alta'; badgeClass = 'bg-danger'; alertClass = 'alert-danger'; } elements.densityLevel.textContent = level; elements.densityBadge.textContent = '~' + estimatedAds + ' ads'; elements.densityBadge.className = 'badge ' + badgeClass + ' ms-1'; elements.densityIndicator.className = 'alert ' + alertClass + ' small mb-3'; // Mostrar/ocultar warning de densidad alta if (estimatedAds > 10) { elements.highDensityWarning.classList.remove('d-none'); } else { elements.highDensityWarning.classList.add('d-none'); } } /** * Aplica un preset de modo */ function applyPreset(presetName) { const preset = modePresets[presetName]; if (!preset) return; isApplyingPreset = true; // Aplicar max ads y spacing if (elements.maxAds) elements.maxAds.value = preset.maxAds; if (elements.minSpacing) elements.minSpacing.value = preset.minSpacing; // Aplicar ubicaciones const locationKeys = ['h2', 'h3', 'paragraphs', 'images', 'lists', 'blockquotes', 'tables']; locationKeys.forEach((key, index) => { const loc = elements.locations[index]; const presetLoc = preset[key]; if (loc.el && presetLoc) { loc.el.checked = presetLoc.enabled; if (loc.prob) loc.prob.value = presetLoc.prob; } }); isApplyingPreset = false; updateDensityIndicator(); } /** * Habilita/deshabilita campos según modo */ function toggleFieldsState() { const currentMode = elements.mode.value; const isParagraphsOnly = currentMode === 'paragraphs_only'; // Toggle details sections if (elements.locationsDetails) { if (isParagraphsOnly) { elements.locationsDetails.removeAttribute('open'); } else { elements.locationsDetails.setAttribute('open', ''); } } if (elements.limitsDetails) { if (isParagraphsOnly) { elements.limitsDetails.removeAttribute('open'); } else { elements.limitsDetails.setAttribute('open', ''); } } // Toggle campos if (elements.maxAds) elements.maxAds.disabled = isParagraphsOnly; if (elements.minSpacing) elements.minSpacing.disabled = isParagraphsOnly; elements.locations.forEach(loc => { if (loc.el) loc.el.disabled = isParagraphsOnly; if (loc.prob) loc.prob.disabled = isParagraphsOnly; }); // Toggle banner informativo if (elements.paragraphsOnlyBanner) { if (isParagraphsOnly) { elements.paragraphsOnlyBanner.classList.remove('d-none'); } else { elements.paragraphsOnlyBanner.classList.add('d-none'); } } // Toggle descripciones de modo (mostrar solo la activa) if (elements.modeDescriptions) { Object.keys(elements.modeDescriptions).forEach(mode => { const descEl = elements.modeDescriptions[mode]; if (descEl) { if (mode === currentMode) { descEl.classList.remove('d-none'); } else { descEl.classList.add('d-none'); } } }); } // Actualizar indicador updateDensityIndicator(); } /** * Maneja cambio de modo */ function handleModeChange(e) { const newMode = e.target.value; const currentMode = e.target.dataset.previousValue || 'paragraphs_only'; // Si cambia de custom a preset, mostrar confirmación if (currentMode === 'custom' && newMode !== 'custom' && modePresets[newMode]) { showConfirmModal( 'Cambiar modo', 'Al cambiar a un modo preconfigurado se perderán tus ajustes personalizados. ¿Continuar?', function() { applyPreset(newMode); toggleFieldsState(); e.target.dataset.previousValue = newMode; }, function() { // Cancelar: restaurar valor anterior e.target.value = currentMode; } ); return; } // Aplicar preset si corresponde if (modePresets[newMode]) { applyPreset(newMode); } toggleFieldsState(); e.target.dataset.previousValue = newMode; } /** * Maneja cambios en campos (auto-switch a custom) */ function handleFieldChange() { if (isApplyingPreset) return; const currentMode = elements.mode.value; if (currentMode !== 'custom' && currentMode !== 'paragraphs_only') { elements.mode.value = 'custom'; elements.mode.dataset.previousValue = 'custom'; showNotice('info', 'Modo cambiado a "Personalizado" por tus ajustes manuales.'); updateDensityIndicator(); } else { updateDensityIndicator(); } } // Inicializar estado elements.mode.dataset.previousValue = elements.mode.value; toggleFieldsState(); updateDensityIndicator(); // Event listeners elements.mode.addEventListener('change', handleModeChange); if (elements.maxAds) { elements.maxAds.addEventListener('change', handleFieldChange); } if (elements.minSpacing) { elements.minSpacing.addEventListener('change', handleFieldChange); } elements.locations.forEach(loc => { if (loc.el) { loc.el.addEventListener('change', handleFieldChange); } if (loc.prob) { loc.prob.addEventListener('change', handleFieldChange); } }); } /** * Muestra un modal de confirmación con callback de cancelación */ function showConfirmModal(title, message, onConfirm, onCancel) { // Crear modal si no existe let modal = document.getElementById('roiConfirmModal'); if (!modal) { const modalHTML = ` `; document.body.insertAdjacentHTML('beforeend', modalHTML); modal = document.getElementById('roiConfirmModal'); } // Actualizar contenido document.getElementById('roiConfirmModalTitle').textContent = title; document.getElementById('roiConfirmModalBody').textContent = message; // Configurar callback de confirmación const confirmButton = document.getElementById('roiConfirmModalConfirm'); const newConfirmButton = confirmButton.cloneNode(true); confirmButton.parentNode.replaceChild(newConfirmButton, confirmButton); newConfirmButton.addEventListener('click', function() { const bsModal = bootstrap.Modal.getInstance(modal); bsModal.hide(); if (typeof onConfirm === 'function') { onConfirm(); } }); // Configurar callback de cancelación if (typeof onCancel === 'function') { modal.addEventListener('hidden.bs.modal', function handler() { modal.removeEventListener('hidden.bs.modal', handler); // Solo llamar onCancel si no fue por confirmación if (!modal.dataset.confirmed) { onCancel(); } delete modal.dataset.confirmed; }); newConfirmButton.addEventListener('click', function() { modal.dataset.confirmed = 'true'; }); } // Mostrar modal const bsModal = new bootstrap.Modal(modal); bsModal.show(); } })();