Wextensible

Introducción a la clase calendar

En los temas anteriores vimos que una primera ventaja de los objetos es que podemos reutilizar el código de la clase en tantas instancias como queramos. Supongamos que en una página hay un formulario que se envía a un servidor donde el usuario ha de poner varias fechas. Pero queremos controlar que introduzca fechas válidas. Es usual ver que en estos casos suele ponerse un acceso a un pequeño calendario y así el usuario ha de seleccionar la fecha en el mismo.

Pues eso es lo que intentaremos hacer: declarar una clase calendar para luego poder crear múltiples instancias de la misma. Este clase es única y no se aplican relaciones de composición o herencia con otras subclases o superclases creadas.

En esta página vamos a exponer varios ejemplos, donde los primeros nos servirán para usar directamente la clase calendar, aún sin saber como esta diseñada, lo cual se explica en los últimos apartados. Lo hacemos así porque uno de los objetivos de la Programación Orientada a Objetos (POO) es la abstracción y encapsulamiento principalmente, con lo que el programador que usa una clase no tiene porque saber necesariamente los detalles de como está diseñada, sino sólo las reglas para usar esa clase cuando vaya a instanciar objetos de élla.

Ejemplos de uso de la clase calendar para instanciar objetos calendario

A modo de resumen, las características generales del calendario son:

Ejemplo de calendario con interfaz visual

El constructor de la clase es el siguiente:

new calendar(nombreInstancia, dondeTabla
[, dondeTitulo][, fecha][, conSeleccionDia])
    

Nota aclaratoria: Esta clase calendar la he modificado en Agosto 2010 con una nueva distribución de los argumentos del constructor. Antes se declaraba new calendar(elDia, elMes, elAnyo, dondeTabla, dondeTitulo[, conSeleccionDia, nombreInstancia]). Ahora he agrupado día, mes y año que eran números enteros en un string fecha que se pasa como "d/m/aaaa" siendo un argumento opcional. Además se cambia el orden de los argumentos poniendo en primer lugar el nombreInstancia porque también ahora es obligatorio. Sin embargo el argumento dondeTitulo es ahora opcional.

La razón de estos cambios es debido a un problema detectado con las referencias a elementos HTML que se pasan a un objeto de JavaScript. Antes dondeTabla y dondeTitulo eran referencias a elementos HTML, ahora son tipos string con los identificadores id de esos elementos. Ver más en un problema con las referencias al DOM en objetos JavaScript. Por otro lado hacer obligatorio el nombreInstancia es debido a lo que también se comenta más abajo sobre un problema con this en JavaScript, pero también porque he incorporado control de errores en la clase y es necesario tener ahí el nombre de la instancia para ofrecerlo en un mensaje.

Los dos primeros argumentos son obligatorios, el resto opcionales. Los opcionales del final que no se pasen no es necesario ni siquiera indicarlos, pero si están antes de otro que si se pasa, entonces ha de ponerse una cadena vacía "" para ignorarlo.

El primer argumento nombreInstancia es un string obligatorio que será el mismo que el nombre de la variable con la que hemos instanciado el objeto. Por ejemplo var x = new calendar("x",...). El argumento dondeTabla es una referencia a un elemento HTML de bloque, un <div> por ejemplo. Es ahí donde se construirá dinámicamente una tabla (elemento table) con los días en las celdas. Pero ha de pasarse un string con el identificador id de ese elemento, no la referencia al propio elemento. Este argumento debe ser una referencia válida, pues la clase no lo verifica.

<div id="div1"></div>
<script>
    var calendario1 = new calendar("calendario1", "div1");
    if (calendario1.creado) calendario1.construyeCalendario();

</script>
    
NOV-2012: El código de este script ho he trasladado al window.onlad, para que se ejecute tras cargar el módulo calendar.js

