const body = document.body;
const stickyBar = document.getElementById("uBar");
const burger = document.getElementById("uBurger");
const drawerShell = document.getElementById("uDrawerShell");
const drawer = document.getElementById("uDrawer");
const drawerCloseButtons = document.querySelectorAll("[data-drawer-close]");
const searchOverlay = document.getElementById("uSearchDialog");
const searchPanel = searchOverlay?.querySelector(".u-search-panel") || null;
const searchInput = document.getElementById("uSiteSearchInput");
const searchResults = document.getElementById("uSearchResults");
const searchStatus = document.getElementById("uSearchStatus");
const searchToggles = document.querySelectorAll("[data-site-search-toggle]");
const searchCloseButtons = document.querySelectorAll("[data-site-search-close]");
let drawerOpen = false;
let searchOpen = false;
let drawerOrigin = null;
let searchOrigin = null;
let siteSearchItems = [];
let siteSearchLoaded = false;
let siteSearchRequest = null;
const FOCUSABLE_SELECTOR = [
'a[href]',
'button:not([disabled])',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"])',
].join(",");
const syncBodyLock = () => {
body.classList.toggle("u-lock-scroll", drawerOpen || searchOpen);
};
const getFocusableElements = (container) => {
if (!container) {
return [];
}
return Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)).filter((element) => {
if (!(element instanceof HTMLElement)) {
return false;
}
return !element.hasAttribute("hidden") && element.offsetParent !== null;
});
};
const trapFocus = (event, container) => {
if (event.key !== "Tab") {
return;
}
const focusable = getFocusableElements(container);
if (!focusable.length) {
return;
}
const first = focusable[0];
const last = focusable[focusable.length - 1];
const active = document.activeElement;
if (event.shiftKey && active === first) {
event.preventDefault();
last.focus();
return;
}
if (!event.shiftKey && active === last) {
event.preventDefault();
first.focus();
}
};
const restoreFocus = (element) => {
if (element instanceof HTMLElement) {
window.setTimeout(() => element.focus(), 0);
}
};
if (stickyBar) {
const onScroll = () => stickyBar.classList.toggle("stuck", window.scrollY > 6);
window.addEventListener("scroll", onScroll, { passive: true });
onScroll();
}
const setDrawerState = (open, options = {}) => {
if (!drawerShell || !burger) {
return;
}
const { restoreOnClose = true } = options;
drawerOpen = open;
drawerShell.classList.toggle("is-open", open);
drawerShell.setAttribute("aria-hidden", open ? "false" : "true");
burger.setAttribute("aria-expanded", String(open));
syncBodyLock();
if (open) {
drawerOrigin = document.activeElement instanceof HTMLElement ? document.activeElement : burger;
const firstFocusable = getFocusableElements(drawer)[0];
window.setTimeout(() => {
(firstFocusable || drawer || burger).focus();
}, 20);
return;
}
if (restoreOnClose) {
restoreFocus(drawerOrigin);
}
};
const closeDrawer = (options) => setDrawerState(false, options);
const openDrawer = () => setDrawerState(true);
if (burger && drawerShell) {
burger.addEventListener("click", () => {
if (drawerOpen) {
closeDrawer();
return;
}
openDrawer();
});
}
drawerCloseButtons.forEach((button) => {
button.addEventListener("click", () => closeDrawer());
});
if (drawer) {
drawer.querySelectorAll("a").forEach((link) => {
link.addEventListener("click", () => closeDrawer());
});
}
const escapeHtml = (value = "") => value.replace(/[&<>"']/g, (match) => {
const map = {
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
};
return map[match] || match;
});
const normalizeText = (value = "") => value
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
.trim();
const loadSiteSearchItems = async () => {
if (siteSearchLoaded) {
return siteSearchItems;
}
if (siteSearchRequest) {
return siteSearchRequest;
}
siteSearchRequest = fetch("site-search-index.php", {
headers: { Accept: "application/json" },
})
.then((response) => {
if (!response.ok) {
throw new Error(`Search index request failed: ${response.status}`);
}
return response.json();
})
.then((parsed) => {
siteSearchItems = Array.isArray(parsed)
? parsed.map((item, index) => {
const title = String(item.title || "");
const description = String(item.description || "");
const section = String(item.section || "General");
const url = String(item.url || "#");
const keywords = Array.isArray(item.keywords) ? item.keywords.map(String) : [];
return {
title,
description,
section,
url,
external: Boolean(item.external),
index,
haystack: normalizeText([title, description, section, ...keywords].join(" ")),
};
})
: [];
siteSearchLoaded = true;
return siteSearchItems;
})
.catch(() => {
siteSearchItems = [];
siteSearchLoaded = true;
return siteSearchItems;
})
.finally(() => {
siteSearchRequest = null;
});
return siteSearchRequest;
};
const scoreSearchItem = (item, query, terms) => {
const normalizedTitle = normalizeText(item.title);
const normalizedDescription = normalizeText(item.description);
let score = 0;
if (normalizedTitle === query) {
score += 12;
}
if (normalizedTitle.startsWith(query)) {
score += 7;
}
if (normalizedTitle.includes(query)) {
score += 5;
}
if (normalizedDescription.includes(query)) {
score += 3;
}
if (item.haystack.includes(query)) {
score += 2;
}
terms.forEach((term) => {
if (normalizedTitle.includes(term)) {
score += 2;
}
if (item.haystack.includes(term)) {
score += 1;
}
});
return score;
};
const renderSearchResults = (rawQuery = "") => {
if (!searchResults || !searchStatus) {
return;
}
const originalQuery = rawQuery.trim();
const normalizedQuery = normalizeText(originalQuery);
const terms = normalizedQuery.split(/\s+/).filter(Boolean);
let results = [];
if (!siteSearchItems.length) {
searchStatus.textContent = "El indice de busqueda no esta disponible.";
searchResults.innerHTML = '<div class="u-search-empty">No fue posible cargar los elementos del buscador.</div>';
return;
}
if (!terms.length) {
results = siteSearchItems.slice(0, 10);
searchStatus.textContent = "Sugerencias rapidas del sitio.";
} else {
results = siteSearchItems
.map((item) => ({
...item,
score: scoreSearchItem(item, normalizedQuery, terms),
}))
.filter((item) => item.score > 0 && terms.every((term) => item.haystack.includes(term)))
.sort((a, b) => {
if (b.score !== a.score) {
return b.score - a.score;
}
return a.index - b.index;
})
.slice(0, 14);
searchStatus.textContent = results.length
? `${results.length} resultado${results.length === 1 ? "" : "s"} para "${originalQuery}".`
: `No encontramos coincidencias para "${originalQuery}".`;
}
if (!results.length) {
searchResults.innerHTML = '<div class="u-search-empty">Prueba con el nombre de un recurso, servicio, tutorial o sede.</div>';
return;
}
searchResults.innerHTML = results.map((item) => {
const target = item.external ? ' target="_blank" rel="noopener noreferrer"' : "";
const flag = item.external ? "Enlace externo" : "Dentro del sitio";
const description = item.description !== ""
? `<p>${escapeHtml(item.description)}</p>`
: "";
return `
<a class="u-search-result" href="${escapeHtml(item.url)}"${target}>
<div class="u-search-result__meta">
<span class="u-search-result__section">${escapeHtml(item.section)}</span>
<span class="u-search-result__flag">${flag}</span>
</div>
<strong>${escapeHtml(item.title)}</strong>
${description}
</a>
`;
}).join("");
};
const setSearchState = async (open) => {
if (!searchOverlay) {
return;
}
searchOpen = open;
searchOverlay.hidden = !open;
searchToggles.forEach((button) => {
button.setAttribute("aria-expanded", String(open));
});
syncBodyLock();
if (open) {
if (searchStatus) {
searchStatus.textContent = "Cargando indice de busqueda...";
}
if (searchResults) {
searchResults.innerHTML = "";
}
await loadSiteSearchItems();
renderSearchResults(searchInput?.value || "");
window.setTimeout(() => {
searchInput?.focus();
searchInput?.select();
}, 40);
return;
}
restoreFocus(searchOrigin);
};
const openSearch = async () => {
if (searchOpen) {
return;
}
searchOrigin = document.activeElement instanceof HTMLElement ? document.activeElement : null;
if (drawerOpen) {
closeDrawer({ restoreOnClose: false });
}
await setSearchState(true);
};
const closeSearch = () => {
if (!searchOpen) {
return;
}
setSearchState(false);
};
searchToggles.forEach((button) => {
button.addEventListener("click", () => {
openSearch();
});
});
searchCloseButtons.forEach((button) => {
button.addEventListener("click", closeSearch);
});
if (searchInput) {
searchInput.addEventListener("input", (event) => {
renderSearchResults(event.currentTarget.value);
});
}
document.addEventListener("keydown", (event) => {
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k") {
event.preventDefault();
openSearch();
return;
}
if (event.key === "Tab") {
if (searchOpen && searchPanel) {
trapFocus(event, searchPanel);
return;
}
if (drawerOpen && drawer) {
trapFocus(event, drawer);
return;
}
}
if (event.key === "Escape") {
if (searchOpen) {
closeSearch();
return;
}
if (drawerOpen) {
closeDrawer();
}
}
});
// Filtros y busqueda de recursos digitales.
const grid = document.getElementById("rdGrid");
const tabs = document.querySelectorAll(".rd-tab");
const query = document.getElementById("rdQuery");
let activeType = "all";
function applyFilters() {
if (!grid) {
return;
}
const textQuery = normalizeText(query?.value || "");
grid.querySelectorAll(".rd-card").forEach((card) => {
const type = card.getAttribute("data-tipo");
const text = normalizeText(card.textContent || "");
const matchType = activeType === "all" || type === activeType;
const matchText = !textQuery || text.includes(textQuery);
card.style.display = matchType && matchText ? "" : "none";
});
}
tabs.forEach((button) => {
button.addEventListener("click", () => {
tabs.forEach((tab) => {
tab.classList.remove("is-active");
tab.setAttribute("aria-selected", "false");
});
button.classList.add("is-active");
button.setAttribute("aria-selected", "true");
activeType = button.getAttribute("data-filter") || "all";
applyFilters();
});
});
if (query) {
query.addEventListener("input", applyFilters);
}