From 8361e148629227703d618c129036866a3efbaff0 Mon Sep 17 00:00:00 2001 From: FrankZamora Date: Sat, 29 Nov 2025 11:07:57 -0600 Subject: [PATCH] fix(toc): inyectar IDs de headings via JavaScript MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../Ui/TableOfContentsRenderer.php | 66 +++++++++++++++---- 1 file changed, 52 insertions(+), 14 deletions(-) diff --git a/Public/TableOfContents/Infrastructure/Ui/TableOfContentsRenderer.php b/Public/TableOfContents/Infrastructure/Ui/TableOfContentsRenderer.php index 83ecb097..243e5eb5 100644 --- a/Public/TableOfContents/Infrastructure/Ui/TableOfContentsRenderer.php +++ b/Public/TableOfContents/Infrastructure/Ui/TableOfContentsRenderer.php @@ -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 = << 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;