viernes, 12 de junio de 2015

SlickGrid. Habilitando la edición (y II) - Creando los editores

Artículos anteriores:

En el último artículo explicaba las propiedades que debemos utilizar para habilitar la edición con SlickGrid y las características que debían cumplir los editores personalizados que creemos.

A continuación voy a poner en práctica toda esa teoría creando diferentes editores para el grid de ejemplo que hemos ido construyendo a lo largo de los diferentes artículos.


Creando la página de prueba

Voy a crear este ejemplo a partir de la página generada en el ejemplo de formateo de celdas pero antes, para ordenar y dejar más limpia de código la página, voy a mover parte del código a otros archivos.
En concreto voy a crearme un archivo productdatasource.js en la carpeta Scripts donde moveré la definición de las variables de datos products y subcategories, así como la función de formateo de subcategorías.
Voy a crearme también una hoja de estilos pildorasgrid.css donde moveré los estilos creados hasta el momento (las clases prKeyHeadColumn, headColumn y numericCell).

De esta forma el código de inicio, tras añadir las referencias a los nuevos archivos, será:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>Habilitando la edición</title>
    <link href="https://code.jquery.com/ui/1.11.4/themes/blitzer/jquery-ui.css" rel="stylesheet" />
    <link href="css/slick.grid.css" rel="stylesheet" />
    <link href="css/theme/slick-default-theme.css" rel="stylesheet" />
    <link href="css/pildorasgrid.css" rel="stylesheet" />
    <script src="http://code.jquery.com/jquery-1.11.3.min.js"></script>
    <script src="http://code.jquery.com/ui/1.11.4/jquery-ui.min.js"></script>
    <script src="Scripts/jquery.event.drag-2.2.js"></script>
    <script src="Scripts/slick.core.js"></script>
    <script src="Scripts/slick.grid.js"></script>
    <script src="Scripts/gridformatters.js"></script>
    <script src="data/productdatasource.js"></script>
    <script src="Scripts/grideditors.js"></script>
    <script type="text/javascript">
        $(function () {
            var columns = [
                { name: "ID", field: "ProductID", id: "ProductID", width: 60, resizable: false
                    , headerCssClass: "prKeyHeadColumn", cssClass: "numericCell" },
                { name: "Nº Producto", field: "ProductNumber", id: "ProductNumber", width: 120, resizable: false
                    , headerCssClass: "headColumn" },
                { name: "Denominación", field: "Name", id: "Name", width: 250, minWidth: 150, maxWidth: 400
                    , headerCssClass: "headColumn" },
                { name: "Color", field: "Color", id: "Color", width: 80, minWidth: 60, maxWidth: 120
                    , headerCssClass: "headColumn", formatter: Slick.Formatters.ColorFormatter },
                { name: "Precio", field: "StandardCost", id: "StandardCost", width: 110, minWidth: 80, maxWidth: 170
                    , headerCssClass: "headColumn", cssClass: "numericCell", formatter: Slick.Formatters.CurrencyFormatter },
                { name: "Sub", field: "ProductSubcategoryID", id: "ProductSubcategoryID", width: 60, resizable: false
                    , headerCssClass: "headColumn", cssClass: "numericCell" },
                { name: "Subcategoría", field: "ProductSubcategoryID", id: "SubcategoryName", width:200, minWidth: 150, maxWidth: 400
                    , headerCssClass: "headColumn", formatter: subcategoryNameFormatter }
            ];

            var grid = new Slick.Grid("#EditGrid", products, columns);
        });
    </script>
</head>
<body>
    <div id="EditGrid" style="width:1024px; height:500px;"></div>
</body>
</html>
.prKeyHeadColumn {
    background: rgba(204,0,0,0.2);
}

.headColumn {
    background: -webkit-linear-gradient(top, white, white 40%, rgba(204,0,0,0.2));
    background: -moz-linear-gradient(top, white, white 40%, rgba(204,0,0,0.2));
    background: -o-linear-gradient(top, white, white 40%, rgba(204,0,0,0.2));
    background: -ms-linear-gradient(top, white, white 40%, rgba(204,0,0,0.2));
}