Puede observar que hemos dispuesto un elemento <div id="div1"> y le hemos pasado la referencia a su id. Una vez creado con new calendar() entonces es aconsejable ver si se ha creado efectivamente y luego construirlo con el método construyeCalendario(). Como en este caso no pasamos el argumento dondeTitulo el calendario lo sitúa en el elemento <caption> de la propia tabla. Al no pasar el argumento fecha el calendario se construye con el mes y año de la fecha actual. El día actual se señala con fondo amarillo, mientras que los festivos tienen también un estilo particular.

Ejemplo:

Un calendario con el título en la tabla

No es necesario aplicar inmediatamente construyeCalendario() para presentar la hoja del mes tras la creación del objeto con new calendar(). En este ejemplo se crea el calendario cuando se carga la página pero se construye la hoja cuando el usuario pulse el botón:

Ejemplo:

Ver calendario de Febrero 2000 con este

<b><i>Ver calendario de Febrero 2000 con este </i></b>
<input type="button" value="botón"
onclick = "calendario1a.construyeCalendario()" />
<div id="div1a"></div>
<script>
    var calendario1a = new calendar("calendario1a", "div1a", "", "1/02/2000");
</script>
    
NOV-2012: El código de este script ho he trasladado al window.onlad, para que se ejecute tras cargar el módulo calendar.js.

También hay otros métodos que construyen el calendario, como situaFecha(fecha), donde el argumento es un literal de fecha "d/m/aaaa", situaHoy() y mueveCalendar(+-1) para mover a una hoja antes o después:

<ul>
<li><input type="button" value="situaFecha('1/12/1995')"
onclick = "calendario1b.situaFecha('1/12/1995')" /> construye...</li>
<li><input type="button" value="situaHoy()"
onclick = "calendario1b.situaHoy()" /> construye...</li>
<li><input type="button" value="mueveCalendar(1)"
onclick = "calendario1b.mueveCalendar(1)" /> construye...</li>
<li><input type="button" value="mueveCalendar(-1)"
onclick = "calendario1b.mueveCalendar(-1)" />, lo mismo...</li>
</ul>
<div id="div1b"></div>
<script>
    var calendario1b = new calendar("calendario1b", "div1b");
</script>
    
NOV-2012: El código de este script ho he trasladado al window.onlad, para que se ejecute tras cargar el módulo calendar.js.

Ejemplo:

Ver calendario con alguno de estos botones:
  • construye la hoja para el mes y año dado con el método situaFecha(fecha)
  • construye la hoja para el mes y año de la fecha actual.
  • construye el calendario para el mes posterior al de la fecha que tenga almacenada.
  • , lo mismo pero mueve un mes atrás.

Ejemplo de calendario con ubicación externa del título

Si no pasamos dondeTitulo el calendario sitúa el título con el mes y el año en el elemento <caption> de la propia tabla. Si no queremos esto le pasamos otro elemento externo HTML para situarlo. Puede ser de bloque o de línea, pero que admita introducir una cadena de texto con elemento.innerHTML. Este argumento dondeTitulo es un string con el atributo id del elemento.

<div>
    <span id="titulo2" style="border: blue double 3px"></span>
    <div id="div2"></div>
</div>
<script>
    var calendario2 = new calendar("calendario2", "div2", "titulo2", "1/7/2010");
    if (calendario2.creado) calendario2.construyeCalendario();
</script>
NOV-2012: El código de este script ho he trasladado al window.onlad, para que se ejecute tras cargar el módulo calendar.js.

Se observa que hemos elegido un elemento <span id="titulo2"> como lugar donde irá el título con el mes y el año, situándolo antes de la tabla. También en este caso pasamos un fecha para que se almacene en el calendario "1/7/2010". Este argumento si se pasa ha de tener el formato "d/m/aaaa". En este ejemplo se construye el calendario para el mes de julio de 2010.

Ejemplo:

Calendario con el título en un elemento externo a la tabla

Ejemplo de calendario con título modificable

Podemos también hacer que el usuario final interaccione con el calendario para que seleccione un mes y un año y obtener esa hoja del calendario. Para ello es necesario que en el argumento dondeTitulo haya un string con el identificador id de un elemento HTML <input type="text">, de tal forma que en ese cuadro de texto pueda modificarse el mes y año:

