Source: fields/advanced/TableField.js

(function($) {

    var Alpaca = $.alpaca;

    /**
     * The table field is used for data representations that consist of an array with objects inside of it.  The objects
     * must have a uniform structure.  The table field renders a standard HTML table using the table.  The individual
     * columns are either editable (in edit mode) or simply displayed in read-only mode.
     */
    Alpaca.Fields.TableField = Alpaca.Fields.ArrayField.extend(
    /**
     * @lends Alpaca.Fields.TableField.prototype
     */
    {
        setup: function()
        {
            var self = this;

            if (!self.options)
            {
                self.options = {};
            }

            if (typeof(self.options.animate) === "undefined")
            {
                self.options.animate = false;
            }

            // assume toolbar sticky if not otherwise specified
            if (typeof(this.options.toolbarSticky) === "undefined")
            {
                this.options.toolbarSticky = true;
            }

            this.base();

            if (!this.options.items.type)
            {
                this.options.items.type = "tablerow";
            }

            // support for either "datatable" or "datatables"
            if (this.options.datatable) {
                this.options.datatables = this.options.datatable;
            }

            // assume empty options for datatables
            if (typeof(this.options.datatables) === "undefined")
            {
                this.options.datatables = {
                    "paging": false,
                    "lengthChange": false,
                    "info": false,
                    "searching": false,
                    "ordering": true
                };

                // draggable reorder of rows
                if (typeof(this.options.dragRows) == "undefined")
                {
                    this.options.dragRows = false;
                }

                if (this.options.readonly)
                {
                    this.options.dragRows = false;
                }

                if (this.isDisplayOnly())
                {
                    this.options.dragRows = false;
                }
            }

            // assume actions column to be shown
            if (typeof(this.options.showActionsColumn) === "undefined")
            {
                this.options.showActionsColumn = true;

                if (this.options.readonly)
                {
                    this.options.showActionsColumn = false;
                }

                if (this.isDisplayOnly())
                {
                    this.options.showActionsColumn = false;
                }
            }

            // data tables columns
            this.options.datatables.columns = [];

            // initialize data tables to detect alpaca field types and perform alpaca field sorting and filtering
            if ($.fn.dataTableExt && !$.fn.DataTable.ext.type.search["alpaca"])
            {
                $.fn.DataTable.ext.order["alpaca"] = function (settings, col) {

                    return this.api().column( col, {order:'index'} ).nodes().map( function ( td, i ) {
                        var alpacaId = $(td).children().attr("data-alpaca-field-id");
                        return Alpaca.fieldInstances[alpacaId].getValue();
                    } );

                };

                // this is a kind of hacky function at the moment, trying to do filtering that takes into account
                // alpaca field values
                //
                // according to data tables authors, need to wait for next release for refactoring of filtering
                // logic in data tables to really take control of this and do it right
                // this "sort of" works for now
                //
                $.fn.dataTableExt.afnFiltering.push(function(settings, fields, fieldIndex, data, dataIndex) {

                    var text = $(settings.nTableWrapper).find(".dataTables_filter input[type='search']").val();

                    if (!text) {
                        return true;
                    }

                    text = "" + text;

                    text = $.trim(text);
                    text = text.toLowerCase();

                    var match = false;

                    for (var i = 0; i < data.length; i++)
                    {
                        var dataValue = data[i];
                        if (dataValue)
                        {
                            var z = dataValue.indexOf("data-alpaca-field-id=");
                            if (z > -1)
                            {
                                var alpacaId = $(dataValue).attr("data-alpaca-field-id");

                                var alpacaValue = Alpaca.fieldInstances[alpacaId].getValue();
                                if (alpacaValue)
                                {
                                    alpacaValue = "" + alpacaValue;
                                    alpacaValue = alpacaValue.toLowerCase();

                                    if (alpacaValue.indexOf(text) > -1)
                                    {
                                        match = true;
                                        break;
                                    }
                                }
                            }
                        }
                    }

                    return match;
                });
            }
        },

        /**
         * @see Alpaca.ControlField#getFieldType
         */
        getFieldType: function() {
            return "table";
        },

        prepareContainerModel: function(callback)
        {
            var self = this;

            self.base(function(model) {

                // build a separate "items" array that we'll use to build out the table header
                model.headers = [];
                if (self.schema.items && self.schema.items.properties)
                {
                    for (var k in self.schema.items.properties)
                    {
                        var header = {};
                        header.id = k;
                        header.title = self.schema.items.properties[k].title;
                        header.hidden = false;
                        if (self.options.items && self.options.items.fields && self.options.items.fields[k])
                        {
                            if (self.options.items.fields[k].label)
                            {
                                header.title = self.options.items.fields[k].label;
                            }

                            if (self.options.items.fields[k].type === "hidden")
                            {
                                header.hidden = true;
                            }
                        }

                        model.headers.push(header);
                    }
                }

                callback(model);
            });
        },

        /**
         * The table field uses the "array" container convention to render the DOM.  As such, nested objects are wrapped
         * in "field" elements that result in slightly incorrect table structures.  Part of the reason for this is that
         * browsers are very fussy when it comes to injection of nested TR or TD partials.  Here, we generate most
         * things as DIVs and then do some cleanup in this method to make sure that the table is put togehter in the
         * right way.
         *
         * @param model
         * @param callback
         */
        afterRenderContainer: function(model, callback)
        {
            var self = this;

            this.base(model, function() {

                self.cleanupDomInjections();

                // apply styles of underlying "table"
                var table = $(this.container).find("table");
                self.applyStyle("table", table);

                // if the DataTables plugin is available, use it
                if (self.options.datatables)
                {
                    if ($.fn.DataTable)
                    {
                        // if we're setting up for dragging rows, then add that column
                        if (self.options.dragRows)
                        {
                            self.options.datatables.columns.push({
                                "orderable": false,
                                "name": "dragRowsIndex",
                                "hidden": true
                            });

                            self.options.datatables.columns.push({
                                "orderable": false,
                                "name": "dragRowsDraggable"
                            });
                        }

                        // mix in fields from the items
                        for (var k in self.schema.items.properties)
                        {
                            self.options.datatables.columns.push({
                                "orderable": true,
                                "orderDataType": "alpaca"
                            });
                        }

                        // if we have an actions column enabled, then turn off sorting for the actions column (assumed to be last)
                        if (self.options.showActionsColumn)
                        {
                            self.options.datatables.columns.push({
                                "orderable": false,
                                "name": "actions"
                            });
                        }

                        if (self.options.dragRows)
                        {
                            self.options.datatables["rowReorder"] = {
                                "selector": "tr td.alpaca-table-reorder-draggable-cell",
                                "dataSrc": 0,
                                "snapX": true,
                                "update": true
                            };
                        }

                        // EVENT HANDLERS

                        // listen for the "ready" event and when it fires, init data tables
                        // this ensures that the DOM and anything wrapping our table field instance is ready to rock
                        // before we proceed
                        self.off("ready");
                        self.on("ready", function() {

                            // tear down old data tables data if it is still around
                            if (self._dt) {
                                self._dt.destroy();
                                self._dt = undefined;
                            }

                            // table dom element
                            var table = $(self.container).find("table");

                            // data table reference
                            self._dt = $(table).DataTable(self.options.datatables);

                            // listen for the "row-reorder" event
                            self._dt.on("row-reorder", function(e, diff, edit) {

                                if (self._dt._disableAlpacaHandlers) {
                                    return;
                                }

                                // update our data structure to reflect the shift in positions
                                if (diff.length > 0)
                                {
                                    if (diff[0].oldPosition !== diff[0].newPosition)
                                    {
                                        self._dt._disableAlpacaHandlers = true;
                                        self.moveItem(diff[0].oldPosition, diff[0].newPosition, false, function() {
                                            // all done
                                        });
                                    }
                                }
                            });

                            // listen for the underlying table DOM element being destroyed
                            // when that happens, tear down the datatables implementation as well
                            $(self.container).bind('destroyed', function() {
                                if (self._dt) {
                                    self._dt.destroy();
                                    self._dt = undefined;
                                }
                            });

                            // listen for the sorting event
                            // change the order of children and refresh
                            self._dt.on('order', function ( e, ctx, sorting, columns ) {

                                if (self._dt._disableAlpacaHandlers) {
                                    return;
                                }

                                // if we don't have an original copy of the children, make one
                                // we're about to re-order the children and datatable assumes we know the original order
                                if (!self._dt._originalChildren) {
                                    self._dt._originalChildren = [];
                                    for (var k = 0; k < self.children.length; k++) {
                                        self._dt._originalChildren.push(self.children[k]);
                                    }
                                }

                                // re-order based on the order that datatables believes is right
                                var newChildren = [];
                                for (var z = 0; z < ctx.aiDisplay.length; z++)
                                {
                                    var index = ctx.aiDisplay[z];
                                    newChildren.push(self._dt._originalChildren[index]);
                                }
                                self.children = newChildren;

                                self._dt._disableAlpacaHandlers = false;
                            });

                        });
                    }
                }

                // walk through headers and allow for callback-based config
                $(table).find("thead > tr > th[data-header-id]").each(function() {

                    var key = $(this).attr("data-header-id");

                    var schema = self.schema.items.properties[key];
                    var options = null;
                    if (self.options.items.fields && self.options.items.fields[key]) {
                        options = self.options.items.fields[key];
                    }

                    // CALLBACK: "tableHeaderRequired" or "tableHeaderOptional"
                    if (schema.required || (options && options.required))
                    {
                        // CALLBACK: "tableHeaderRequired"
                        self.fireCallback("tableHeaderRequired", schema, options, this);
                    }
                    else
                    {
                        // CALLBACK: "tableHeaderOptional"
                        self.fireCallback("tableHeaderOptional", schema, options, this);
                    }

                });

                callback();

            }.bind(self));
        },

        cleanupDomInjections: function()
        {
            /**
             * Takes a DOM element and merges it "up" to the parent element.  Data attributes and some classes are
             * copied from DOM element into the parent element.  The children of the DOM element are added to the
             * parent and the DOM element is removed.
             *
             * @param mergeElement
             */
            var mergeElementUp = function(mergeElement)
            {
                var mergeElementParent = $(mergeElement).parent();
                var mergeElementChildren = $(mergeElement).children();

                // copy merge element classes to parent
                var classNames =$(mergeElement).attr('class').split(/\s+/);
                $.each( classNames, function(index, className){
                    if (className === "alpaca-merge-up") {
                        // skip
                    } else {
                        $(mergeElementParent).addClass(className);
                    }
                });

                // copy attributes to TR
                $.each($(mergeElement)[0].attributes, function() {
                    if (this.name && this.name.indexOf("data-") === 0)
                    {
                        $(mergeElementParent).attr(this.name, this.value);
                    }
                });

                // replace field with children
                if (mergeElementChildren.length > 0)
                {
                    $(mergeElement).replaceWith(mergeElementChildren);
                }
                else
                {
                    $(mergeElement).remove();
                }
            };

            // find each TR's .alpaca-field and merge up
            this.getFieldEl().find("tr > .alpaca-field").each(function() {
                mergeElementUp(this);
            });

            // find each TR's .alpaca-container and merge up
            this.getFieldEl().find("tr > .alpaca-container").each(function() {
                mergeElementUp(this);
            });

            // find the action bar and slip a TD around it
            var alpacaArrayActionbar = this.getFieldEl().find("." + Alpaca.MARKER_CLASS_ARRAY_ITEM_ACTIONBAR);
            if (alpacaArrayActionbar.length > 0)
            {
                alpacaArrayActionbar.each(function() {
                    var td = $("<td class='actionbar' nowrap='nowrap'></td>");
                    $(this).before(td);
                    $(td).append(this);
                });
            }

            // find the alpaca-table-reorder-draggable-cell and slip a TD around it
            var alpacaTableReorderDraggableCells = this.getFieldEl().find(".alpaca-table-reorder-draggable-cell");
            if (alpacaTableReorderDraggableCells.length > 0)
            {
                alpacaTableReorderDraggableCells.each(function() {
                    var td = $("<td class='alpaca-table-reorder-draggable-cell'></td>");
                    $(this).before(td);
                    $(td).append($(this).children());
                    $(this).remove();
                });
            }

            // find the alpaca-table-reorder-index-cell, slip a TD around it and insert value
            var alpacaTableReorderIndexCells = this.getFieldEl().find(".alpaca-table-reorder-index-cell");
            if (alpacaTableReorderIndexCells.length > 0)
            {
                alpacaTableReorderIndexCells.each(function(i) {
                    var td = $("<td class='alpaca-table-reorder-index-cell'>" + i + "</td>");
                    $(this).before(td);
                    $(this).remove();
                });
            }

            // find anything else with .alpaca-merge-up and merge up
            this.getFieldEl().find(".alpaca-merge-up").each(function() {
                mergeElementUp(this);
            });
        },

        doResolveItemContainer: function()
        {
            var self = this;

            return $(self.container).find("table tbody");
        },

        doAfterAddItem: function(item, callback)
        {
            var self = this;

            self.data = self.getValue();

            self.cleanupDomInjections();

            // if we're using dragRows support, we have no choice here except to completely reboot the table in order
            // to get DataTables to bind things correctly for drag-drop support
            // TODO: change dragRows to use our own drag/drop tooling and get rid of DataTables Row Reorder Plugin
            // we also have do this if we've added the first row to get DataTables to redraw
            var usingDataTables = self.options.datatables && $.fn.DataTable;
            if (self.options.dragRows || (usingDataTables && self.data.length === 1))
            {
                // refresh
                self.refresh(function() {

                    callback();
                });
            }
            else
            {
                callback();
            }
        },

        doAfterRemoveItem: function(childIndex, callback)
        {
            var self = this;

            self.data = self.getValue();

            self.cleanupDomInjections();

            // TODO: see above

            var usingDataTables = self.options.datatables && $.fn.DataTable;
            if (self.options.dragRows || (usingDataTables && self.data.length === 0))
            {
                // refresh
                self.refresh(function () {
                    callback();
                });

                callback();
            }
            else
            {
                callback();
            }
        },

        /**
         * @see Alpaca.ControlField#getType
         */
        getType: function() {
            return "array";
        }


        /* builder_helpers */
        ,

        /**
         * @see Alpaca.ControlField#getTitle
         */
        getTitle: function() {
            return "Table Field";
        },

        /**
         * @see Alpaca.ControlField#getDescription
         */
        getDescription: function() {
            return "Renders array items into a table";
        },

        /**
         * @private
         * @see Alpaca.Fields.TextField#getSchemaOfOptions
         */
        getSchemaOfOptions: function() {
            return Alpaca.merge(this.base(), {
                "properties": {
                    "datatables": {
                        "title": "DataTables Configuration",
                        "description": "Optional configuration to be passed to the underlying DataTables Plugin.",
                        "type": "object"
                    },
                    "showActionsColumn": {
                        "title": "Show Actions Column",
                        "default": true,
                        "description": "Whether to show or hide the actions column.",
                        "type": "boolean"
                    },
                    "dragRows": {
                        "title": "Drag Rows",
                        "default": false,
                        "description": "Whether to enable the dragging of rows via a draggable column.  This requires DataTables and the DataTables Row Reorder Plugin.",
                        "type": "boolean"
                    }
                }
            });
        },

        /**
         * @private
         * @see Alpaca.Fields.TextField#getOptionsForOptions
         */
        getOptionsForOptions: function() {
            return Alpaca.merge(this.base(), {
                "fields": {
                    "datatables": {
                        "type": "object"
                    },
                    "showActionsColumn": {
                        "type": "checkbox"
                    },
                    "dragRows": {
                        "type": "checkbox"
                    }
                }
            });
        }

        /* end_builder_helpers */
    });

    Alpaca.registerFieldClass("table", Alpaca.Fields.TableField);

})(jQuery);