Wextensible

Introducción a los formularios emergentes

El objeto window de Javascript permite abrir ventanas emergentes con el método window.open(). Un mensaje emergente puede ser simplemente el que suministra window.alert(), window.confirm() o window.prompt(). Ambos tienen la característica de situarse por encima de la página para que el usuario se obligue a interactuar con esa ventana o mensaje emergente. A veces se les llama pop-up a partir del término en inglés.

El abuso de las ventanas emergentes para publicidad ha ocasionado que los navegadores puedan bloquearlas a deseo del usuario. Particularmente no me gusta estar leyendo algo y que salte una de estas ventanas o que se oculten detrás sin saber lo que están haciendo. Sin embargo a veces son necesarias y no para dar publicidad, sino para aportar información adicional sobre el contenido con el que está interactuando el usuario. O bien para ayudarle a la hora de rellenar campos de formulario, como el clásico calendario emergente sobre el que eliges una fecha correcta.

Aquí creamos una ventana o mensaje emergente mediante objetos de Javascript y HTML dinámico. Al buscar un nombre adecuado para este objeto pensé que dado que se construía dentro de un elemento <form>, lo más adecuado sería llamarles formularios emergentes. De hecho este elemento sirve para enviar datos al servidor, lo que se puede hacer con nuestro formulario emergente, pero también es una manera de implementar una interfaz con los controles de formularios.

Requisitos del diseño de un formulario emergente

Se trata de crear un objeto de JavaScript que denominaremos formEmerge para construir dinámicamente una porción de HTML que representa un formulario emergente. Los requisitos básicos de este objeto son:

El módulo form-emerge.js contiene esa clase formEmerge y algunas constantes, variables y funciones globales.

El constructor del formulario emergente

El constructor del formulario emergente tiene la siguiente sintaxis (vea la nota siguiente acerca de lo que está resaltado):

formEmerge(nombreForm
[,cadenaTitulo
[,conPantalla
[,conBotones
[,moverForm
[,accionEnvio
[,metodoEnvio
[,dondePonerForm
[,ajustarAncho]]]]]]]])

Ajustar al ancho del dispositivo

(Noviembre 2013)

Un problema importante para adaptar el formulario emergente a dispositivos móviles surge cuando el ancho supera al de la pantalla. Con el nuevo argumento ajustarAncho con valor true le damos estilo max-width al crear el emergente, específicamente a un elemento interior del formulario, para que no supere el ancho del elemento body. Pero como ese ancho puede variar si redimensionamos la ventana, en cada ejecución del método abrir(..., ancho, ...) portando el argumento ancho con algún valor consultamos el ancho de body y reajustamos el ancho del formulario para que no lo sobrepase. Si el argumento ajustarAncho del constructor no se declara, se pasa null o false no se realizará ningún tipo de ajuste.

Para probar este efecto puede ver el siguiente formulario que se abrirá con un ancho de 40em (unos 640px). Si la ventana es mayor se presentará sin modificar. Si es más estrecha el emergente se ajustará para no sobrepasarla:

El código para crear el formulario es:

emerge9 = new Wextensible.FormEmerge("emerge9",
    "", true, 3, "todo", "", "", "", true);
emerge9.nuevoInterior(mensajeLoremIpsum); 

Al abrirlo hemos de especificar el ancho:

<input type="button" value="abrir emerge9"
onclick="emerge9.abrir('', '',
this.offsetLeft, this.offsetTop, '40em')" />

Nombrando constructores

(Diciembre 2012)

Inicialmente declaraba los constructores de objetos como este formEmerge, con la misma estructura que las variables: primera letra con minúsculas y usar una mayúscula al inicio de cada palabra (esto se le llama lowerCamelCase). Pero es mejor diferenciar los constructores de las instancias de los objetos, que no son otra cosa que variables. Así que ahora los constructores tienen la primera letra en mayúsculas (UpperCamelCase), por ejemplo var instancia = new FormEmerge(...).

