martes, 30 de octubre de 2012

Reestructurar el html de una tabla dinámicamente tras cambios de rowspan o colspan.

Dentro de mi editor quiero que se puedan cambiar los atributos colspan y rowspan de cualquier celda de la tabla y que esta se adapte a esos cambios. En Firefox, por ejemplo, si a una celda existente le modificas uno de estos atributos la tabla mostrara el exceso o defecto de celdas (td o th) ya que el navegador se limita a pintar lo que figura en el html y en él los tds por defecto o por exceso existen.

El primer intento ha sido intentar escribir una función que modifique el colspan de una celda directamente. El resultado ha sido una función que no me da muchas garantías en las que no tengo claro si cubro todos los casos y con limitaciones del tipo “si la celda adyacente tiene un rowspan, no se puede modificar el colspan de la celda previa”. El punto culminante ha sido al intentar duplicar esa función para tratar cambios del rowspan, con lo que podría suceder que “para modificar el rowspan la celda no debe tener un colspan”. En definitiva podría darse un caso recursivo que impidiese el realizar ciertas estructuras.  Al menos, me ha ayudado a ir viendo los distintos casos y a ver más claramente el problema.

El siguiente intento ha sido simplificar el problema a un primer paso, incrementos del colspan en una única celda y del rowspan en una única celda. Con esto se simplifican los casos y, posteriormente,  iterando se llega al mismo resultado (más costoso para el ordenador pero mucho más sencillo, más mantenible, y mucho más fiable).

Colspan + 1
1. Partimos de una celda seleccionada y queremos añadir 1 a su colspan dejando la tabla html en condiciones y teniendo en cuenta que las celdas adyacentes pueden tener muchas disposiciones posibles:
El asterisco representa la celda seleccionada que queremos ampliar con un colspan +1. Las celdas rojas son las afectadas. A la derecha de la flecha aparece el resultado final, la celda amplia su colspan, la roja desaparece o se reduce y pueden aparecer celdas nuevas (amarillas).

2. Creación de un modelo para la tabla. La mejor forma, en informática, de representar algo es mediante números. En este caso, he decidido asignar un número a cada color, o a cada celda. Con este modelo en mente, el dibujo anterior se traduce en:
Hay que tener en cuenta, también, que la celda inicial puede tener su colspan y su rowspan y que debe seguir funcionando.

3. Creamos, primero, unas funciones para representar las dos tablas, el modelo y su representación con colspans y rowspans y la página html. Las pintamos utilizando jquery. Cada celda de la tabla tiene como id su id en el modelo.

4. Dada una celda seleccionada (click) en la tabla pulsamos el botón de colspan+1. El proceso, mirando en los ejemplos es el siguiente:
  • Cogemos el id de la celda seleccionada y calculamos su cuadrado (filaIni, columnaIni, filaFin, columnaFin) utilizando para ello los valores almacenados en el modelo para ese id.
  • Recorremos la columna adyacente a la columnaFin (si no es la última, columnaFin+1) desde la filaIni hasta la filaFin.
    • Hacemos idAEliminar = modeloTabla[filaFin+1][j];
    • Si el identificador de la siguiente columna (columnaFin+2) es igual al que vamos a eliminar entonces debemos eliminar ese identificador de la columna columnaFin+1.
  • Ponemos el id de la celda seleccionada en lugar del idAEliminar.
El código de ejemplo y su funcionamiento (probado en Firefox 15) está en:

Colspan - 1
El colspan -1 es bastante más sencillo:
Simplemente se crean nuevos ids para la columna que se pierde. Si la celda tiene colspan 1 no se permite reducirlo.
El ejemplo:
Rowspan + 1 y Rowspan -1
Tal y como debería haber sido desde el principio si cambiamos col por row, i por j, y filas por columnas, tendremos los métodos para modificar el rowspan directamente desde los anteriores, sin necesidad de comernos el tarro. 
El ejemplo:
Iterando
Aquí dejo el resultado final. He modificado las funciones anteriores  para que retornen un booleano indicando si el incremento o decremento en uno se puede o no hacer en lugar de mostrar un mensaje de error. La iteración se realizará hasta que se llegue al final o se obtenga un false en una de las iteraciones. Simplemente, calculamos la diferencia entre el valor actual y el introducido para saber cuantos pasos tenemos que realizar.
El código final en:

