feat(php): implement advanced in-content ads with multi-element targeting

- 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>
This commit is contained in:
FrankZamora
2025-12-10 10:42:53 -06:00
parent c2fff49961
commit 2896e2d006
5 changed files with 1263 additions and 19 deletions

View File

@@ -518,4 +518,402 @@
});
}
// =========================================================================
// 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();
}
})();