Scriptia. Javascript y buenas prácticas en español



Acerca de Scriptia

Saltar a Anotaciones recientes

Scriptia forma parte del PDM de Choan C. Gálvez, desarrollador web residente en Barcelona. Scriptia pretende mejorar la calidad de la documentación acerca de javascript disponible en español.


Anotaciones recientes

Número de días en un mes

Ver una vaca encima de un cobertizo no es nada comparado con las cosas que uno se encuentra en los scripts que corren por estos mundos de dios.

Uno de los campos en los que cualquier mindundi es capaz de aplicar al máximo su desconocimiento de javascript es el del cálculo de días en un mes. Utilizar una docena de condicionales es de flojos.

La máxima: cuando tengas que echar cuentas, sobre cualquier cosa, utiliza las herramientas del lenguaje. No hagas cuentas de la vieja.

El código. Función que recibe un mes (en notación numérica humana) y un año (opcional, si no se recibe tomamos el año actual) y retorna el número de días en el mes. Ningún condicional fue dañado durante el experimento.

function daysInMonth(humanMonth, year) {
  return new Date(year || new Date().getFullYear(), humanMonth, 0).getDate();
}
daysInMonth(2, 2009); // 28
daysInMonth(2, 2008); // 29

Las claves:

  • ECMAScript maneja internamente las fechas como datos numéricos (véase Tiempo Unix)
  • en ECMAScript, los meses se numeran del 0 al 11 y los días de 1 a N. Nosotros alimentamos la función con un mes humano. Así que la fecha que se crea (d) corresponderá al día 0 del mes siguiente al que buscamos. El algoritmo de creación de fechas es tolerante (básicamente suma lo que le demos y obtiene un entero, así que el día 0 de un mes es siempre el último día del mes anterior).

En fin, que todo esto va de que la máquina es más lista que tú y tus nudillos. Y que todos los blogueros juntos, incluido el menda.

Adendum

Podemos usar una estratagema similar para saber si el año es bisiesto:

function isLeapYear(year) {
  return new Date(year, 0, 366).getFullYear() == year;
}


Trabajando con jQuery: timeline arrastrable, del espaguetismo al plugin

En el capítulo de hoy veremos cómo reconvertir un código espagueti en un bonito plugin reutilizable.

Objetivo del widget: permitir desplazar horizontalmente mediante arrastre (pincho, arrastro, suelto) un bloque de contenido. Si tienes prisa por ver en qué acaba la cosa, ve directamente a la demo.

Elementos: un contenedor (con overflow: auto) y su contenido. Para nuestro ejemplo:

<div id="#timeline">
  <ul>
    <li>...</li>
    <li>...</li>
  </ul>
</div>

Con unos estilos tal que:

#timeline {
 width: 600px;
 height: 400px;
 overflow: auto;
}

#timeline ul {
 width: 1000px;
 margin: 0;
 padding: 0;
}

#timeline li {
 display: block;
 width: 200px;
 padding: 10px;
 margin: 0;
 float: left;
}

Sobre estas piedras edificaremos nuestra iglesia. Veamos paso a paso el código necesario para hacer que la cosa se mueva. Pondremos en marcha la maquinaria cuando pinchemos sobre el contenedor:

$('#timeline')
  .bind('mousedown', function(e) {
    // aquí vendrá nuestro código
  });

A fin de evitar el uso de variables globales, utilizarmos data, un método de jQuery que nos permite mantener relaciones entre valores y elementos DOM. Vamos a mantener los siguientes datos: posición actual del scroll, elemento al que afecta y posición del puntero en el momento del mousedown.

$(document)
  .data('timeline', {
    element: this,
    scroll: this.scrollLeft,
    x : e.clientX
  });

Observa que estamos conservando los datos en document. En cada momento existirá un máximo de un elemento en desplazamiento.

Bien, ha empezado la acción… pero todavía no estamos siguiendo los movimientos del puntero. Asignemos un par de manejadores para los eventos mousemove y mouseup de document.

