UNPKG

alpaca

Version:

Alpaca provides the easiest and fastest way to generate interactive forms for the web and mobile devices. It runs simply as HTML5 or more elaborately using Bootstrap, jQuery Mobile or jQuery UI. Alpaca uses Handlebars to process JSON schema and provide

695 lines (578 loc) 26.4 kB
(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"]; delete 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) { // ensure that data property has latest value self.data = self.getValue(); var propertyName = null; // find the property by index var c = 0; for (var k in self.schema.items.properties) { if (c === col) { propertyName = k; break; } c++; } // collect values var values = []; if (self.data) { for (var i = 0; i < self.data.length; i++) { values.push(self.data[i][propertyName]); } } // sort values values.sort(); return values; }; // 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) { // TODO 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); }); }, getTableEl: function() { var self = this; return $($(self.container).find("table")[0]); }, /** * 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 = self.getTableEl(); self.applyStyle("table", table); // if the DataTables plugin is available, use it if (self.options.datatables) { if ($.fn.DataTable) { if (self.options.datatables.columns.length === 0) { // 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) { var colConfig = { "orderable": true, "orderDataType": "alpaca" }; self.options.datatables.columns.push(colConfig); } // 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.getTableEl(); // 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).children("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() { var self = this; /** * 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(); } }; var trElements = self.getTableEl().children("tbody").children("tr"); // find each TR's .alpaca-field and merge up $(trElements).children(".alpaca-field").each(function() { mergeElementUp(this); }); // find each TR's .alpaca-container and merge up $(trElements).children(".alpaca-container").each(function() { mergeElementUp(this); }); // find any action bars for our field and slip a TD around them var alpacaArrayActionbar = self.getFieldEl().find("." + Alpaca.MARKER_CLASS_ARRAY_ITEM_ACTIONBAR + "[" + Alpaca.MARKER_DATA_ARRAY_ITEM_FIELD_ID + "='" + self.getId() + "']"); if (alpacaArrayActionbar.length > 0) { alpacaArrayActionbar.each(function() { var td = $("<td class='actionbar' nowrap='nowrap'></td>"); $(this).before(td); $(td).append(this); }); } // find any alpaca-table-reorder-draggable-cells and slip a TD around them var alpacaTableReorderDraggableCells = self.getTableEl().children("tbody").children("tr").children("td.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 any alpaca-table-reorder-draggable-cell elements and slip a TD around them //var alpacaTableReorderIndexCells = self.getTableEl().children("tbody").children("tr").children("td.alpaca-table-reorder-draggable-cell"); // find any alpaca-table-reorder-index-cell elements and slip a TD around them var alpacaTableReorderIndexCells = self.getTableEl().children("tbody").children("tr").children("td.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[data-merge-up-field-id='" + self.getId() + "']").each(function() { mergeElementUp(this); }); */ // find anything else with .alpaca-merge-up and merge up $(trElements).each(function() { var trAlpacaId = $(this).attr("data-alpaca-field-id"); // inject the TD to wrap $(this).find(".alpaca-merge-up[data-merge-up-field-id='" + trAlpacaId + "'][data-alpaca-merge-tag='td']").each(function() { var td = $("<td></td>"); $(this).before(td); $(td).append(this); mergeElementUp(this); $(td).attr("data-alpaca-merge-tag", null); $(td).attr("data-merge-up-field-id", null); }); }); }, doResolveItemContainer: function() { var self = this; return self.getTableEl().children("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 { // inform data tables that we've added a row // we do this by finding the TR and then adding that way if (self._dt) { // TODO var tr = self.field.find("[data-alpaca-field-path='" + item.path + "']"); self._dt.row.add(tr);//.draw(false); } 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(); }); } else { // inform data tables that we've removed a row if (self._dt) { self._dt.rows(childIndex).remove();//.draw(false); } 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);