- Add incontent_advanced group with 19 configurable fields in schema - Support 5 density modes: paragraphs_only, conservative, balanced, aggressive, custom - Enable ad placement after H2, H3, paragraphs, images, lists, blockquotes, and tables - Add probability-based selection (25-100%) per element type - Implement priority-based and position-based ad selection strategies - Add detailed mode descriptions in admin UI for better UX - Rename 'legacy' terminology to 'paragraphs_only' for clarity - Support deterministic randomization using post_id + date seed 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
920 lines
36 KiB
JavaScript
920 lines
36 KiB
JavaScript
/**
|
|
* 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 = '<p>' + escapeHtml(message) + '</p>';
|
|
|
|
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 = '<i class="bi bi-hourglass-split me-1"></i> 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 = '<i class="bi bi-hourglass-split me-1"></i> 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 = `
|
|
<div id="${toastId}" class="toast align-items-center text-white bg-${config.bg} border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
|
<div class="d-flex">
|
|
<div class="toast-body">
|
|
<i class="bi ${config.icon} me-2"></i>
|
|
<strong>${escapeHtml(message)}</strong>
|
|
</div>
|
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
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 = `
|
|
<div class="modal fade" id="roiConfirmModal" tabindex="-1" aria-labelledby="roiConfirmModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header" style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-bottom: none;">
|
|
<h5 class="modal-title text-white" id="roiConfirmModalLabel">
|
|
<i class="bi bi-question-circle me-2" style="color: #FF8600;"></i>
|
|
<span id="roiConfirmModalTitle">Confirmar</span>
|
|
</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body" id="roiConfirmModalBody" style="padding: 2rem;">
|
|
Mensaje de confirmación
|
|
</div>
|
|
<div class="modal-footer" style="border-top: 1px solid #dee2e6; padding: 1rem 1.5rem;">
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
|
|
<i class="bi bi-x-circle me-1"></i>
|
|
Cancelar
|
|
</button>
|
|
<button type="button" class="btn text-white" id="roiConfirmModalConfirm" style="background-color: #FF8600; border-color: #FF8600;">
|
|
<i class="bi bi-check-circle me-1"></i>
|
|
Confirmar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
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 = `
|
|
<div class="modal fade" id="roiConfirmModal" tabindex="-1" aria-labelledby="roiConfirmModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header" style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-bottom: none;">
|
|
<h5 class="modal-title text-white" id="roiConfirmModalLabel">
|
|
<i class="bi bi-question-circle me-2" style="color: #FF8600;"></i>
|
|
<span id="roiConfirmModalTitle">Confirmar</span>
|
|
</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body" id="roiConfirmModalBody" style="padding: 2rem;">
|
|
Mensaje de confirmación
|
|
</div>
|
|
<div class="modal-footer" style="border-top: 1px solid #dee2e6; padding: 1rem 1.5rem;">
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
|
|
<i class="bi bi-x-circle me-1"></i>
|
|
Cancelar
|
|
</button>
|
|
<button type="button" class="btn text-white" id="roiConfirmModalConfirm" style="background-color: #FF8600; border-color: #FF8600;">
|
|
<i class="bi bi-check-circle me-1"></i>
|
|
Confirmar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
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();
|
|
}
|
|
|
|
})();
|