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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user