.numericCell {
    text-align: right;
}
var products = [
    { ProductID: 514, ProductNumber: "SA-M198", Name: "LL Mountain Seat Assembly", Color: null, StandardCost: 98.77, ProductSubcategoryID: null },
    { ProductID: 515, ProductNumber: "SA-M237", Name: "ML Mountain Seat Assembly", Color: null, StandardCost: 108.99, ProductSubcategoryID: null },
    { ProductID: 516, ProductNumber: "SA-M687", Name: "HL Mountain Seat Assembly", Color: null, StandardCost: 145.87, ProductSubcategoryID: null },
    { ProductID: 517, ProductNumber: "SA-R127", Name: "LL Road Seat Assembly", Color: null, StandardCost: 98.77, ProductSubcategoryID: null },
    { ProductID: 518, ProductNumber: "SA-R430", Name: "ML Road Seat Assembly", Color: null, StandardCost: 108.99, ProductSubcategoryID: null },
    { ProductID: 519, ProductNumber: "SA-R522", Name: "HL Road Seat Assembly", Color: null, StandardCost: 145.87, ProductSubcategoryID: null },
    { ProductID: 520, ProductNumber: "SA-T467", Name: "LL Touring Seat Assembly", Color: null, StandardCost: 98.77, ProductSubcategoryID: null },
    { ProductID: 521, ProductNumber: "SA-T612", Name: "ML Touring Seat Assembly", Color: null, StandardCost: 108.99, ProductSubcategoryID: null },
    { ProductID: 522, ProductNumber: "SA-T872", Name: "HL Touring Seat Assembly", Color: null, StandardCost: 145.87, ProductSubcategoryID: null },
    { ProductID: 680, ProductNumber: "FR-R92B-58", Name: "HL Road Frame - Black, 58", Color: "Black", StandardCost: 1059.31, ProductSubcategoryID: 14 },
    { ProductID: 706, ProductNumber: "FR-R92R-58", Name: "HL Road Frame - Red, 58", Color: "Red", StandardCost: 1059.31, ProductSubcategoryID: 14 },
    { ProductID: 707, ProductNumber: "HL-U509-R", Name: "Sport-100 Helmet, Red", Color: "Red", StandardCost: 13.09, ProductSubcategoryID: 31 },
    { ProductID: 708, ProductNumber: "HL-U509", Name: "Sport-100 Helmet, Black", Color: "Black", StandardCost: 13.09, ProductSubcategoryID: 31 },
    { ProductID: 709, ProductNumber: "SO-B909-M", Name: "Mountain Bike Socks, M", Color: "White", StandardCost: 3.40, ProductSubcategoryID: 23 },
    { ProductID: 710, ProductNumber: "SO-B909-L", Name: "Mountain Bike Socks, L", Color: "White", StandardCost: 3.40, ProductSubcategoryID: 23 },
    { ProductID: 711, ProductNumber: "HL-U509-B", Name: "Sport-100 Helmet, Blue", Color: "Blue", StandardCost: 13.09, ProductSubcategoryID: 31 },
    { ProductID: 712, ProductNumber: "CA-1098", Name: "AWC Logo Cap", Color: "Multi", StandardCost: 6.92, ProductSubcategoryID: 19 },
    { ProductID: 713, ProductNumber: "LJ-0192-S", Name: "Long-Sleeve Logo Jersey, S", Color: "Multi", StandardCost: 38.49, ProductSubcategoryID: 21 },
    { ProductID: 714, ProductNumber: "LJ-0192-M", Name: "Long-Sleeve Logo Jersey, M", Color: "Multi", StandardCost: 38.49, ProductSubcategoryID: 21 },
    { ProductID: 715, ProductNumber: "LJ-0192-L", Name: "Long-Sleeve Logo Jersey, L", Color: "Multi", StandardCost: 38.49, ProductSubcategoryID: 21 },
    { ProductID: 716, ProductNumber: "LJ-0192-X", Name: "Long-Sleeve Logo Jersey, XL", Color: "Multi", StandardCost: 38.49, ProductSubcategoryID: 21 },
    { ProductID: 717, ProductNumber: "FR-R92R-62", Name: "HL Road Frame - Red, 62", Color: "Red", StandardCost: 868.63, ProductSubcategoryID: 14 },
    { ProductID: 718, ProductNumber: "FR-R92R-44", Name: "HL Road Frame - Red, 44", Color: "Red", StandardCost: 868.63, ProductSubcategoryID: 14 },
    { ProductID: 719, ProductNumber: "FR-R92R-48", Name: "HL Road Frame - Red, 48", Color: "Red", StandardCost: 868.63, ProductSubcategoryID: 14 },
    { ProductID: 720, ProductNumber: "FR-R92R-52", Name: "HL Road Frame - Red, 52", Color: "Red", StandardCost: 868.63, ProductSubcategoryID: 14 },
    { ProductID: 721, ProductNumber: "FR-R92R-56", Name: "HL Road Frame - Red, 56", Color: "Red", StandardCost: 868.63, ProductSubcategoryID: 14 },
    { ProductID: 722, ProductNumber: "FR-R38B-58", Name: "LL Road Frame - Black, 58", Color: "Black", StandardCost: 204.63, ProductSubcategoryID: 14 },
    { ProductID: 723, ProductNumber: "FR-R38B-60", Name: "LL Road Frame - Black, 60", Color: "Black", StandardCost: 204.63, ProductSubcategoryID: 14 },
    { ProductID: 724, ProductNumber: "FR-R38B-62", Name: "LL Road Frame - Black, 62", Color: "Black", StandardCost: 204.63, ProductSubcategoryID: 14 },
    { ProductID: 725, ProductNumber: "FR-R38R-44", Name: "LL Road Frame - Red, 44", Color: "Red", StandardCost: 187.16, ProductSubcategoryID: 14 },
    { ProductID: 726, ProductNumber: "FR-R38R-48", Name: "LL Road Frame - Red, 48", Color: "Red", StandardCost: 187.16, ProductSubcategoryID: 14 },
    { ProductID: 727, ProductNumber: "FR-R38R-52", Name: "LL Road Frame - Red, 52", Color: "Red", StandardCost: 187.16, ProductSubcategoryID: 14 },
    { ProductID: 728, ProductNumber: "FR-R38R-58", Name: "LL Road Frame - Red, 58", Color: "Red", StandardCost: 187.16, ProductSubcategoryID: 14 },
    { ProductID: 729, ProductNumber: "FR-R38R-60", Name: "LL Road Frame - Red, 60", Color: "Red", StandardCost: 187.16, ProductSubcategoryID: 14 },
    { ProductID: 730, ProductNumber: "FR-R38R-62", Name: "LL Road Frame - Red, 62", Color: "Red", StandardCost: 187.16, ProductSubcategoryID: 14 },
    { ProductID: 731, ProductNumber: "FR-R72R-44", Name: "ML Road Frame - Red, 44", Color: "Red", StandardCost: 352.14, ProductSubcategoryID: 14 },
    { ProductID: 732, ProductNumber: "FR-R72R-48", Name: "ML Road Frame - Red, 48", Color: "Red", StandardCost: 352.14, ProductSubcategoryID: 14 },
    { ProductID: 733, ProductNumber: "FR-R72R-52", Name: "ML Road Frame - Red, 52", Color: "Red", StandardCost: 352.14, ProductSubcategoryID: 14 },
    { ProductID: 734, ProductNumber: "FR-R72R-58", Name: "ML Road Frame - Red, 58", Color: "Red", StandardCost: 352.14, ProductSubcategoryID: 14 },
    { ProductID: 735, ProductNumber: "FR-R72R-60", Name: "ML Road Frame - Red, 60", Color: "Red", StandardCost: 352.14, ProductSubcategoryID: 14 }
];