viernes, 14 de septiembre de 2012

Propiedad toJSON de un objeto Javascript

A la hora de serializar un objeto javascript con JSON utilizando la función
JSON.stringify(value[, replacer [, space]]) 
aparte de poder utilizar el "replacer" para "customizar" (personalizar) la salida JSON de ese objeto podemos incluir la función (propiedad) toJSON en el propio objeto. El parseador la verá y hará uso de ella para crear el JSON. Esta propiedad no debe devolver el string JSON ya creado sino que deberá devolver qué propiedades del objeto se deben o no incluir en el string JSON final. Si, por ejemplo, sólo queremos que aparezca la propiedad "foo" del objeto en nuestra serialización haríamos:
     MiObjeto.prototype.toJSON = function() {
          return { foo: this.foo };
     };
Si queremos varias propiedades pero no todas, podríamos escribir:
     MiObjeto.prototype.JSON = function() {
          return { prop1: this.prop1,
                       prop2: this.prop2,
                       propQueEsArray: this.propQueEsArray };
     }

jueves, 24 de mayo de 2012

Utilizando JSON para serializar y des-serializar objetos de javascript incluyendo sus métodos (Parte 2).


Tras ojear el libro "Professional Javascript for web developers" (tercera edición), en mi opinión bastante bueno, he visto que tenía bastantes cosas que corregir en la parte 1 de este tema.
Primero, tal y como se indica allí, si codificamos nuestros objetos con el "patrón siguiente":

     function Direccion(calle, codigoPostal) {
          this.tipo = "Direccion";
         this.calle = calle,
         this.codigoPostal = codigoPostal;
         this.getCodigoPostal = function() {
              return this.codigoPostal;
         }
     }

Al crear dos instancias del objeto "Dirección" se crearán también dos instancias distintas de la función (método, objeto) getCodigoPostal. Obviamente, esto es bastante malo si queremos aprovechar memoria. Aunque en el libro se comentan otros modos para codificar nuestras "clases" parece ser que lo más correcto es:

     function Direccion(calle, codigoPostal) {
          this.tipo = "Direccion";
         this.calle = calle,
         this.codigoPostal = codigoPostal;
     }

     Direccion.prototype = {
          constructor : Direccion,
         getCodigoPostal : function() {
              return this.codigoPostal;
         }
     };

Es decir, utilizar un constructor para los atributos y luego definir los métodos extendiendo el prototipo. De este modo se consigue que cada objeto tenga sus propios atributos y que todos los del mismo tipo compartan los mismos (objetos) métodos.

En ECMASCRIPT 6 aparecerá el concepto de clase.

Si, además, consideramos que estamos ejecutando ECMASCRIPT 5 entonces ya dispondremos de JSON (sin necesidad de incluir json2.js) y del método Array.isArray(value) para comprobar si "value" es un array o no.
Con estos cambios la página queda así:
Resumiendo, si incluimos el atributo “tipo” con el nombre de la clase de nuestros objetos en cada uno de ellos, con la función siguiente podremos des-serializar nuestros objetos almecenados con JSON, conservando su tipo y sus métodos.

     function recrearObjetoDesdeJSON(objetoDesdeJSON) {
          if (!objetoDesdeJSON.tipo)
              return objetoDesdeJSON;
         var nuevoObjeto = new this[objetoDesdeJSON.tipo];
         var valorAtributo;
         for(var atributo in objetoDesdeJSON) {
              valorAtributo = objetoDesdeJSON[atributo];
              if (typeof valorAtributo == "object") {
                   if (Array.isArray(valorAtributo)) {
                       var nuevoArray = new Array();
                      for (var i=0; i<valorAtributo.length; i++) {
                           nuevoArray[i] = recrearObjetoDesdeJSON(valorAtributo[i]);
                      }
                      nuevoObjeto[atributo] = nuevoArray;
                  }
                  else
                       nuevoObjeto[atributo] = recrearObjetoDesdeJSON(valorAtributo);
}
else
                  nuevoObjeto[atributo] = valorAtributo;
}
return nuevoObjeto;
}

Debemos seguir teniendo cuidado para no referenciar un atributo desde otro:
this.atr1 = ….
this.atr2 = new Array(this.atr1, …)
Ya que el atributo del array se creara como un objeto distinto al atributo de "la clase".

miércoles, 23 de mayo de 2012