El código fuente de estas páginas está minificado. Puede leerlo usando el botón CÓDIGO de la cabecera de este documento. Si observa la declaración de instancias de los ejemplos de esta página, verá también que estoy usando un espacio de nombres, por ejemplo emerge3 = new Wextensible.FormEmerge("emerge3").

Ambos cambios fueron agregados a todos los JavaScript de los ejemplos de esta serie de temas, pero no modifiqué los textos que siguen estando igual que al inicio. Estas dos diferencias no suponen una modificación sustancial en lo que pretendo exponer aquí.

Los argumentos opcionales se indican entre corchetes. El primero es obligatorio y todos los demás opcionales. Si se pasa alguno opcional y antes hay otros opcionales que no se pasan, deben pasarse con una cadena vacía "". Exponemos cada argumento aunque en este primer tema sólo veremos hasta moverForm y el último argumento dondePonerForm. Los argumentos accionEnvio y metodoEnvio se exponen en el tema 5 en relación con un ejemplo para enviar datos al servidor.

Para probar este objeto hemos creado varias instancias en esta misma página que hemos incluido en un <script> en el <head>:

<script>
var emerge1 = null;
var emerge2 = null;
var emerge2a = null;
var emerge2b = null;
var emerge3 = null;
var emerge4 = null;
var emerge5 = null;
var emerge6 = null;
var emerge7 = null;
window.onload = function() {
    emerge1 = new formEmerge("emerge1", "un título", false, 0);
    emerge2 = new formEmerge("emerge2", "", true, 1, "todo");
    emerge2a = new formEmerge("emerge2a", "", true, 2, "marco");
    emerge2b = new formEmerge("emerge2b", "", true, 3, "nada");
    emerge3 = new formEmerge("emerge3");
    emerge4 = new formEmerge("emerge4");
    emerge5 = new formEmerge("emerge5");
    emerge6 = new formEmerge("emerge6");
    emerge7 = new formEmerge("emerge7", "", "",
        "", "", "poner-aqui-form");
}
</script>

Se observan las variables emerge1, emerge2, ... cuyos nombres se pasan exactamente igual en el primer argumento del constructor. El primer objeto se creó con todos los argumentos de forma explícita. Otros objetos tienen el argumento conPantalla igual a true, por lo que al disponer este argumento el anterior hay que pasarlo como una cadena vacía si es que no se necesita.

En este tema veremos unos ejemplos muy simples que mostrarán un uso básico de estos formularios emergentes mediante un método para abrir el formulario. El resto de métodos y usos se verá en los temas siguientes.

El método para abrir el formulario emergente

abrir([interior
[,titulo
[,izquierda
[,arriba
[,ancho
[,alto
[,sobresale
[,foco]]]]]]]])

Si no se pasa ningún argumento se abre un formulario sin nada en su interior y se sitúa en el punto (0,0) de la página. Los argumentos no pasados que estén antes de otros que si se pasan deben llevar una cadena vacía ("") para que sean ignorados (o bien el valor null).

Como se trata de formularios que actúan como ventanas emergentes, es obvio que los argumentos que lo posicionan no forman parte de las propiedades del objeto, sino que son valores provisionales para situar el mismo en pantalla. Por eso no se incluyen con el constructor formEmerge(). Igual consideración tienen el contenido del interior y el cambio de título que son valores específicos para cuando se vaya a abrir el formulario. Así un mismo formulario puede servir para abrirlo con diferentes mensajes, títulos y posiciones en una misma página web.

Por la misma razón el ancho, alto y el control de lo que sobresale forman parte de las condiciones específicas de cada uso cuando abrimos el formulario, pues depende del interior que se incluya con el método para abrirlo. De esta forma podemos construir formularios con new formEmerge() y luego darle múltiples usos mediante este método, o incluso la reescritura de sus propiedades y otros métodos como veremos más adelante.

Ejemplo para abrir, situar y mover un formulario emergente en la página

Ahora hacemos una utilización de este método que nos permite abrir el formulario emergente mediante el evento onclick de estos botones situados aquí mismo a continuación:

Ejemplo:


Botones para dimensionar formulario actuando sobre font-size

(20 Septiembre 2013)

