Tabla de contenidos con Javascript

TOC: Table of contents - Tabla de contenidos

Un documento HTML es, o puede ser, esencialmente un contenido de texto. Los buscadores aconsejan estructurar los contenidos de texto usando encabezados. Se trata de los elementos h1 a h6. Una tabla de contenidos (TOC de table of contents en inglés) podemos definirla como un índice a los encabezados de un documento. Así el usuario podrá ver de un vistazo los diferentes apartados del documento. Las páginas en este sitio incluyen esa tabla de contenidos o índice de encabezados ubicados en el inicio de cada página, por lo que esta utilidad realmente sólo está implementada en esta página (botón TOC), pues no tiene sentido recargar con otro elemento que no aportará nada nuevo. Pero es posible que sea de su interés implementarlo en las páginas de su sitio.

El siguiente es una imagen de ejemplo de una página de un sitio web:

Toc de php.net

Este cuadro permanece fijo sobre la página y contiene vínculos a los encabezados. Esta solución es atrayente pero tiene el inconveniente de que el contenedor del TOC no puede ser muy ancho para que no oculte en exceso lo que estamos leyendo, pues como dijimos ese contenedor tiene posición fija y no se puede ocultar. Si nuestros encabezados son largos entonces esta solución no sería la más adecuada. Observe como la tercera columna del esquema de la página es sólo para poder ubicar el contenedor del TOC.

Mi propuesta pasa por ubicar un botón (el de "TOC" en la barra superior de esta página) para presentar esta tabla de contenidos en pantalla, pero con posición absoluta en lugar de fija. Es decir, al desplazar hacia arriba el contenido de la página con la barra de desplazamiento vertical ese TOC también se mueve con el contenido. Además hay la posibilidad de cerrarlo pulsando el botón "Cerrar" en el propio contenedor.

Estructura del HTML y CSS necesarios para hacer un TOC

Para incluir un TOC en una página necesitamos ubicar este elemento HTML:

<div id="toc" style="display: none;"></div>

En principio como lo vamos a posicionar posteriormente, lo podemos ubicar directamente en el <body> del documento. Si tiene un contenedor total con especificación de la fuente de texto para hacer zoom (como es mi caso), es preferible incluirlo en su interior para que el zoom también le aplique.

Luego incorporamos estilo para ese elemento. En este ejemplo lo he puesto en un elemento <style> en la cabecera de este documento:

<style type="text/css">
    div#toc {
        z-index: 1;
        display: none;
        position: absolute;
        border: rgb(80, 80, 80) solid 1px;
        padding: 0.4em;
        margin-left: 0.2em;
        width: 90%;
        background-color: rgb(255, 245, 235);
        font-family: Arial;    
        }
    div#toc ul {
        height: 11em;
        overflow: auto;
        border: gray solid 1px;
        margin: 0.2em;
        font-size: 0.9em;    
        }
    span.toc-cerrar {
        float: right;
        cursor: pointer;
        }
</style>    
    

