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:
@@ -118,6 +118,27 @@ final class AdsensePlacementFieldMapper implements FieldMapperInterface
|
||||
'adsense-placementExcludeCategoriesAdv' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'adsense-placementExcludePostIdsAdv' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'adsense-placementExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// INCONTENT ADVANCED (In-Content Ads Avanzado)
|
||||
'adsense-placementIncontentMode' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_mode'],
|
||||
'adsense-placementIncontentAfterH2Enabled' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_h2_enabled'],
|
||||
'adsense-placementIncontentAfterH2Probability' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_h2_probability'],
|
||||
'adsense-placementIncontentAfterH3Enabled' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_h3_enabled'],
|
||||
'adsense-placementIncontentAfterH3Probability' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_h3_probability'],
|
||||
'adsense-placementIncontentAfterParagraphsEnabled' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_paragraphs_enabled'],
|
||||
'adsense-placementIncontentAfterParagraphsProbability' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_paragraphs_probability'],
|
||||
'adsense-placementIncontentAfterImagesEnabled' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_images_enabled'],
|
||||
'adsense-placementIncontentAfterImagesProbability' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_images_probability'],
|
||||
'adsense-placementIncontentAfterListsEnabled' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_lists_enabled'],
|
||||
'adsense-placementIncontentAfterListsProbability' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_lists_probability'],
|
||||
'adsense-placementIncontentAfterBlockquotesEnabled' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_blockquotes_enabled'],
|
||||
'adsense-placementIncontentAfterBlockquotesProbability' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_blockquotes_probability'],
|
||||
'adsense-placementIncontentAfterTablesEnabled' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_tables_enabled'],
|
||||
'adsense-placementIncontentAfterTablesProbability' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_tables_probability'],
|
||||
'adsense-placementIncontentMaxTotalAds' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_max_total_ads'],
|
||||
'adsense-placementIncontentMinSpacing' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_min_spacing'],
|
||||
'adsense-placementIncontentFormat' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_format'],
|
||||
'adsense-placementIncontentPriorityMode' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_priority_mode'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ final class AdsensePlacementFormBuilder
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildDiagramSection();
|
||||
$html .= $this->buildPostLocationsGroup($componentId);
|
||||
$html .= $this->buildInContentAdvancedGroup($componentId);
|
||||
$html .= $this->buildInContentAdsGroup($componentId);
|
||||
$html .= $this->buildExclusionsGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
@@ -342,6 +343,290 @@ final class AdsensePlacementFormBuilder
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Seccion avanzada para In-Content Ads con multiples tipos de ubicacion
|
||||
* Incluye: modo de densidad, ubicaciones por elemento, limites y espaciado
|
||||
*/
|
||||
private function buildInContentAdvancedGroup(string $cid): string
|
||||
{
|
||||
// Obtener valores actuales del grupo incontent_advanced
|
||||
$mode = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_mode', 'paragraphs_only');
|
||||
$mode = is_string($mode) ? $mode : 'paragraphs_only';
|
||||
|
||||
// Ubicaciones
|
||||
$h2Enabled = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_h2_enabled', true);
|
||||
$h2Prob = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_h2_probability', '100');
|
||||
$h3Enabled = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_h3_enabled', true);
|
||||
$h3Prob = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_h3_probability', '50');
|
||||
$paragraphsEnabled = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_paragraphs_enabled', true);
|
||||
$paragraphsProb = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_paragraphs_probability', '75');
|
||||
$imagesEnabled = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_images_enabled', true);
|
||||
$imagesProb = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_images_probability', '75');
|
||||
$listsEnabled = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_lists_enabled', false);
|
||||
$listsProb = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_lists_probability', '50');
|
||||
$blockquotesEnabled = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_blockquotes_enabled', false);
|
||||
$blockquotesProb = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_blockquotes_probability', '50');
|
||||
$tablesEnabled = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_tables_enabled', false);
|
||||
$tablesProb = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_tables_probability', '50');
|
||||
|
||||
// Limites
|
||||
$maxAds = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_max_total_ads', '8');
|
||||
$minSpacing = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_min_spacing', '3');
|
||||
$format = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_format', 'in-article');
|
||||
$priorityMode = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_priority_mode', 'position');
|
||||
|
||||
// Cast to string where needed
|
||||
$h2Prob = is_string($h2Prob) ? $h2Prob : '100';
|
||||
$h3Prob = is_string($h3Prob) ? $h3Prob : '50';
|
||||
$paragraphsProb = is_string($paragraphsProb) ? $paragraphsProb : '75';
|
||||
$imagesProb = is_string($imagesProb) ? $imagesProb : '75';
|
||||
$listsProb = is_string($listsProb) ? $listsProb : '50';
|
||||
$blockquotesProb = is_string($blockquotesProb) ? $blockquotesProb : '50';
|
||||
$tablesProb = is_string($tablesProb) ? $tablesProb : '50';
|
||||
$maxAds = is_string($maxAds) ? $maxAds : '8';
|
||||
$minSpacing = is_string($minSpacing) ? $minSpacing : '3';
|
||||
$format = is_string($format) ? $format : 'in-article';
|
||||
$priorityMode = is_string($priorityMode) ? $priorityMode : 'position';
|
||||
|
||||
$isParagraphsOnly = $mode === 'paragraphs_only';
|
||||
$disabledAttr = $isParagraphsOnly ? 'disabled' : '';
|
||||
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #198754;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-body-text me-2" style="color: #198754;"></i>';
|
||||
$html .= ' In-Content Ads Avanzado';
|
||||
$html .= ' <span class="badge bg-success ms-2">Nuevo</span>';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Indicador de densidad
|
||||
$html .= ' <div id="roiIncontentDensityIndicator" class="alert alert-info small mb-3">';
|
||||
$html .= ' <i class="bi bi-speedometer2 me-1"></i>';
|
||||
$html .= ' Densidad estimada: <strong id="roiDensityLevel">Calculando...</strong>';
|
||||
$html .= ' <span id="roiDensityBadge" class="badge bg-secondary ms-1">~? ads</span>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Banner informativo para modo Solo parrafos
|
||||
$html .= ' <div id="roiParagraphsOnlyBanner" class="alert alert-light border small mb-3' . ($isParagraphsOnly ? '' : ' d-none') . '">';
|
||||
$html .= ' <i class="bi bi-info-circle me-1 text-primary"></i>';
|
||||
$html .= ' <strong>Solo parrafos:</strong> Los anuncios se insertan unicamente despues de parrafos, ';
|
||||
$html .= ' usando la configuracion de la seccion "Post Content". Cambia a otro modo para elegir ubicaciones adicionales.';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Selector de modo con descripciones
|
||||
$html .= ' <div class="mb-4">';
|
||||
$html .= ' <label for="' . esc_attr($cid) . 'IncontentMode" class="form-label fw-semibold">';
|
||||
$html .= ' <i class="bi bi-sliders me-1" style="color: #FF8600;"></i>Estrategia de insercion';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <p class="text-muted small mb-2">Define donde y con que frecuencia se insertaran anuncios dentro del contenido.</p>';
|
||||
$html .= ' <select class="form-select mb-3" id="' . esc_attr($cid) . 'IncontentMode">';
|
||||
$modeOptions = [
|
||||
'paragraphs_only' => 'Solo parrafos (clasico)',
|
||||
'conservative' => 'Conservador - H2 y parrafos',
|
||||
'balanced' => 'Balanceado - Multiples elementos',
|
||||
'aggressive' => 'Intensivo - Todos los elementos',
|
||||
'custom' => 'Personalizado'
|
||||
];
|
||||
foreach ($modeOptions as $value => $label) {
|
||||
$selected = selected($mode, $value, false);
|
||||
$html .= '<option value="' . esc_attr($value) . '" ' . $selected . '>' . esc_html($label) . '</option>';
|
||||
}
|
||||
$html .= ' </select>';
|
||||
|
||||
// Descripciones de cada modo
|
||||
$html .= ' <div id="roiModeDescriptions" class="small">';
|
||||
|
||||
// Solo parrafos
|
||||
$html .= ' <div id="roiModeDescParagraphsOnly" class="alert alert-light border py-2 px-3' . ($mode !== 'paragraphs_only' ? ' d-none' : '') . '">';
|
||||
$html .= ' <strong class="text-primary"><i class="bi bi-text-paragraph me-1"></i>Solo parrafos</strong>';
|
||||
$html .= ' <p class="mb-1 mt-1">Inserta anuncios unicamente despues de parrafos. Usa la configuracion de la seccion "Post Content" (numero de anuncios, parrafos entre ads, etc).</p>';
|
||||
$html .= ' <span class="text-muted"><i class="bi bi-lightbulb me-1"></i>Ideal si: Tu contenido tiene pocos encabezados o prefieres la configuracion tradicional.</span>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Conservador
|
||||
$html .= ' <div id="roiModeDescConservative" class="alert alert-light border py-2 px-3' . ($mode !== 'conservative' ? ' d-none' : '') . '">';
|
||||
$html .= ' <strong class="text-success"><i class="bi bi-shield-check me-1"></i>Conservador</strong>';
|
||||
$html .= ' <p class="mb-1 mt-1">Maximo 5 anuncios con espaciado amplio (5 elementos). Solo inserta despues de titulos H2 y parrafos.</p>';
|
||||
$html .= ' <span class="text-muted"><i class="bi bi-lightbulb me-1"></i>Ideal si: Priorizas la experiencia del usuario sobre los ingresos. Articulos cortos o medianos.</span>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Balanceado
|
||||
$html .= ' <div id="roiModeDescBalanced" class="alert alert-light border py-2 px-3' . ($mode !== 'balanced' ? ' d-none' : '') . '">';
|
||||
$html .= ' <strong class="text-primary"><i class="bi bi-balance-scale me-1"></i>Balanceado</strong>';
|
||||
$html .= ' <p class="mb-1 mt-1">Hasta 8 anuncios con espaciado moderado (3 elementos). Usa H2, H3, parrafos e imagenes.</p>';
|
||||
$html .= ' <span class="text-muted"><i class="bi bi-lightbulb me-1"></i>Ideal si: Buscas equilibrio entre ingresos y experiencia. Articulos medianos a largos.</span>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Intensivo
|
||||
$html .= ' <div id="roiModeDescAggressive" class="alert alert-light border py-2 px-3' . ($mode !== 'aggressive' ? ' d-none' : '') . '">';
|
||||
$html .= ' <strong class="text-warning"><i class="bi bi-lightning-charge me-1"></i>Intensivo</strong>';
|
||||
$html .= ' <p class="mb-1 mt-1">Hasta 15 anuncios con espaciado minimo (2 elementos). Usa todos los tipos de elementos disponibles.</p>';
|
||||
$html .= ' <span class="text-muted"><i class="bi bi-lightbulb me-1"></i>Ideal si: Priorizas maximizar ingresos. Solo para articulos muy largos (+3000 palabras).</span>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Personalizado
|
||||
$html .= ' <div id="roiModeDescCustom" class="alert alert-light border py-2 px-3' . ($mode !== 'custom' ? ' d-none' : '') . '">';
|
||||
$html .= ' <strong class="text-secondary"><i class="bi bi-gear me-1"></i>Personalizado</strong>';
|
||||
$html .= ' <p class="mb-1 mt-1">Tu configuras manualmente cada ubicacion, probabilidad y limites.</p>';
|
||||
$html .= ' <span class="text-muted"><i class="bi bi-lightbulb me-1"></i>Ideal si: Quieres control total sobre donde aparecen los anuncios.</span>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Subseccion: Ubicaciones por elemento
|
||||
$html .= ' <details class="mb-3 border rounded" id="roiLocationsDetails"' . ($isParagraphsOnly ? '' : ' open') . '>';
|
||||
$html .= ' <summary class="p-3 bg-light fw-bold" style="cursor: pointer;">';
|
||||
$html .= ' <i class="bi bi-geo-alt me-1"></i>';
|
||||
$html .= ' Ubicaciones por Elemento';
|
||||
$html .= ' </summary>';
|
||||
$html .= ' <div class="p-3">';
|
||||
|
||||
// Grid de ubicaciones
|
||||
$locations = [
|
||||
['id' => 'H2', 'label' => 'Despues de H2 (titulos)', 'enabled' => $h2Enabled, 'prob' => $h2Prob, 'icon' => 'bi-type-h2'],
|
||||
['id' => 'H3', 'label' => 'Despues de H3 (subtitulos)', 'enabled' => $h3Enabled, 'prob' => $h3Prob, 'icon' => 'bi-type-h3'],
|
||||
['id' => 'Paragraphs', 'label' => 'Despues de parrafos', 'enabled' => $paragraphsEnabled, 'prob' => $paragraphsProb, 'icon' => 'bi-text-paragraph'],
|
||||
['id' => 'Images', 'label' => 'Despues de imagenes', 'enabled' => $imagesEnabled, 'prob' => $imagesProb, 'icon' => 'bi-image'],
|
||||
['id' => 'Lists', 'label' => 'Despues de listas', 'enabled' => $listsEnabled, 'prob' => $listsProb, 'icon' => 'bi-list-ul'],
|
||||
['id' => 'Blockquotes', 'label' => 'Despues de citas', 'enabled' => $blockquotesEnabled, 'prob' => $blockquotesProb, 'icon' => 'bi-quote'],
|
||||
['id' => 'Tables', 'label' => 'Despues de tablas', 'enabled' => $tablesEnabled, 'prob' => $tablesProb, 'icon' => 'bi-table'],
|
||||
];
|
||||
|
||||
$probOptions = [
|
||||
'100' => '100%',
|
||||
'75' => '75%',
|
||||
'50' => '50%',
|
||||
'25' => '25%'
|
||||
];
|
||||
|
||||
foreach ($locations as $loc) {
|
||||
$enabledId = $cid . 'IncontentAfter' . $loc['id'] . 'Enabled';
|
||||
$probId = $cid . 'IncontentAfter' . $loc['id'] . 'Probability';
|
||||
$checked = checked($loc['enabled'], true, false);
|
||||
|
||||
$html .= ' <div class="row g-2 mb-2 align-items-center">';
|
||||
$html .= ' <div class="col-7">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input type="checkbox" class="form-check-input roi-incontent-location" ';
|
||||
$html .= ' id="' . esc_attr($enabledId) . '" ' . $checked . ' ' . $disabledAttr . '>';
|
||||
$html .= ' <label class="form-check-label small" for="' . esc_attr($enabledId) . '">';
|
||||
$html .= ' <i class="bi ' . esc_attr($loc['icon']) . ' me-1" style="color: #0d6efd;"></i>';
|
||||
$html .= ' ' . esc_html($loc['label']);
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-5">';
|
||||
$html .= ' <select class="form-select form-select-sm roi-incontent-prob" ';
|
||||
$html .= ' id="' . esc_attr($probId) . '" ' . $disabledAttr . '>';
|
||||
foreach ($probOptions as $pValue => $pLabel) {
|
||||
$pSelected = selected($loc['prob'], $pValue, false);
|
||||
$html .= '<option value="' . esc_attr($pValue) . '" ' . $pSelected . '>' . esc_html($pLabel) . '</option>';
|
||||
}
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
}
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= ' </details>';
|
||||
|
||||
// Subseccion: Limites y espaciado
|
||||
$html .= ' <details class="mb-3 border rounded" id="roiLimitsDetails"' . ($isLegacy ? '' : ' open') . '>';
|
||||
$html .= ' <summary class="p-3 bg-light fw-bold" style="cursor: pointer;">';
|
||||
$html .= ' <i class="bi bi-sliders me-1"></i>';
|
||||
$html .= ' Limites y Espaciado';
|
||||
$html .= ' </summary>';
|
||||
$html .= ' <div class="p-3">';
|
||||
$html .= ' <div class="row g-3">';
|
||||
|
||||
// Max total ads
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$html .= ' <label for="' . esc_attr($cid) . 'IncontentMaxTotalAds" class="form-label small fw-semibold">';
|
||||
$html .= ' Maximo total de ads';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select class="form-select form-select-sm" id="' . esc_attr($cid) . 'IncontentMaxTotalAds" ' . $disabledAttr . '>';
|
||||
for ($i = 1; $i <= 15; $i++) {
|
||||
$iStr = (string)$i;
|
||||
$adSelected = selected($maxAds, $iStr, false);
|
||||
$label = $i === 1 ? '1 anuncio' : $i . ' anuncios';
|
||||
$html .= '<option value="' . esc_attr($iStr) . '" ' . $adSelected . '>' . esc_html($label) . '</option>';
|
||||
}
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Min spacing
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$html .= ' <label for="' . esc_attr($cid) . 'IncontentMinSpacing" class="form-label small fw-semibold">';
|
||||
$html .= ' Espaciado minimo (elementos)';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select class="form-select form-select-sm" id="' . esc_attr($cid) . 'IncontentMinSpacing" ' . $disabledAttr . '>';
|
||||
$spacingOptions = [
|
||||
'2' => '2 elementos',
|
||||
'3' => '3 elementos',
|
||||
'4' => '4 elementos',
|
||||
'5' => '5 elementos',
|
||||
'6' => '6 elementos'
|
||||
];
|
||||
foreach ($spacingOptions as $sValue => $sLabel) {
|
||||
$sSelected = selected($minSpacing, $sValue, false);
|
||||
$html .= '<option value="' . esc_attr($sValue) . '" ' . $sSelected . '>' . esc_html($sLabel) . '</option>';
|
||||
}
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Formato de ads
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$html .= ' <label for="' . esc_attr($cid) . 'IncontentFormat" class="form-label small fw-semibold">';
|
||||
$html .= ' Formato de ads';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select class="form-select form-select-sm" id="' . esc_attr($cid) . 'IncontentFormat" ' . $disabledAttr . '>';
|
||||
$formatOptions = [
|
||||
'in-article' => 'In-Article (fluid)',
|
||||
'auto' => 'Auto (responsive)'
|
||||
];
|
||||
foreach ($formatOptions as $fValue => $fLabel) {
|
||||
$fSelected = selected($format, $fValue, false);
|
||||
$html .= '<option value="' . esc_attr($fValue) . '" ' . $fSelected . '>' . esc_html($fLabel) . '</option>';
|
||||
}
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Priority mode
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$html .= ' <label for="' . esc_attr($cid) . 'IncontentPriorityMode" class="form-label small fw-semibold">';
|
||||
$html .= ' Estrategia de seleccion';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select class="form-select form-select-sm" id="' . esc_attr($cid) . 'IncontentPriorityMode" ' . $disabledAttr . '>';
|
||||
$priorityOptions = [
|
||||
'position' => 'Por posicion (distribucion uniforme)',
|
||||
'priority' => 'Por prioridad (maximizar H2/H3)'
|
||||
];
|
||||
foreach ($priorityOptions as $pmValue => $pmLabel) {
|
||||
$pmSelected = selected($priorityMode, $pmValue, false);
|
||||
$html .= '<option value="' . esc_attr($pmValue) . '" ' . $pmSelected . '>' . esc_html($pmLabel) . '</option>';
|
||||
}
|
||||
$html .= ' </select>';
|
||||
$html .= ' <small class="text-muted">Como resolver conflictos cuando dos ubicaciones estan muy cerca</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </details>';
|
||||
|
||||
// Warning para densidad alta
|
||||
$html .= ' <div id="roiHighDensityWarning" class="alert alert-warning small d-none">';
|
||||
$html .= ' <i class="bi bi-exclamation-triangle me-1"></i>';
|
||||
$html .= ' <strong>Atencion:</strong> Densidad alta (>10 ads) puede afectar UX y violar politicas de AdSense.';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seccion especial para in-content ads con configuracion de 1-8 random
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user