He actualizado la aplicación para que recoga los eventos de toque (touch events). Pero otro problema que se presenta en dispositivos de pequeño tamaño es cuando las dimensiones del formulario sobrepasan la reducida ventana de un móvil. Por eso acompaño de forma automática dos botones al pie del formulario para ampliar o reducir el tamaño de la fuente de texto y, en consecuencia, también se ampliará o reducirá el tamaño total del formulario ya que tiene medidas relativas en em.

Si no deseamos que aparezcan en ningún formulario emergente podemos ir al archivo de estilo form-emerge.css y buscar la clase form.CLPREform-emerge div.CLPREform-emerge-dimensionar, a la que agregaremos display: none.

Si sólo queremos desactivarlo para un formulario, como este ejemplo emerge8, podemos poner dentro de un elemento <style> en esta página un display: none al elemento div#IDPREemerge8Dimensionar. Note como se compone este ID: un prefijo IDPRE, el nombre de la instancia del formulario emerge8 y el término Dimensionar para el contenedor que alberga los botones para dimensionar.

Para el primer botón que abre emerge1 en la esquina superior izquierda de la pantalla, no hemos usado argumentos. El código para abrirlo es mediante un evento onclick situado en ese botón <input>.

<input type="button" value="abre emerge1 sin argumentos en x=0, y=0" 
onclick = "emerge1.abrir()" />

Cuando se pasa sin argumentos emerge1.abrir() pone el título igual que el nombre de la variable emerge1, sin interior y posicionado en el punto (0,0) del <body> del documento, que fue el lugar donde se construyó el emergente (lo encontrará arriba y a la izquierda de esta página). El botón para abrir emerge1 con argumentos es el siguiente:

<input type="button" value="abre emerge1 con argumentos" 
onclick = "emerge1.abrir(
'Esto es un 1er &lt;b&gt;mensaje&lt;/b&gt; emergente', 
'',
this.offsetLeft, 
this.offsetTop + this.offsetHeight)" />

Podemos abrir el mismo emerge1 pero ahora lo hacemos dándole argumentos. El mensaje que se pone en el interior es un literal HTML cualquiera. Se observa que hemos incluido la palabra "mensaje" en el elemento <b>, para lo cual se ha de escapar los caracteres "< >". Luego hay una cadena vacía para el título con lo cual no se modifica el que se puso cuando se construyó el objeto. A continuación viene la posición izquierda y arriba. Usamos this.offsetLeft que nos da el valor computado en píxeles de la posición izquierda del botón que estamos pulsando. De la misma forma situamos en la posición arriba tomando el offsetTop más la altura del botón con offsetHeight. Estos valores offset funcionan con los navegadores Internet Explorer 8, Firefox 3.6, Opera 10.6 y Safari 4.0, aunque no he probado si en versiones anteriores o en otros navegadores también sirven.

Al crear este objeto emerge1 no hemos pasado el argumento moverForm con lo que toma su valor por defecto "todo". Así pinchando en la barra de título con el ratón podremos arrastrarlo por la pantalla.

Para el segundo botón anterior que abre el formulario emerge2 tenemos el siguiente código:

<input type="button" value="abre emerge2" 
onclick = "emerge2.abrir(
'Esto es un 2º...',
'mensaje de emerge2',
this.offsetLeft,
this.offsetTop + this.offsetHeight
)" />

En este caso se trataba del formulario emerge2 que fue construido activando los botones aceptar y cancelar. Aquí sólo hemos incluido un título, "mensaje de emerge2", para modificar el dado en la construcción del objeto. El botón aceptar así como cancelar y aplicar, se usan para formularios emergentes que reciben y devuelven valores, que veremos en un tema posterior con más detalle.

También con ese ejemplo hemos usado las transparencias que se sitúa como una pantalla entre la página y el formulario emergente. Esta propiedad de usar esa pantalla se la dimos al formulario emerge2 cuando lo construímos con new formEmerge(). Así obligamos al usuario a interaccionar sólo con el emergente. Además si hay otro formulario abierto, el de pantalla cierra todos los demás que estén abiertos. Esto lo puede probar abriendo emerge1 y, sin cerrar este, abrir emerge2. Además con un doble click en la pantalla se cierran todos los formularios abiertos (sólo habrá uno) y se desactiva la pantalla.

