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
JavaScript
(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);