var subcategories = [
    { ProductSubcategoryID: 1, Name: "Mountain Bikes" },
    { ProductSubcategoryID: 2, Name: "Road Bikes" },
    { ProductSubcategoryID: 3, Name: "Touring Bikes" },
    { ProductSubcategoryID: 4, Name: "Handlebars" },
    { ProductSubcategoryID: 5, Name: "Bottom Brackets" },
    { ProductSubcategoryID: 6, Name: "Brakes" },
    { ProductSubcategoryID: 7, Name: "Chains" },
    { ProductSubcategoryID: 8, Name: "Cranksets" },
    { ProductSubcategoryID: 9, Name: "Derailleurs" },
    { ProductSubcategoryID: 10, Name: "Forks" },
    { ProductSubcategoryID: 11, Name: "Headsets" },
    { ProductSubcategoryID: 12, Name: "Mountain Frames" },
    { ProductSubcategoryID: 13, Name: "Pedals" },
    { ProductSubcategoryID: 14, Name: "Road Frames" },
    { ProductSubcategoryID: 15, Name: "Saddles" },
    { ProductSubcategoryID: 16, Name: "Touring Frames" },
    { ProductSubcategoryID: 17, Name: "Wheels" },
    { ProductSubcategoryID: 18, Name: "Bib-Shorts" },
    { ProductSubcategoryID: 19, Name: "Caps" },
    { ProductSubcategoryID: 20, Name: "Gloves" },
    { ProductSubcategoryID: 21, Name: "Jerseys" },
    { ProductSubcategoryID: 22, Name: "Shorts" },
    { ProductSubcategoryID: 23, Name: "Socks" },
    { ProductSubcategoryID: 24, Name: "Tights" },
    { ProductSubcategoryID: 25, Name: "Vests" },
    { ProductSubcategoryID: 26, Name: "Bike Racks" },
    { ProductSubcategoryID: 27, Name: "Bike Stands" },
    { ProductSubcategoryID: 28, Name: "Bottles and Cages" },
    { ProductSubcategoryID: 29, Name: "Cleaners" },
    { ProductSubcategoryID: 30, Name: "Fenders" },
    { ProductSubcategoryID: 31, Name: "Helmets" },
    { ProductSubcategoryID: 32, Name: "Hydration Packs" },
    { ProductSubcategoryID: 33, Name: "Lights" },
    { ProductSubcategoryID: 34, Name: "Locks" },
    { ProductSubcategoryID: 35, Name: "Panniers" },
    { ProductSubcategoryID: 36, Name: "Pumps" },
    { ProductSubcategoryID: 37, Name: "Tires and Tubes" }
];