En este caso hemos pasado el argumento moverForm con el valor "todo", que es el valor por defecto igual que si no hubiesémos pasado el argumento.

Los otros dos emergentes emerge2a y emerge2b son iguales que emerge2 a excepción de que presentan más botones:

<input type="button" value="abre emerge2a" 
onclick = "emerge2a.abrir('Esto es un 3er...',
'mensaje de emerge2a',
this.offsetLeft,
this.offsetTop + this.offsetHeight)" />

<input type="button" value="abre emerge2b" 
onclick = "emerge2b.abrir('Esto es un 4º...',
'mensaje de emerge2b',
this.offsetLeft,
this.offsetTop + this.offsetHeight)" />

Se debe tener en cuenta que el texto que se va a pasar al interior debe contemplar los saltos de línea, pues en otro caso el texto se extenderá horizontalmente hasta ocupar un ancho de pantalla y luego saltará. Luego veremos como contener el texto declarando el ancho del formulario. Pero en estos dos casos hemos puesto un salto de línea <br /> donde creímos conveniente, por ejemplo para el primero de los anteriores (omitimos el resto de la cadena para mayor claridad pues no es significativo):

'Esto es un 3er ... emergente&lt;br /&gt;con botones...

donde puede observar que pasamos el elemento <br /> pero escapando las referencias a los caracteres reservados: &lt;br /&gt;, pues estamos poniendo un literal HTML dentro de un atributo de texto, lugar donde no se admite los caracteres <, > o &.

Como ejemplo del argumento moverForm declarado en el constructor para estas instancias emerge2a y emerge2b, en una de ellos pasamos el valor "marco" para mover un rectángulo a modo de marco en lugar del propio formulario. En el otro pasamos el valor "nada", con lo que el formulario no podrá ser movido. El hecho de haber creado dos posibles formas de mover el formulario (mover "todo" y mover con "marco"), se debe a que todos los navegadores no se comportan exactamente igual. Además si el formulario tiene mucho contenido, es posible que el movimiento del mismo por la pantalla se ralentice, incluso más si el ordenador tiene pocos recursos. En principio mover un marco sin contenido debería resultar con menos carga para el navegador. Sea como fuere existen dos posibilidades que hay que probar antes de usarlas.

Posicionado emergente en celdas de una tabla

Si el elemento sobre el que tenemos que hacer click para abrir un formulario está dentro de una tabla tenemos que tener en cuenta el posicionamiento de la tabla:

1,11,21,3
1,11,21,3
1,11,21,3

El código en el evento onclick sobre la celda azul de la tabla con identificador id="una-tabla" es el siguiente:

var unaCelda = document.getElementById("una-celda");
unaCelda.addEventListener("click", function(){
    var unaTabla = document.getElementById("una-tabla");
    emerge2a.abrir("Mensaje", 
        "emerge2a", 
        this.offsetLeft + unaTabla.offsetLeft, 
        this.offsetTop + unaTabla.offsetTop + this.offsetHeight);
}, false);

Ejemplo para redimensionar el ancho y alto de un formulario emergente al abrirlo

El método abrir() tenía además los argumentos ancho, alto y sobresale. Veámos como actúan con estos tres botones que ejecutan el método abrir sobre los formularios emerge3, emerge4 y emerge5:

Ejemplo:

Veámos el código relacionado con estos botones. En primer lugar hemos situado a esta altura de la página el siguiente script para almacenar una cadena de texto muy larga que representa un literal HTML para insertar en el interior del formulario. La hemos cortado pues no tiene mayor interés y su propósito es usarla con los botones:

<script>
    var mensajeLargo = "Este es un <big><big>mensaje</big></big> muy largo...";
</script>

Ese mensajeLargo lo vamos a pasar al botón emerge3:

