fix(analytics): Corregir URLs en analytics usando post_name desde BD
Problema: - URLs en analytics mostraban dominio incorrecto (HTTP_HOST) - URLs usaban formato /?p=ID en lugar de permalinks Solución: - class-search-engine.php: Agregar propiedad $prefix, incluir p.post_name en 5 queries fetch, agregar helpers getSiteUrlFromDb(), getPermalinkStructure() y buildPermalink() - search-endpoint.php: Obtener site_url y permalink_structure desde wp_options, construir URLs con post_name - click-endpoint.php: Fallback de dest usando post_name desde BD Archivos modificados: - includes/class-search-engine.php - api/search-endpoint.php - api/click-endpoint.php 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
704
assets/js/search-handler.js
Normal file
704
assets/js/search-handler.js
Normal file
@@ -0,0 +1,704 @@
|
||||
/**
|
||||
* ROI APU Search Handler
|
||||
*
|
||||
* Handles AJAX search requests with debouncing and pagination
|
||||
*
|
||||
* @package ROI_APU_Search
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Configuration from WordPress
|
||||
const config = window.roiApuSearch || {
|
||||
ajaxUrl: '/wp-admin/admin-ajax.php',
|
||||
apiUrl: '',
|
||||
clickUrl: '',
|
||||
nonce: '',
|
||||
ads: { enabled: false }
|
||||
};
|
||||
|
||||
// State management per instance
|
||||
const instances = new Map();
|
||||
|
||||
// =============================================================================
|
||||
// FUNCIONES DE ADSENSE
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Genera HTML de anuncio AdSense
|
||||
* @param {string} format - Formato del anuncio (auto, in-article, autorelaxed, display)
|
||||
* @param {string|number} position - Posicion del anuncio para identificacion
|
||||
* @returns {string} HTML del anuncio o string vacio si hay error
|
||||
*/
|
||||
function generateAdHtml(format, position) {
|
||||
try {
|
||||
var ads = config.ads;
|
||||
if (!ads || !ads.enabled) return '';
|
||||
|
||||
var slotInfo = getSlotAndFormatByType(format, ads.slots);
|
||||
if (!slotInfo.slot || !ads.publisherId) return '';
|
||||
|
||||
// Determinar tipo de script segun delay
|
||||
var scriptType = ads.delay && ads.delay.enabled ? 'text/plain' : 'text/javascript';
|
||||
var dataAttr = ads.delay && ads.delay.enabled ? ' data-adsense-push' : '';
|
||||
|
||||
// Generar atributos segun formato
|
||||
var formatAttrs = getAdFormatAttributes(format);
|
||||
|
||||
return '<div class="roi-apu-ad-item" data-ad-position="' + position + '">' +
|
||||
'<ins class="adsbygoogle" ' +
|
||||
'style="display:block;min-height:250px" ' +
|
||||
'data-ad-client="' + ads.publisherId + '" ' +
|
||||
'data-ad-slot="' + slotInfo.slot + '" ' +
|
||||
formatAttrs + '></ins>' +
|
||||
'<script type="' + scriptType + '"' + dataAttr + '>' +
|
||||
'try { (adsbygoogle = window.adsbygoogle || []).push({}); } catch(e) { console.warn("AdSense push failed:", e); }' +
|
||||
'<\/script>' +
|
||||
'</div>';
|
||||
} catch (e) {
|
||||
console.warn('generateAdHtml error:', e);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el slot y formato correcto segun el tipo
|
||||
* @param {string} format - Tipo de formato solicitado
|
||||
* @param {object} slots - Objeto con los slots disponibles
|
||||
* @returns {object} {slot, format}
|
||||
*/
|
||||
function getSlotAndFormatByType(format, slots) {
|
||||
if (!slots) return { slot: null, format: 'auto' };
|
||||
|
||||
switch(format) {
|
||||
case 'in-article':
|
||||
return { slot: slots.inArticle || slots.auto, format: 'fluid' };
|
||||
case 'autorelaxed':
|
||||
return { slot: slots.autorelaxed || slots.auto, format: 'autorelaxed' };
|
||||
case 'display':
|
||||
return { slot: slots.display || slots.auto, format: 'rectangle' };
|
||||
case 'auto':
|
||||
default:
|
||||
return { slot: slots.auto, format: 'auto' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera atributos HTML segun el formato de anuncio
|
||||
* @param {string} format - Tipo de formato
|
||||
* @returns {string} Atributos HTML
|
||||
*/
|
||||
function getAdFormatAttributes(format) {
|
||||
switch(format) {
|
||||
case 'in-article':
|
||||
return 'data-ad-layout="in-article" data-ad-format="fluid"';
|
||||
case 'autorelaxed':
|
||||
return 'data-ad-format="autorelaxed"';
|
||||
case 'display':
|
||||
return 'data-ad-format="rectangle" data-full-width-responsive="false"';
|
||||
case 'auto':
|
||||
default:
|
||||
return 'data-ad-format="auto" data-full-width-responsive="true"';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mezcla un array aleatoriamente (Fisher-Yates)
|
||||
* @param {Array} array - Array a mezclar
|
||||
* @returns {Array} Array mezclado
|
||||
*/
|
||||
function shuffleArray(array) {
|
||||
var arr = array.slice(); // Copia para no mutar original
|
||||
for (var i = arr.length - 1; i > 0; i--) {
|
||||
var j = Math.floor(Math.random() * (i + 1));
|
||||
var temp = arr[i];
|
||||
arr[i] = arr[j];
|
||||
arr[j] = temp;
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcula posiciones donde insertar anuncios
|
||||
* @param {number} totalResults - Total de resultados de busqueda
|
||||
* @param {object} adsConfig - Configuracion de ads.betweenAds
|
||||
* @returns {number[]} Array de posiciones donde insertar anuncios
|
||||
*/
|
||||
function calculateAdPositions(totalResults, adsConfig) {
|
||||
try {
|
||||
if (!adsConfig || !adsConfig.enabled || totalResults < 2) return [];
|
||||
|
||||
// Max 3 por politicas AdSense, y no mas que resultados-1
|
||||
var max = Math.min(adsConfig.max || 1, totalResults - 1, 3);
|
||||
var positions = [];
|
||||
var pool, i;
|
||||
|
||||
if (adsConfig.position === 'random') {
|
||||
// Posiciones aleatorias entre resultados
|
||||
pool = [];
|
||||
for (i = 1; i < totalResults; i++) {
|
||||
pool.push(i);
|
||||
}
|
||||
return shuffleArray(pool).slice(0, max);
|
||||
|
||||
} else if (adsConfig.position === 'fixed') {
|
||||
// Cada N resultados
|
||||
var every = adsConfig.every || 5;
|
||||
for (i = every; i <= totalResults && positions.length < max; i += every) {
|
||||
positions.push(i);
|
||||
}
|
||||
|
||||
} else if (adsConfig.position === 'first_half') {
|
||||
// Solo en la primera mitad de resultados
|
||||
var halfPoint = Math.ceil(totalResults / 2);
|
||||
pool = [];
|
||||
for (i = 1; i < halfPoint; i++) {
|
||||
pool.push(i);
|
||||
}
|
||||
return shuffleArray(pool).slice(0, max);
|
||||
}
|
||||
|
||||
return positions;
|
||||
} catch (e) {
|
||||
console.warn('calculateAdPositions error:', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activa slots de AdSense despues de insertar en DOM
|
||||
*/
|
||||
function activateAdsenseSlots() {
|
||||
try {
|
||||
var ads = config.ads;
|
||||
if (!ads || !ads.enabled) return;
|
||||
|
||||
if (ads.delay && ads.delay.enabled) {
|
||||
// Disparar evento para que adsense-loader.js active los scripts
|
||||
window.dispatchEvent(new CustomEvent('roi-adsense-activate'));
|
||||
} else {
|
||||
// Delay deshabilitado - los scripts inline ya se ejecutaron
|
||||
// pero debemos asegurar que adsbygoogle.push() se llame
|
||||
// para slots insertados dinamicamente
|
||||
setTimeout(function() {
|
||||
var newSlots = document.querySelectorAll('.roi-apu-ad-item ins.adsbygoogle:not([data-adsbygoogle-status])');
|
||||
if (newSlots.length > 0 && typeof adsbygoogle !== 'undefined') {
|
||||
newSlots.forEach(function() {
|
||||
try {
|
||||
(adsbygoogle = window.adsbygoogle || []).push({});
|
||||
} catch(e) {
|
||||
// Ignorar - el slot ya fue procesado
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Observar estado de los ads para mostrar solo cuando esten llenos
|
||||
watchAdSlots();
|
||||
} catch (e) {
|
||||
console.warn('activateAdsenseSlots error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Observa los slots de AdSense y oculta los que no se llenan
|
||||
* Los ads estan VISIBLES por defecto via CSS para que AdSense pueda medirlos
|
||||
* Si no se llenan despues del timeout, se ocultan con clase .ad-unfilled
|
||||
* AdSense agrega data-ad-status="filled" o "unfilled" cuando procesa el slot
|
||||
*/
|
||||
function watchAdSlots() {
|
||||
var adContainers = document.querySelectorAll('.roi-apu-ad-item:not(.ad-watched)');
|
||||
if (!adContainers.length) return;
|
||||
|
||||
adContainers.forEach(function(container) {
|
||||
// Marcar como observado para evitar duplicados
|
||||
container.classList.add('ad-watched');
|
||||
|
||||
var ins = container.querySelector('ins.adsbygoogle');
|
||||
if (!ins) {
|
||||
// Sin elemento ins, ocultar
|
||||
container.classList.add('ad-unfilled');
|
||||
return;
|
||||
}
|
||||
|
||||
// Funcion para verificar estado y actuar
|
||||
function checkStatus() {
|
||||
var status = ins.getAttribute('data-ad-status');
|
||||
if (status === 'filled') {
|
||||
// Ad llenado correctamente - mantener visible
|
||||
container.classList.remove('ad-unfilled');
|
||||
return true;
|
||||
} else if (status === 'unfilled') {
|
||||
// Ad no llenado - ocultar
|
||||
container.classList.add('ad-unfilled');
|
||||
return true;
|
||||
}
|
||||
return false; // Status aun no definido
|
||||
}
|
||||
|
||||
// Verificar si ya tiene estado
|
||||
if (checkStatus()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Usar MutationObserver para detectar cuando AdSense procesa el slot
|
||||
var observer = new MutationObserver(function(mutations) {
|
||||
mutations.forEach(function(mutation) {
|
||||
if (mutation.type === 'attributes' && mutation.attributeName === 'data-ad-status') {
|
||||
if (checkStatus()) {
|
||||
observer.disconnect();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(ins, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-ad-status']
|
||||
});
|
||||
|
||||
// Timeout de seguridad: despues de 5s si no hay status, ocultar
|
||||
// Esto maneja el caso donde AdSense no establece data-ad-status
|
||||
setTimeout(function() {
|
||||
observer.disconnect();
|
||||
var status = ins.getAttribute('data-ad-status');
|
||||
if (status !== 'filled') {
|
||||
// No se lleno o sin respuesta - ocultar para evitar espacio vacio
|
||||
container.classList.add('ad-unfilled');
|
||||
}
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all search containers on the page
|
||||
*/
|
||||
function init() {
|
||||
const containers = document.querySelectorAll('.roi-apu-search-container');
|
||||
containers.forEach(initContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a single search container
|
||||
*/
|
||||
function initContainer(container) {
|
||||
const id = container.id;
|
||||
if (instances.has(id)) return;
|
||||
|
||||
const state = {
|
||||
container,
|
||||
input: container.querySelector('.roi-apu-search-input'),
|
||||
button: container.querySelector('.roi-apu-search-btn'),
|
||||
errorEl: container.querySelector('.roi-apu-error'),
|
||||
loadingEl: container.querySelector('.roi-apu-loading'),
|
||||
infoEl: container.querySelector('.roi-apu-info'),
|
||||
resultsEl: container.querySelector('.roi-apu-results'),
|
||||
paginationEl: container.querySelector('.roi-apu-pagination'),
|
||||
categories: container.dataset.categories || '',
|
||||
perPage: parseInt(container.dataset.perPage, 10) || 10,
|
||||
minChars: parseInt(container.dataset.minChars, 10) || 3,
|
||||
showInfo: container.dataset.showInfo === 'true',
|
||||
currentPage: 1,
|
||||
currentTerm: '',
|
||||
totalResults: 0,
|
||||
searchId: null,
|
||||
debounceTimer: null,
|
||||
abortController: null
|
||||
};
|
||||
|
||||
instances.set(id, state);
|
||||
|
||||
// Event listeners
|
||||
state.input.addEventListener('input', () => handleInput(state));
|
||||
state.input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
doSearch(state);
|
||||
}
|
||||
});
|
||||
state.button.addEventListener('click', () => doSearch(state));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle input with debounce
|
||||
*/
|
||||
function handleInput(state) {
|
||||
clearTimeout(state.debounceTimer);
|
||||
hideError(state);
|
||||
|
||||
const term = state.input.value.trim();
|
||||
|
||||
if (term.length === 0) {
|
||||
clearResults(state);
|
||||
return;
|
||||
}
|
||||
|
||||
if (term.length < state.minChars) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Debounce search
|
||||
state.debounceTimer = setTimeout(() => {
|
||||
doSearch(state);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute search
|
||||
*/
|
||||
async function doSearch(state, page = 1) {
|
||||
const term = state.input.value.trim();
|
||||
|
||||
if (term.length < state.minChars) {
|
||||
showError(state, `Ingresa al menos ${state.minChars} caracteres`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel previous request
|
||||
if (state.abortController) {
|
||||
state.abortController.abort();
|
||||
}
|
||||
state.abortController = new AbortController();
|
||||
|
||||
state.currentTerm = term;
|
||||
state.currentPage = page;
|
||||
|
||||
showLoading(state);
|
||||
hideError(state);
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'roi_apu_search');
|
||||
formData.append('nonce', config.nonce);
|
||||
formData.append('term', term);
|
||||
formData.append('page', page.toString());
|
||||
formData.append('per_page', state.perPage.toString());
|
||||
if (state.categories) {
|
||||
formData.append('categories', state.categories);
|
||||
}
|
||||
|
||||
// Try fast endpoint first, fallback to admin-ajax
|
||||
let url = config.apiUrl || config.ajaxUrl;
|
||||
let response;
|
||||
|
||||
try {
|
||||
response = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
signal: state.abortController.signal
|
||||
});
|
||||
} catch (err) {
|
||||
// Fallback to admin-ajax if fast endpoint fails
|
||||
if (url !== config.ajaxUrl) {
|
||||
url = config.ajaxUrl;
|
||||
response = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
signal: state.abortController.signal
|
||||
});
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.data?.message || 'Error en la busqueda');
|
||||
}
|
||||
|
||||
state.totalResults = data.data.total || 0;
|
||||
state.searchId = data.data.search_id || null;
|
||||
renderResults(state, data.data);
|
||||
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') {
|
||||
return; // Ignore aborted requests
|
||||
}
|
||||
console.error('ROI APU Search Error:', err);
|
||||
showError(state, 'Error al realizar la busqueda. Intenta de nuevo.');
|
||||
clearResults(state);
|
||||
} finally {
|
||||
hideLoading(state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render search results (con soporte para ads)
|
||||
*/
|
||||
function renderResults(state, data) {
|
||||
var rows = data.rows;
|
||||
var total = data.total;
|
||||
var time_ms = data.time_ms;
|
||||
var ads = config.ads;
|
||||
|
||||
// Show info
|
||||
if (state.showInfo && total > 0) {
|
||||
state.infoEl.innerHTML =
|
||||
'<span class="roi-apu-info-total">' + formatNumber(total) + ' resultados</span>' +
|
||||
'<span class="roi-apu-info-time">' + time_ms + 'ms</span>';
|
||||
state.infoEl.style.display = 'flex';
|
||||
} else {
|
||||
state.infoEl.style.display = 'none';
|
||||
}
|
||||
|
||||
// Render no results message
|
||||
if (rows.length === 0) {
|
||||
state.resultsEl.innerHTML =
|
||||
'<div class="roi-apu-no-results">' +
|
||||
'<p>No se encontraron resultados para "<strong>' + escapeHtml(state.currentTerm) + '</strong>"</p>' +
|
||||
'<p class="roi-apu-suggestions">Intenta con terminos mas generales o revisa la ortografia.</p>' +
|
||||
'</div>';
|
||||
state.paginationEl.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
// Calcular posiciones de anuncios (solo si hay resultados y ads habilitados)
|
||||
var adPositions = {};
|
||||
if (ads && ads.enabled && ads.betweenAds && ads.betweenAds.enabled && rows.length > 1) {
|
||||
var positions = calculateAdPositions(rows.length, ads.betweenAds);
|
||||
for (var p = 0; p < positions.length; p++) {
|
||||
adPositions[positions[p]] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Generar anuncio superior (solo en primera pagina y si hay resultados)
|
||||
var topAdHtml = '';
|
||||
if (ads && ads.enabled && ads.topAd && ads.topAd.enabled && state.currentPage === 1 && total > 0) {
|
||||
topAdHtml = generateAdHtml(ads.topAd.format, 'top');
|
||||
}
|
||||
|
||||
// Renderizar resultados con anuncios intercalados
|
||||
var resultsHtml = '';
|
||||
for (var index = 0; index < rows.length; index++) {
|
||||
var row = rows[index];
|
||||
var position = (state.currentPage - 1) * state.perPage + index + 1;
|
||||
var title = highlightTerm(row.post_title, state.currentTerm);
|
||||
var date = formatDate(row.post_date);
|
||||
|
||||
// Build click tracking URL if available
|
||||
var href = row.permalink;
|
||||
if (config.clickUrl && state.searchId) {
|
||||
var params = new URLSearchParams({
|
||||
sid: state.searchId,
|
||||
pid: row.ID,
|
||||
pos: position,
|
||||
page: state.currentPage,
|
||||
dest: row.permalink
|
||||
});
|
||||
href = config.clickUrl + '?' + params.toString();
|
||||
}
|
||||
|
||||
resultsHtml +=
|
||||
'<a href="' + escapeHtml(href) + '" class="roi-apu-result-item" target="_blank" rel="noopener">' +
|
||||
'<span class="roi-apu-result-position">' + position + '</span>' +
|
||||
'<div class="roi-apu-result-content">' +
|
||||
'<h3 class="roi-apu-result-title">' + title + '</h3>' +
|
||||
'<span class="roi-apu-result-date">' + date + '</span>' +
|
||||
'</div>' +
|
||||
'<svg class="roi-apu-result-arrow" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">' +
|
||||
'<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>' +
|
||||
'</svg>' +
|
||||
'</a>';
|
||||
|
||||
// Insertar anuncio despues de este item si corresponde
|
||||
if (adPositions[index + 1]) {
|
||||
resultsHtml += generateAdHtml(ads.betweenAds.format, 'between-' + (index + 1));
|
||||
}
|
||||
}
|
||||
|
||||
// Construir HTML final
|
||||
state.resultsEl.innerHTML = topAdHtml + '<div class="roi-apu-results-list">' + resultsHtml + '</div>';
|
||||
|
||||
// Render pagination
|
||||
renderPagination(state);
|
||||
|
||||
// Activar anuncios despues de insertar en DOM
|
||||
var hasAds = topAdHtml || Object.keys(adPositions).length > 0;
|
||||
if (ads && ads.enabled && hasAds) {
|
||||
activateAdsenseSlots();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render pagination controls
|
||||
*/
|
||||
function renderPagination(state) {
|
||||
const totalPages = Math.ceil(state.totalResults / state.perPage);
|
||||
|
||||
if (totalPages <= 1) {
|
||||
state.paginationEl.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const currentPage = state.currentPage;
|
||||
let html = '<div class="roi-apu-pagination-inner">';
|
||||
|
||||
// Previous button
|
||||
if (currentPage > 1) {
|
||||
html += `<button type="button" class="roi-apu-page-btn roi-apu-prev" data-page="${currentPage - 1}">« Anterior</button>`;
|
||||
}
|
||||
|
||||
// Page numbers
|
||||
const startPage = Math.max(1, currentPage - 2);
|
||||
const endPage = Math.min(totalPages, startPage + 4);
|
||||
|
||||
if (startPage > 1) {
|
||||
html += `<button type="button" class="roi-apu-page-btn" data-page="1">1</button>`;
|
||||
if (startPage > 2) {
|
||||
html += `<span class="roi-apu-page-ellipsis">...</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
const activeClass = i === currentPage ? ' roi-apu-page-active' : '';
|
||||
html += `<button type="button" class="roi-apu-page-btn${activeClass}" data-page="${i}">${i}</button>`;
|
||||
}
|
||||
|
||||
if (endPage < totalPages) {
|
||||
if (endPage < totalPages - 1) {
|
||||
html += `<span class="roi-apu-page-ellipsis">...</span>`;
|
||||
}
|
||||
html += `<button type="button" class="roi-apu-page-btn" data-page="${totalPages}">${totalPages}</button>`;
|
||||
}
|
||||
|
||||
// Next button
|
||||
if (currentPage < totalPages) {
|
||||
html += `<button type="button" class="roi-apu-page-btn roi-apu-next" data-page="${currentPage + 1}">Siguiente »</button>`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
state.paginationEl.innerHTML = html;
|
||||
state.paginationEl.style.display = 'flex';
|
||||
|
||||
// Add click handlers
|
||||
state.paginationEl.querySelectorAll('.roi-apu-page-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const page = parseInt(btn.dataset.page, 10);
|
||||
doSearch(state, page);
|
||||
// Scroll to top of results
|
||||
state.container.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight search term in text
|
||||
*/
|
||||
function highlightTerm(text, term) {
|
||||
const escaped = escapeHtml(text);
|
||||
const terms = term.split(/\s+/).filter(t => t.length >= 2);
|
||||
|
||||
let result = escaped;
|
||||
terms.forEach(t => {
|
||||
const regex = new RegExp(`(${escapeRegex(t)})`, 'gi');
|
||||
result = result.replace(regex, '<mark>$1</mark>');
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format number with thousands separator
|
||||
*/
|
||||
function formatNumber(num) {
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date
|
||||
*/
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('es-MX', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape HTML entities
|
||||
*/
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape regex special characters
|
||||
*/
|
||||
function escapeRegex(str) {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show loading state
|
||||
*/
|
||||
function showLoading(state) {
|
||||
state.loadingEl.style.display = 'flex';
|
||||
state.button.disabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide loading state
|
||||
*/
|
||||
function hideLoading(state) {
|
||||
state.loadingEl.style.display = 'none';
|
||||
state.button.disabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error message
|
||||
*/
|
||||
function showError(state, message) {
|
||||
state.errorEl.textContent = message;
|
||||
state.errorEl.style.display = 'block';
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide error message
|
||||
*/
|
||||
function hideError(state) {
|
||||
state.errorEl.style.display = 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear results
|
||||
*/
|
||||
function clearResults(state) {
|
||||
state.resultsEl.innerHTML = '';
|
||||
state.paginationEl.style.display = 'none';
|
||||
state.infoEl.style.display = 'none';
|
||||
state.totalResults = 0;
|
||||
state.currentPage = 1;
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
// Expose for external use
|
||||
window.ROI_APU_Search = {
|
||||
init,
|
||||
search: (containerId, term) => {
|
||||
const state = instances.get(containerId);
|
||||
if (state) {
|
||||
state.input.value = term;
|
||||
doSearch(state);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user