¿Por qué para document y no para el contenedor con el que estamos tratando? Aunque el puntero salga del contenedor durante el arrastre, queremos seguir desplazando el contenido. Y si saliera y no controláramos mouseup a nivel de documento, nuestro script nunca abandonaría el modo “estamos arrastrando cositas”.

Como somos chulos y modernos, aprovecharemos los eventos.con-espacio-de-nombres que jQuery gentilmente pone a nuestra disposición. Para mousemove, recuperamos los datos que hemos almacenado en mousedown y modificamos la propiedad scrollLeft del contenedor sumándole la diferencia entre la posición actual del ratón y la almacenada. En mouseup, limpiamos los datos almacenados y eliminamos los manejadores de eventos del espacio timeline asignados a document.

jQuery(document)
  .bind('mousemove.timeline', function(e) {
    var data = jQuery(this).data('timeline');
    data.element.scrollLeft = data.scroll + data.x - e.clientX;
  })
  .bind('mouseup.timeline', function(e) {
    jQuery(this)
      .removeData('timeline')
      .unbind('.timeline');
  });

Para rematar, cancelaremos la acción por defecto de mousedown para no seleccionar texto mientras arrastramos:

e.preventDefault();

Poniéndolo todo junto y dentro de una llamada a ready, tenemos la cosita funcionando:

jQuery(document).ready(function() {

  jQuery('#timeline')
    .bind('mousedown', function(e) {

      jQuery(document)
        .data('timeline', {
          element: this,
          scroll: this.scrollLeft,
          x : e.clientX
        })
        .bind('mousemove.timeline', function(e) {
          var data = jQuery(this).data('timeline');
          data.element.scrollLeft = data.scroll + data.x - e.clientX;
        })
        .bind('mouseup.timeline', function(e) {
          jQuery(this)
            .removeData('timeline')
            .unbind('.timeline');
        });

      e.preventDefault();

    });
});

¡Genial! Con esto ya tenemos el comportamiento deseado, pero… no parece muy reutilizable. Pluguinifiquemos.

Construyendo un plugin

Cuando invocamos la función jQuery (o su alias habitual, $), recuperamos un objeto. Dicho objeto tiene un prototipo. Para añadir métodos al objeto devuelto por jQuery, los añadimos a su prototipo. Así que algo tal que:

jQuery.prototype.miMetodo = function() {

};

Ya nos serviría para ir tirando. Pero hay un idioma más jotacueriesco para decir lo mismo. De paso añadamos un return this a nuestro método para permitir el encadenamiento de llamadas:

jQuery.extend(jQuery.fn, {
  miMetodo : function() {
    return this;
  }
});

Con esto ya podemos darnos el gustazo de escribir:

jQuery('#miCosa')
  .miMetodo()
  .etcetera();

Pero escribir jQuery (que es lo que debemos hacer si queremos asegurarnos de que nuestro plugin es compatible con otras bibliotecas) una y otra vez es escribir mucho. Vamos a meter todo el código de nuestro plugin dentro de una función anónima que nos permita tener un alias seguro para jQuery.

(function($) {

})(jQuery);

Y vamos a rellenarlo poquito a poco.

(function($) {
  $.extend($.fn, {
    timeline : function() {
      this
        .bind('mousedown', handleMouseDown);
      return this;
    }
  });

  function handleMouseDown(e) {
    // bla bla
  }

})(jQuery);

Fíjate: extraemos el código de manejo del evento mousedown. ¿Por qué? No necesitamos ningún dato local y por tanto no necesitamos del cierre funcional para acceder a dicho dato (ni generar la función al vuelo para cada widget). Haremos lo mismo para los manejadores de mousemove y mouseup:

