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');
|
$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)) {
|
if (empty($existingId)) {
|
||||||
$anchor = $this->generateAnchorId($text);
|
$anchor = $this->generateAnchorId($text);
|
||||||
$this->addIdToHeading($text, $anchor);
|
|
||||||
} else {
|
} else {
|
||||||
$anchor = $existingId;
|
$anchor = $existingId;
|
||||||
}
|
}
|
||||||
@@ -428,16 +429,58 @@ final class TableOfContentsRenderer implements RendererInterface
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Intersection Observer elimina forced reflows del ScrollSpy
|
// Script con inyección de IDs client-side para mayor fiabilidad
|
||||||
// En lugar de leer offsetTop en cada scroll event (60+/seg),
|
// Resuelve problemas de orden de filtros WordPress y caché
|
||||||
// el navegador notifica solo cuando secciones entran/salen del viewport
|
|
||||||
$script = <<<JS
|
$script = <<<JS
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
var tocLinks = document.querySelectorAll('.toc-link');
|
var tocLinks = document.querySelectorAll('.toc-link');
|
||||||
var offsetTop = {$scrollOffset};
|
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) {
|
tocLinks.forEach(function(link) {
|
||||||
link.addEventListener('click', function(e) {
|
link.addEventListener('click', function(e) {
|
||||||
e.preventDefault();
|
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 sections = [];
|
||||||
var sectionMap = {};
|
var sectionMap = {};
|
||||||
|
|
||||||
@@ -471,14 +516,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
if (sections.length === 0) return;
|
if (sections.length === 0) return;
|
||||||
|
|
||||||
// Track de secciones visibles para determinar la activa
|
|
||||||
var visibleSections = new Set();
|
var visibleSections = new Set();
|
||||||
var currentActive = null;
|
var currentActive = null;
|
||||||
|
|
||||||
function updateActiveFromVisible() {
|
function updateActiveFromVisible() {
|
||||||
if (visibleSections.size === 0) return;
|
if (visibleSections.size === 0) return;
|
||||||
|
|
||||||
// Encontrar la sección visible más arriba en el documento
|
|
||||||
var topMostSection = null;
|
var topMostSection = null;
|
||||||
var topMostPosition = Infinity;
|
var topMostPosition = Infinity;
|
||||||
|
|
||||||
@@ -494,12 +537,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (topMostSection && topMostSection !== currentActive) {
|
if (topMostSection && topMostSection !== currentActive) {
|
||||||
// Remover active de todos
|
|
||||||
tocLinks.forEach(function(link) {
|
tocLinks.forEach(function(link) {
|
||||||
link.classList.remove('active');
|
link.classList.remove('active');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Activar el correspondiente
|
|
||||||
if (sectionMap[topMostSection]) {
|
if (sectionMap[topMostSection]) {
|
||||||
sectionMap[topMostSection].classList.add('active');
|
sectionMap[topMostSection].classList.add('active');
|
||||||
currentActive = topMostSection;
|
currentActive = topMostSection;
|
||||||
@@ -507,7 +548,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Intersection Observer - el navegador maneja la detección eficientemente
|
|
||||||
var observerOptions = {
|
var observerOptions = {
|
||||||
root: null,
|
root: null,
|
||||||
rootMargin: '-' + offsetTop + 'px 0px -50% 0px',
|
rootMargin: '-' + offsetTop + 'px 0px -50% 0px',
|
||||||
@@ -524,16 +564,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Usar requestAnimationFrame para batch DOM updates
|
|
||||||
requestAnimationFrame(updateActiveFromVisible);
|
requestAnimationFrame(updateActiveFromVisible);
|
||||||
}, observerOptions);
|
}, observerOptions);
|
||||||
|
|
||||||
// Observar todas las secciones
|
|
||||||
sections.forEach(function(section) {
|
sections.forEach(function(section) {
|
||||||
observer.observe(section);
|
observer.observe(section);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Activar la primera sección visible al cargar
|
// Activar primera sección al cargar
|
||||||
requestAnimationFrame(function() {
|
requestAnimationFrame(function() {
|
||||||
if (visibleSections.size === 0 && sections.length > 0) {
|
if (visibleSections.size === 0 && sections.length > 0) {
|
||||||
var firstId = sections[0].id;
|
var firstId = sections[0].id;
|
||||||
|
|||||||
Reference in New Issue
Block a user