<input type="button" value="abre emerge3" 
onclick = "emerge3.abrir(
mensajeLargo, 
'', 
this.offsetLeft, 
this.offsetTop + this.offsetHeight
)" />

El resultado es que el texto del mensaje, que recordemos que se inserta como HTML mediante el comando innerHTML, se expandirá a la derecha hasta ocupar todo el ancho de su contenedor. Esta es la forma de actuar por defecto de los elementos de bloque en HTML, pues se trata de los propiedades de estilo width y height, que si no están especificadas se asumen con valor auto. Así el ancho de un bloque se ajusta al de su contenedor y el alto se ajusta al contenido.

Alineado derecha y redimensionamiento ventana

(Noviembre 2013)

Con las actualizaciones llevadas a cabo para adaptar el formulario emergente a los dispositivos móviles, ahora antes de abrir un formulario se observa que su borde derecho no sobrepase el de la pantalla, en cuyo caso se se alinea a la derecha. Esto es especialmente útil en dispositivos móviles de anchos de pantalla reducidos, pues así evitamos que el body se alargue por la derecha y al mismo tiempo tendremos acceso a la parte derecha del formulario donde está el botón para cerrarlo o los botones de aceptar, cancelar y aplicar.

Otra actualización está relacionada con el redimensionamiento del ancho de la ventana o el cambio de orientación en móviles. Si hay un formulario abierto estará posicionado en un left, top determinado y relativo a un elemento padre o en última instancia al body. Al modificar el ancho del body esos valores left, top siguen siendo iguales pero no se corresponden con el punto donde antes estaba localizado el formulario. Así éste puede alejarse mucho del punto inicial e incluso salirse del área de cliente (superficie visible de la ventana) o incrementar el alto del body. Hay que recordar que la mayor parte de los elementos de una página pueden tener posicionamiento estático, de tal forma que al incrementar el ancho de la ventana se disminuye el alto de esos elementos y en consecuencia también el alto del body. Para evitar este efecto usamos el evento window.onresize que detectará si hay cambio en el ancho y cerrará todos los formularios abiertos.

Se podía haber evitado con elementos <br /> en el texto que son saltos de línea HTML, pero podemos hacer uso de los argumentos que especifican el ancho y el alto como vemos en el segundo botón para emerge4:

<input type="button" value="abre emerge4" 
onclick = "emerge4.abrir(
mensajeLargo, 
'', 
this.offsetLeft, 
this.offsetTop + this.offsetHeight,
'20em'
)" />

Recuerde que los argumentos ancho y alto son valores de estilo CSS para las propiedades width y height. Le dimos a este botón un ancho de 20em, es decir, 20 veces el valor del tamaño de su fuente.

Por último también podemos especificar el alto para el botón que abre el formulario emerge5:

<input type="button" value="abre emerge5" 
onclick = "emerge5.abrir(mensajeLargo, 
'', 
this.offsetLeft, 
this.offsetTop + this.offsetHeight, 
'20em', 
'4em', 
'auto'
)" />
    

Le damos un alto de 4em a propósito para ver el efecto con el valor auto para controlar lo que sobresale con la propiedad de estilo overflow. De esa forma el navegador incluirá una barra de desplazamiento vertical para acceder al contenido oculto.

Otro aspecto que puede observarse cuando no se usa la pantalla de transparencia, con lo que podemos tener varios emergentes en pantalla como los de esos tres últimos ejemplos, es que haciendo click en cualquier parte del formulario lo traemos al frente.

Un mismo formulario para diversos usos

En este primer tema estamos presentando el método para abrir el formulario emergente, para lo cual hemos usado 5 instancias de la clase formEmerge. Pero el método abrir() nos permite adaptar el formulario a cada momento. Veámos un ejemplo de como un único formulario nos sirve para esto. Usaremos otra instancia emerge6 que fue creada con emerge6 = new formEmerge("emerge6") sin más argumentos, con lo que es un simple mensaje emergente sin botones aceptar/cancelar y sin pantalla.