(function($) {
  $.extend($.fn, {
    timeline : function() {
      this
        .bind('mousedown', handleMouseDown);
      return this;
    }
  });

  function handleMouseDown(e) {
    $(document)
      .data('timeline', {
        element: this,
        scroll: this.scrollLeft,
        x : e.clientX
      })
      .bind('mousemove.timeline', handleMouseMove)
      .bind('mouseup.timeline', handleMouseUp);
    e.preventDefault();
  }

  function handleMouseMove(e) {
    var data = $(this).data('timeline');
    data.element.scrollLeft = data.scroll + data.x - e.clientX;
  }

  function handleMouseUp(e) {
    $(this)
      .removeData('timeline')
      .unbind('.timeline');
  }

})(jQuery);

Con esto nuestro código espagueti ya parece algo más ordenadito. Pero un plugin sin opciones sabe a poco. Vamos a considerar la posibilidad de configurar la proporción de desplazamiento/arrastre y de decidir si ocultamos o no la barra de scroll nativa. Por partes:

$.extend($.fn, {
  timeline : function(opts) {
    var config = $.extend({
      speed: 1.5,
      accesible: true
    }, opts || {});

    this
      .bind('mousedown', config, handleMouseDown);

    if (!config.accesible) {
      this.css('overflow', 'hidden');
    }

    return this;
  }
});

Ahora el método timeline acepta un argumento que corresponde a un objeto de configuración. Utilizamos jQuery.extend para aplicar los valores sobre una configuración por defecto. Y al asignar el manejador para mousedown lo pasamos como argumento. Si esto resulta nuevo para ti, lo que te falta saber es que con una asignación de este tipo, el manejador recibirá estos datos extra en la propiedad data del objeto que representa el evento. Modifiquemos el manejador para transmitir la configuración:

function handleMouseDown(e) {
  $(document)
    .data('timeline', {
      element: this,
      scroll: this.scrollLeft,
      x : e.clientX,
      config : e.data
    })
    .bind('mousemove.timeline', handleMouseMove)
    .bind('mouseup.timeline', handleMouseUp);
  e.preventDefault();
}

Utilicemos la configuración en handleMouseMove:

function handleMouseMove(e) {
  var data = $(this).data('timeline');
  data.element.scrollLeft = data.scroll + (data.x - e.clientX) * data.config.speed;
}

Y listo. Ya tenemos un plugin bastante decente. Para ponerlo en marcha:

jQuery(document)
  .ready(function() {
    $('el.selector-de-dios')
      .timeline({ speed: 2, accesible: false })
  });

Bola extra 1: configuración por defecto

Si nuestro usuario siempre va a preferir la opción no accesible del plugin, no por ser idiota (u obedecer órdenes) le vamos a obligar a pasar la configuración una y otra vez. Apañemos:

$.extend($.fn, {
  timeline : function(opts) {
    var config = $.extend({}, $.fn.timeline.defaults, opts || {});
    // ...
    return this;
  }
});

$.fn.timeline.defaults = {
  speed: 1.5,
  accesible: true
};

De esta manera permitimos al usuario configurar las opciones de manera global manipulando jQuery.fn.timeline.defaults a su antojo.

Bola extra 2: mousewheel

El plugin mousewheel permite controlar los movimientos de la ruedita del ratón. Aprovechémoslo:

$.extend($.fn, {
    timeline : function(opts) {
      // ...
      if ($.fn.mousewheel) {
        this
          .mousewheel(function(e, delta) {
            this.scrollLeft -= delta * config.mousewheelSpeed;
            e.preventDefault();
          });
      }
      return this;
    }
  });

Puedes probar la combinación de todos estos elementos en la demo correspondiente.

Coda

Choan no inventa nada nuevo, lo tratado aquí viene a ser una combinación de mis conocimientos, gustos y experiencia y lo expuesto en estos dos artículos:

Ten presente que la arquitectura propuesta para este plugin no siempre es la más adecuada —ya hablaremos de ello— y que el plugin de ejemplo no está publicado ni oficial ni extra oficialmente y nadie te dará soporte.