Actualizaciones:

Este código es OBSOLETO. Por favor, consulte el actual en los enlaces de esta nota.

He incorporado un resaltador de código que mostrará el código actual de un documento HTML, JS, CSS o PHP. Lo que se expone aquí puede haber sido modificado. Vea los actuales en

Código Javascript de la clase formEmerge


/*
    =========================================================================
    Software: form-emerge.js
    Description: Módulo para crear formularios emergentes con JavaScript
    Contact: http://www.wextensible.com
    Copyright (c) 2010, Andrés de la Paz

    Necesita el estilo en form-emerge.css

    La documentación de la clase la puede consultar en documentacion-clase.html
    
    Septiembre 2010: Las propiedades "opacity" y "user-select" no son aún estándar
    en CSS 2.1. Cómo las tenía incluidas en form-emerge.css, ahora las quitamos para
    poder  validar el documento. Se aplican con el módulo general.js, función
    estiloNoCss, 
    incrustándose dinámicamente en los elementos "Pantalla",  "Marco"
    y "Barra" al crear un formulario emergente.
    

    ==========================================================================
*/

// CONSTANTES Y GLOBALES -----------------------------------------------------

/* Constantes para prefijos de los identificadores (atributos id) y también las
 * clases (atributos class) de los elementos creados dinámicamente en un formulario
 * emergente. La finalidad es que no interaccionen con otras posibles referencias
 * del documento que por alguna casualidad tengan el mismo identificado o
 * nombre de clase respectivamente.
 */
var PID = "IDPRE";
var PCL = "CLPRE";


/* Constante para limitar el número máximo de pestañas. Si se declaran más
 * que estas, sólo se crearan este número.
 */
var MAX_TABS = 20;



/* Una variable globlal para almacenar referencias en un array a los formularios que
 * se vayan creando. Estas referencias son al objeto, es decir, a las instancias de
 * la clase formEmerge. No son referencias al elemento <form> que se crea dinámicamente
 * con cada instancia de formEmerge. Así con var x = forms[n] podemos acceder al
 * objeto formulario enésimo. Luego podemos aplicar el método form() de la clase
 * formEmerge y así con x.form() tenemos la referencia al elemento <form>.
 */
var forms = new Array();


/* Variable global para controlar el número de formularios abiertos. Se usa para
 * el caso en que usemos una pantalla de transparencia sobre la que se abren
 * los formularios emergentes cerrar esa pantalla cuando no queden formularios
 * abiertos.
 */
var formsAbiertos = 0;


/* Esta función global nos permite cerrar todos los formularios en cualquier caso.
 * Se usa principalmente con el método abrir(), pues con pantalla sólo se permite
 * tener un formulario abierto y con esta función se cierran todos los abiertos.
 */
function cerrarFormularios() {
    try {
        for (var i=0; i<forms.length; i++){
            if (forms[i].abierto) {
                //Anulamos el evento cerrar por si estuviera activado
                var temp = forms[i].eventoCerrar;
                forms[i].eventoCerrar = "";
                forms[i].cerrar();
                //Reactivamos el evento cerrar
                forms[i].eventoCerrar = temp;
                //Cerramos el marco si estuviera abierto
                var marco = forms[i].form("marco");
                if (marco != null) marco.style.display = "none";
            }
        }
        formsAbiertos = 0;
        //Cerramos la pantalla si estuviera abierta
        var pantalla = document.getElementById(PID + "pantalla");
        if (pantalla != null) pantalla.style.display = "none";
    } catch (e) {
        alert("Error al cerrar todos los formularios");
    }
}


/* Devuelve una referencia a un nombre de formulario. Esta referencia es al objeto
 * instanciado de la clase formEmerge, no del <form> que cada formulario emergente
 * contiene.
 * - nombreForm: string con el nombre del formulario para abrir.
 */
function formRef(nombreForm) {
    try {
        for (var i=0; i<forms.length; i++) {
            if (forms[i].nombre == nombreForm) return forms[i];
        }
        return null;
    } catch(e) {
        return null;
    }

}



/* CONSTRUCTOR DEL FORMULARIO EMERGENTE -----------------------------------------------------
 * Crea clases de formularios, independientemente de cualquier otro módulo, por lo que es un
 * objeto reutilizable para otros propósitos. Se ha de acompañar de 'form-emerge.css' que es
 * la hoja de estilo CSS 2.1.
 * - nombreForm: string con el nombre del formulario
 * - cadenaTitulo: string con el título que irá en la barra superior
 * - conPantalla: booleano para abrir o no sobre un pantalla de transparencia
 * - conBotones: entero [0-3] para ningún boton, aceptar, aceptar/cancelar o
 *   aceptar/cancelar/aplicar.
 * - accionEnvio: Si se pasa este argumento, construye el formulario para envío de datos
 *   al servidor, poniendo el botón aceptar como "submit" y el aplicar como "reset".
 *   Este argumento se pone en el action, mientras que el method será el siguiente:
 * - metodoEnvio: para el method del <form>. Si no se pasa se usa "post"
 * - dondePonerForm: string con id de un elemento para crear el formulario. Si no se
 *   pasa se pone en el body del documento
 */