Aplicar una transformación xsl desde javascript cogiendo el archivo xml del ordenador del cliente.


Aunque podría poner una xsl  general en este ejemplo voy a utilizar la siguiente (por_un_clavo.xsl):

<?xml version="1.0"?>
     <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
     <xsl:template match="/">
          <xsl:apply-templates/>
     </xsl:template>
     <xsl:template match="por_un_clavo">
          <ul>
       <xsl:apply-templates/>
  </ul>
     </xsl:template>
     <xsl:template match="linea">
          <li>
       <xsl:value-of select="."/>
  </li>
     </xsl:template>
</xsl:stylesheet>

Para procesar, con ella, el xml siguiente (por_un_clavo.xml):

<?xml version="1.0" encoding="UTF-8"?>
<!-- <?xml-stylesheet type="text/xsl" href="por_un_clavo.xsl"?> -->
<por_un_clavo>
     <linea>Por un clavo se perdió una herradura,</linea>
     <linea>por una herradura, se perdió un caballo,</linea>
     <linea>por un caballo, se perdió una batalla,</linea>
     <linea>por una batalla, se perdió el Reino.</linea>
</por_un_clavo>

Si descomentamos la línea comentada podemos abrir el archivo xml con Internet Explorer y éste realizará la transformación directamente. Esto nos servirá para validar rápidamente nuestra xsl.

Si no queremos tener problemas con los acentos, las ñ y demás deberemos poner el encoding dentro del xml con el que realmente hayamos guardado nuestro archivo (trabajar con utf-8 es la mejor elección, olvidaros del ISO-8859-1 por muy latín que sea). Una forma de hacerlo, desde Windows, es escribiendo el xml en Word, guardando como texto y allí eligiendo la codificación (en Windows, por defecto y para variar, la codificación es propietaria de Microsoft y no es utf-8):


Una vez seleccionado el archivo de disco y transformado con nuestra xsl el resultado, con Firefox, será:


La página, con el código completo, se puede ver en:

El body de la página es, simplemente:

<input id="ficheroxml" type="file" name="ficheroxml" onchange="procesarXml();"/>
<div id="resultado"></div>

En él tenemos un input para seleccionar el archivo y un div que albergará el resultado de la transformación. En el momento en el que se selecciona el archivo se producirá el evento “onchange” y se llamará a la función javascript procesarXml().

Dentro del javascript hemos incluido la xsl como una variable de texto con todo su contenido:

     var xsl = "<?xml version=\"1.0\" encoding=\"UTF-8\"?> …

y hemos creado una instancia de un lector de ficheros:

     var reader = new FileReader();

Cuando se llama a la función procesarXml se ejecuta el siguiente código:

     if (document.getElementById("ficheroxml").files.length == 0) return;
     var fichero = document.getElementById("ficheroxml").files[0];
     reader.readAsText(fichero);

donde validamos si se ha seleccionado algún archivo, cogemos el primero de ellos y le pedimos al lector que lo lea como un archivo de texto.
Si se produce algún error se lanzará el evento onerror en el lector que definimos del modo siguiente:

     reader.onerror = function(event) {
           alert("Error leyendo el archivo, code: " + reader.error.code);
     }

Si todo va bien se lanzará el evento onload sobre el reader:

      reader.onload = function(event) {
           xml = event.target.result;
   parsearXml();
      }

Aquí guardaremos el resultado de la lectura en la variable xml y llamaremos a la función que realiza el parseo e introduce el resultado en el div "resultado":

function parsearXml() {
      var xsltProcessor = new XSLTProcessor();
      var parser = new DOMParser();
      xsltProcessor.importStylesheet(parser.parseFromString(xsl, "text/xml"));
      var resultDocument = xsltProcessor.transformToFragment(parser.parseFromString(xml, "text/xml"),                  
                                       document);
      document.getElementById("resultado").appendChild(resultDocument);
}

Para que esto funcione en Chrome (18.0.1025.162 m) es necesario arrancar la instancia de  Chrome con el switch “--allow-file-access-from-files” ya que, si no, no se nos permitirá, por razones de seguridad, acceder al archivo del  disco.

martes, 22 de mayo de 2012

Hora de aventuras.

Hora de aventuras según el peque (7 añitos), con Finn, Jake, la princesa chicle, Lady Arcoiris, la princesa bultos y demás ...