function subcategoryNameFormatter(row, cell, value, columnDef, dataContext) {
    if (value == null) return "";

    var name;
    for (var subcat in subcategories) {
        if (value == subcategories[subcat].ProductSubcategoryID)
            return subcategories[subcat].Name;
    }
    return "";
}

Creando los Editores

En el paquete de descarga del SlickGrid se incluyen unos ejemplos de editores básicos en el fichero slick.editors.js. Aunque no voy a utilizarlos en este ejemplo (en realidad los editores de texto que utilizaremos son prácticamente una copia del incluido en este archivo) es recomendable echarles un vistazo por lo que pueden aportar de cara a comprender el funcionamiento de éstos.

Para este ejemplo voy a crear tres editores: un editor de texto genérico, un editor para la columna del campo Color y un tercer editor para el campo Subcategoría en el que incluiré una regla de validación básica para asegurarme de que la subcategoría introducida por el usuario se corresponde con alguna de las existentes.

Para los dos primeros editores voy a seguir el mismo esquema que el fichero slick.editors.js extendiendo el namespace Slick para añadirle un objeto Editors que contendrá los diferentes Editores. El editor de textos genera un elemento input de tipo text al que le aplico una clase (gridTextEditor), que he incluido en la hoja de estilos pildorasgrid.css, que hace que el elemento se ajuste al tamaño de la celda.
Además intercepto el evento keydown para evitar que la pulsación de las teclas izquierda y derecha se propaguen al grid lo que provocaría que el grid pasara a la celda anterior o posterior.

El Editor de Color es igual que el de texto salvo que crea un elemento input de tipo color. De esta forma si el navegador en el que se visualiza implementa algún tipo de editor especial para estos campos (como en Chrome) se utilizará automáticamente este editor. En el método loadValue (que se encarga de cargar el valor del campo en el Editor) realizo también una traducción de los valores del campo a códigos hexadecimales de los colores.
Ambos editores los he incluido en un nuevo fichero grideditors.js en la carpeta Scripts.