function formEmerge(nombreForm, cadenaTitulo, conPantalla, conBotones,
        moverForm, accionEnvio, metodoEnvio, dondePonerForm) {
    
    /* PROPIEDADES .......................................................................
     * Las propiedades son generalmente privadas en el sentido de que no es necesario
     * acceder directamente para modificarlas. Si acaso para consultarlas como con
     * la propiedad botonPulsado para saber si se ha pulsado ese botón.
     * La mayor parte se modifica directamente a través de los métodos, pero vea
     * la documentación al respecto.
     */
    
    /* Almacena el nombre del formulario
     * Es una propiedad obligatoria y si está vacio el argumento no se crea el
     * formulario.
     */
    this.nombre = "";
    try {
        if ((nombreForm != null)&&(nombreForm != "")){
            if (formRef(nombreForm) != null) {
                alert("Ya ha sido creado el formulario '" + nombreForm + "'");
                return;
            } else {
                this.nombre = nombreForm;
            }
        } else {
            alert("El nombre del formulario es obligatorio");
            return;
        }
    } catch(e) {
        alert("Error al comprobar nombre para nuevo formulario " + nombreForm);
        return;
    }

    
    /* Almacena el título.
     * Por defecto es el mismo que el nombre.
     */
    this.titulo = nombreForm;
    if ((cadenaTitulo != null)&&(cadenaTitulo != "")) this.titulo = cadenaTitulo;

    
    /* Almacena si tiene pantalla
     * NOTA: el objeto pantalla es un <div> genérico para todas las instancias, por lo
     * tanto no se puede aplicar el método this.form() para obtener una referencia sino
     * cogerla directamente con document.getElementById(PID + "pantalla").
     * Con un doble click en la pantalla se cierran todos los formularios y se desactiva
     * la pantalla.
     */
    this.pantalla = false;
    if (conPantalla != null) {
        this.pantalla = conPantalla;
        var pant = document.getElementById(PID + "pantalla");
        if (pant == null) {
            document.body.innerHTML += "<div id='" + PID + "pantalla' " +
            "style = '" + estiloNoCss("opacity", 0.6) + "' " +                   
            "ondblclick = 'cerrarFormularios()' " +
            "></div>";

        }
    }
    
    /* Número de botones a presentar.
     * Botones: 0 sin botones, 1 aceptar, 2 aceptar y cancelar,
     * 3 aceptar, cancelar y aplicar
     */
    this.botones = 0;
    if (conBotones != null) this.botones = conBotones;

    
    /* Este es el identificador ID al objeto respuesta que trae y devuelve los valores.
     * Se trata de una cadena String que inicialmente se construye en base al nombre
     * de cada formulario, aunque puede ser referenciado desde fuera a otro elemento.
     * NOTA: El método this.form() no se puede aplicar a este id porque aquí va ya
     * con los prefijos debido a que luego podemos referenciar desde fuera otro
     * id y no necesariamente deberá llevar los prefijos. Entonces hay que coger
     * la referencia directamente con document.getElementById(this.idObjetoRespuesta)
     * El OBJETO RESPUESTA se construye inicialmente de forma automática para cada
     * formulario. En este caso se trata de un <input> tipo hidden, con un id
     * que se establece en this.idObjetoRespuesta.
     */
    this.idObjetoRespuesta =  PID + this.nombre + "objetoRespuesta";
    try {
        document.body.innerHTML = "<input type='hidden' id='" +
        this.idObjetoRespuesta + "' value='' />" + document.body.innerHTML;

    } catch(e) {
        alert("Error al establecer objeto respuesta para " + this.form + ": " + e.message);
    }

    
    /* Esto es un string para almacenar temporalmente el valor del objeto respuesta.
     * Dentro del formulario podemos trabajar con this.respuesta y si al final se
     * pulsa aceptar pondremos esa respuesta en el objeto respuesta.
     * Con el resto de botones no se actualiza.
     */
    this.respuesta = "";

    
    /* Estos son los eventos. Si tienen true se ejecutará las funcione anónima
     * correspondiente.
     */
    this.eventoAbrir = false;
    this.eventoAceptar = false;
    this.eventoCancelar = false;
    this.eventoAplicar = false;
    this.eventoCerrar = false;

    
    /* Ponemos una función anónima que luego podrá ser sobreescrita externamente.
     * Hay un función para cada evento.
     */
    this.ejecutaEventoAbrir = function(){};
    this.ejecutaEventoAceptar = function(){};
    this.ejecutaEventoCancelar = function(){};
    this.ejecutaEventoAplicar = function(){};
    this.ejecutaEventoCerrar = function(){};

    
    /* Cuando pulsamos un botón pone "aceptar", "cancelar", "aplicar" o "cerrar" según
     * cual sea el botón pulsado.
     */
    this.botonPulsado = "";

    
    /* Indica si el formulario está abierto
     */
    this.abierto = false;

    
    /* Número total de pestañas en la estructura interna de pestañas
     */
    this.totalTabs = 0;

    
    /* Para mover el formulario. Con el valor "nada" para no mover, "todo" mover
     * todo el formulario "marco" para mover un marco. Por defecto tiene el valor
     * "todo". Si no se pasa o se pasa una cadena vacía se aplica este argumento
     * por defecto.
     */
    this.mover = "todo";
    if ((moverForm == null)||(moverForm == "")) {
        this.mover = "todo";
    } else if (moverForm == "nada") {
        this.mover = "nada";
    } else if (moverForm == "todo") {
        this.mover = "todo";
    } else if (moverForm == "marco") {
        this.mover = "marco";
    } else {
        this.mover = "todo";
    }
    this.iniciaMover = false;
    this.xMover = 0;
    this.yMover = 0;
    if (this.mover == "marco") {
        var cadHtml = "<div id='" + PID + this.nombre + "marco' " +
        "class = '" + PCL + "marco' " +
        "style = '" + estiloNoCss("opacity", 0.4);        
        //Este elemento se dotó con position fixed en el CSS externo, pero por
        //alguna razón sólo Safari requiere posición absoluta, lo que se contradice
        //porque hay que situar este marco en cualquier punto de la pantalla.
        //Seguiremos indagando esto hasta ver la causa.
        if (esNavegador("safari")) {
            cadHtml += "; position: absolute" 
        }
        cadHtml += "' onmouseup = '" + this.nombre + ".moverMarco(this, event)' " +
        "onmousemove = '" + this.nombre + ".moverMarco(this, event)' " +
        "onmouseout = '" + this.nombre + ".moverMarco(this, event)' " +
        "onmouseleave = '" + this.nombre + ".moverMarco(this, event)' " +
        ">&nbsp;</div>";
        document.body.innerHTML = cadHtml + document.body.innerHTML;

    }

    
    /* Para usar el formulario para enviar datos al servidor.
     * La propiedad this.paraEnvio será true si accionEnvio tiene un string
     * Si el método no es "get" o "post" se queda como "post"
     */
    this.paraEnvio = false;
    this.accionEnvio = "";
    this.metodoEnvio = "post";
    if ((accionEnvio != null)&&(accionEnvio != "")){
        this.paraEnvio = true;
        this.accionEnvio = accionEnvio;
        if ((metodoEnvio != null)&&(metodoEnvio != "")&&
            ((metodoEnvio.toLowerCase()=="get")||(metodoEnvio.toLowerCase()=="post"))){
            this.metodoEnvio = metodoEnvio;
        }
    }

    
    /* Esta propiedad tiene el id de un elemento de bloque donde construir el
     * formulario. Si está vacío se construirá en el body del documento.
     */
    this.idDondeForm = "";
    if ((dondePonerForm != null)&&(dondePonerForm !="")){
        this.idDondeForm = dondePonerForm;
    }


    
    /* EJECUCIONES DEL PROPIO CONSTRUCTOR ..............................................
     * Este es el cuerpo que ejecuta y crea dinámicamente el formulario emergente
     */


    /* La variable global forms es un array de referencias a las instancias de esta
     * clase formEmerge que se van creando. Con push() metemos una nueva referencia en
     * ese array.
     */
    forms.push(this);

    
    /* Crea un elemento <form> en HTML de forma dinámica y lo rellena con resto de
     * controles para su gestión, con la siguiente estructura básica:
     * · Un <div> con la barra de título y el botón de cerrar.
     * · Un <div> para almacenar todo el interior del formulario.
     * · Un <fieldset> para los botones.
     */
    try {
        var thtml = "";
        // El <form> para todo el formulario
        thtml += "<form id='" + PID + this.nombre + "' unselectable='on' ";
        if (this.paraEnvio) {
            thtml += "action = '" + this.accionEnvio + "' " +
            "method = '" + this.metodoEnvio + "' ";
        }
        if (this.mover == "todo"){
            thtml += "onmousemove = '" + this.nombre + ".moverTodo(1, event)' " +
            "onmouseout = '" + this.nombre + ".moverTodo(1, event)' " +
            "onmouseleave = '" + this.nombre + ".moverTodo(2, event)' ";
        }
        thtml += "onclick = '" + this.nombre + ".traerAlFrente()' " +
        "class='" + PCL + "form-emerge' style='z-index: 10'>";
        // La barra superior es un div y contendrá un span para el título,
        //a la izquierda, con otro span flotado a la derecha para el botón cerrar
        thtml += "<div id='" + PID + this.nombre + "Barra' unselectable='on' " +
        "class='" + PCL + "form-emerge-cabeza' " +
        "style='" + estiloNoCss("user-select", "none") + "' " +        
        "onclick = '" + this.nombre + ".traerAlFrente()' >";
        //El botón cerrar, realmente es un span con un onclick y en el CSS está
        //flotado a la derecha
        thtml += "<span class = '" + PCL + "form-emerge-boton-cerrar' unselectable='on' " +
        "onclick = '" + this.nombre + ".cerrar()' " +
        ">&#x2613;</span>"; //usamos un aspa Unicode hex.2613
        //El título es otro span que quedará a la izquierda
        thtml += "<span id='" + PID + this.nombre + "Titulo' unselectable='on' " +
        "class ='" + PCL + "form-emerge-titulo' ";
        if (this.mover != "nada"){
            thtml += "style = 'cursor: move' ";
            if (this.mover == "todo"){
                thtml += "onmousedown = '" + this.nombre + ".moverTodo(0, event)' " +
                "onmouseup = '" + this.nombre + ".moverTodo(2, event)' ";
            } else {
                thtml += "onmousedown = '" + this.nombre + ".moverMarco(this, event)' " +
                "onmouseup = '" + this.nombre + ".moverMarco(this, event)' ";
            }
        }
        thtml += ">" + this.titulo + "</span></div>";
        //El bloque del interior es un div
        thtml += "<div id='" + PID + this.nombre + "Interior' " +
        "class='" + PCL + "form-emerge-interior' " +
        "></div>";
        //Los botones del pie si se incluyen en un fieldset
        if (this.botones > 0){ //botón aceptar
            thtml +=  "<fieldset id='" + PID + this.nombre + "Botones' " +
            "class='" + PCL + "form-emerge-botones' unselectable='on'>" +
            "<input id='" + PID + this.nombre + "BotonAceptar' ";
            if (this.paraEnvio){
                thtml += "type='submit' value='enviar' ";
            } else {
                thtml += "type='button' value='aceptar' ";
            }
            thtml += "onclick = '" + this.nombre + ".aceptar()' />";
            if (this.botones > 1){//botones aceptar y cancelar
                thtml += "<input type='button' id='" + PID + this.nombre + "BotonCancelar' " +
                "value='cancelar' onclick = '" + this.nombre + ".cancelar()' />";
            }
            if (this.botones > 2) {//botones aceptar, cancelar y aplicar
                thtml += "<input id='" + PID + this.nombre + "BotonAplicar' ";
                if (this.paraEnvio){
                    thtml += "type='reset' value='borrar' ";
                } else {
                    thtml += "type='button' value='aplicar' ";
                }
                thtml += "onclick = '" + this.nombre + ".aplicar()' />";
            }
            thtml += "</fieldset>";
        }
        thtml += "</form>";
        //Lo ideal sería meterlo al final del body, pues si se le quita el estilo
        //a la página, esto parecerá "basura", pero al menos estará al final del documento.
        //Sin embargo los formularios tienen display none, por lo que aparecerá espacio
        //en blanco al final, lugar ocupado por el formulario. Entonces lo ponemos al
        //principio del body y como tiene z-index mayor que el resto, se situará por encima.
        if (this.idDondeForm == ""){
            document.body.innerHTML = thtml + document.body.innerHTML;
        } else {
            var elemento = document.getElementById(this.idDondeForm);
            if (elemento != null) {
                elemento.innerHTML += thtml;
            } else {
                alert("Error al crear dinámicamente el nuevo formulario " + this.nombre +
                        ": no existe el elemento '" + this.idDondeForm + "' para ubicar " +
                        "el formulario.");
            }
        }
    } catch(e){
        alert("Error al crear dinámicamente el nuevo formulario " +
        this.nombre + ": " + e.message);
    }

    
    /* MÉTODOS PÚBLICOS .......................................................................
     * Métodos a usar externamente
     */

    
    /* Devuelve una referencia del formulario, es decir, del elemento <form> que engloba a
     * todo el formulario construido dinámicamente. Para obtener una referencia a la instancia
     * creada de esta clase formEmerge, debe usarse la función global formRef() o bien acceder
     * al array forms.
     * - idElementoSinPrefijo: string con el atributo 'id' del elemento que queremos
     *   referenciar SIN PASAR EL PREFIJO PID+NOMBRE y siempre que omitirPrefijo sea false o bien
     *   nulo (argumento opcional que cuando no se pasa será nulo).
     *   Si no se pasa el argumento obtenemos una referencia al propio elemento <form>.
     * - omitirPrefijo: un booleano. Si no se pasa o es false se agreaga el prefijo. En otro
     *   caso se obtiene el elemento con el id tal como se pasa.
     */
    this.form = function form(idElementoSinPrefijo, omitirPrefijo) {
        try {
            if (idElemento == null) { //Referencia el elemento <form>
                return document.getElementById(PID + this.nombre)
            } else { //Referencia el elemento directamente
                 if ((omitirPrefijo == null) || (!omitirPrefijo)){
                    return document.getElementById(PID + this.nombre + idElementoSinPrefijo)
                } else {
                    return document.getElementById(idElementoSinPrefijo)
                }
            }

        } catch(e) {
            return null;
         }
    }

    
    /* Introduce html en el interior, sin preservar lo que haya.
     * - html: string que representa un literal HTML para construir dinámicamente en el interior
     *   del emergente.
     */
    this.nuevoInterior = function nuevoInterior(html) {
        try {
            var inter = this.form("Interior")
            inter.innerHTML = html
        } catch (e) {
            alert("Error con nuevo interior para " + this.nombre + ": " + e.message);
        }
    }

    
    /* Introduce html en el interior, preservando lo que haya.
     * - html: string que representa un literal HTML para construir dinámicamente en el interior
     *   del emergente.
     * - antesDespues: string con los valores "antes" o "despues" para anexar más interior
     *   al que ya existe.
     */
    this.anexaInterior = function anexaInterior(html, antesDespues) {
        try {
            var inter = this.form("Interior");
            var ad = "despues";
            if ((antesDespues == null)||(antesDespues == "")) {
                ad = "despues";
            } else if ((antesDespues == "antes")||(antesDespues == "despues")){
                ad = antesDespues;
            }
            if (ad == "antes"){
                inter.innerHTML = html + inter.innerHTML;
            } else {
                inter.innerHTML = inter.innerHTML + html;
            }
        } catch (e) {
            alert("Error al anexar interior para " + this.nombre + ": " + e.message);
        }
     }

     
     /* Crea pestañas en el interior del emergente.
      * - arrayNombres: un array de string que pueden ser literales HTML para poner como
      *   títulos en las pestañas.
      * - arrayHtmls: un array también string literal HTML para el interior de cada contenido
      *   para cada pestaña. Deben coincidir en número ambos arrays.
      * - ancho, alto: strings para pasar el ancho y alto como estilo CSS, p.e., "20em"
      * - antesDespues: anexa las pestañas antes o después del contenido interior que pueda
      *   ya existir.
      */
     this.creaTabs = function creaTabs(arrayNombres, arrayHtmls, ancho, alto, antesDespues) {
        try {
            //Gestionamos los argumento ancho y alto para construir un style que luego
            //incorporaremos en los bloques interiores de las pestañas.
            var anchoAltoTabdiv = "";
            if ((ancho != null)&&(ancho != "")){
                 anchoAltoTabdiv = "width: " + ancho + ";";
            }
            if ((alto != null)&&(alto != "")){
                anchoAltoTabdiv += "height: " + alto + ";";
            }
            if (anchoAltoTabdiv != "") anchoAltoTabdiv = "style = '" + anchoAltoTabdiv + "' ";
            //Actualizamos la proiedad totalTabs, número de pestañas
            this.totalTabs = arrayNombres.length;
            //Controlamos el número máximo de pestañas permitidas.
            //Sólo se crearan MAX_TABS pestañas y el resto se ignora.
            if (this.totalTabs > MAX_TABS) this.totalTabs = MAX_TABS;
            //Debe tener al menos una pestaña
            if (this.totalTabs < 1) {
                alert("Error en el número de pestañas de " + this.nombre);
            } else {
                //Si los dos arrays no son de la misma longitud solo tendremos un problema
                //cuando el arrayHtmls tenga menos elementos, pues si tiene más no se
                //van a poner sino el mismo número de items que el primer arrayNombres
                if (arrayHtmls.length < this.totalTabs) {
                    for (var i = arrayHtmls.length; i<this.totalTabs; i++){
                        arrayHtmls[i] = "&nbsp";
                    }
                }
                //La estructura de pestañas es una tabla, donde la primera fila
                //contiene una celda para cada pestaña...
                var thtml = "<table class = '" + PCL + "form-emerge-tabs'><tr>";
                for (var i=0; i<this.totalTabs; i++){
                    thtml += "<th class = '" + PCL + "form-emerge-tabcks'> </th>" +
                    "<th id = '" + PID + this.nombre + "Tabck" + i + "' " +
                    "class = '" + PCL + "form-emerge-tabck' " +
                    "onclick = '" + this.nombre + ".activaTab(this)'>";
                    thtml += arrayNombres[i] + "</th>"
                }
                thtml += "</tr>";
                //...y luego una segunda fila con una única celda que abarca todas
                //las columnas para incluir los contenidos.
                thtml += "<tr><td colspan='" + (2*this.totalTabs) + "' >";
                for (var i=0; i<this.totalTabs; i++){
                    thtml +=  "<div id = '" + PID + this.nombre + "Tabdiv" + i + "' " +
                    "class = '" + PCL + "form-emerge-tabdiv' " + anchoAltoTabdiv + ">" +
                    arrayHtmls[i] + "</div>";
                }
                thtml +=  "</td></tr></table>";
                //Por defecto se anexa antes del contenido actual
                var ad = "antes";
                if ((antesDespues != null)&&(antesDespues != "")) ad = antesDespues;
                this.anexaInterior(thtml, ad);
                //Activamos la primera pestaña
                this.activaTab(0);
            }
        } catch (e) {
            alert("Error al crear pestañas para " + this.nombre + ": " + e.message);
        }

     }


    
    /* ABRE EL FORMULARIO en una posición dada del cuerpo de la página y
     * le pone el título que se pase modificando el dado en el constructor.
     * Argumentos:
     * - interior: string para el interior, que puede ser HTML.
     * - izquierda: número entero en píxeles para el punto izquierda del formulario
     * - arriba: número entero en píxeles para el punto arriba del formulario
     * - titulo: string, aunque el título del formulario se suministra al crearlo, aquí
     *   podemos modificarlo. Si se pasa "" no se modifica.
     * - botones: entero, 0 sin botones, 1 aceptar, 2 aceptar y cancelar, 3 aceptar
     *   cancelar y aplicar.
     * - alto, ancho: string que representa el estilo como "5em", "100px", etc.
     * - sobresale: valores del estilo overflow, como "hidden", "scroll", "auto", etc.
     * - foco: una referencia a un control de formulario o elemento que pueda recibir el
     *   foco. Puede ser un entero para la matriz de controles de <form>, un string
     *   para el 'id' del elemento o una referencia específica a un elemento HTML.
     * Los argumentos todos son opcionales, si alguno no se suministra antes que
     * otro que si se pasa, debe ponerse "" (o null) con lo que será ignorado.
     */
    this.abrir = function abrir(interior, titulo, izquierda, arriba,
                                ancho, alto, sobresale, foco) {
        try {
            this.botonPulsado = "";
            //Si es un formulario con pantalla se cierran todos los emergentes abiertos
            if (this.pantalla) {
                cerrarFormularios();
                document.getElementById(PID + "pantalla").style.display = "block";
            }
            //Ponemos el titulo
            if ((titulo != null)&&(titulo != "")) {
                if (titulo != ""){
                    this.titulo = titulo;
                    setInnerText(this.form("Titulo"), titulo);
                }
            }
            //Ponemos visible el formulario
            this.form().style.visibility = "visible";
            //Ponemos el interior
            if ((interior != null)&&(interior != "")) this.nuevoInterior(interior);
            //Ponemos el alto
            if ((alto != null)&&(alto != "")) this.form("Interior").style.height = alto;
            //Ponemos el ancho
            if ((ancho != null)&&(ancho != "")) this.form("Interior").style.width = ancho;
            //Ponemos sobresale
            if ((sobresale != null)&&(sobresale != ""))
                this.form("Interior").style.overflow = sobresale;
            //Ponemos izquierda, inicialmente a cero
            this.form().style.left = 0;
            if ((izquierda != null)&&(izquierda != "")){
                if (isNaN(izquierda)) {
                    izquierda = 0;
                } else {
                    //Controla que no se salga por la derecha de la ventana.
                    //El ancho con this.form().offsetWidth del formulario no queda establecido
                    //hasta que se construye dinámicamente. Por lo tanto todo esto
                    //tiene que ir al final de este método 
                    var derechaBody = document.body.offsetWidth; //siendo body.offsetLeft = 0
                    var derechaForm = izquierda + this.form().offsetWidth;
                    if (derechaForm > derechaBody) izquierda = izquierda -
                        (derechaForm - derechaBody);
                }
                this.form().style.left = izquierda + "px";
            }
            //Ponemos arriba  inicialmente a cero
            this.form().style.top = 0;
            if ((arriba != null)&&(arriba != "")) {
                if (isNaN(arriba)) arriba = 0;
                this.form().style.top = arriba + "px";
            }
            //Ponemos el valor del objeto respuesta en la respuesta
            var objetoRespuesta = document.getElementById(this.idObjetoRespuesta);
            var attrValue = objetoRespuesta.getAttribute("value");
            if (attrValue == null){
                this.respuesta = getInnerText(objetoRespuesta);
            } else {
                this.respuesta = objetoRespuesta.value;
            }
            //Ponemos el foco
            var focoEnBoton = true;
            if (foco != null){
                switch (typeof(foco)){
                    case "number":
                    this.form()[foco].focus();
                    focoEnBoton = false;
                    break;
                case "object":
                    foco.focus();
                    focoEnBoton = false;
                    break;
                case "string": //al id no se le agrega prefijo
                    if (foco != ""){
                        this.form(foco, true).focus();
                        focoEnBoton = false;
                        break;
                    }
            }
            if (focoEnBoton){
                if (this.botones == 1) {
                    this.form("BotonAceptar").focus();
                } else if (this.botones > 1) {
                    this.form("BotonCancelar").focus();
                }
            }
            //Actualizamos formularios abiertos
            this.abierto = true;
            formsAbiertos++;
            if (this.eventoAbrir) this.ejecutaEventoAbrir();
        } catch(e){
            alert("Error al abrir " + this.nombre + ": " + e.message);
        }
    }


    
    /* MÉTODOS PRIVADOS ...............................................................
     * Métodos que generalmente no es necesario usar externamente y son para la gestión
     * interna de la clase.
     */

    
    /* Activa y desactiva la pestaña visible.
     * Este método es usado por la estructura interna de pestañas para activar el
     * contenido de cada una.
     * - unTab: un número de pestaña según se crearon con el orden de los arrays
     *   en los argumentos de creaTabs() o bien una referencia explícita a un
     *   elemento para activa la pestaña correspondiente.
     */
    this.activaTab = function activaTab(unTab) {
        try {
            //Los elementos div de todas las pestañas están todos juntos en una
            //única celda de la segunda fila de la tabla de estructura de pestañas.
            //Hemos de buscar la seleccionada, ponerle display block y al resto
            //ponerle display none.
            var numTab = 0;
            if (typeof(unTab) == "object") {
                var cad = unTab.id;
                var idTab = cad.split("Tabck");
                numTab = parseInt(idTab[1]);
            } else if (typeof(unTab) == "number"){
                numTab = unTab;
            } else {
                numTab = 0;
            }
            //Las "Tabdiv" son los bloques interiores mientras que los "Tabck"
            //son las pestañas.
            var esteTabDiv = this.form("Tabdiv" + numTab);
            for (var i=0; i<this.totalTabs; i++) {
                var tabdiv = this.form("Tabdiv" + i);
                var tabck = this.form("Tabck" + i);
                if (tabdiv.id == esteTabDiv.id) {
                    tabdiv.style.display = "block";
                    tabck.style.color = "blue";
                    tabck.style.backgroundColor = "rgb(230,230,205)";
                    tabck.style.borderBottom = "rgb(230,230,205) solid 1px";
                } else {
                    tabdiv.style.display = "none";
                    tabck.style.color = "white";
                    tabck.style.backgroundColor = "gray";
                    tabck.style.borderBottom = "gray solid 1px";

                }
            }
        } catch (e) {
            alert("Error al activar una pestaña de " + this.nombre + ": " + e.message);
        }

    }

    
    /* Gestiona el evento del botón 'aceptar'.
     * Pone this.respuesta en this.objetoRespuesta.value, cierra el formulario y ejecuta
     * el evento correspondiente si estuviera activado. También pone botonPulsado = "aceptar".
     * En estos eventos de los botones hay que volver a refrescar el objeto respuesta
     * pues aunque no se pierde la referencia si es verdad que no se puede aplicar estilo.
     */
    this.aceptar = function aceptar() {
        try {
            this.botonPulsado = "aceptar";
            this.form().style.visibility = "hidden";
            if (this.eventoAceptar) this.ejecutaEventoAceptar();
            var objetoRespuesta = document.getElementById(this.idObjetoRespuesta);
            //Si el elemento no tiene el atributo value entonces debería devolver
            //nulo. Pero IE con un input con value="" también devuelve nulo, lo
            //que no sucede con FF, OP y SA. Incluso si usamos hasAttribute("value")
            //también nos dice que no tiene el atributo value si este es "".
            var attrValue = objetoRespuesta.getAttribute("value");
            if (attrValue == null){ //Para elemento sin value o el caso de IE
                 //Esta función setInnerText() devuelve el propio elemento si
                 //se consigue introducir el texto y null en otro caso.
                 //Debido al tema de que IE no reconoce atributos con ""
                 //entonces si no se mete el texto como en un input que es un
                 //elemento vacío, entonces lo ponemos en el value.
                 var elem = setInnerText(objetoRespuesta, this.respuesta);
                 if (elem == null) objetoRespuesta.value = this.respuesta;
            } else {
                objetoRespuesta.value = this.respuesta;
            }
            formsAbiertos--;
            if ((this.pantalla)&&(formsAbiertos == 0)) {
                document.getElementById(PID + "pantalla").style.display = "none";
            }
            this.abierto = false;
        } catch(e) {
            alert(this.nombre + ", error método aceptar(): " + e.name + ", " + e.message);
            this.botonPulsado = "";
        }

    }

    
    /* Gestiona el evento del botón 'aplicar'.
     * Lo mismo que aceptar pero no cierra el formulario y pone botonPulsado = "aplicar".
     */
    this.aplicar = function aplicar() {
        try {
            this.botonPulsado = "aplicar";
            if (this.eventoAplicar) this.ejecutaEventoAplicar();
            var objetoRespuesta = document.getElementById(this.idObjetoRespuesta)
            var attrValue = objetoRespuesta.getAttribute("value");
            if (attrValue == null){
                 var elem = setInnerText(objetoRespuesta, this.respuesta);
                 if (elem == null) objetoRespuesta.value = this.respuesta;
            } else {
                objetoRespuesta.value = this.respuesta;
            }
        } catch(e) {
            alert(this.nombre + ", error método aplicar(): " + e.name + ", " + e.message);
            this.botonPulsado = "";
         }

    }

    
    /* Gestiona el evento del botón 'cancelar'
     * Cierra el formulario sin poner la respuesta en el objetoRespuesta.
     * Pone botonPulsado = "cancelar" y ejecuta evento.
     */
    this.cancelar = function cancelar() {
        this.botonPulsado = "cancelar";
        try {
            this.respuesta = "";
            this.form().style.visibility = "hidden";
            if (this.eventoCancelar) this.ejecutaEventoCancelar();
            //no se modifica objetoRespuesta.value  
            formsAbiertos--;
            if ((this.pantalla)&&(formsAbiertos == 0)) {
                document.getElementById(PID + "pantalla").style.display = "none";
            }
            this.abierto = false;
        } catch(e) {
            alert(this.nombre + ", error método cancelar(): " + e.name + ", " + e.message)
        }

    }

    
    /* Gestiona el evento del botón 'cerrar'.
     * Hace lo mismo que el botón cancelar.
     */
    this.cerrar = function cerrar() {
        this.botonPulsado = "cerrar";
        try {
            this.respuesta = ""
            this.form().style.visibility = "hidden";
            if (this.eventoCerrar) this.ejecutaEventoCerrar();
            //no se modifica objetoRespuesta.value
            formsAbiertos--;
            if ((this.pantalla)&&(formsAbiertos == 0)) {
                document.getElementById(PID + "pantalla").style.display = "none";
            }
            this.abierto = false;
        } catch(e) {
           alert(this.nombre + ", error método cerrar(): " + e.name + ", " + e.message);
        }

    }

    
    /* Mueve el formulario completamente.
     * Este método mueve el formulario por la pantalla cuando el usuario pulsa en la barra
     * del título y la arrastra.
     * Se basa en tres eventos que hay en el <form> creado para cada emergente:
     * "onmousemove = '" + this.nombre + ".mover(1, event)' "
     * "onmouseout = '" + this.nombre + ".mover(1, event)' "
     * "onmouseleave = '" + this.nombre + ".mover(2, event)' "
     * y en estos eventos de la barra superior:
     * "onmousedown = '" + this.nombre + ".mover(0, event)' "
     * "onmouseup = '" + this.nombre + ".mover(2, event)' "
     * Los argumentos son:
     * - modo: es un entero 0, 1 o 2 para controlar lo que hace el puntero.
     * - evento: el evento producido.
     */
    this.moverTodo = function moverTodo(modo, evento) {
        try{
            var evt = window.event || evento;
            switch (modo) {
                //Iniciamos el movimiento, lo que se produce cuando pinchamos sobre 
                //la barra superior
                case 0 : {
                    this.xMover = evt.clientX;
                    this.yMover = evt.clientY;
                    this.iniciaMover = true;
                    break
                }
                //Movemos el formulario
                case 1 : {
                    if (this.iniciaMover) {
                        this.form().style.left = (parseInt(this.form().style.left) +
                            evt.clientX - this.xMover)  + "px";
                        this.form().style.top = (parseInt(this.form().style.top) +
                            evt.clientY - this.yMover) + "px";
                        this.xMover = evt.clientX;
                        this.yMover = evt.clientY;
                    }
                    break
                }
                //Paramos el movimiento del formulario
                case 2 : {
                    this.iniciaMover = false;
                    break;
                }
            }
        } catch(e) {
            //no sacamos nigún mensaje
        }
    }

    
    /* Mueve el formulario mediante un marco.
     * Método alternativo a mover todo el formulario.
     * Se trata de mover un marco o rectángulo del mismo tamaño que el formulario y
     * cuando se finaliza el movimiento, poner en ese sitio el propio formulario.
     * - elemento: en las declaraciones de eventos pasamos 'this' para referinos
     *   al elemento que causó el evento, pues puede interferir con el propio
     *   'this' del objeto.
     * - evento: el evento traído como argumento mediante el objeto event.
     */
    this.moverMarco = function moverMarco(elemento, evento){
        try {
            var evt = window.event || evento;
            var marco = this.form("marco");
            //Para IE hemos de recuperar el elemento
            if (evt.srcElement) elemento = evt.srcElement;
            //El evento onmousedown genera el inicio del movimiento. Está puesto en el
            //fieldset de toda la barra y también en su span interior que contiene
            //el título. Pero además en el fieldset hay otro span con el botón
            //cerrar que es necesario detectar para que no haga nada aquí.
            var tag = elemento.tagName.toLowerCase();
            var classTag = elemento.className
            //Se inicia el movimiento al detectar onmousedown en la barra o en el título
            if (evt.type == "mousedown"){
                if ((tag == "fieldset")||((tag == "span")&&
                (classTag != (PCL + "form-emerge-boton-cerrar")))){
                    marco.style.left =  (this.form().offsetLeft -
                        document.documentElement.scrollLeft) + "px";
                    marco.style.top = (this.form().offsetTop -
                        document.documentElement.scrollTop) + "px";
                    marco.style.width = this.form().offsetWidth + "px";
                    marco.style.height = this.form().offsetHeight + "px";
                    this.xMover = evt.clientX;
                    this.yMover = evt.clientY;
                    this.iniciaMover = true;
                    marco.style.display = "block";
                }
            //Movemos el marco cuando el ratón se mueve por el propio marco, un div
            } else if (evt.type == "mousemove") {
                //Para evitar este efecto, sólo se mueve si ha habido un onmousedown anterior
                if (this.iniciaMover) {
                    marco.style.left = (marco.offsetLeft + evt.clientX - this.xMover) + "px";
                    marco.style.top = (marco.offsetTop + evt.clientY - this.yMover) + "px";
                    this.xMover = evt.clientX;
                    this.yMover = evt.clientY;
                }
            //Cuando en el marco (un div) levantamos el ratón finalizamos el movimiento.
            //Por si nos salimos del marco, también finalizamos. Para IE se usa 
            //onmouseout y también onmouseleave pero en Firefox no se usa sino el primero.
            } else if ((evt.type == "mouseup")||(evt.type == "mouseout")||
            (evt.type == "mouseleave")) {
                if (this.iniciaMover){
                    this.iniciaMover = false;
                    this.form().style.left =  (marco.offsetLeft +
                        document.documentElement.scrollLeft)+"px";
                    this.form().style.top =  (marco.offsetTop +
                        document.documentElement.scrollTop)+"px";
                    marco.style.display = "none";
                }
            }
        } catch(e) {
            //no sacamos nigún mensaje pero desactivamos el marco
            this.form("marco").style.display = "none";
        }
    }


    
    /* Trae al frente el formulario sobre el que se hace click.
     *
     */
    this.traerAlFrente = function traerAlFrente() {
        try {
            var indice = 0;
            for (var i=0; i<forms.length; i++) {
                if (forms[i].nombre == this.nombre) {
                    indice = i;
                    break;
                }
            }
            if (indice > 0){
                for (var i=0; i<forms.length; i++) {
                    forms[i].form().style.zIndex = 10;
                }
                forms[indice].form().style.zIndex = 100;
            }
        } catch (e) {
            alert("Error al traer al frente " + this.nombre + ": " + e.message);
        }
    }
}

    

Código CSS de la clase formEmerge

/* ========================================================================= Software: form-emerge.css Description: Estilo CSS para el módulo para crear formularios emergentes form-emerge.js Contact: http://www.wextensible.com Copyright (c) 2010, Andrés de la Paz La documentación de la clase la puede consultar en http://www.wextensible.com/como-se-hace/emergente-javascript/documentacion-clase.html ACTUALIZACIONES: Septiembre 2010: Las propiedades "opacity" y "user-select" no son aún estándar en CSS 2.1. Cómo las tenía incluidas aquí, ahora las quitamos para poder validar el documento. Se aplican con el módulo general.js, función estiloNoCss(). ========================================================================== */ /* ==================== CONTENEDOR FORMULARIO GENERAL==================== */ form.CLPREform-emerge { /*el tamaño de la fuente de todo el emergente afecta a la presentación*/ font-size: 1em; font-family: 'Arial Narrow'; border: rgb(230,230,205) outset 3px; background-color: rgb(230,230,205); position: absolute; height: auto; padding: 0.1em; visibility: hidden; } /* =========== BARRA SUPERIOR DE TITULO Y BOTÓN "X" DEL EMERGENTE ========= */ form.CLPREform-emerge div.CLPREform-emerge-cabeza { background-color: rgb(49, 99, 98); color: orange; border: 0; padding: 0; font-weight: bold; cursor: default; /*Esta propiedad aún no es estándar, se aplica con JavaScript en el módulo general.js, función estiloNoCss()*/ /*-moz-user-select: -moz-none;*/ cursor: default; } form.CLPREform-emerge div.CLPREform-emerge-cabeza span.CLPREform-emerge-boton-cerrar { margin: 0.15em; text-align: center; vertical-align: middle; line-height: 1em; font-weight: bold; float: right; border: white solid 1px; background-color: maroon; color: white; cursor: pointer; } form.CLPREform-emerge div.CLPREform-emerge-cabeza span.CLPREform-emerge-titulo { padding-left: 0.2em; line-height: 1.4em; border: 0; } /* ============== CUERPO INTERIOR DEL EMERGENTE =======================*/ form.CLPREform-emerge div.CLPREform-emerge-interior { clear: both; padding: 0; margin: 0.2em; font-size: 1em; } form.CLPREform-emerge div.CLPREform-emerge-interior input.CLPREform-emerge-tipo-texto { border: 0; border-bottom: dotted 1px; margin-right: 0.2em; color: maroon; font-family: Courier New; font-size: 0.8em; background-color: rgb(230,230,205); } /* ===========CUERPO INFERIOR CON BOTONES "ACEPTAR" Y "CANCELAR" =======*/ form.CLPREform-emerge fieldset.CLPREform-emerge-botones { text-align: right; margin: 0.2em; } /* ==========CONTENEDORES DE PESTAÑAS================================= */ form.CLPREform-emerge table.CLPREform-emerge-tabs { /* Antes tenía aquí border-collapse: collapse pero había un problema con el pintado de bordes. Ver más información en contenedores con pestañas*/ border-collapse: separate; border-spacing: 0; border: 0; } form.CLPREform-emerge table.CLPREform-emerge-tabs th.CLPREform-emerge-tabck { border: gray solid 1px; border-bottom: 0; background-color: gray; padding-left: 0.2em; padding-right: 0.2em; font-weight: normal; cursor: pointer; } form.CLPREform-emerge table.CLPREform-emerge-tabs th.CLPREform-emerge-tabcks { border: 0; border-bottom: gray solid 1px; } form.CLPREform-emerge table.CLPREform-emerge-tabs td { border: gray solid 1px; /*border-top: 0; debería ser esto, pero Opera no refresca style.borderBottom en form-emerge.js cuando hacemos activaTab(). Esto no soluciona totalmente el problema, pero en parte sí. */ border-top: transparent solid 1px; } form.CLPREform-emerge div.CLPREform-emerge-tabdiv { padding: 0.2em; overflow: auto; display: none; width: 30em; height: 5em; } /* =================PANTALLA================================= */ div#IDPREpantalla { display: none; /*Esta propiedad aún no es estándar, se aplica con JavaScript en el módulo general.js, función estiloNoCss()*/ /*opacity: 0.4;*/ /*-ms-filter: "alpha(opacity=40)";*/ position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: white; z-index: 5; } /* =================MARCO MOVER================================= */ div.CLPREmarco { position: fixed; /*Esta propiedad aún no es estándar, se aplica con JavaScript en el módulo general.js, función estiloNoCss()*/ /*opacity: 0.4;*/ /*-ms-filter: "alpha(opacity=40)";*/ display: none; background-color: navy; border: 0; top: 0; left: 0; z-index: 1000; }