Utilizando JSON para serializar y des-serializar objetos de javascript incluyendo sus métodos (Parte 1).

Ver Parte 2 de este tema.
En el desarrollo real del editor que estoy realizando me he visto en la necesidad de serializar mis objetos javascript a disco para lo que pensaba que seria suficiente con convertir mis objetos al formato JSON utilizando json2.js de https://github.com/douglascrockford/JSON-js) :

localStorage.setItem(nombredelapantalla, JSON.stringify(pantalla));

¿Cual ha sido mi sorpresa cuando he visto que este formato no admite que los objetos javascript tengan funciones? Es decir, si tenemos un objeto (función) del tipo:

function Persona(nombre, trabajo, nacimiento) {
     this.nombre = nombre;
     this.trabajo = trabajo;
     this.nacimiento = nacimiento;
     this.getNombre = function() {
          return this.nombre;
     }
}

Su JSON será:
{"nombre":"Pedro Picapriedra","trabajo":"Picapedrero","nacimiento":-1000}. 
Aquí no existe ninguna referencia a la clase, objeto o tipo “Persona”. Si des-serializamos este objeto tendremos un nuevo objeto con los atributos nombre, trabajo, y nacimiento pero sin el método getNombre() (Sabremos que su nombre es Pedro Picapiedra, que es Picapedrero pero no sabremos si es una Persona y no le podremos preguntar su nombre).
Tras varias vueltas por internet y revisar arriba y abajo JQuery sólo he visto el método toSource que convierte un objeto a un formato similar al de JSON con referencias al método y al tipo del objeto. Lástima que, una vez más, este método no esté disponible en todos los navegadores y sólo sea válido para Mozilla.
En definitiva, toca hacer en javascript algo que muchos otros lenguajes hacen de serie.

1ª Prueba: Introducimos un atributo para identificar el tipo del objeto en nuestra “clase” y lo utilizamos para regenerar el objeto copiando los valores de los atributos del objeto des-serializado por json2:

if (objetoSinMetodos.tipo == "persona") {
     var nuevoPedro = new Persona();
     // Copiamos los valores de los atributos
     for(var member in objetoSinMetodos)
          nuevoPedro[member] = objetoSinMetodos[member];
     alert(nuevoPedro.getNombre() + ", " + nuevoPedro.trabajo + "," + nuevoPedro.nacimiento);
}

El ejemplo en:

2ª Prueba: Introducimos el objeto "Direccion" con sus atributos y métodos como atributo del objeto Persona. Al des-serializar queremos poder llamar a objPersona.getDireccion().getCodigoPostal(). 
El código siguiente se explica por sí mismo, si un atributo es un objeto le metemos la propiedad “tipo” y así sabemos de qué tipo es (aunque en el código lo creemos a capón en esta versión):
var valorAtributo;

if (objetoSinMetodos.tipo == "persona") {
     var nuevoPedro = new Persona();
     // Copiamos los valores de los atributos
     for(var atributo in objetoSinMetodos) {
          valorAtributo = objetoSinMetodos[atributo];
    if (typeof valorAtributo == "object") {
         var nuevaDireccion = new Direccion();
         for (var atributo2 in valorAtributo)
              nuevaDireccion[atributo2] = valorAtributo[atributo2];
         nuevoPedro[atributo] = nuevaDireccion;
    } 
    else
         nuevoPedro[atributo] = valorAtributo;
     }
     alert(nuevoPedro.getDireccion().getCodigoPostal());
}

El ejemplo completo en:

3.- Quitando el “a capón”: Algo muy bueno que tiene Javascript es que a partir de una variable, un string, un nombre, podemos hacer casi todo. Podemos, en este caso, crear un nuevo objeto del tipo persona o del tipo dirección a partir de los valores, string, de los atributos tipo:

function recrearObjetoDesdeJSON(objetoDesdeJSON) {
     var nuevoObjeto = new this[objetoDesdeJSON.tipo];
     var valorAtributo;
     for(var atributo in objetoDesdeJSON) {
          valorAtributo = objetoDesdeJSON[atributo];
    if (typeof valorAtributo == "object")
         nuevoObjeto[atributo] = recrearObjetoDesdeJSON(valorAtributo);
    else
         nuevoObjeto[atributo] = valorAtributo;
     }
     return nuevoObjeto;
}