Se trata de un simple cuadro de texto que recoge a modo de ejemplo la introducción de un nombre de usuario para una hipotética alta en un registro. De alguna forma queremos controlar lo que el usuario introduce. Para simplificar el ejemplo queremos que el usuario introduzca una cadena de texto que tenga entre 4 y 8 caracteres:

Ejemplo:

Para este ejemplo usamos el elemento <input type="text"> siguiente:

<label>Introduzca nuevo usuario: <input type="text" value=""
onblur = "validaUsuario(this)"/>
</label>    
    

El script que controla la entrada de texto es el siguiente, que hemos situado a esta altura de la página pero que recordamos que en un uso real es recomendable ubicarlo en archivos externos:

<script>
    function validaUsuario(inpute) {
        var texto = inpute.value;
        var mensaje = "";
        var titulo = "error";
        if (texto == "") {
            mensaje = "con error: <big>cadena de texto vacía</big>.";
        } else if (texto.length > 8) {
            mensaje = "con error: <big>cadena mayor de 8 caracteres</big>.";
        } else if (texto.length < 4) {
            mensaje = "con error: <big>cadena menor de 4 caracteres</big>.";
        } else {
            mensaje = "correcto";
            titulo = "usuario validado";
        }
        mensaje = "Nombre de usuario " + mensaje;
        var izquierda = inpute.offsetLeft;
        var arriba = inpute.offsetTop + inpute.offsetHeight;
        emerge6.abrir(mensaje, titulo, izquierda, arriba, "12em");
        //A continuación debería ejecutarse algo si la entrada 
        //fue correcta como dar de alta al usuario en un registro.
    }
</script>

Podemos volver a abrir emerge6 para otra cosa

Ejemplo:

usando el código

<input type="button" value="emerge6"
onclick = "emerge6.abrir('Otro mensaje de emerge6','EMERGE6', 
this.offsetLeft, this.offsetTop + this.offsetHeight)" />

por lo que el mismo formulario emergente sirve para diversos propósitos Si hemos de usar ventanas como mensajes emergentes en una página, sólo hace falta declarar un formulario y adaptar sus características con el método abrir().

Ubicando el formulario dentro de un elemento de bloque

El último argumento del constructor es dondePonerForm. Si no se pasa este argumento, el elemento <form> que encierra todo el emergente se ubica en el <body> del documento. Esta es la mejor opción, pero dejamos la posibilidad abierta de elegir cualquier elemento de bloque para construirlo ahí. La instancia emerge7 la construimos así:

emerge7 = new formEmerge("emerge7", "", "", "", "", 
"poner-aqui-form") ;       
    

Se observa el primer argumento obligatorio y el último dondePonerForm igual al identificador "poner-aqui-form", que es el id de un elemento de bloque <div> que está a continuación, con borde azul:

Ejemplo:

Este elemento de bloque <div> lo usamos para tener un lugar donde poner el formulario. Eso significa que el posicionamiento inicial, es decir, cuando abrimos con emerge7.abrir(), será el (0,0). Pero para que sea relativo a este elemento hay que darle el estilo position: relative. El código de ese elemento y del botón previo que abre el formulario es:

<input type="button" value="emerge7.abrir()"
onclick = "emerge7.abrir()" />
<div id="poner-aqui-form"
style="border: blue solid 1px;
position: relative; "></div>

Se observa que le damos el posicionamiento del que habíamos hablado. Esto realmente no hace falta para otro objetivo que establecer la apertura en (0,0), pues el formulario no está contenido en el elemento desde un punto de vista formal, aunque si estructuralmente hablando, porque de hecho está en una capa superior. Se observa que podemos desplazarlo incluso fuera del bloque.

Por ejemplo, con esto otro botón lo abrimos usando
emerge7.abrir('','',this.offsetLeft,this.offsetTop), lo cual es indiferente de que el bloque contenedor tenga o no posición relativa.

Pero como hemos dicho, estructuralmente si está dentro de ese <div id="poner-aqui-form">, lo que puede observarse si se quita el estilo a la página. Entonces se verán en primer lugar todos los formularios mientras que este emerge7 estará hacia esta altura de la página.