<input type="text" id="input3"
    onfocus = "calendario3.entraFecha(this)"
    onblur = "calendario3.mueveMesAnyo(this)" />
<div id="div3"></div>
<script>
    var calendario3 = new calendar("calendario3", "div3", "input3");
    if (calendario3.creado) calendario3.construyeCalendario();
</script>
    
NOV-2012: El código de este script ho he trasladado al window.onlad, para que se ejecute tras cargar el módulo calendar.js.

El elemento <input> contiene el título con el formato del nombre del mes en 3 letras primeras mayúsculas, una espacio y el año. Por ejemplo, "JUN 2010". Cuando el usuario entra en el cuadro se activa el evento onfocus que ejecuta el método entraFecha(), el cual convierte "JUN 2010" en "6/2010". Así el usuario puede cambiar de mes y año para ir a otra hoja del calendario con el evento onblur que ejecuta el método mueveMesAnyo(). Estos dos métodos son exclusivos para usar con un <input>, por lo que no funcionarán con cualquier otro elemento.

Ejemplo:

Calendario con el título modificable

Ejemplo de calendario con selección de día

Otra posibilidad que tiene el usuario final del calendario es seleccionar un día del mes y que este pase a la propiedad dia del objeto. Para ello es necesario pasar el argumento conSeleccionDia igual a true.

<div class="cal">
    <input type="button" class="mov" value="<"
        onclick="calendario4.mueveCalendar(-1)" />
    <input type="text" class="titulo"" id="input4"
        size="8" maxlength="7"
        onfocus = "calendario4.entraFecha(this)"
        onblur = "calendario4.mueveMesAnyo(this)" />
    <input type="button" class="mov" value=">"
        onclick="calendario4.mueveCalendar(1)" />
    <div id="div4" class="tab"></div>
</div>
<fieldset>
    <input type="button" value="Información"
        onclick="calendario4.mensaje()" />
    <input type="button" value="Ir a hoy"
        onclick="calendario4.situaHoy()" />
    <br />
    <input type="text" id="inputfecha" value="31/12/1999"
        size="10" maxlength="10" title="dd/mm/aaaa" />
    <input type="button" value="Ir a esa fecha"
        onclick = "document.getElementById('inputfecha').value =
            calendario4.situaFecha(document.getElementById('inputfecha').value)" />
    <br />
    <input type="button" value="Fecha seleccionada"
        onclick="alert(calendario4.devuelveFecha(document.getElementById('formato').value))" />
    Formato:
    <select id="formato">
        <option value="fecha-larga">"fecha-larga" p.e. "10/6/2010"</option>
        <option value="fecha-corta">"fecha-corta" p.e. "10/6/10"</option>
        <option value="fecha-dia-semana">
            "fecha-dia-semana" p.e. "Jueves, 10 de Junio de 2010"</option>
        <option value="array">"array": objeto Array()</option>
        <option value="date">"date": objeto Date()</option>
    </select>
</fieldset>
<script>
    var calendario4 = new calendar("calendario4", "div4", "input4", "", true);
    if (calendario4.creado) calendario4.construyeCalendario();
</script>
    
NOV-2012: El código de este script ho he trasladado al window.onlad, para que se ejecute tras cargar el módulo calendar.js.

Ahora pasamos el argumento conSeleccionDia y valor true para que el usuario pueda seleccionar el día pinchando con el ratón. Ese día se almacenará en la propiedad dia del objeto y se aplicará un estilo para resaltarlo visualmente. Con el método devuelveFecha(formato) podemos entonces rescatar ese día incluido en una fecha con formato.

El método del calendario para seleccionar el día necesita adjudicar eventos a las celdas <td> de la tabla y existe un problema con la palabra reservada this de JavaScript que, por ahora, no he podido resolver. Se explica con más detalle en la parte de diseño del calendario este problema con this.

