"use strict";
+function($) {
var Table = function(element, options) {
var _this = this;
this.$element = $(element);
this.options = $.extend({}, Table.DEFAULTS, options);
this.$selectedRow = null;
this.selectionHandler = function(event) {
if(this) {
$(this).addClass(_this.options.selectionCSS).siblings().removeClass(_this.options.selectionCSS);
_this.$selectedRow = $(this);
}
else {
_this.$element.find('tr').removeClass(_this.options.selectionCSS);
_this.$selectedRow = null;
}
//Note: We are not currently using events for the selection handling since it is really not necessary. Instead we are using a handler provided by the app code when the table is initialized.
//Create a selection event indicating the table has changed its selection.
//var e = $.Event('selection.linkedtable', {relatedTarget: _this.$element[0]});
//Fire the event.
//_this.$element.trigger(e);
if(_this.options.selectionChanged) {
//Notify the handler of the new selection, and the selection's model.
_this.options.selectionChanged(_this.$selectedRow, _this.$selectedRow ? _this.$selectedRow.data(_this.options.modelPropName) : null);
}
_this.$element.focus();
return true;
};
//Takes a callback function with three parameters:
// data: The array of model data returned by the server.
// attributes: The array of attributes in column order, one per column.
// supportingData: The map of additional data (array of model objects) returned by the server for each supporting query, indexed by the name given to the query when the table was created.
this.load = function(callback) {
var supportingData = {};
var deferreds = [];
var params;
var _this = this;
//Build the parameters for the query for the primary set of data.
if(typeof this.options.parameters == 'function') {
params = this.options.parameters();
//Must be an object.
if(typeof params != 'object') {
params = {};
}
}
else if(typeof this.options.parameters == 'object') {
params = this.options.parameters;
}
else {params = {};}
//Load the primary set of data.
deferreds.push($.post(this.options.url, params ? {request: JSON.stringify(params)} : {}, 'json'));
//deferreds.push($.post({url: this.options.url, data: {request: JSON.stringify(params)}, dataType: 'json', contentType: 'application/json; charset=utf-8'}));
//deferreds.push($.ajax({type: 'POST', url: this.options.url, data: {request: JSON.stringify(params)}, dataType: 'json'}));
//Load all supporting data.
for(var supportingDataIndex = 0; supportingDataIndex < this.options.supportingData.length; supportingDataIndex++) {
var nextSupportingData = this.options.supportingData[supportingDataIndex];
//Build the parameters for each supporting set of data.
if(typeof nextSupportingData.parameters == 'function') {
params = nextSupportingData.parameters();
//Must be an object.
if(typeof params != 'object') {
params = {};
}
}
else if(typeof nextSupportingData.parameters == 'object') {
params = nextSupportingData.parameters;
}
else {params = {};}
//Load the supporting data.
deferreds.push($.post(nextSupportingData.url, {request: JSON.stringify(params)}, 'json'));
}
//Use apply to convert an array of parameters to individual parameters for the $.when(..) function. The success function will receive the results in order passed to when().
$.when.apply($, deferreds).then(function() {
//Note: If there is only one query, then the argument array contains the results for the one query, otherwise the argument array is an array of result arrays, one for each query.
var data = (deferreds.length > 1 ? arguments[0][0] : arguments[0]); //Note: For multiple queries, the result of the first query returns an array of three values: the data (array in this case), the message, and XHR object. We only care about the data.
if(deferreds.length > 1) {
//Save each supporting set of data by name into the supportingData object.
for(var argumentIndex = 1; argumentIndex < arguments.length; argumentIndex++) {
var supportingResult = arguments[argumentIndex][0]; //We only care about the data. The other two array elements are the status and the XHR object.
if(_this.options.supportingData[argumentIndex - 1].postProcess) {
supportingResult = _this.options.supportingData[argumentIndex - 1].postProcess(supportingResult);
}
supportingData[_this.options.supportingData[argumentIndex - 1].name] = supportingResult;
}
}
callback.call(_this, data, _this.collectAttributes.call(_this), supportingData);
}).fail(function(err) {
console.error("Unexpected error loading the table data?!? " + err);
});
};
//Collects the set of attributes to display, one per column, in column order.
this.collectAttributes = function() {
var table = this.$element;
var thead = table.find("thead tr");
var headers = thead.children();
var attributes = [];
var attrName = this.options.attr;
//Read the table headers to get the data object keys.
for(var headerIndex = 0; headerIndex < headers.length; headerIndex++) {
var nextHeader = headers[headerIndex];
attributes[headerIndex] = $(nextHeader).attr(attrName);
//Replace the attribute name with the handler function if there is one in the mapping.
if(this.options.cellDataHandlers[attributes[headerIndex]]) {
attributes[headerIndex] = this.options.cellDataHandlers[attributes[headerIndex]];
}
}
return attributes;
};
//Creates a new row in the table. Returns the row created. Does NOT attach the row to the table.
this.createRow = function(model, attributes, supportingData) {
var row = $("
");
//Save the model attached to the table row. Can be retrieved later to get the model sent by the server.
row.data(this.options.modelPropName, model);
for(var attributeIndex = 0; attributeIndex < attributes.length; attributeIndex++) {
var attribute = attributes[attributeIndex];
var $cell = $(" | ");
//Fill in the cell's data.
this.updateCell($cell, attribute, model, supportingData);
//Add the cell to the row.
$cell.appendTo(row);
$cell.disableSelection();
}
if(this.options.postAddRowHandler) {
//Call the optional handler after adding the row, passing the jquery row object, and the row data object sent by the server. Allows post processing on the row (adding classes to the row for example).
this.options.postAddRowHandler(row, model);
}
return row;
};
this.updateRow = function($row, model, attributes, supportingData) {
var $cells = $row.children("td");
//Attach the new model to the row.
$row.data(this.options.modelPropName, model);
//Iterate over the cell data elements and update their values.
for(var cellIndex = 0; cellIndex < $cells.length; cellIndex++) {
var $cell = $($cells[cellIndex]);
//Note: There should be exactly one attribute for each cell in the row.
this.updateCell($cell, attributes[cellIndex], model, supportingData);
}
if(this.options.postUpdateRowHandler) {
//Call the optional handler after adding the row, passing the jquery row object, and the row data object sent by the server. Allows post processing on the row (adding classes to the row for example).
this.options.postUpdateRowHandler($row, model);
}
};
//Initializes or updates the cell with data from the model.
//The attribute can either be an attribute of the model, or a function that takes ($cell, model, supportingData) and returns nothing.
this.updateCell = function($cell, attribute, model, supportingData) {
if($.isFunction(attribute)) {
attribute($cell, model, supportingData);
}
else {
$cell.text(model[attribute]);
}
};
};
Table.DEFAULTS = {
url: '', //The absolute or relative path to use to query the data. Server is expected to respond with a JSON array of objects.
attr: 'data-key-name', //The attribute name used by the HTML | tags to specify either the model attribute used to fill the cells for the row, or the function that will update the cell data. The function should take three parameters: $cell, model, supportingData. $cell is the jquery wrapped | element. Model is the json model sent by the server for this row. SupportingData is a map of query results for supporting queries as setup when defining the table.
modelPropName: 'model', //The property name to use when attaching the model to the table row. Not normally changed.
idAttr: 'id', //The id attribute for model objects. All model objects must have an id attribute that is unique for the model. TODO: Add the possibility of a function that might generate an id from other attributes of the model.
selectionCSS: 'selected',
selection: 'row', //Currently only row is supported.
selectionChanged: undefined, //An optional handler called when the selection changes, and passed the selected element (jquery wrapper for the table row 'tr' HTML element currently selected), and the model associated with the row.
selectionOpened: undefined, //An optional handler called when the user double clicks a row.
keyDownHandler: undefined, //An optional handler called when a key is pressed on a row.
keyUpHandler: undefined, //An optional handler called when a key is pressed on a row.
supportingData: [], //An array of objects, one for each collection of supporting data. Each object must have a 'name', 'url', and optional 'parameters'. The url and parameters are used the same as for the primary query. The name is used to store the results for use in rendering the data. Optional 'postProcess' attribute can be a function that takes the data and returns a modified set of data.
cellDataHandlers: {}, //An object containing a function for each attribute, where the attribute is the table header object key, and the function is called to convert the primary data object into a cell value. The function will be passed the jquery table data wrapper object for the cell, the primary data object for the row, and the object containing the supporting data returned from running the supporting data queries.
postAddRowHandler: null, //Optional function that is passed the jquery table row and the data object sent by the server for that row. Allows post processing of the row prior to display.
postUpdateRowHandler: null, //Optional function that works just like the postAddRowHandler, but is called when a row is updated. This may be the same function as the postRowAddHandler, or not if so desired.
parameters: null //Optional function that returns an object, or an object whose attributes are passed to the URL as parameters.
};
Table.prototype.getSelectedRow = function() {
return this.$selectedRow;
};
//Refreshes the table data with updated server data. Does not clear the table or rebuild it.
Table.prototype.refresh = function() {
//Load the results from the server, then build the table.
this.load.call(this, function(data, attributes, supportingData) {
var $table = this.$element;
var $tbody = $table.find("tbody");
var $rows = $tbody.children("tr");
var idAttr = this.options.idAttr;
//Create a map of model objects by id.
var dataMap = data.reduce(function(map, obj) {
map[obj[idAttr]] = obj;
return map;
}, {});
//Iterate over the existing table rows. Find those that have objects in the map and update them. Delete those not in the map. Remove objects from the map as we go.
for(var rowIndex = 0; rowIndex < $rows.length; rowIndex++) {
var $row = $($rows[rowIndex]);
var oldModel = $row.data(this.options.modelPropName);
var newModel = dataMap[oldModel[this.options.idAttr]];
if(newModel) {
//Update the table row.
this.updateRow($row, newModel, attributes, supportingData);
//Remove the model from the map so we know we have already updated its row in the table.
delete dataMap[oldModel[this.options.idAttr]];
}
else {
//Clear the selection if the row is removed.
if(this.$selectedRow && $row.is(this.$selectedRow)) {
//Clear the selection.
this.selectionHandler.call(null, null);
}
//Remove the row from the table.
$row.remove();
}
}
var keys = Object.keys(dataMap);
//Add all objects left in the map to the table as new rows.
for(var keyIndex = 0; keyIndex < keys.length; keyIndex++) {
var key = keys[keyIndex];
var model = dataMap[key];
var $row = this.createRow(model, attributes, supportingData);
$row.appendTo($tbody);
}
});
};
//A function that will clean and rebuild the table displaying all the users.
//Note that each row of the table will have a data element attached to the table row. The key will be "model" and the value will be the object sent by the server.
Table.prototype.build = function() {
var table = this.$element;
var thead = table.find("thead tr");
var tbody = table.find("tbody");
var selection = this.options.selection;
var selectionHandler = this.selectionHandler;
if(thead.length == 0) {
return;
}
//Empty or Create the table body.
if(tbody.length != 0) {
//Remove the row selection handler.
if(selection == 'row') this.$element.off('click', 'tbody tr', selectionHandler);
//Empty the table of data.
tbody.empty();
}
else {
tbody = $("");
tbody.appendTo(table);
}
//Load the results from the server, then build the table.
this.load.call(this, function(data, attributes, supportingData) {
//Add the table data.
for(var dataIndex = 0; dataIndex < data.length; dataIndex++) {
var row = this.createRow.call(this, data[dataIndex], attributes, supportingData);
//Add the row to the end of the table.
row.appendTo(tbody);
}
//Setup the row selection handler.
if(selection == 'row') table.on('click', 'tbody tr', selectionHandler);
});
//This does not work: Cannot unregister the handler because we never get the notification that the table has been removed from the DOM. We could listen for all removals and find any that are any of the parents of this table, but that would be rediculously expensive.
// //Handle any key event that bubbles back to the document while the page is being shown. This allows the table elements to be navigated without focusing the table.
// var keyHandler = function(event) {
// console.log(event.keyCode);
// };
// var parent = this.$element.parent()[0];
// $(document).on('keyup', keyHandler);
// var observer = new MutationObserver(function(mutations) {
// for(var i = 0; i < mutations.length; i++) {
// if(mutations[i].removedNodes && mutations[i].removedNodes.length) {
// for(var n = 0; n < mutations[i].removedNodes.length; n++) {
// if(mutations[i].removedNodes[n] == parent) {
// $(document).off('keyup', keyHandler);
// observer.disconnect();
// }
// }
// }
// }
// });
// //Remove the key handler when the table is removed from the DOM.
// observer.observe(parent, {childList: true, subtree: true});
};
$.fn.buildTable = function(options) {
for(var index = 0; index < this.length; index++) {
var $next = $(this[index]);
var nextTable = new Table($next, options);
$next.data("de.table", nextTable);
nextTable.build();
}
};
$.fn.getTable = function() {
if(this.length > 0) {
return $(this[0]).data('de.table');
}
};
}(jQuery);