Finalmente en la página EdicionBasica.html he creado un objeto options, que habilita la edición del grid y desactiva la edición automática de las celdas, que le paso al constructor del grid. Además, a través de la propiedad editor de las columnas, le he asignado los nuevos editores a las columnas ID, Nº Producto, Denominación, Color y Precio.

<script src="Scripts/gridformatters.js"></script>
<script src="data/productdatasource.js"></script>
<script src="Scripts/grideditors.js"></script>
<script type="text/javascript">
 $(function () {
  var columns = [
   { name: "ID", field: "ProductID", id: "ProductID", width: 60, resizable: false
    , headerCssClass: "prKeyHeadColumn", cssClass: "numericCell", editor: Slick.Editors.Text },
   { name: "Nº Producto", field: "ProductNumber", id: "ProductNumber", width: 120, resizable: false
    , headerCssClass: "headColumn", editor: Slick.Editors.Text },
   { name: "Denominación", field: "Name", id: "Name", width: 250, minWidth: 150, maxWidth: 400
    , headerCssClass: "headColumn", editor: Slick.Editors.Text },
   { name: "Color", field: "Color", id: "Color", width: 80, minWidth: 60, maxWidth: 120
    , headerCssClass: "headColumn", formatter: Slick.Formatters.ColorFormatter
    , editor: Slick.Editors.Color },
   { name: "Precio", field: "StandardCost", id: "StandardCost", width: 110, minWidth: 80, maxWidth: 170
    , headerCssClass: "headColumn", cssClass: "numericCell", formatter: Slick.Formatters.CurrencyFormatter
    , editor: Slick.Editors.Text },
   { name: "Sub", field: "ProductSubcategoryID", id: "ProductSubcategoryID", width: 60, resizable: false
    , headerCssClass: "headColumn", cssClass: "numericCell" },
   { name: "Subcategoría", field: "ProductSubcategoryID", id: "SubcategoryName", width:200, minWidth: 150, maxWidth: 400
    , headerCssClass: "headColumn", formatter: subcategoryNameFormatter }
  ];

  var options = {
   editable: true,
   autoEdit: false
  };

  var grid = new Slick.Grid("#EditGrid", products, columns, options);
 });
$(function () {
    $.extend(true, window, {
        "Slick": {
            "Editors": {
                "Text": TextEditor,
                "Color": ColorEditor
            }
        }
    });

    function TextEditor(args) {
        var input;
        var oldValue;

        var init = function () {
            var inputElement = document.createElement("input");
            inputElement.setAttribute("type", "text");
            inputElement.className = "gridTextEditor";
            args.container.appendChild(inputElement);
            input = $(inputElement);
            input.bind("keydown", function (e) {
                if (e.keyCode === $.ui.keyCode.LEFT || e.keyCode === $.ui.keyCode.RIGHT) {
                    e.stopImmediatePropagation();
                }
            })
          .focus()
          .select();
        }();

        this.destroy = function () { input.remove(); }

        this.focus = function () { input.focus(); }

        this.getValue = function () { return input.val(); }

        this.setValue = function (value) { input.val(value); }

        this.loadValue = function (item) {
            oldValue = item[args.column.field] || "";
            input.val(oldValue).attr("defaultValue", oldValue);
            input.select();
        }

        this.serializeValue = function () { return input.val(); }

        this.applyValue = function(item, state){ item[args.column.field] = state; }

        this.isValueChanged = function () { return (input.val() != oldValue); }

        this.validate = function () { return { valid: true, msg: null }; }
    }

    function ColorEditor(args) {
        var input;
        var oldValue;

        var init = function () {
            var inputElement = document.createElement("input");
            inputElement.setAttribute("type", "color");
            inputElement.className = "gridTextEditor";
            args.container.appendChild(inputElement);
            input = $(inputElement);
            input.bind("keydown", function (e) {
                if (e.keyCode === $.ui.keyCode.LEFT || e.keyCode === $.ui.keyCode.RIGHT) {
                    e.stopImmediatePropagation();
                }
            })
          .focus()
          .select();
        }();

        this.destroy = function () { input.remove(); }

        this.focus = function () { input.focus(); }

        this.getValue = function () { return input.val(); }

        this.setValue = function (value) { input.val(color); }

        this.loadValue = function (item) {
            oldValue = item[args.column.field] || "";
            switch (oldValue) {
                case "Black":
                    oldValue = "#000000";
                    break;
                case "Red":
                    oldValue = "#ee0000";
                    break;
                case "White":
                    oldValue = "#ffffff";
                    break;
                case "Blue":
                    oldValue = "#0000ee";
                    break;
            }
            input.val(oldValue).attr("defaultValue", oldValue);
            input.select();
        }

        this.serializeValue = function () { return input.val(); }

        this.applyValue = function (item, state) { item[args.column.field] = state; }

        this.isValueChanged = function () { return (input.val() != oldValue); }

        this.validate = function () { return { valid: true, msg: null }; }
    }

});
input.gridTextEditor{
    width: 100%;
    height: 100%;
    border: 0;
    margin: 0;
    background: transparent;
    outline: 0;
    padding: 0;
}