Antes del script está todo el código HTML. Primero hay un <div class="cal"> donde ponemos el título, dos botones para navegar por los meses y la propia tabla del calendario. Luego hay un <fieldset> para albergar elementos y aplicar otros métodos adicionales del objeto. En cada elemento hemos puesto un atributo title que nos enseña que método de la clase se va a ejecutar. En la documentación de la clase se puede leer el funcionamiento de estos métodos.

Ejemplo:




Formato:

La tabla se crea sin estilo de ninguna clase a excepción de los días festivos y del día actual. En este ejemplo hemos puesto un estilo que está situado en la cabecera de este documento. Los elementos de la tabla no tienen nombre de clase para luego dar estilo, por lo que es más fácil acceder a ellos metiendo la tabla, el título y los dos botones de navegación dentro de algún bloque tal como hicimos aquí.

/* Estilo para el ejemplo calendario4 */
div.cal div.tab {
    padding-bottom: 0.2em;
    }
div.cal input.titulo {
    border: none;
    color: green;
    font-size: 2em;
    font-weight: bold;
    text-align: center;
    }
div.cal input.mov {
    color: green;
    font-size: 2em;
    font-weight: bold;
    }
div.cal div.tab table {
    font-family: Courier New;
    font-size: 2em;
    border-collapse: collapse;
    }
div.cal div.tab table th {
    color: navy;
    font: 1.1em 'Arial';
    padding: 0.3em;
    border: navy solid 1px;
    }
div.cal div.tab table td {
    color: black;
    font-weight: bold;
    text-align: right;
    padding: 0.3em;
    border: navy solid 1px;
    }

Ejemplo de calendario para integrar en un formulario

NOTA: Estos dos ejemplos funcionaron en Explorer 8, Firefox 3.6 y Safari 4.0. En Opera 10.6 también funcionó pero no se actualizan los bordes internos de la tabla en la primera presentación. Este efecto sucede también con otros objetos que crean dinámicamente tablas en el DOM, pero hasta el momento no he conseguido aislar el problema.

En meses posteriores he creado una nueva clase de objetos, los mensajes o formularios emergentes. Hay un ejemplo que usa un emergente para un calendario con igual propósito que el presente.

Nota de arreglo del pintado de bordes en Opera: En Julio 2011 por fin he encontrado la raíz del problema y ya Opera actualiza estos bordes. Ver cómo se hacen los contenedores con pestañas para más información. En el estilo CSS se cambia border-collapase: collapse por el valor separate y se declara border-spacing: 0. Aunque ahora el grosor de los bordes se duplica, sin embargo se corrige el problema del pintado de Opera.

Como último ejemplo proponemos un uso de nuestro calendario para el caso en que un usuario haya de introducir una fecha. Así podemos obligar a que use el calendario y sólo se introduzcan fechas válidas. En primer lugar hemos de crear un objeto calendar que, en este caso, es preferible situarlo en un script de un archivo externo que puede ver en la página codigo-calendario-form.html, donde también se expone el código del estilo CSS también situado en un archivo externo. Con la carga de la página creamos el calendario:

NOV-2012: Este ejemplo usaba dos archivos vinculados con esta página. Uno era calendario-form.js con el JavaScript y otro calendario-form.css con el estilo. Al modificar el módulo calendar.js para que se carge en el window.onload, he tenido que traspasar el JavaScript a esta página. De paso también he puesto el CSS para evitar una petición HTTP adicional.
//Variable global que apunta al calendario
var calendarioForm;
//Variable global que apuntará al elemento de el calendario.
//Puede ser un <input>, un /<span> o cualquier otro similar
var dondeFecha = null;

/* Esta es la función que se ejecuta cuando se completa la carga de la página, lo que
 * es ideal para crear objetos a los que luego se accede desde elementos y eventos
 * en la página.
 */