El ejemplo completo en:

4. Incluyendo arrays: Al ir a introducir el código anterior en mi estructura de objetos he visto que algunos de ellos tenían atributos que eran arrays por lo que la función dejaba de funcionar. Para resolver esto, debemos saber si un objeto de Javascript es un array o no. En 
nos explican la solución:

function isArray(a) {
     return Object.prototype.toString.apply(a) === '[object Array]';
}

Quedando, finalmente:

function recrearObjetoDesdeJSON(objetoDesdeJSON) {
  if (!objetoDesdeJSON.tipo)
return objetoDesdeJSON;
var nuevoObjeto = new this[objetoDesdeJSON.tipo];
var valorAtributo;
for(var atributo in objetoDesdeJSON) {
valorAtributo = objetoDesdeJSON[atributo];
if (typeof valorAtributo == "object") {
if (isArray(valorAtributo)) {
var nuevoArray = new Array();
for (var i=0; i<valorAtributo.length; i++) {
nuevoArray[i] = recrearObjetoDesdeJSON(valorAtributo[i]);
}
nuevoObjeto[atributo] = nuevoArray;
}
else
nuevoObjeto[atributo] = recrearObjetoDesdeJSON(valorAtributo);
}
else
nuevoObjeto[atributo] = valorAtributo;
}
return nuevoObjeto;
}

Recordar que para que esto funcione cada uno de nuestros objetos deberá disponer de un atributo “tipo” con el nombre de la clase.
Es muy probable que en el caso de un atributo que sea un array de arrays el código no funcione correctamente. No es mi caso ;D. 

¡CUIDADO! En mi caso tenia varios atributos que luego volvia a referencias desde otro que era un array:
this.atr1
this.atr2
this.atr3 = new Array(this.atr1, this.atr2);
Al des-serializarse el objeto con mi función los atributos del array no son los mismos objetos (son objetos del mismo tipo pero son instancias distintas).  En mi caso la solución ha sido poner una función:
this.getAtributos = function() {
return new Array(this.atr1, this.atr2);
}

Estoy leyendo un buen libro de javascript "Professional javascript for web developers", tercera edición, en el que comentan el problema de codificar los objetos javascript tal y como lo estoy haciendo yo aquí, definiendo las funciones dentro del objeto con "this.funcion". Parece ser que en este caso cada vez que se crea un objeto se crea tambien uno por cada una de las funciones que aparecen en ese objeto de modo tal que si creamos dos instancias de uno de mis objetos se crearan también instancias nuevas de cada una de las funciones. En definitiva debería escribir mis "clases" de otro modo y cuando lo haga deberé buscar el modo de serializar mis objetos de nuevo. En ese momento, espero que pronto, pondré aquí la parte 2.
Por cierto, con ECMASCRIPT 6 parece ser que Javascript resolverá muchos de sus problemas. Se prodrán definir clases, constantes, etc.

Ver Parte 2 de este tema.

jueves, 29 de marzo de 2012

Un editor html WISIWING con JQuery.

Aquí dejo el código de una pequeña prueba para hacer un editor de html (ver código fuente del enlace). Aunque para mis necesidades no es necesario que fuese WISIWING (lo que ves es lo que tienes) si quería ver cómo podría hacer algo que diese una imagen real del resultado final.
Ahora mismo con JQuery (simplifica el trabajo de javascript), con los nuevos navegadores (esto no funcionara en cualquiera si no estamos actualizados), lo tenemos más fácil aunque tenemos que pasar por el aro de trabajar con javascript. Manipulando el dom de la página podemos agregar y eliminar elementos, podemos introducir contenedores (div, párrafos, tablas, …), podemos modificar las propiedades css de los elementos y podemos jugar con sus atributos mientras vemos el resultado directamente en pantalla. Lo mejor, es que el código apenas llega a las 100 líneas e introducir el resto de propiedades y atributos para completar el editor es tremendamente sencillo. 
El enlace al editor:
Una imagen:

Para introducir los elementos en un contenedor, seleccionaremos el elemento y pulsaremos dentro del contenedor. Si, por ejemplo, queremos introducir un botón como el de la imagen, pondremos un elemento input (por defecto se incluye de tipo texto), pulsaremos sobre él, e iremos a los atributos y cambiaremos a tipo "button". Despues en el value podemos poner el texto a mostrar.