Se dota de posición absolute pues luego en el javascript vamos a modificar su posición. Lo ponemos en una capa superior con z-index: 1 para que permanezca encima de la página. El resto son propiedades de estilo para que resulte más atractivo. La estructura interior del contenedor TOC (el elemento <div id="toc"> se genera dinámicamente con javascript quedando así:

<div id="toc" style="display: none;">
    <b>TABLA DE CONTENIDOS</b>
    <span onclick="cerrarToc()">cerrar</span>
    <ul>
        <li>
            <a href="..." onclick="cerrarToc()">Encabezado1</a>
        </li>
    </ul>
</div>

El TOC se cierra con el botón "cerrar" o bien tras pulsar uno de los vínculos. El botón que incorporaremos en nuestro documento para abrirlo es algo como esto:

<a id="boton-toc" class="boton" 
href="javascript: abrirToc()">TOC</a>

Necesitamos identificar este botón pues lo usaremos para posicionar en altura el contenedor. En este documento está ubicado en la barra de botones de la parte superior.

Generación dinámica del TOC con javascript

Este es el código de la función que abre el TOC generándolo dinámicamente:

/* Esta cadena sirve para identificar los encabezados 
 * que no lo estuvieran. Así se agrega un id="_tocn" donde "n" es un número
 * correlativo. 
 */
var prefijoToc = "_toc";

/* Con estas variables podemos limitar los elementos de 
 * encabezado, por ejemplo desde h1 hasta h6 (van de h1 a h6)
 */
var desdeH = 1;
var hastaH = 6;

/* Función para generar el TOC y mostrarlo en el documento
 */    
function abrirToc(){
    //Necesitamos que estos dos elementos ya estén
    //ubicados en el documento
    var boton = document.getElementById("boton-toc");
    var toc = document.getElementById("toc");
    if (boton && toc){
        //Lo generamos si no estuviera ya en pantalla
        if (toc.style.display == "none"){
            var todos = document.body.getElementsByTagName("*");
            var haches = "";
            var numHaches = 0;
            var numId = 0;
            for (var i=0; i<todos.length; i++){
                if (todos[i].tagName){
                    var tag = todos[i].tagName.toLowerCase();
                    if  ((tag.length == 2) && (tag[0] == "h")){
                        var numH = parseInt(tag[1]);
                        if ((!isNaN(numH)) && (numH >= desdeH)
                        &&(numH <= hastaH)) {
                            //Si no posee un id se lo ponemos
                            if (!todos[i].id) {
                                numId++;
                                todos[i].setAttribute("id", prefijoToc + numId);
                            }                        
                            haches += '<li style="text-indent:' + numH +
                            'em"><a href="#' + 
                            todos[i].id + '" onclick="cerrarToc()">' + 
                            todos[i].innerHTML + '</a></li>';
                            numHaches++;
                        }
                    }
                }
            }
            if (numHaches == 0) {
                haches += '<li>No hay encabezados</li>';
            }
            haches = "<b>TABLA DE CONTENIDOS</b>" +
            '<span class="toc-cerrar" onclick="cerrarToc()")>cerrar</span>' +
            '<ul>' + haches +  "</ul>";
            toc.innerHTML = haches;
            //Buscamos un elemento de la página para posicionar el TOC
            //en su parte inferior. En este caso lo voy a posicionar
            //inmediatamente por debajo de la barra de botones.
            //Pero en su caso podría ser otro elemento.
            padreBoton = nodoPadre(nodoPadre(boton));
            toc.style.top = (padreBoton.offsetTop + padreBoton.offsetHeight) + "px";
            toc.style.display = "block";
        }
    }
}

Primero comprobamos que existe el elemento <div id="toc"> así como el botón que lo abre <a id="boton-toc"..., que en mi caso es un elemento vínculo con un javascript en su href (como vimos más arriba). Si el TOC tiene el estilo {display} a none es que está cerrado y lo abrimos. Con document.body.getElementsByTagName("*") extraemos una colección de todos los elementos del documento. Iteramos por ellos y nos paramos en los tag hn, con el valor n entre los seleccionados en las variables globales desdeH y hastaH.

Existe la función document.all que extrae todos los elementos del documento. Pero no es un estándar W3C, por lo que Firefox (5.0) ya no la soporta aunque otros navegadores aún si lo hacen (Explorer 8.0, Opera 11.11, Chrome 12.0 o Safari 5.0). Es preferible usar la señalada getElementsByTagName() usando un asterisco "*" para extraer todos los elementos.

La función tagName nos da el tag o etiqueta del elemento. Por ejemplo h2. Pero en navegadores como Explorer este tag viene en mayúsculas, por eso lo pasamos a minúsculas con toLowerCase(). Como el tag es una cadena comprobamos si la primera letra es una "h". Luego forzamos la conversión a un entero de la segunda letra con parseInt(). La función isNaN() nos devuelve cierto si no es un número. Por lo tanto sólo nos quedamos con aquellos tag cuya segunda letra sea un número.

Una vez que está entre los números de encabezados declarados, analizamos si está identificado con if (!todos[i].id). Si no lo estuviera le agregamos ese atributo con setAttribute("id", prefijoToc + numId). El prefijo lo declaramos como variable global usando uno adecuado para que no interfiera con otros identificadores que ya existiesen en el documento. Le concatenamos un número correlativo.

Cuando tengamos identificado el encabezado, construimos un elemento vínculo dentro de una entrada de lista. El href del elemento <a> es el identificador del encabezado precedido por el caracter #. También incluimos un evento onclick para cerrar el TOC. Para mejorar la presentación dotamos a la entrada de lista (elemento <li>) de un indentado o sangrado proporcional al nivel del encabezado usando la propiedad de estilo {text-indent}.

Luego sólo resta meter todas las entradas de lista dentro de un <ul>. Antes ponemos un título en negrita y el botón "cerrar" que es un <span> flotado a la derecha y con un evento onclick para cerrar el TOC. Metemos todo dentro del contenedor TOC usando toc.innerHTML. La función que cierra el TOC es:

/* Función para cerrar el TOC
 */
function cerrarToc(){
    var toc = document.getElementById("toc");
    if (toc) toc.style.display = "none";
}

Posicionar el TOC en el documento

Para saber más acerca del posicionamiento de elementos puede consultar el tema de CSS Posiciones, definiéndose con con más detalle los elementos posicionados. Para presentar el contenedor TOC en pantalla hemos de poner su propiedad {display}, que estaba con valor none, a valor block. Pero antes tenemos que posicionarlo en el documento. El contenedor TOC se declaró con la propiedad position: absolute. Así luego podemos cambiar su posición actuando sobre las propiedades top o left. En nuestro ejemplo sólo vamos a modificar la posición vertical arriba, el top. Para ello seleccionamos un elemento que nos sirva de referencia. En este ejemplo uso la barra de botones superior, pues el botón que abre el TOC (<a href="abrirToc()"...) está insertado en ella. Hacemos esto para obtener la referencia a esa barra y modificar la posición del TOC:

padreBoton = nodoPadre(nodoPadre(boton));
toc.style.top = (padreBoton.offsetTop + padreBoton.offsetHeight)
                + "px";

La función nodoPadre(nodo) nos devuelve el padre de un nodo. Es una de las funciones que no se ejecuta igual en todos los navegadores. Yo la tengo incluida en un módulo general de funciones, funciones.js (nodoPadre()) que me sirve para unificar el distinto comportamiento de los navegadores con Javascript o el manejo del DOM (también llamado crossbrowser). Este término crossbrowser se aplica al desarrollo de páginas web que evitan que distintos navegadores presenten la misma página con diferencias. Una parte importante es la destinada a las diferencias en Javascript o el manejo del DOM, aunque también pueden producirse diferencias en la presentación al interpretar el CSS y, en menor medida, el propio HTML.

En el ejemplo seleccionamos el padre del botón que es un <div> que a su vez está dentro de otro <div> que ya si es la barra de referencia donde vamos a posicionar. Por eso buscamos el padre del nodo dos veces. Pero si no queremos seleccionar el elemento de esta forma, simplemente hemos de referenciarlo con document.getElementById() por ejemplo (en el supuesto de que esté identificado). En todo caso una vez seleccionado, posicionamos la parte superior del TOC haciendo que su toc.style.top sea igual que la posición en altura del elemento de referencia más su altura. En este caso hemos de usar las propiedades offsetTop y offsetHeight, que son las que actualmente tiene el elemento ya presentado. Concatenamos "px" al final pues esas propiedades offset vienen en píxeles.