fix(toc): inyectar IDs de headings via JavaScript

Problema: Los headings no tenían atributo id porque el filtro
PHP the_content se agregaba después de procesar el contenido.

Solución: El script del TOC ahora:
1. Busca cada link del TOC
2. Encuentra el heading correspondiente por texto
3. Asigna el ID esperado al heading
4. Luego configura smooth scroll e IntersectionObserver

Esto resuelve:
- Links del TOC no clickeables
- Smooth scroll no funcionaba
- ScrollSpy no rastreaba secciones

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
FrankZamora
2025-11-29 11:07:57 -06:00
parent 77a59d0db8
commit 8361e14862

View File

@@ -156,9 +156,10 @@ final class TableOfContentsRenderer implements RendererInterface
$existingId = $heading->getAttribute('id');
// Generar anchor ID - la inyección de IDs se hace via JavaScript
// para evitar problemas con orden de filtros WordPress y caché
if (empty($existingId)) {
$anchor = $this->generateAnchorId($text);
$this->addIdToHeading($text, $anchor);
} else {
$anchor = $existingId;
}
@@ -428,16 +429,58 @@ final class TableOfContentsRenderer implements RendererInterface
return '';
}
// Intersection Observer elimina forced reflows del ScrollSpy
// En lugar de leer offsetTop en cada scroll event (60+/seg),
// el navegador notifica solo cuando secciones entran/salen del viewport
// Script con inyección de IDs client-side para mayor fiabilidad
// Resuelve problemas de orden de filtros WordPress y caché
$script = <<<JS
<script>
document.addEventListener('DOMContentLoaded', function() {
var tocLinks = document.querySelectorAll('.toc-link');
var offsetTop = {$scrollOffset};
// Smooth scroll on click - usa getBoundingClientRect una sola vez por click
// ===========================================
// PASO 1: Generar IDs para headings sin ID
// ===========================================
function generateSlug(text) {
return text
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '') // Remover acentos
.replace(/[^a-z0-9]+/g, '-') // Reemplazar caracteres especiales
.replace(/^-+|-+$/g, ''); // Trim guiones
}
// Mapeo de IDs existentes para evitar duplicados
var existingIds = {};
document.querySelectorAll('[id]').forEach(function(el) {
existingIds[el.id] = true;
});
// Para cada link del TOC, buscar el heading correspondiente y asignarle ID
tocLinks.forEach(function(link) {
var targetId = link.getAttribute('href').substring(1); // Remover #
var existingElement = document.getElementById(targetId);
// Si ya existe el elemento con ese ID, no hacer nada
if (existingElement) return;
// Buscar el heading por su texto
var linkText = link.textContent.trim();
var headings = document.querySelectorAll('h2, h3, h4, h5, h6');
headings.forEach(function(heading) {
// Comparar texto normalizado
var headingText = heading.textContent.trim();
if (headingText === linkText && !heading.id) {
// Asignar el ID que espera el TOC
heading.id = targetId;
existingIds[targetId] = true;
}
});
});
// ===========================================
// PASO 2: Smooth scroll on click
// ===========================================
tocLinks.forEach(function(link) {
link.addEventListener('click', function(e) {
e.preventDefault();
@@ -456,7 +499,9 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
// ScrollSpy con Intersection Observer (sin forced reflows)
// ===========================================
// PASO 3: ScrollSpy con Intersection Observer
// ===========================================
var sections = [];
var sectionMap = {};
@@ -471,14 +516,12 @@ document.addEventListener('DOMContentLoaded', function() {
if (sections.length === 0) return;
// Track de secciones visibles para determinar la activa
var visibleSections = new Set();
var currentActive = null;
function updateActiveFromVisible() {
if (visibleSections.size === 0) return;
// Encontrar la sección visible más arriba en el documento
var topMostSection = null;
var topMostPosition = Infinity;
@@ -494,12 +537,10 @@ document.addEventListener('DOMContentLoaded', function() {
});
if (topMostSection && topMostSection !== currentActive) {
// Remover active de todos
tocLinks.forEach(function(link) {
link.classList.remove('active');
});
// Activar el correspondiente
if (sectionMap[topMostSection]) {
sectionMap[topMostSection].classList.add('active');
currentActive = topMostSection;
@@ -507,7 +548,6 @@ document.addEventListener('DOMContentLoaded', function() {
}
}
// Intersection Observer - el navegador maneja la detección eficientemente
var observerOptions = {
root: null,
rootMargin: '-' + offsetTop + 'px 0px -50% 0px',
@@ -524,16 +564,14 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
// Usar requestAnimationFrame para batch DOM updates
requestAnimationFrame(updateActiveFromVisible);
}, observerOptions);
// Observar todas las secciones
sections.forEach(function(section) {
observer.observe(section);
});
// Activar la primera sección visible al cargar
// Activar primera sección al cargar
requestAnimationFrame(function() {
if (visibleSections.size === 0 && sections.length > 0) {
var firstId = sections[0].id;