/************************************************************************
* CORE jTable module *
*************************************************************************/
(function ($) {
$.widget("hik.jtable", {
/************************************************************************
* DEFAULT OPTIONS / EVENTS *
*************************************************************************/
options: {
//Options
actions: {},
fields: {},
animationsEnabled: true,
defaultDateFormat: 'yy-mm-dd',
dialogShowEffect: 'fade',
dialogHideEffect: 'fade',
showCloseButton: false,
loadingAnimationDelay: 500,
ajaxSettings: {
type: 'POST',
dataType: 'json'
},
//Events
closeRequested: function (event, data) { },
formCreated: function (event, data) { },
formSubmitting: function (event, data) { },
formClosed: function (event, data) { },
loadingRecords: function (event, data) { },
recordsLoaded: function (event, data) { },
rowInserted: function (event, data) { },
rowsRemoved: function (event, data) { },
//Localization
messages: {
serverCommunicationError: 'An error occured while communicating to the server.',
loadingMessage: 'Loading records...',
noDataAvailable: 'No data available!',
areYouSure: 'Are you sure?',
save: 'Save',
saving: 'Saving',
cancel: 'Cancel',
error: 'Error',
close: 'Close',
cannotLoadOptionsFor: 'Can not load options for field {0}'
}
},
/************************************************************************
* PRIVATE FIELDS *
*************************************************************************/
_$mainContainer: null, //Reference to the main container of all elements that are created by this plug-in (jQuery object)
_$table: null, //Reference to the main
(jQuery object)
_$tableBody: null, //Reference to in the table (jQuery object)
_$tableRows: null, //Array of all
in the table (except "no data" row) (jQuery object array)
_$bottomPanel: null, //Reference to the panel at the bottom of the table (jQuery object)
_$busyDiv: null, //Reference to the div that is used to block UI while busy (jQuery object)
_$busyMessageDiv: null, //Reference to the div that is used to show some message when UI is blocked (jQuery object)
_$errorDialogDiv: null, //Reference to the error dialog div (jQuery object)
_columnList: null, //Name of all data columns in the table (select column and command columns are not included) (string array)
_fieldList: null, //Name of all fields of a record (defined in fields option) (string array)
_keyField: null, //Name of the key field of a record (that is defined as 'key: true' in the fields option) (string)
_firstDataColumnOffset: 0, //Start index of first record field in table columns (some columns can be placed before first data column, such as select checkbox column) (integer)
_lastPostData: null, //Last posted data on load method (object)
_cache: null, //General purpose cache dictionary (object)
/************************************************************************
* CONSTRUCTOR AND INITIALIZATION METHODS *
*************************************************************************/
/* Contructor.
*************************************************************************/
_create: function () {
//Initialization
this._normalizeFieldsOptions();
this._initializeFields();
this._createFieldAndColumnList();
//Creating DOM elements
this._createMainContainer();
this._createTableTitle();
this._createTable();
this._createBottomPanel();
this._createBusyPanel();
this._createErrorDialogDiv();
this._addNoDataRow();
},
/* Normalizes some options for all fields (sets default values).
*************************************************************************/
_normalizeFieldsOptions: function () {
var self = this;
$.each(self.options.fields, function (fieldName, props) {
self._normalizeFieldOptions(fieldName, props);
});
},
/* Normalizes some options for a field (sets default values).
*************************************************************************/
_normalizeFieldOptions: function (fieldName, props) {
props.listClass = props.listClass || '';
props.inputClass = props.inputClass || '';
},
/* Intializes some private variables.
*************************************************************************/
_initializeFields: function () {
this._lastPostData = {};
this._$tableRows = [];
this._columnList = [];
this._fieldList = [];
this._cache = [];
},
/* Fills _fieldList, _columnList arrays and sets _keyField variable.
*************************************************************************/
_createFieldAndColumnList: function () {
var self = this;
$.each(self.options.fields, function (name, props) {
//Add field to the field list
self._fieldList.push(name);
//Check if this field is the key field
if (props.key == true) {
self._keyField = name;
}
//Add field to column list if it is shown in the table
if (props.list != false && props.type != 'hidden') {
self._columnList.push(name);
}
});
},
/* Creates the main container div.
*************************************************************************/
_createMainContainer: function () {
this._$mainContainer = $('')
.addClass('jtable-main-container')
.appendTo(this.element);
},
/* Creates title of the table if a title supplied in options.
*************************************************************************/
_createTableTitle: function () {
var self = this;
if (!self.options.title) {
return;
}
var $titleDiv = $('')
.addClass('jtable-title')
.appendTo(self._$mainContainer);
$('')
.addClass('jtable-title-text')
.appendTo($titleDiv)
.append(self.options.title);
if (self.options.showCloseButton) {
var $textSpan = $('')
.html(self.options.messages.close);
$('')
.addClass('jtable-command-button jtable-close-button')
.attr('title', self.options.messages.close)
.append($textSpan)
.appendTo($titleDiv)
.click(function (e) {
e.preventDefault();
e.stopPropagation();
self._onCloseRequested();
});
}
},
/* Creates the table.
*************************************************************************/
_createTable: function () {
this._$table = $('
')
.addClass('jtable')
.appendTo(this._$mainContainer);
this._createTableHead();
this._createTableBody();
},
/* Creates header (all column headers) of the table.
*************************************************************************/
_createTableHead: function () {
var $thead = $('')
.appendTo(this._$table);
this._addRowToTableHead($thead);
},
/* Adds tr element to given thead element
*************************************************************************/
_addRowToTableHead: function ($thead) {
var $tr = $('
')
.appendTo($thead);
this._addColumnsToHeaderRow($tr);
},
/* Adds column header cells to given tr element.
*************************************************************************/
_addColumnsToHeaderRow: function ($tr) {
for (var i = 0; i < this._columnList.length; i++) {
var fieldName = this._columnList[i];
var $headerCell = this._createHeaderCellForField(fieldName, this.options.fields[fieldName]);
$headerCell.appendTo($tr);
}
},
/* Creates a header cell for given field.
* Returns th jQuery object.
*************************************************************************/
_createHeaderCellForField: function (fieldName, field) {
field.width = field.width || '10%'; //default column width: 10%.
var $headerTextSpan = $('')
.addClass('jtable-column-header-text')
.html(field.title);
var $headerContainerDiv = $('')
.addClass('jtable-column-header-container')
.append($headerTextSpan);
var $th = $('
')
.addClass('jtable-column-header')
.css('width', field.width)
.data('fieldName', fieldName)
.append($headerContainerDiv);
return $th;
},
/* Creates an empty header cell that can be used as command column headers.
*************************************************************************/
_createEmptyCommandHeader: function () {
return $('
')
.addClass('jtable-command-column-header')
.css('width', '1%');
},
/* Creates tbody tag and adds to the table.
*************************************************************************/
_createTableBody: function () {
this._$tableBody = $('').appendTo(this._$table);
},
/* Creates bottom panel and adds to the page.
*************************************************************************/
_createBottomPanel: function () {
this._$bottomPanel = $('')
.addClass('jtable-bottom-panel')
.appendTo(this._$mainContainer);
$('').addClass('jtable-left-area').appendTo(this._$bottomPanel);
$('').addClass('jtable-right-area').appendTo(this._$bottomPanel);
},
/* Creates a div to block UI while jTable is busy.
*************************************************************************/
_createBusyPanel: function () {
this._$busyMessageDiv = $('').addClass('jtable-busy-message').prependTo(this._$mainContainer);
this._$busyDiv = $('').addClass('jtable-busy-panel-background').prependTo(this._$mainContainer);
this._hideBusy();
},
/* Creates and prepares error dialog div.
*************************************************************************/
_createErrorDialogDiv: function () {
var self = this;
self._$errorDialogDiv = $('').appendTo(self._$mainContainer);
self._$errorDialogDiv.dialog({
autoOpen: false,
show: self.options.dialogShowEffect,
hide: self.options.dialogHideEffect,
modal: true,
title: self.options.messages.error,
buttons: [{
text: self.options.messages.close,
click: function () {
self._$errorDialogDiv.dialog('close');
}
}]
});
},
/************************************************************************
* PUBLIC METHODS *
*************************************************************************/
/* Loads data using AJAX call, clears table and fills with new data.
*************************************************************************/
load: function (postData, completeCallback) {
this._lastPostData = postData;
this._reloadTable(completeCallback);
},
/* Refreshes (re-loads) table data with last postData.
*************************************************************************/
reload: function (completeCallback) {
this._reloadTable(completeCallback);
},
/* Gets a jQuery row object according to given record key
*************************************************************************/
getRowByKey: function (key) {
for (var i = 0; i < this._$tableRows.length; i++) {
if (key == this._getKeyValueOfRecord(this._$tableRows[i].data('record'))) {
return this._$tableRows[i];
}
}
return null;
},
/* Completely removes the table from it's container.
*************************************************************************/
destroy: function () {
this.element.empty();
$.Widget.prototype.destroy.call(this);
},
/************************************************************************
* PRIVATE METHODS *
*************************************************************************/
/* LOADING RECORDS *****************************************************/
/* Performs an AJAX call to reload data of the table.
*************************************************************************/
_reloadTable: function (completeCallback) {
var self = this;
//Disable table since it's busy
self._showBusy(self.options.messages.loadingMessage, self.options.loadingAnimationDelay);
//Generate URL (with query string parameters) to load records
var loadUrl = self._createRecordLoadUrl();
//Load data from server
self._onLoadingRecords();
self._ajax({
url: loadUrl,
data: self._lastPostData,
success: function (data) {
self._hideBusy();
//Show the error message if server returns error
if (data.Result != 'OK') {
self._showError(data.Message);
return;
}
//Re-generate table rows
self._removeAllRows('reloading');
self._addRecordsToTable(data.Records);
self._onRecordsLoaded(data);
//Call complete callback
if (completeCallback) {
completeCallback();
}
},
error: function () {
self._hideBusy();
self._showError(self.options.messages.serverCommunicationError);
}
});
},
/* Creates URL to load records.
*************************************************************************/
_createRecordLoadUrl: function () {
return this.options.actions.listAction;
},
/* TABLE MANIPULATION METHODS *******************************************/
/* Creates a row from given record
*************************************************************************/
_createRowFromRecord: function (record) {
var $tr = $('
')
.addClass('jtable-data-row')
.attr('data-record-key', this._getKeyValueOfRecord(record))
.data('record', record);
this._addCellsToRowUsingRecord($tr);
return $tr;
},
/* Adds all cells to given row.
*************************************************************************/
_addCellsToRowUsingRecord: function ($row) {
var record = $row.data('record');
for (var i = 0; i < this._columnList.length; i++) {
this._createCellForRecordField(record, this._columnList[i])
.appendTo($row);
}
},
/* Create a cell for given field.
*************************************************************************/
_createCellForRecordField: function (record, fieldName) {
return $('
')
.addClass(this.options.fields[fieldName].listClass)
.append((this._getDisplayTextForRecordField(record, fieldName) || ''));
},
/* Adds a list of records to the table.
*************************************************************************/
_addRecordsToTable: function (records) {
var self = this;
$.each(records, function (index, record) {
self._addRow(self._createRowFromRecord(record));
});
self._refreshRowStyles();
},
/* Adds a single row to the table.
* NOTE: THIS METHOD IS DEPRECATED AND WILL BE REMOVED FROM FEATURE RELEASES.
* USE _addRow METHOD.
*************************************************************************/
_addRowToTable: function ($tableRow, index, isNewRow, animationsEnabled) {
var options = {
index: this._normalizeNumber(index, 0, this._$tableRows.length, this._$tableRows.length)
};
if (isNewRow == true) {
options.isNewRow = true;
}
if (animationsEnabled == false) {
options.animationsEnabled = false;
}
this._addRow($tableRow, options);
},
/* Adds a single row to the table.
*************************************************************************/
_addRow: function ($row, options) {
//Set defaults
options = $.extend({
index: this._$tableRows.length,
isNewRow: false,
animationsEnabled: true
}, options);
//Remove 'no data' row if this is first row
if (this._$tableRows.length <= 0) {
this._removeNoDataRow();
}
//Add new row to the table according to it's index
options.index = this._normalizeNumber(options.index, 0, this._$tableRows.length, this._$tableRows.length);
if (options.index == this._$tableRows.length) {
//add as last row
this._$tableBody.append($row);
this._$tableRows.push($row);
} else if (options.index == 0) {
//add as first row
this._$tableBody.prepend($row);
this._$tableRows.unshift($row);
} else {
//insert to specified index
this._$tableRows[options.index - 1].after($row);
this._$tableRows.splice(options.index, 0, $row);
}
this._onRowInserted($row, options.isNewRow);
//Show animation if needed
if (options.isNewRow) {
this._refreshRowStyles();
if (this.options.animationsEnabled && options.animationsEnabled) {
this._showNewRowAnimation($row);
}
}
},
/* Shows created animation for a table row
* TODO: Make this animation cofigurable and changable
*************************************************************************/
_showNewRowAnimation: function ($tableRow) {
$tableRow.addClass('jtable-row-created', 'slow', '', function () {
$tableRow.removeClass('jtable-row-created', 5000);
});
},
/* Removes a row or rows (jQuery selection) from table.
*************************************************************************/
_removeRowsFromTable: function ($rows, reason) {
var self = this;
//Check if any row specified
if ($rows.length <= 0) {
return;
}
//remove from DOM
$rows.remove();
//remove from _$tableRows array
$rows.each(function () {
self._$tableRows.splice(self._findRowIndex($(this)), 1);
});
self._onRowsRemoved($rows, reason);
//Add 'no data' row if all rows removed from table
if (self._$tableRows.length == 0) {
self._addNoDataRow();
}
self._refreshRowStyles();
},
/* Finds index of a row in table.
*************************************************************************/
_findRowIndex: function ($row) {
return this._findIndexInArray($row, this._$tableRows, function ($row1, $row2) {
return $row1.data('record') == $row2.data('record');
});
},
/* Removes all rows in the table and adds 'no data' row.
*************************************************************************/
_removeAllRows: function (reason) {
//If no rows does exists, do nothing
if (this._$tableRows.length <= 0) {
return;
}
//Select all rows (to pass it on raising _onRowsRemoved event)
var $rows = this._$tableBody.find('tr.jtable-data-row');
//Remove all rows from DOM and the _$tableRows array
this._$tableBody.empty();
this._$tableRows = [];
this._onRowsRemoved($rows, reason);
//Add 'no data' row since we removed all rows
this._addNoDataRow();
},
/* Adds "no data available" row to the table.
*************************************************************************/
_addNoDataRow: function () {
var $tr = $('
')
.addClass('jtable-no-data-row')
.appendTo(this._$tableBody);
var totalColumnCount = this._$table.find('thead th').length;
$('
')
.attr('colspan', totalColumnCount)
.html(this.options.messages.noDataAvailable)
.appendTo($tr);
},
/* Removes "no data available" row from the table.
*************************************************************************/
_removeNoDataRow: function () {
this._$tableBody.find('.jtable-no-data-row').remove();
},
/* Refreshes styles of all rows in the table
*************************************************************************/
_refreshRowStyles: function () {
for (var i = 0; i < this._$tableRows.length; i++) {
if (i % 2 == 0) {
this._$tableRows[i].addClass('jtable-row-even');
} else {
this._$tableRows[i].removeClass('jtable-row-even');
}
}
},
/* RENDERING FIELD VALUES ***********************************************/
/* Gets text for a field of a record according to it's type.
*************************************************************************/
_getDisplayTextForRecordField: function (record, fieldName) {
var field = this.options.fields[fieldName];
var fieldValue = record[fieldName];
//if this is a custom field, call display function
if (field.display) {
return field.display({ record: record });
}
if (field.type == 'date') {
return this._getDisplayTextForDateRecordField(field, fieldValue);
} else if (field.type == 'checkbox') {
return this._getCheckBoxTextForFieldByValue(fieldName, fieldValue);
} else if (field.options) {
return this._getOptionsWithCaching(fieldName)[fieldValue];
} else {
return fieldValue;
}
},
/* Gets text for a date field.
*************************************************************************/
_getDisplayTextForDateRecordField: function (field, fieldValue) {
if (!fieldValue) {
return '';
}
var displayFormat = field.displayFormat || this.options.defaultDateFormat;
var date = this._parseDate(fieldValue);
return $.datepicker.formatDate(displayFormat, date);
},
/* Parses given date string to a javascript Date object.
* Given string must be formatted one of the samples shown below:
* /Date(1320259705710)/
* 2011-01-01 20:32:42 (YYYY-MM-DD HH:MM:SS)
* 2011-01-01 (YYYY-MM-DD)
*************************************************************************/
_parseDate: function (dateString) {
if (dateString.indexOf('Date') >= 0) { //Format: /Date(1320259705710)/
return new Date(
parseInt(dateString.substr(6))
);
} else if (dateString.length == 10) { //Format: 2011-01-01
return new Date(
parseInt(dateString.substr(0, 4)),
parseInt(dateString.substr(5, 2)) - 1,
parseInt(dateString.substr(8, 2))
);
} else if (dateString.length == 19) { //Format: 2011-01-01 20:32:42
return new Date(
parseInt(dateString.substr(0, 4)),
parseInt(dateString.substr(5, 2)) - 1,
parseInt(dateString.substr(8, 2)),
parseInt(dateString.substr(11, 2)),
parseInt(dateString.substr(14, 2)),
parseInt(dateString.substr(17, 2))
);
} else {
this._logWarn('Given date is not properly formatted: ' + dateString);
return 'format error!';
}
},
/* ERROR DIALOG *********************************************************/
/* Shows error message dialog with given message.
*************************************************************************/
_showError: function (message) {
this._$errorDialogDiv.html(message).dialog('open');
},
/* BUSY PANEL ***********************************************************/
/* Shows busy indicator and blocks table UI.
* TODO: Make this cofigurable and changable
*************************************************************************/
_setBusyTimer: null, //TODO: Think for a better way!
_showBusy: function (message, delay) {
var self = this;
var show = function () {
if (!self._$busyMessageDiv.is(':visible')) {
self._$busyDiv.width(self._$mainContainer.width());
self._$busyDiv.height(self._$mainContainer.height());
self._$busyDiv.show();
self._$busyMessageDiv.show();
}
self._$busyMessageDiv.html(message);
};
//TODO: Put an overlay always (without color) to not allow to click the table
//TODO: and change it visible when timeout occurs.
if (delay) {
self._setBusyTimer = setTimeout(show, delay);
} else {
show();
}
},
/* Hides busy indicator and unblocks table UI.
*************************************************************************/
_hideBusy: function () {
clearTimeout(this._setBusyTimer);
this._$busyDiv.hide();
this._$busyMessageDiv.html('').hide();
},
/* Returns true if jTable is busy.
*************************************************************************/
_isBusy: function () {
return this._$busyMessageDiv.is(':visible');
},
/* COMMON METHODS *******************************************************/
/* Performs an AJAX call to specified URL.
* THIS METHOD IS DEPRECATED AND WILL BE REMOVED FROM FEATURE RELEASES.
* USE _ajax METHOD.
*************************************************************************/
_performAjaxCall: function (url, postData, async, success, error) {
this._ajax({
url: url,
data: postData,
async: async,
success: success,
error: error
});
},
/* This method is used to perform AJAX calls in jTable instead of direct
* usage of jQuery.ajax method.
*************************************************************************/
_ajax: function (options) {
var opts = $.extend({}, this.options.ajaxSettings, options);
//Override success
opts.success = function (data) {
if (options.success) {
options.success(data);
}
};
//Override error
opts.error = function () {
if (options.error) {
options.error();
}
};
//Override complete
opts.complete = function () {
if (options.complete) {
options.complete();
}
};
$.ajax(opts);
},
/* Gets value of key field of a record.
*************************************************************************/
_getKeyValueOfRecord: function (record) {
return record[this._keyField];
},
/************************************************************************
* EVENT RAISING METHODS *
*************************************************************************/
_onLoadingRecords: function () {
this._trigger("loadingRecords", null, {});
},
_onRecordsLoaded: function (data) {
this._trigger("recordsLoaded", null, { records: data.Records, serverResponse: data });
},
_onRowInserted: function ($row, isNewRow) {
this._trigger("rowInserted", null, { row: $row, record: $row.data('record'), isNewRow: isNewRow });
},
_onRowsRemoved: function ($rows, reason) {
this._trigger("rowsRemoved", null, { rows: $rows, reason: reason });
},
_onCloseRequested: function () {
this._trigger("closeRequested", null, {});
}
});
}(jQuery));