Cargando la página podremos ver el efecto conseguido. Para editar una celda deberemos posicionarnos en ella y hacer doble click o pulsar Enter. Recuerda que el resultado del Editor de colores puede variar mucho dependiendo del navegador utilizado, ya que utilizamos el editor de colores del propio navegador (si lo tiene).
También puedes activar la edición automática cambiando la propiedad autoEdit a true para ver la diferencia de comportamiento.

Editores de texto y color


Por último voy a crear un tercer Editor para el campo de código de subcategoría. Este Editor es exactamente igual que el de textos salvo que le voy a añadir una comprobación en el método validate para comprobar que el valor introducido por el usuario se corresponde con una subcategoría existente. En caso de que no exista establezco la propiedad valid del resultado a false e incuyo un mensaje en la propiedad msg con el motivo por el que el valor no es válido. Este Editor, al ser específico de un campo de datos concreto y depender de los datos cargados, voy a incluirlo en el fichero productdatasource.js. Para terminar le asignaré el Editor a la columna ID de subcategoría a través de la propiedad editor de la columna.

function subcategoryEditor(args) {
    var input;
    var oldValue;

    var init = function () {
        var inputElement = document.createElement("input");
        inputElement.setAttribute("type", "text");
        inputElement.className = "gridTextEditor";
        args.container.appendChild(inputElement);
        input = $(inputElement);
        input.bind("keydown", function (e) {
            if (e.keyCode === $.ui.keyCode.LEFT || e.keyCode === $.ui.keyCode.RIGHT) {
                e.stopImmediatePropagation();
            }
        })
        .focus()
        .select();
    }();

    this.destroy = function () { input.remove(); }

    this.focus = function () { input.focus(); }

    this.getValue = function () { return input.val(); }

    this.setValue = function (value) { input.val(value); }

    this.loadValue = function (item) {
        oldValue = item[args.column.field] || "";
        input.val(oldValue).attr("defaultValue", oldValue);
        input.select();
    }

    this.serializeValue = function () { return input.val(); }

    this.applyValue = function (item, state) { item[args.column.field] = state; }

    this.isValueChanged = function () { return (input.val() != oldValue); }

    this.validate = function () {
        var value = input.val();
        var valid = (value == null || value == "");
        if (!valid) {
            for (var subcat in subcategories) {
                if (value == subcategories[subcat].ProductSubcategoryID)
                {
                    valid = true;
                    break;
                }
            }
        }
        return {
            valid: valid,
            msg: (valid ? null : "La subcategoría " + value + " no existe.")
        };
    }
}

Si recargamos la página podremos ver que si introducimos códigos de subcategoría validos el grid nos acepta el valor y carga la denominación correspondiente. En cambio, si introducimos un códgio no valido, el borde de la celda se vuelve rojo y el grid no nos deja abandonar la celda hasta introducir un valor válido.

Editor con validación


El código


Puedes descargar el código de todos los ejemplos de SlickGrid de:



Artículo siguiente:
SlickGrid. Carga asíncrona de celdas (I) - La propiedad asyncPostRender

No hay comentarios:

Publicar un comentario