Ir directamente al contenido de esta página

codexexempla.org

childNodes y el problema de los nodos de texto vacíos

Tabla de contenidos

  1. Introducción
  2. El problema
  3. La solución de Aaron Gustafson
  4. La solución de Aaron Gustafson REDUX (por DJ Saúl)
  5. Pruebas en navegadores

Introducción

Entre los métodos del DOM contamos con childNodes para obtener una lista de los nodos hijos de un elemento. Así, por ejemplo, document.getElementsByTagName('body')[0].childNodes.length nos devolvería el número de hijos del body de nuestro documento. O casi.

En Firefox tenemos un pequeño problema, y es que los saltos de línea que aparecen en el código cuentan también como hijos, lo que en ocasiones puede provocar resultados inesperados a la hora de seleccionar, por ejemplo, document.getElementsByTagName('body')[0].childNodes.

Primero, veamos el caso de estudio.

El problema

Los ejemplos 1 a 4 que presento aquí presentan una serie de errores al ser validados. Ello se debe a que incluyo los scripts en los propios documentos, para que puedan consultarse por medio de Ver → Ver código fuente de la página. Los errores se deben a que habría que sustituir ciertos caracteres por sus referencias de entidad —por ejemplo & por &amp; o < por &lt;—, pero en ese caso los scripts no funcionarían.

En el ejemplo número 1, tengo un elemento blockquote que marca unos versos de Robert Frost:


    <blockquote xml:lang="en">
        <p>We dance around in a ring and suppose<br />but the secret stands in the middle and knows</p>
        <p id="autor"><cite>Robert Frost</cite></p>
    </blockquote>
            

Es decir, el bloque de cita tiene dos hijos, que son los párrafos. Al lanzar la función, en Internet Explorer obtengo correctamente en la alerta 2, pero Firefox me da un resultado de 5. ¿A qué se debe? Pues a está contabilizando como un nodo de texto el salto de línea que tengo en mi documento XHTML.

Si modifico el marcado para tener una sola línea…


    <blockquote xml:lang="en"><p>We dance around in a ring and suppose<br />but the secret stands in the middle and knows</p><p id="autor"><cite>Robert Frost</cite></p></blockquote>
            

…con el mismo script obtengo el resultado de 2, como se puede ver en el ejemplo número 2.

Bueno, una posible solución es escribir todo el código de la página en una sola línea. Claro que es la solución suicida para el desarrollador que en un futuro tenga que modificar o corregir el código.

Otra posible solución es crear un script que elimine los nodos no deseados.

La solución de Aaron Gustafson

Para solucionar este pequeño problema, Aaron Gustafson en Web Design in a Nutshell (O'Reilly, 2006, pg. 493; la autora del libro es Jennifer Niederst) propone este script:


 01  function stripsWS(el){
 02      for (var i=0;i<el.childNodes.length;i++){
 03          var node = el.childNodes[i];
 04          if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
 05              node.parentNode.removeChild(node);
 06      }    
 07  }    
        

Lo que hace aquí es crear una función a la que se le puede enviar como parámetro el elemento que se desea limpiar. Para cada hijo del elemento, comprueba si se trata de un nodo de texto (node.nodeType == 3), y además si su valor es el de salto de línea (!/\S/.test(node.nodeValue)); en tal caso, elimina el nodo. Aquí se puede ver un ejemplo de su funcionamiento.

Bueno, ya que estamos, mejorémoslo un poco.

La solución de Aaron Gustafson REDUX (por DJ Saúl)

Se me ocurre que el script se podría mejorar si pudiera hacer tres cosas más:

  1. Ejecutarse directamente al cargarse en una página.
  2. Limpiar todos y cada uno de los elementos.
  3. Acabar con otros nodos igualmente indeseables a la hora de recorrer el DOM: los comentarios.

Con respecto al punto 3, supongamos que añadiese un comentario al blockquote ejemplo número 3:


    <blockquote xml:lang="en">
        <!-- falta localizar la obra, que la tengo en la punta de la lengua -->
        <p>We dance around in a ring and suppose<br />but the secret stands in the middle and knows</p>
        <p id="autor"><cite>Robert Frost</cite></p>
    </blockquote>
            

Si comprobamos lo que ocurre con el ejemplo 4, sin aplicar la función de Gustafson obtenemos 7 hijos, y aplicándola, 3, con lo que deducimos que en ambos casos el comentario cuenta como hijo. Los comentarios, como tales, son de una gran ayuda para el desarrollo, pero creo que los navegadores no deberían considerarlos parte del árbol del documento: estrictamente, son una herramienta del programador, y no un elemento del documento.

Así pues, primero probé con esta versión:


 01  function buscar_y_destruir(){
 02      var elementos = document.getElementsByTagName('*');
 03      for (var k=0;k<elementos.length;k++){
 04          for (var i=0;i<elementos[k].childNodes.length;i++){
 05              var hijo = elementos[k].childNodes[i];
 06              if ((hijo.nodeType == 3 && !/\S/.test(hijo.nodeValue))||(hijo.nodeType == 8)){
 07                  hijo.parentNode.removeChild(hijo);
 08              }
 09          }
 10      }
 11  }
 12  buscar_y_destruir();
        

Lo que hago es obtener una lista de todos los elementos del documento (línea 2), y después para cada elemento —elegido sucesivamente en el primer bucle— creo un bucle secundario que analiza cada uno de sus hijos (líneas 4 a 9), y si cumplen las condiciones de Gustafson, o su nodeType es 8 —el valor que identifica los comentarios—, los elimino (líneas 6 a 8). Inmediatamente después lanzo la función para que limpie el documento.

A simple vista puede parecer que la programación es correcta, pero al comprobar su funcionamiento en el ejemplo 5 me encontré con un problema en un primer momento desconcertante: los comentarios incluidos en body y en blockquote —que existen se puede comprobar en el ejemplo con Ver → Ver código fuente de la página— no se eliminaban (!).

La solución se merece otro artículo, concretamente «Problemas al eliminar elementos de una lista de nodos».

En definitiva, como donde hay una voluntad hay un camino, al final conseguí lo que pretendía:


 01  function buscar(){
 02      var elementos = document.getElementsByTagName('*');
 03      for (var k=0;k<elementos.length;k++){
 04          for (var i=0;i<elementos[k].childNodes.length;i++){
 05              var hijo = elementos[k].childNodes[i];
 06              if ((hijo.nodeType == 3 && !/\S/.test(hijo.nodeValue))||(hijo.nodeType==8)){
 07                  nodos_a_eliminar[nodos_a_eliminar.length] = hijo;
 08              }
 09          }
 10      }
 11      destruir();
 12  }
 13            
 14  function destruir(){
 15      for(var d=0;d<nodos_a_eliminar.length;d++){
 16          nodos_a_eliminar[d].parentNode.removeChild(nodos_a_eliminar[d]);
 17      }
 18  }
            

Ver un ejemplo de su funcionamiento.

Descargar el archivo .js.

Como ya he dicho, para que funcione correctamente debe incluirse el head del documento.

He comprobado que este script funciona correctamente —sobre Windows— en:

Contacto

En virtud de la Ley Orgánica 15/1999 de Protección de Datos de Carácter Personal le informo de que los datos que proporcione no serán empleados para otro fin que el de responder a su mensaje. En especial, me comprometo a no cederlos a terceros ni a emplearlos para enviar información no solicitada.

Del blog de Digital Icon