window.onload = function() {
    ...
    calendarioForm = new calendar("calendarioForm", "cf-tabla", "cf-titulo", "", true);
    if (calendarioForm.creado) calendarioForm.construyeCalendario();
    calendarioForm.cambiaEstiloDias("outline: red solid 3px", "dia-seleccionado");
    ...
}

Se puede ver que usamos una variable global calendarioForm para apuntar al objeto instanciado de la clase calendar. También usamos dondeFecha que tiene valor nulo al inicio, pero cuando abramos el calendario pasará una referencia del elemento HTML sobre el que interactuará el calendario. Además hay otras funciones que, brevemente, hacen lo siguiente:

Para que el código anterior se ejecute, hemos de montar el bloque contenedor del calendario y botones de navegación:

<div id="cf-cal">
    <input type="button" class="cf-mov" value="<"
        title = "Un mes atrás"
        onclick="calendarioForm.mueveCalendar(-1)"
    /><input type="text" id="cf-titulo"
        size = "10" maxlength="10"
        onfocus = "calendarioForm.entraFecha(this)"
        onblur = "calendarioForm.mueveMesAnyo(this)"
    /><input type="button" class="cf-mov" value=">"
        title = "Un mes adelante"
        onclick="calendarioForm.mueveCalendar(1)"
    /><input type="button" class="cf-mov" value="OK"
        title = "Aceptar fecha"
        onclick="recogeFechaCalendario()"
    /><input type="button" class="cf-mov" value="X"
        title = "Cerrar"
        onclick="cierraCalendarioForm()" />
    <div id="cf-tabla"></div>
</div>
    

Aquí hemos puesto este código en esta altura de la página, pero se puede poner en cualquier sitio, puesto que inicialmente se ha dotado del siguiente estilo al bloque contenedor <div id="cf-cal">:

div#cf-cal {
    position: absolute;
    top: 0;
    left: 0;
    z-index: 1;
    visibility: hidden;
    ...
    }
    

Esto quiere decir que se dota de posición absoluta por lo que se posicionará según los valores de top y left, que inicialmente son (0,0), pero que con visibility: hidden lo ocultamos de la vista. Luego con la con la función abreCalendarioForm(donde) lo situamos con su top y left en la posición donde queremos que se vuelva visible. Además hay que ponerlo en una capa superior con z-index: 1.

Y esto es en resumen lo que hay que hacer. Veámos dos aplicaciones de ejemplo, una en primer lugar con un elemento <span>, para lo cual insertamos en este punto de la página el siguiente código:

Introduzca fecha: <span id="span-fecha" class="cf-span"></span>
<input type="button" value="calendario"
    onclick = "abreCalendarioForm(document.getElementById('span-fecha'))" />
    

Ejemplo:

Introduzca fecha:

Observe que abreCalendarioForm(donde) pasa como argumento la referencia al <span> donde queremos que se abra el calendario. El segundo y último ejemplo lo hacemos con un elemento <input>:

Introduzca fecha:
<input type="text" id="input-fecha" class="cf-span" value="" />
<input type="button" value="calendario"
    onclick = "abreCalendarioForm(document.getElementById('input-fecha'))" />
    

Ejemplo:

Introduzca fecha:

Al cambiar de un caso a otro el calendario actualiza la fecha del elemento correspondiente. Si el elemento está vacío o la fecha no es valida, en el calendario no se actualiza nada y devuelve la palabra "Error" que se escribe en el elemento.

Documentación de la clase calendar

Para observar la estructura de la clase a efectos de que un programador pueda usarla sin conocer los detalles de su diseño e implantación, se puede crear un documento donde se relacionan las propiedades y métodos. Se explican de forma que el programador pueda usar la clase, sin entrar en detalles de diseño que no le aporten nada sobre ese uso. Algunos entornos de programación como los que usualmente se usan con Java, permiten obtener un documento similar de forma automática, tomando los comentarios previos que se ponen en el código de clase y método, pero respetando unas ciertas reglas. En todo caso aquí lo hemos realizado "a mano" para ofrecer como podría presentarse un documento de esta naturaleza:

Anterior Arriba Siguiente