diff options
author | Halil İbrahim Kalkan <hikalkan@gmail.com> | 2017-03-29 15:38:02 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-03-29 15:38:02 +0300 |
commit | bcd862d5077bbef9078a95c3d3f24ffb865e4716 (patch) | |
tree | 5c55b14ab27b2b60d9833724a9dc0305bf2c0c6f | |
parent | 802857992844caa2cdc8fa027ff131977eaae098 (diff) | |
parent | 460915214caeadc9b6b68caadbc8875dbfc2abe7 (diff) | |
download | jtable-bcd862d5077bbef9078a95c3d3f24ffb865e4716.zip jtable-bcd862d5077bbef9078a95c3d3f24ffb865e4716.tar.gz jtable-bcd862d5077bbef9078a95c3d3f24ffb865e4716.tar.bz2 |
Merge pull request #2038 from ismcagdas/master
Implemented dropdown actions using bootstrap and tether
-rw-r--r-- | dev/jquery.jtable.build.txt | 2 | ||||
-rw-r--r-- | dev/jquery.jtable.core.js | 23 | ||||
-rw-r--r-- | jquery.jtable.js | 5037 | ||||
-rw-r--r-- | lib/extensions/jquery.jtable.record-actions.js | 147 |
4 files changed, 5203 insertions, 6 deletions
diff --git a/dev/jquery.jtable.build.txt b/dev/jquery.jtable.build.txt index d561e39..49f8ca7 100644 --- a/dev/jquery.jtable.build.txt +++ b/dev/jquery.jtable.build.txt @@ -10,4 +10,4 @@ add jquery.jtable.selecting.js add jquery.jtable.paging.js
add jquery.jtable.sorting.js
add jquery.jtable.dynamiccolumns.js
-add jquery.jtable.masterchild.js
+add jquery.jtable.masterchild.js
\ No newline at end of file diff --git a/dev/jquery.jtable.core.js b/dev/jquery.jtable.core.js index 105f419..84cb4a7 100644 --- a/dev/jquery.jtable.core.js +++ b/dev/jquery.jtable.core.js @@ -95,6 +95,8 @@ _cache: null, //General purpose cache dictionary (object) + _extraFieldTypes:[], + /************************************************************************ * CONSTRUCTOR AND INITIALIZATION METHODS * *************************************************************************/ @@ -160,6 +162,7 @@ this._columnList = []; this._fieldList = []; this._cache = []; + this._extraFieldTypes = []; }, /* Fills _fieldList, _columnList arrays and sets _keyField variable. @@ -711,7 +714,11 @@ return field.display({ record: record }); } - if (field.type == 'date') { + var extraFieldType = this._findItemByProperty(this._extraFieldTypes, 'type', field.type); + if(extraFieldType && extraFieldType.creator){ + return extraFieldType.creator(record, field); + } + else if (field.type == 'date') { return this._getDisplayTextForDateRecordField(field, fieldValue); } else if (field.type == 'checkbox') { return this._getCheckBoxTextForFieldByValue(fieldName, fieldValue); @@ -746,13 +753,19 @@ /* Finds an option object by given value. *************************************************************************/ _findOptionByValue: function (options, value) { - for (var i = 0; i < options.length; i++) { - if (options[i].Value == value) { - return options[i]; + return this._findItemByProperty(options, 'Value', value); + }, + + /* Finds an option object by given value. + *************************************************************************/ + _findItemByProperty: function (items, key, value) { + for (var i = 0; i < items.length; i++) { + if (items[i][key] == value) { + return items[i]; } } - return {}; //no option found + return {}; //no item found }, /* Gets text for a date field. diff --git a/jquery.jtable.js b/jquery.jtable.js new file mode 100644 index 0000000..b07a50f --- /dev/null +++ b/jquery.jtable.js @@ -0,0 +1,5037 @@ +/* + +jTable 2.4.0 +http://www.jtable.org + +--------------------------------------------------------------------------- + +Copyright (C) 2011-2014 by Halil İbrahim Kalkan (http://www.halilibrahimkalkan.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + +/************************************************************************ +* CORE jTable module * +*************************************************************************/ +(function ($) { + + var unloadingPage; + + $(window).on('beforeunload', function () { + unloadingPage = true; + }); + $(window).on('unload', function () { + unloadingPage = false; + }); + + $.widget("hik.jtable", { + + /************************************************************************ + * DEFAULT OPTIONS / EVENTS * + *************************************************************************/ + options: { + + //Options + actions: {}, + fields: {}, + animationsEnabled: true, + defaultDateFormat: 'yy-mm-dd', + dialogShowEffect: 'fade', + dialogHideEffect: 'fade', + showCloseButton: false, + loadingAnimationDelay: 500, + saveUserPreferences: true, + jqueryuiTheme: false, + unAuthorizedRequestRedirectUrl: null, + + ajaxSettings: { + type: 'POST', + dataType: 'json' + }, + + toolbar: { + hoverAnimation: true, + hoverAnimationDuration: 60, + hoverAnimationEasing: undefined, + items: [] + }, + + //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) + + _$titleDiv: null, //Reference to the title div (jQuery object) + _$toolbarDiv: null, //Reference to the toolbar div (jQuery object) + + _$table: null, //Reference to the main <table> (jQuery object) + _$tableBody: null, //Reference to <body> in the table (jQuery object) + _$tableRows: null, //Array of all <tr> in the table (except "no data" row) (jQuery object array) + + _$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) + + _extraFieldTypes:[], + + /************************************************************************ + * CONSTRUCTOR AND INITIALIZATION METHODS * + *************************************************************************/ + + /* Contructor. + *************************************************************************/ + _create: function () { + + //Initialization + this._normalizeFieldsOptions(); + this._initializeFields(); + this._createFieldAndColumnList(); + + //Creating DOM elements + this._createMainContainer(); + this._createTableTitle(); + this._createToolBar(); + this._createTable(); + this._createBusyPanel(); + this._createErrorDialogDiv(); + this._addNoDataRow(); + + this._cookieKeyPrefix = this._generateCookieKeyPrefix(); + }, + + /* 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) { + if (props.listClass == undefined) { + props.listClass = ''; + } + if (props.inputClass == undefined) { + props.inputClass = ''; + } + if (props.placeholder == undefined) { + props.placeholder = ''; + } + + //Convert dependsOn to array if it's a comma seperated lists + if (props.dependsOn && $.type(props.dependsOn) === 'string') { + var dependsOnArray = props.dependsOn.split(','); + props.dependsOn = []; + for (var i = 0; i < dependsOnArray.length; i++) { + props.dependsOn.push($.trim(dependsOnArray[i])); + } + } + }, + + /* Intializes some private variables. + *************************************************************************/ + _initializeFields: function () { + this._lastPostData = {}; + this._$tableRows = []; + this._columnList = []; + this._fieldList = []; + this._cache = []; + this._extraFieldTypes = []; + }, + + /* 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 = $('<div />') + .addClass('jtable-main-container') + .appendTo(this.element); + + this._jqueryuiThemeAddClass(this._$mainContainer, 'ui-widget'); + }, + + /* Creates title of the table if a title supplied in options. + *************************************************************************/ + _createTableTitle: function () { + var self = this; + + if (!self.options.title) { + return; + } + + var $titleDiv = $('<div />') + .addClass('jtable-title') + .appendTo(self._$mainContainer); + + self._jqueryuiThemeAddClass($titleDiv, 'ui-widget-header'); + + $('<div />') + .addClass('jtable-title-text') + .appendTo($titleDiv) + .append(self.options.title); + + if (self.options.showCloseButton) { + + var $textSpan = $('<span />') + .html(self.options.messages.close); + + $('<button></button>') + .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(); + }); + } + + self._$titleDiv = $titleDiv; + }, + + /* Creates the table. + *************************************************************************/ + _createTable: function () { + this._$table = $('<table></table>') + .addClass('jtable') + .appendTo(this._$mainContainer); + + if (this.options.tableId) { + this._$table.attr('id', this.options.tableId); + } + + this._jqueryuiThemeAddClass(this._$table, 'ui-widget-content'); + + this._createTableHead(); + this._createTableBody(); + }, + + /* Creates header (all column headers) of the table. + *************************************************************************/ + _createTableHead: function () { + var $thead = $('<thead></thead>') + .appendTo(this._$table); + + this._addRowToTableHead($thead); + }, + + /* Adds tr element to given thead element + *************************************************************************/ + _addRowToTableHead: function ($thead) { + var $tr = $('<tr></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 = $('<span />') + .addClass('jtable-column-header-text') + .html(field.title); + + var $headerContainerDiv = $('<div />') + .addClass('jtable-column-header-container') + .append($headerTextSpan); + + var $th = $('<th></th>') + .addClass('jtable-column-header') + .addClass(field.listClass) + .css('width', field.width) + .data('fieldName', fieldName) + .append($headerContainerDiv); + + this._jqueryuiThemeAddClass($th, 'ui-state-default'); + + return $th; + }, + + /* Creates an empty header cell that can be used as command column headers. + *************************************************************************/ + _createEmptyCommandHeader: function () { + var $th = $('<th></th>') + .addClass('jtable-command-column-header') + .css('width', '1%'); + + this._jqueryuiThemeAddClass($th, 'ui-state-default'); + + return $th; + }, + + /* Creates tbody tag and adds to the table. + *************************************************************************/ + _createTableBody: function () { + this._$tableBody = $('<tbody></tbody>').appendTo(this._$table); + }, + + /* Creates a div to block UI while jTable is busy. + *************************************************************************/ + _createBusyPanel: function () { + this._$busyMessageDiv = $('<div />').addClass('jtable-busy-message').prependTo(this._$mainContainer); + this._$busyDiv = $('<div />').addClass('jtable-busy-panel-background').prependTo(this._$mainContainer); + this._jqueryuiThemeAddClass(this._$busyMessageDiv, 'ui-widget-header'); + this._hideBusy(); + }, + + /* Creates and prepares error dialog div. + *************************************************************************/ + _createErrorDialogDiv: function () { + var self = this; + + self._$errorDialogDiv = $('<div></div>').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 * + *************************************************************************/ + + /* Used to change options dynamically after initialization. + *************************************************************************/ + _setOption: function (key, value) { + + }, + + /* LOADING RECORDS *****************************************************/ + + /* Performs an AJAX call to reload data of the table. + *************************************************************************/ + _reloadTable: function (completeCallback) { + var self = this; + + var completeReload = 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(); + } + }; + + self._showBusy(self.options.messages.loadingMessage, self.options.loadingAnimationDelay); //Disable table since it's busy + self._onLoadingRecords(); + + //listAction may be a function, check if it is + if ($.isFunction(self.options.actions.listAction)) { + + //Execute the function + var funcResult = self.options.actions.listAction(self._lastPostData, self._createJtParamsForLoading()); + + //Check if result is a jQuery Deferred object + if (self._isDeferredObject(funcResult)) { + funcResult.done(function(data) { + completeReload(data); + }).fail(function() { + self._showError(self.options.messages.serverCommunicationError); + }).always(function() { + self._hideBusy(); + }); + } else { //assume it's the data we're loading + completeReload(funcResult); + } + + } else { //assume listAction as URL string. + + //Generate URL (with query string parameters) to load records + var loadUrl = self._createRecordLoadUrl(); + + //Load data from server using AJAX + self._ajax({ + url: loadUrl, + data: self._lastPostData, + success: function (data) { + completeReload(data); + }, + error: function () { + self._hideBusy(); + self._showError(self.options.messages.serverCommunicationError); + } + }); + + } + }, + + /* Creates URL to load records. + *************************************************************************/ + _createRecordLoadUrl: function () { + return this.options.actions.listAction; + }, + + _createJtParamsForLoading: function() { + return { + //Empty as default, paging, sorting or other extensions can override this method to add additional params to load request + }; + }, + + /* TABLE MANIPULATION METHODS *******************************************/ + + /* Creates a row from given record + *************************************************************************/ + _createRowFromRecord: function (record) { + var $tr = $('<tr></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 $('<td></td>') + .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) { + var className = 'jtable-row-created'; + if (this.options.jqueryuiTheme) { + className = className + ' ui-state-highlight'; + } + + $tableRow.addClass(className, 'slow', '', function () { + $tableRow.removeClass(className, 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.addClass('jtable-row-removed').remove(); + + //remove from _$tableRows array + $rows.each(function () { + var index = self._findRowIndex($(this)); + if (index >= 0) { + self._$tableRows.splice(index, 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 () { + if (this._$tableBody.find('>tr.jtable-no-data-row').length > 0) { + return; + } + + var $tr = $('<tr></tr>') + .addClass('jtable-no-data-row') + .appendTo(this._$tableBody); + + var totalColumnCount = this._$table.find('thead th').length; + $('<td></td>') + .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 }); + } + + var extraFieldType = this._findItemByProperty(this._extraFieldTypes, 'type', field.type); + if(extraFieldType && extraFieldType.creator){ + return extraFieldType.creator(record, field); + } + else if (field.type == 'date') { + return this._getDisplayTextForDateRecordField(field, fieldValue); + } else if (field.type == 'checkbox') { + return this._getCheckBoxTextForFieldByValue(fieldName, fieldValue); + } else if (field.options) { //combobox or radio button list since there are options. + var options = this._getOptionsForField(fieldName, { + record: record, + value: fieldValue, + source: 'list', + dependedValues: this._createDependedValuesUsingRecord(record, field.dependsOn) + }); + return this._findOptionByValue(options, fieldValue).DisplayText; + } else { //other types + return fieldValue; + } + }, + + /* Creates and returns an object that's properties are depended values of a record. + *************************************************************************/ + _createDependedValuesUsingRecord: function (record, dependsOn) { + if (!dependsOn) { + return {}; + } + + var dependedValues = {}; + for (var i = 0; i < dependsOn.length; i++) { + dependedValues[dependsOn[i]] = record[dependsOn[i]]; + } + + return dependedValues; + }, + + /* Finds an option object by given value. + *************************************************************************/ + _findOptionByValue: function (options, value) { + return this._findItemByProperty(options, 'Value', value); + }, + + /* Finds an option object by given value. + *************************************************************************/ + _findItemByProperty: function (items, key, value) { + for (var i = 0; i < items.length; i++) { + if (items[i][key] == value) { + return items[i]; + } + } + + return {}; //no item found + }, + + /* 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); + }, + + /* Gets options for a field according to user preferences. + *************************************************************************/ + _getOptionsForField: function (fieldName, funcParams) { + var field = this.options.fields[fieldName]; + var optionsSource = field.options; + + if ($.isFunction(optionsSource)) { + //prepare parameter to the function + funcParams = $.extend(true, { + _cacheCleared: false, + dependedValues: {}, + clearCache: function () { + this._cacheCleared = true; + } + }, funcParams); + + //call function and get actual options source + optionsSource = optionsSource(funcParams); + } + + var options; + + //Build options according to it's source type + if (typeof optionsSource == 'string') { //It is an Url to download options + var cacheKey = 'options_' + fieldName + '_' + optionsSource; //create a unique cache key + if (funcParams._cacheCleared || (!this._cache[cacheKey])) { + //if user calls clearCache() or options are not found in the cache, download options + this._cache[cacheKey] = this._buildOptionsFromArray(this._downloadOptions(fieldName, optionsSource)); + this._sortFieldOptions(this._cache[cacheKey], field.optionsSorting); + } else { + //found on cache.. + //if this method (_getOptionsForField) is called to get option for a specific value (on funcParams.source == 'list') + //and this value is not in cached options, we need to re-download options to get the unfound (probably new) option. + if (funcParams.value != undefined) { + var optionForValue = this._findOptionByValue(this._cache[cacheKey], funcParams.value); + if (optionForValue.DisplayText == undefined) { //this value is not in cached options... + this._cache[cacheKey] = this._buildOptionsFromArray(this._downloadOptions(fieldName, optionsSource)); + this._sortFieldOptions(this._cache[cacheKey], field.optionsSorting); + } + } + } + + options = this._cache[cacheKey]; + } else if (jQuery.isArray(optionsSource)) { //It is an array of options + options = this._buildOptionsFromArray(optionsSource); + this._sortFieldOptions(options, field.optionsSorting); + } else { //It is an object that it's properties are options + options = this._buildOptionsArrayFromObject(optionsSource); + this._sortFieldOptions(options, field.optionsSorting); + } + + return options; + }, + + /* Download options for a field from server. + *************************************************************************/ + _downloadOptions: function (fieldName, url) { + var self = this; + var options = []; + + self._ajax({ + url: url, + async: false, + success: function (data) { + if (data.Result != 'OK') { + self._showError(data.Message); + return; + } + + options = data.Options; + }, + error: function () { + var errMessage = self._formatString(self.options.messages.cannotLoadOptionsFor, fieldName); + self._showError(errMessage); + } + }); + + return options; + }, + + /* Sorts given options according to sorting parameter. + * sorting can be: 'value', 'value-desc', 'text' or 'text-desc'. + *************************************************************************/ + _sortFieldOptions: function (options, sorting) { + + if ((!options) || (!options.length) || (!sorting)) { + return; + } + + //Determine using value of text + var dataSelector; + if (sorting.indexOf('value') == 0) { + dataSelector = function (option) { + return option.Value; + }; + } else { //assume as text + dataSelector = function (option) { + return option.DisplayText; + }; + } + + var compareFunc; + if ($.type(dataSelector(options[0])) == 'string') { + compareFunc = function (option1, option2) { + return dataSelector(option1).localeCompare(dataSelector(option2)); + }; + } else { //asuume as numeric + compareFunc = function (option1, option2) { + return dataSelector(option1) - dataSelector(option2); + }; + } + + if (sorting.indexOf('desc') > 0) { + options.sort(function (a, b) { + return compareFunc(b, a); + }); + } else { //assume as asc + options.sort(function (a, b) { + return compareFunc(a, b); + }); + } + }, + + /* Creates an array of options from given object. + *************************************************************************/ + _buildOptionsArrayFromObject: function (options) { + var list = []; + + $.each(options, function (propName, propValue) { + list.push({ + Value: propName, + DisplayText: propValue + }); + }); + + return list; + }, + + /* Creates array of options from giving options array. + *************************************************************************/ + _buildOptionsFromArray: function (optionsArray) { + var list = []; + + for (var i = 0; i < optionsArray.length; i++) { + if ($.isPlainObject(optionsArray[i])) { + list.push(optionsArray[i]); + } else { //assumed as primitive type (int, string...) + list.push({ + Value: optionsArray[i], + DisplayText: optionsArray[i] + }); + } + } + + return list; + }, + + /* 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), 10) + ); + } else if (dateString.length == 10) { //Format: 2011-01-01 + return new Date( + parseInt(dateString.substr(0, 4), 10), + parseInt(dateString.substr(5, 2), 10) - 1, + parseInt(dateString.substr(8, 2), 10) + ); + } else if (dateString.length == 19) { //Format: 2011-01-01 20:32:42 + return new Date( + parseInt(dateString.substr(0, 4), 10), + parseInt(dateString.substr(5, 2), 10) - 1, + parseInt(dateString.substr(8, 2), 10), + parseInt(dateString.substr(11, 2), 10), + parseInt(dateString.substr(14, 2), 10), + parseInt(dateString.substr(17, 2), 10) + ); + } else { + this._logWarn('Given date is not properly formatted: ' + dateString); + return 'format error!'; + } + }, + + /* TOOL BAR *************************************************************/ + + /* Creates the toolbar. + *************************************************************************/ + _createToolBar: function () { + this._$toolbarDiv = $('<div />') + .addClass('jtable-toolbar') + .appendTo(this._$titleDiv); + + for (var i = 0; i < this.options.toolbar.items.length; i++) { + this._addToolBarItem(this.options.toolbar.items[i]); + } + }, + + /* Adds a new item to the toolbar. + *************************************************************************/ + _addToolBarItem: function (item) { + + //Check if item is valid + if ((item == undefined) || (item.text == undefined && item.icon == undefined)) { + this._logWarn('Can not add tool bar item since it is not valid!'); + this._logWarn(item); + return null; + } + + var $toolBarItem = $('<span></span>') + .addClass('jtable-toolbar-item') + .appendTo(this._$toolbarDiv); + + this._jqueryuiThemeAddClass($toolBarItem, 'ui-widget ui-state-default ui-corner-all', 'ui-state-hover'); + + //cssClass property + if (item.cssClass) { + $toolBarItem + .addClass(item.cssClass); + } + + //tooltip property + if (item.tooltip) { + $toolBarItem + .attr('title', item.tooltip); + } + + //icon property + if (item.icon) { + var $icon = $('<span class="jtable-toolbar-item-icon"></span>').appendTo($toolBarItem); + if (item.icon === true) { + //do nothing + } else if ($.type(item.icon === 'string')) { + $icon.css('background', 'url("' + item.icon + '")'); + } + } + + //text property + if (item.text) { + $('<span class=""></span>') + .html(item.text) + .addClass('jtable-toolbar-item-text').appendTo($toolBarItem); + } + + //click event + if (item.click) { + $toolBarItem.click(function () { + item.click(); + }); + } + + //set hover animation parameters + var hoverAnimationDuration = undefined; + var hoverAnimationEasing = undefined; + if (this.options.toolbar.hoverAnimation) { + hoverAnimationDuration = this.options.toolbar.hoverAnimationDuration; + hoverAnimationEasing = this.options.toolbar.hoverAnimationEasing; + } + + //change class on hover + $toolBarItem.hover(function () { + $toolBarItem.addClass('jtable-toolbar-item-hover', hoverAnimationDuration, hoverAnimationEasing); + }, function () { + $toolBarItem.removeClass('jtable-toolbar-item-hover', hoverAnimationDuration, hoverAnimationEasing); + }); + + return $toolBarItem; + }, + + /* 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, + _showBusy: function (message, delay) { + var self = this; // + + //Show a transparent overlay to prevent clicking to the table + self._$busyDiv + .width(self._$mainContainer.width()) + .height(self._$mainContainer.height()) + .addClass('jtable-busy-panel-background-invisible') + .show(); + + var makeVisible = function () { + self._$busyDiv.removeClass('jtable-busy-panel-background-invisible'); + self._$busyMessageDiv.html(message).show(); + }; + + if (delay) { + if (self._setBusyTimer) { + return; + } + + self._setBusyTimer = setTimeout(makeVisible, delay); + } else { + makeVisible(); + } + }, + + /* Hides busy indicator and unblocks table UI. + *************************************************************************/ + _hideBusy: function () { + clearTimeout(this._setBusyTimer); + this._setBusyTimer = null; + this._$busyDiv.hide(); + this._$busyMessageDiv.html('').hide(); + }, + + /* Returns true if jTable is busy. + *************************************************************************/ + _isBusy: function () { + return this._$busyMessageDiv.is(':visible'); + }, + + /* Adds jQueryUI class to an item. + *************************************************************************/ + _jqueryuiThemeAddClass: function ($elm, className, hoverClassName) { + if (!this.options.jqueryuiTheme) { + return; + } + + $elm.addClass(className); + + if (hoverClassName) { + $elm.hover(function () { + $elm.addClass(hoverClassName); + }, function () { + $elm.removeClass(hoverClassName); + }); + } + }, + + /* 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 + }); + }, + + _unAuthorizedRequestHandler: function() { + if (this.options.unAuthorizedRequestRedirectUrl) { + location.href = this.options.unAuthorizedRequestRedirectUrl; + } else { + location.reload(true); + } + }, + + /* This method is used to perform AJAX calls in jTable instead of direct + * usage of jQuery.ajax method. + *************************************************************************/ + _ajax: function (options) { + var self = this; + + //Handlers for HTTP status codes + var opts = { + statusCode: { + 401: function () { //Unauthorized + self._unAuthorizedRequestHandler(); + } + } + }; + + opts = $.extend(opts, this.options.ajaxSettings, options); + + //Override success + opts.success = function (data) { + //Checking for Authorization error + if (data && data.UnAuthorizedRequest == true) { + self._unAuthorizedRequestHandler(); + } + + if (options.success) { + options.success(data); + } + }; + + //Override error + opts.error = function (jqXHR, textStatus, errorThrown) { + if (unloadingPage) { + jqXHR.abort(); + return; + } + + if (options.error) { + options.error(arguments); + } + }; + + //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]; + }, + + /************************************************************************ + * COOKIE * + *************************************************************************/ + + /* Sets a cookie with given key. + *************************************************************************/ + _setCookie: function (key, value) { + key = this._cookieKeyPrefix + key; + + var expireDate = new Date(); + expireDate.setDate(expireDate.getDate() + 30); + document.cookie = encodeURIComponent(key) + '=' + encodeURIComponent(value) + "; expires=" + expireDate.toUTCString(); + }, + + /* Gets a cookie with given key. + *************************************************************************/ + _getCookie: function (key) { + key = this._cookieKeyPrefix + key; + + var equalities = document.cookie.split('; '); + for (var i = 0; i < equalities.length; i++) { + if (!equalities[i]) { + continue; + } + + var splitted = equalities[i].split('='); + if (splitted.length != 2) { + continue; + } + + if (decodeURIComponent(splitted[0]) === key) { + return decodeURIComponent(splitted[1] || ''); + } + } + + return null; + }, + + /* Generates a hash key to be prefix for all cookies for this jtable instance. + *************************************************************************/ + _generateCookieKeyPrefix: function () { + + var simpleHash = function (value) { + var hash = 0; + if (value.length == 0) { + return hash; + } + + for (var i = 0; i < value.length; i++) { + var ch = value.charCodeAt(i); + hash = ((hash << 5) - hash) + ch; + hash = hash & hash; + } + + return hash; + }; + + var strToHash = ''; + if (this.options.tableId) { + strToHash = strToHash + this.options.tableId + '#'; + } + + strToHash = strToHash + this._columnList.join('$') + '#c' + this._$table.find('thead th').length; + return 'jtable#' + simpleHash(strToHash); + }, + + /************************************************************************ + * 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)); + + +/************************************************************************ +* Some UTULITY methods used by jTable * +*************************************************************************/ +(function ($) { + + $.extend(true, $.hik.jtable.prototype, { + + /* Gets property value of an object recursively. + *************************************************************************/ + _getPropertyOfObject: function (obj, propName) { + if (propName.indexOf('.') < 0) { + return obj[propName]; + } else { + var preDot = propName.substring(0, propName.indexOf('.')); + var postDot = propName.substring(propName.indexOf('.') + 1); + return this._getPropertyOfObject(obj[preDot], postDot); + } + }, + + /* Sets property value of an object recursively. + *************************************************************************/ + _setPropertyOfObject: function (obj, propName, value) { + if (propName.indexOf('.') < 0) { + obj[propName] = value; + } else { + var preDot = propName.substring(0, propName.indexOf('.')); + var postDot = propName.substring(propName.indexOf('.') + 1); + this._setPropertyOfObject(obj[preDot], postDot, value); + } + }, + + /* Inserts a value to an array if it does not exists in the array. + *************************************************************************/ + _insertToArrayIfDoesNotExists: function (array, value) { + if ($.inArray(value, array) < 0) { + array.push(value); + } + }, + + /* Finds index of an element in an array according to given comparision function + *************************************************************************/ + _findIndexInArray: function (value, array, compareFunc) { + + //If not defined, use default comparision + if (!compareFunc) { + compareFunc = function (a, b) { + return a == b; + }; + } + + for (var i = 0; i < array.length; i++) { + if (compareFunc(value, array[i])) { + return i; + } + } + + return -1; + }, + + /* Normalizes a number between given bounds or sets to a defaultValue + * if it is undefined + *************************************************************************/ + _normalizeNumber: function (number, min, max, defaultValue) { + if (number == undefined || number == null || isNaN(number)) { + return defaultValue; + } + + if (number < min) { + return min; + } + + if (number > max) { + return max; + } + + return number; + }, + + /* Formats a string just like string.format in c#. + * Example: + * _formatString('Hello {0}','Halil') = 'Hello Halil' + *************************************************************************/ + _formatString: function () { + if (arguments.length == 0) { + return null; + } + + var str = arguments[0]; + for (var i = 1; i < arguments.length; i++) { + var placeHolder = '{' + (i - 1) + '}'; + str = str.replace(placeHolder, arguments[i]); + } + + return str; + }, + + /* Checks if given object is a jQuery Deferred object. + */ + _isDeferredObject: function (obj) { + return obj.then && obj.done && obj.fail; + }, + + //Logging methods //////////////////////////////////////////////////////// + + _logDebug: function (text) { + if (!window.console) { + return; + } + + console.log('jTable DEBUG: ' + text); + }, + + _logInfo: function (text) { + if (!window.console) { + return; + } + + console.log('jTable INFO: ' + text); + }, + + _logWarn: function (text) { + if (!window.console) { + return; + } + + console.log('jTable WARNING: ' + text); + }, + + _logError: function (text) { + if (!window.console) { + return; + } + + console.log('jTable ERROR: ' + text); + } + + }); + + /* Fix for array.indexOf method in IE7. + * This code is taken from http://www.tutorialspoint.com/javascript/array_indexof.htm */ + if (!Array.prototype.indexOf) { + Array.prototype.indexOf = function (elt) { + var len = this.length; + var from = Number(arguments[1]) || 0; + from = (from < 0) + ? Math.ceil(from) + : Math.floor(from); + if (from < 0) + from += len; + for (; from < len; from++) { + if (from in this && + this[from] === elt) + return from; + } + return -1; + }; + } + +})(jQuery); + + +/************************************************************************ +* FORMS extension for jTable (base for edit/create forms) * +*************************************************************************/ +(function ($) { + + $.extend(true, $.hik.jtable.prototype, { + + /************************************************************************ + * PRIVATE METHODS * + *************************************************************************/ + + /* Submits a form asynchronously using AJAX. + * This method is needed, since form submitting logic can be overrided + * by extensions. + *************************************************************************/ + _submitFormUsingAjax: function (url, formData, success, error) { + this._ajax({ + url: url, + data: formData, + success: success, + error: error + }); + }, + + /* Creates label for an input element. + *************************************************************************/ + _createInputLabelForRecordField: function (fieldName) { + //TODO: May create label tag instead of a div. + return $('<div />') + .addClass('jtable-input-label') + .html(this.options.fields[fieldName].inputTitle || this.options.fields[fieldName].title); + }, + + /* Creates an input element according to field type. + *************************************************************************/ + _createInputForRecordField: function (funcParams) { + var fieldName = funcParams.fieldName, + value = funcParams.value, + record = funcParams.record, + formType = funcParams.formType, + form = funcParams.form; + + //Get the field + var field = this.options.fields[fieldName]; + + //If value if not supplied, use defaultValue of the field + if (value == undefined || value == null) { + value = field.defaultValue; + } + + //Use custom function if supplied + if (field.input) { + var $input = $(field.input({ + value: value, + record: record, + formType: formType, + form: form + })); + + //Add id attribute if does not exists + if (!$input.attr('id')) { + $input.attr('id', 'Edit-' + fieldName); + } + + //Wrap input element with div + return $('<div />') + .addClass('jtable-input jtable-custom-input') + .append($input); + } + + //Create input according to field type + if (field.type == 'date') { + return this._createDateInputForField(field, fieldName, value); + } else if (field.type == 'textarea') { + return this._createTextAreaForField(field, fieldName, value); + } else if (field.type == 'password') { + return this._createPasswordInputForField(field, fieldName, value); + } else if (field.type == 'checkbox') { + return this._createCheckboxForField(field, fieldName, value); + } else if (field.options) { + if (field.type == 'radiobutton') { + return this._createRadioButtonListForField(field, fieldName, value, record, formType); + } else { + return this._createDropDownListForField(field, fieldName, value, record, formType, form); + } + } else { + return this._createTextInputForField(field, fieldName, value); + } + }, + + //Creates a hidden input element with given name and value. + _createInputForHidden: function (fieldName, value) { + if (value == undefined) { + value = ""; + } + + return $('<input type="hidden" name="' + fieldName + '" id="Edit-' + fieldName + '"></input>') + .val(value); + }, + + /* Creates a date input for a field. + *************************************************************************/ + _createDateInputForField: function (field, fieldName, value) { + var $input = $('<input class="' + field.inputClass + '" id="Edit-' + fieldName + '" type="text" name="' + fieldName + '"></input>'); + if(value != undefined) { + $input.val(value); + } + + var displayFormat = field.displayFormat || this.options.defaultDateFormat; + $input.datepicker({ dateFormat: displayFormat }); + return $('<div />') + .addClass('jtable-input jtable-date-input') + .append($input); + }, + + /* Creates a textarea element for a field. + *************************************************************************/ + _createTextAreaForField: function (field, fieldName, value) { + var $textArea = $('<textarea class="' + field.inputClass + '" id="Edit-' + fieldName + '" name="' + fieldName + '"></textarea>'); + if (value != undefined) { + $textArea.val(value); + } + + return $('<div />') + .addClass('jtable-input jtable-textarea-input') + .append($textArea); + }, + + /* Creates a standart textbox for a field. + *************************************************************************/ + _createTextInputForField: function (field, fieldName, value) { + var $input = $('<input class="' + field.inputClass + '" placeholder="' + field.placeholder + '" id="Edit-' + fieldName + '" type="text" name="' + fieldName + '"></input>'); + if (value != undefined) { + $input.val(value); + } + + return $('<div />') + .addClass('jtable-input jtable-text-input') + .append($input); + }, + + /* Creates a password input for a field. + *************************************************************************/ + _createPasswordInputForField: function (field, fieldName, value) { + var $input = $('<input class="' + field.inputClass + '" placeholder="' + field.placeholder + '" id="Edit-' + fieldName + '" type="password" name="' + fieldName + '"></input>'); + if (value != undefined) { + $input.val(value); + } + + return $('<div />') + .addClass('jtable-input jtable-password-input') + .append($input); + }, + + /* Creates a checkboxfor a field. + *************************************************************************/ + _createCheckboxForField: function (field, fieldName, value) { + var self = this; + + //If value is undefined, get unchecked state's value + if (value == undefined) { + value = self._getCheckBoxPropertiesForFieldByState(fieldName, false).Value; + } + + //Create a container div + var $containerDiv = $('<div />') + .addClass('jtable-input jtable-checkbox-input'); + + //Create checkbox and check if needed + var $checkBox = $('<input class="' + field.inputClass + '" id="Edit-' + fieldName + '" type="checkbox" name="' + fieldName + '" />') + .appendTo($containerDiv); + if (value != undefined) { + $checkBox.val(value); + } + + //Create display text of checkbox for current state + var $textSpan = $('<span>' + (field.formText || self._getCheckBoxTextForFieldByValue(fieldName, value)) + '</span>') + .appendTo($containerDiv); + + //Check the checkbox if it's value is checked-value + if (self._getIsCheckBoxSelectedForFieldByValue(fieldName, value)) { + $checkBox.attr('checked', 'checked'); + } + + //This method sets checkbox's value and text according to state of the checkbox + var refreshCheckBoxValueAndText = function () { + var checkboxProps = self._getCheckBoxPropertiesForFieldByState(fieldName, $checkBox.is(':checked')); + $checkBox.attr('value', checkboxProps.Value); + $textSpan.html(field.formText || checkboxProps.DisplayText); + }; + + //Register to click event to change display text when state of checkbox is changed. + $checkBox.click(function () { + refreshCheckBoxValueAndText(); + }); + + //Change checkbox state when clicked to text + if (field.setOnTextClick != false) { + $textSpan + .addClass('jtable-option-text-clickable') + .click(function () { + if ($checkBox.is(':checked')) { + $checkBox.attr('checked', false); + } else { + $checkBox.attr('checked', true); + } + + refreshCheckBoxValueAndText(); + }); + } + + return $containerDiv; + }, + + /* Creates a drop down list (combobox) input element for a field. + *************************************************************************/ + _createDropDownListForField: function (field, fieldName, value, record, source, form) { + + //Create a container div + var $containerDiv = $('<div />') + .addClass('jtable-input jtable-dropdown-input'); + + //Create select element + var $select = $('<select class="' + field.inputClass + '" id="Edit-' + fieldName + '" name="' + fieldName + '"></select>') + .appendTo($containerDiv); + + //add options + var options = this._getOptionsForField(fieldName, { + record: record, + source: source, + form: form, + dependedValues: this._createDependedValuesUsingForm(form, field.dependsOn) + }); + + this._fillDropDownListWithOptions($select, options, value); + + return $containerDiv; + }, + + /* Fills a dropdown list with given options. + *************************************************************************/ + _fillDropDownListWithOptions: function ($select, options, value) { + $select.empty(); + for (var i = 0; i < options.length; i++) { + $('<option' + (options[i].Value == value ? ' selected="selected"' : '') + '>' + options[i].DisplayText + '</option>') + .val(options[i].Value) + .appendTo($select); + } + }, + + /* Creates depended values object from given form. + *************************************************************************/ + _createDependedValuesUsingForm: function ($form, dependsOn) { + if (!dependsOn) { + return {}; + } + + var dependedValues = {}; + + for (var i = 0; i < dependsOn.length; i++) { + var dependedField = dependsOn[i]; + + var $dependsOn = $form.find('select[name=' + dependedField + ']'); + if ($dependsOn.length <= 0) { + continue; + } + + dependedValues[dependedField] = $dependsOn.val(); + } + + + return dependedValues; + }, + + /* Creates a radio button list for a field. + *************************************************************************/ + _createRadioButtonListForField: function (field, fieldName, value, record, source) { + var $containerDiv = $('<div />') + .addClass('jtable-input jtable-radiobuttonlist-input'); + + var options = this._getOptionsForField(fieldName, { + record: record, + source: source + }); + + $.each(options, function(i, option) { + var $radioButtonDiv = $('<div class=""></div>') + .addClass('jtable-radio-input') + .appendTo($containerDiv); + + var $radioButton = $('<input type="radio" id="Edit-' + fieldName + '-' + i + '" class="' + field.inputClass + '" name="' + fieldName + '"' + ((option.Value == (value + '')) ? ' checked="true"' : '') + ' />') + .val(option.Value) + .appendTo($radioButtonDiv); + + var $textSpan = $('<span></span>') + .html(option.DisplayText) + .appendTo($radioButtonDiv); + + if (field.setOnTextClick != false) { + $textSpan + .addClass('jtable-option-text-clickable') + .click(function () { + if (!$radioButton.is(':checked')) { + $radioButton.attr('checked', true); + } + }); + } + }); + + return $containerDiv; + }, + + /* Gets display text for a checkbox field. + *************************************************************************/ + _getCheckBoxTextForFieldByValue: function (fieldName, value) { + return this.options.fields[fieldName].values[value]; + }, + + /* Returns true if given field's value must be checked state. + *************************************************************************/ + _getIsCheckBoxSelectedForFieldByValue: function (fieldName, value) { + return (this._createCheckBoxStateArrayForFieldWithCaching(fieldName)[1].Value.toString() == value.toString()); + }, + + /* Gets an object for a checkbox field that has Value and DisplayText + * properties. + *************************************************************************/ + _getCheckBoxPropertiesForFieldByState: function (fieldName, checked) { + return this._createCheckBoxStateArrayForFieldWithCaching(fieldName)[(checked ? 1 : 0)]; + }, + + /* Calls _createCheckBoxStateArrayForField with caching. + *************************************************************************/ + _createCheckBoxStateArrayForFieldWithCaching: function (fieldName) { + var cacheKey = 'checkbox_' + fieldName; + if (!this._cache[cacheKey]) { + + this._cache[cacheKey] = this._createCheckBoxStateArrayForField(fieldName); + } + + return this._cache[cacheKey]; + }, + + /* Creates a two element array of objects for states of a checkbox field. + * First element for unchecked state, second for checked state. + * Each object has two properties: Value and DisplayText + *************************************************************************/ + _createCheckBoxStateArrayForField: function (fieldName) { + var stateArray = []; + var currentIndex = 0; + $.each(this.options.fields[fieldName].values, function (propName, propValue) { + if (currentIndex++ < 2) { + stateArray.push({ 'Value': propName, 'DisplayText': propValue }); + } + }); + + return stateArray; + }, + + /* Searches a form for dependend dropdowns and makes them cascaded. + */ + _makeCascadeDropDowns: function ($form, record, source) { + var self = this; + + $form.find('select') //for each combobox + .each(function () { + var $thisDropdown = $(this); + + //get field name + var fieldName = $thisDropdown.attr('name'); + if (!fieldName) { + return; + } + + var field = self.options.fields[fieldName]; + + //check if this combobox depends on others + if (!field.dependsOn) { + return; + } + + //for each dependency + $.each(field.dependsOn, function (index, dependsOnField) { + //find the depended combobox + var $dependsOnDropdown = $form.find('select[name=' + dependsOnField + ']'); + //when depended combobox changes + $dependsOnDropdown.change(function () { + + //Refresh options + var funcParams = { + record: record, + source: source, + form: $form, + dependedValues: {} + }; + funcParams.dependedValues = self._createDependedValuesUsingForm($form, field.dependsOn); + var options = self._getOptionsForField(fieldName, funcParams); + + //Fill combobox with new options + self._fillDropDownListWithOptions($thisDropdown, options, undefined); + + //Thigger change event to refresh multi cascade dropdowns. + $thisDropdown.change(); + }); + }); + }); + }, + + /* Updates values of a record from given form + *************************************************************************/ + _updateRecordValuesFromForm: function (record, $form) { + for (var i = 0; i < this._fieldList.length; i++) { + var fieldName = this._fieldList[i]; + var field = this.options.fields[fieldName]; + + //Do not update non-editable fields + if (field.edit == false) { + continue; + } + + //Get field name and the input element of this field in the form + var $inputElement = $form.find('[name="' + fieldName + '"]'); + if ($inputElement.length <= 0) { + continue; + } + + //Update field in record according to it's type + if (field.type == 'date') { + var dateVal = $inputElement.val(); + if (dateVal) { + var displayFormat = field.displayFormat || this.options.defaultDateFormat; + try { + var date = $.datepicker.parseDate(displayFormat, dateVal); + record[fieldName] = '/Date(' + date.getTime() + ')/'; + } catch (e) { + //TODO: Handle incorrect/different date formats + this._logWarn('Date format is incorrect for field ' + fieldName + ': ' + dateVal); + record[fieldName] = undefined; + } + } else { + this._logDebug('Date is empty for ' + fieldName); + record[fieldName] = undefined; //TODO: undefined, null or empty string? + } + } else if (field.options && field.type == 'radiobutton') { + var $checkedElement = $inputElement.filter(':checked'); + if ($checkedElement.length) { + record[fieldName] = $checkedElement.val(); + } else { + record[fieldName] = undefined; + } + } else { + record[fieldName] = $inputElement.val(); + } + } + }, + + /* Sets enabled/disabled state of a dialog button. + *************************************************************************/ + _setEnabledOfDialogButton: function ($button, enabled, buttonText) { + if (!$button) { + return; + } + + if (enabled != false) { + $button + .removeAttr('disabled') + .removeClass('ui-state-disabled'); + } else { + $button + .attr('disabled', 'disabled') + .addClass('ui-state-disabled'); + } + + if (buttonText) { + $button + .find('span') + .text(buttonText); + } + } + + }); + +})(jQuery); + + +/************************************************************************ +* CREATE RECORD extension for jTable * +*************************************************************************/ +(function ($) { + + //Reference to base object members + var base = { + _create: $.hik.jtable.prototype._create + }; + + //extension members + $.extend(true, $.hik.jtable.prototype, { + + /************************************************************************ + * DEFAULT OPTIONS / EVENTS * + *************************************************************************/ + options: { + + //Events + recordAdded: function (event, data) { }, + + //Localization + messages: { + addNewRecord: 'Add new record' + } + }, + + /************************************************************************ + * PRIVATE FIELDS * + *************************************************************************/ + + _$addRecordDiv: null, //Reference to the adding new record dialog div (jQuery object) + + /************************************************************************ + * CONSTRUCTOR * + *************************************************************************/ + + /* Overrides base method to do create-specific constructions. + *************************************************************************/ + _create: function () { + base._create.apply(this, arguments); + + if (!this.options.actions.createAction) { + return; + } + + this._createAddRecordDialogDiv(); + }, + + /* Creates and prepares add new record dialog div + *************************************************************************/ + _createAddRecordDialogDiv: function () { + var self = this; + + //Create a div for dialog and add to container element + self._$addRecordDiv = $('<div />') + .appendTo(self._$mainContainer); + + //Prepare dialog + self._$addRecordDiv.dialog({ + autoOpen: false, + show: self.options.dialogShowEffect, + hide: self.options.dialogHideEffect, + width: 'auto', + minWidth: '300', + modal: true, + title: self.options.messages.addNewRecord, + buttons: + [{ //Cancel button + text: self.options.messages.cancel, + click: function () { + self._$addRecordDiv.dialog('close'); + } + }, { //Save button + id: 'AddRecordDialogSaveButton', + text: self.options.messages.save, + click: function () { + self._onSaveClickedOnCreateForm(); + } + }], + close: function () { + var $addRecordForm = self._$addRecordDiv.find('form').first(); + var $saveButton = self._$addRecordDiv.parent().find('#AddRecordDialogSaveButton'); + self._trigger("formClosed", null, { form: $addRecordForm, formType: 'create' }); + self._setEnabledOfDialogButton($saveButton, true, self.options.messages.save); + $addRecordForm.remove(); + } + }); + + if (self.options.addRecordButton) { + //If user supplied a button, bind the click event to show dialog form + self.options.addRecordButton.click(function (e) { + e.preventDefault(); + self._showAddRecordForm(); + }); + } else { + //If user did not supplied a button, create a 'add record button' toolbar item. + self._addToolBarItem({ + icon: true, + cssClass: 'jtable-toolbar-item-add-record', + text: self.options.messages.addNewRecord, + click: function () { + self._showAddRecordForm(); + } + }); + } + }, + + _onSaveClickedOnCreateForm: function () { + var self = this; + + var $saveButton = self._$addRecordDiv.parent().find('#AddRecordDialogSaveButton'); + var $addRecordForm = self._$addRecordDiv.find('form'); + + if (self._trigger("formSubmitting", null, { form: $addRecordForm, formType: 'create' }) != false) { + self._setEnabledOfDialogButton($saveButton, false, self.options.messages.saving); + self._saveAddRecordForm($addRecordForm, $saveButton); + } + }, + + /************************************************************************ + * PUBLIC METHODS * + *************************************************************************/ + + /* Shows add new record dialog form. + *************************************************************************/ + showCreateForm: function () { + this._showAddRecordForm(); + }, + + /* Adds a new record to the table (optionally to the server also) + *************************************************************************/ + addRecord: function (options) { + var self = this; + options = $.extend({ + clientOnly: false, + animationsEnabled: self.options.animationsEnabled, + success: function () { }, + error: function () { } + }, options); + + if (!options.record) { + self._logWarn('options parameter in addRecord method must contain a record property.'); + return; + } + + if (options.clientOnly) { + self._addRow( + self._createRowFromRecord(options.record), { + isNewRow: true, + animationsEnabled: options.animationsEnabled + }); + + options.success(); + return; + } + + var completeAddRecord = function (data) { + if (data.Result != 'OK') { + self._showError(data.Message); + options.error(data); + return; + } + + if (!data.Record) { + self._logError('Server must return the created Record object.'); + options.error(data); + return; + } + + self._onRecordAdded(data); + self._addRow( + self._createRowFromRecord(data.Record), { + isNewRow: true, + animationsEnabled: options.animationsEnabled + }); + + options.success(data); + }; + + //createAction may be a function, check if it is + if (!options.url && $.isFunction(self.options.actions.createAction)) { + + //Execute the function + var funcResult = self.options.actions.createAction($.param(options.record)); + + //Check if result is a jQuery Deferred object + if (self._isDeferredObject(funcResult)) { + //Wait promise + funcResult.done(function (data) { + completeAddRecord(data); + }).fail(function () { + self._showError(self.options.messages.serverCommunicationError); + options.error(); + }); + } else { //assume it returned the creation result + completeAddRecord(funcResult); + } + + } else { //Assume it's a URL string + + //Make an Ajax call to create record + self._submitFormUsingAjax( + options.url || self.options.actions.createAction, + $.param(options.record), + function (data) { + completeAddRecord(data); + }, + function () { + self._showError(self.options.messages.serverCommunicationError); + options.error(); + }); + + } + }, + + /************************************************************************ + * PRIVATE METHODS * + *************************************************************************/ + + /* Shows add new record dialog form. + *************************************************************************/ + _showAddRecordForm: function () { + var self = this; + + //Create add new record form + var $addRecordForm = $('<form id="jtable-create-form" class="jtable-dialog-form jtable-create-form"></form>'); + + //Create input elements + for (var i = 0; i < self._fieldList.length; i++) { + + var fieldName = self._fieldList[i]; + var field = self.options.fields[fieldName]; + + //Do not create input for fields that is key and not specially marked as creatable + if (field.key == true && field.create != true) { + continue; + } + + //Do not create input for fields that are not creatable + if (field.create == false) { + continue; + } + + if (field.type == 'hidden') { + $addRecordForm.append(self._createInputForHidden(fieldName, field.defaultValue)); + continue; + } + + //Create a container div for this input field and add to form + var $fieldContainer = $('<div />') + .addClass('jtable-input-field-container') + .appendTo($addRecordForm); + + //Create a label for input + $fieldContainer.append(self._createInputLabelForRecordField(fieldName)); + + //Create input element + $fieldContainer.append( + self._createInputForRecordField({ + fieldName: fieldName, + formType: 'create', + form: $addRecordForm + })); + } + + self._makeCascadeDropDowns($addRecordForm, undefined, 'create'); + + $addRecordForm.submit(function () { + self._onSaveClickedOnCreateForm(); + return false; + }); + + //Open the form + self._$addRecordDiv.append($addRecordForm).dialog('open'); + self._trigger("formCreated", null, { form: $addRecordForm, formType: 'create' }); + }, + + /* Saves new added record to the server and updates table. + *************************************************************************/ + _saveAddRecordForm: function ($addRecordForm, $saveButton) { + var self = this; + + var completeAddRecord = function (data) { + if (data.Result != 'OK') { + self._showError(data.Message); + self._setEnabledOfDialogButton($saveButton, true, self.options.messages.save); + return; + } + + if (!data.Record) { + self._logError('Server must return the created Record object.'); + self._setEnabledOfDialogButton($saveButton, true, self.options.messages.save); + return; + } + + self._onRecordAdded(data); + self._addRow( + self._createRowFromRecord(data.Record), { + isNewRow: true + }); + self._$addRecordDiv.dialog("close"); + }; + + $addRecordForm.data('submitting', true); //TODO: Why it's used, can remove? Check it. + + //createAction may be a function, check if it is + if ($.isFunction(self.options.actions.createAction)) { + + //Execute the function + var funcResult = self.options.actions.createAction($addRecordForm.serialize()); + + //Check if result is a jQuery Deferred object + if (self._isDeferredObject(funcResult)) { + //Wait promise + funcResult.done(function (data) { + completeAddRecord(data); + }).fail(function () { + self._showError(self.options.messages.serverCommunicationError); + self._setEnabledOfDialogButton($saveButton, true, self.options.messages.save); + }); + } else { //assume it returned the creation result + completeAddRecord(funcResult); + } + + } else { //Assume it's a URL string + + //Make an Ajax call to create record + self._submitFormUsingAjax( + self.options.actions.createAction, + $addRecordForm.serialize(), + function (data) { + completeAddRecord(data); + }, + function () { + self._showError(self.options.messages.serverCommunicationError); + self._setEnabledOfDialogButton($saveButton, true, self.options.messages.save); + }); + } + }, + + _onRecordAdded: function (data) { + this._trigger("recordAdded", null, { record: data.Record, serverResponse: data }); + } + + }); + +})(jQuery); + + +/************************************************************************ +* EDIT RECORD extension for jTable * +*************************************************************************/ +(function ($) { + + //Reference to base object members + var base = { + _create: $.hik.jtable.prototype._create, + _addColumnsToHeaderRow: $.hik.jtable.prototype._addColumnsToHeaderRow, + _addCellsToRowUsingRecord: $.hik.jtable.prototype._addCellsToRowUsingRecord + }; + + //extension members + $.extend(true, $.hik.jtable.prototype, { + + /************************************************************************ + * DEFAULT OPTIONS / EVENTS * + *************************************************************************/ + options: { + + //Events + recordUpdated: function (event, data) { }, + rowUpdated: function (event, data) { }, + + //Localization + messages: { + editRecord: 'Edit Record' + } + }, + + /************************************************************************ + * PRIVATE FIELDS * + *************************************************************************/ + + _$editDiv: null, //Reference to the editing dialog div (jQuery object) + _$editingRow: null, //Reference to currently editing row (jQuery object) + + /************************************************************************ + * CONSTRUCTOR AND INITIALIZATION METHODS * + *************************************************************************/ + + /* Overrides base method to do editing-specific constructions. + *************************************************************************/ + _create: function () { + base._create.apply(this, arguments); + + if (!this.options.actions.updateAction) { + return; + } + + this._createEditDialogDiv(); + }, + + /* Creates and prepares edit dialog div + *************************************************************************/ + _createEditDialogDiv: function () { + var self = this; + + //Create a div for dialog and add to container element + self._$editDiv = $('<div></div>') + .appendTo(self._$mainContainer); + + //Prepare dialog + self._$editDiv.dialog({ + autoOpen: false, + show: self.options.dialogShowEffect, + hide: self.options.dialogHideEffect, + width: 'auto', + minWidth: '300', + modal: true, + title: self.options.messages.editRecord, + buttons: + [{ //cancel button + text: self.options.messages.cancel, + click: function () { + self._$editDiv.dialog('close'); + } + }, { //save button + id: 'EditDialogSaveButton', + text: self.options.messages.save, + click: function () { + self._onSaveClickedOnEditForm(); + } + }], + close: function () { + var $editForm = self._$editDiv.find('form:first'); + var $saveButton = self._$editDiv.parent().find('#EditDialogSaveButton'); + self._trigger("formClosed", null, { form: $editForm, formType: 'edit', row: self._$editingRow }); + self._setEnabledOfDialogButton($saveButton, true, self.options.messages.save); + $editForm.remove(); + } + }); + }, + + /* Saves editing form to server. + *************************************************************************/ + _onSaveClickedOnEditForm: function () { + var self = this; + + //row maybe removed by another source, if so, do nothing + if (self._$editingRow.hasClass('jtable-row-removed')) { + self._$editDiv.dialog('close'); + return; + } + + var $saveButton = self._$editDiv.parent().find('#EditDialogSaveButton'); + var $editForm = self._$editDiv.find('form'); + if (self._trigger("formSubmitting", null, { form: $editForm, formType: 'edit', row: self._$editingRow }) != false) { + self._setEnabledOfDialogButton($saveButton, false, self.options.messages.saving); + self._saveEditForm($editForm, $saveButton); + } + }, + + /************************************************************************ + * PUBLIC METHODS * + *************************************************************************/ + + /* Updates a record on the table (optionally on the server also) + *************************************************************************/ + updateRecord: function (options) { + var self = this; + options = $.extend({ + clientOnly: false, + animationsEnabled: self.options.animationsEnabled, + success: function () { }, + error: function () { } + }, options); + + if (!options.record) { + self._logWarn('options parameter in updateRecord method must contain a record property.'); + return; + } + + var key = self._getKeyValueOfRecord(options.record); + if (key == undefined || key == null) { + self._logWarn('options parameter in updateRecord method must contain a record that contains the key field property.'); + return; + } + + var $updatingRow = self.getRowByKey(key); + if ($updatingRow == null) { + self._logWarn('Can not found any row by key "' + key + '" on the table. Updating row must be visible on the table.'); + return; + } + + if (options.clientOnly) { + $.extend($updatingRow.data('record'), options.record); + self._updateRowTexts($updatingRow); + self._onRecordUpdated($updatingRow, null); + if (options.animationsEnabled) { + self._showUpdateAnimationForRow($updatingRow); + } + + options.success(); + return; + } + + var completeEdit = function (data) { + if (data.Result != 'OK') { + self._showError(data.Message); + options.error(data); + return; + } + + $.extend($updatingRow.data('record'), options.record); + self._updateRecordValuesFromServerResponse($updatingRow.data('record'), data); + + self._updateRowTexts($updatingRow); + self._onRecordUpdated($updatingRow, data); + if (options.animationsEnabled) { + self._showUpdateAnimationForRow($updatingRow); + } + + options.success(data); + }; + + //updateAction may be a function, check if it is + if (!options.url && $.isFunction(self.options.actions.updateAction)) { + + //Execute the function + var funcResult = self.options.actions.updateAction($.param(options.record)); + + //Check if result is a jQuery Deferred object + if (self._isDeferredObject(funcResult)) { + //Wait promise + funcResult.done(function (data) { + completeEdit(data); + }).fail(function () { + self._showError(self.options.messages.serverCommunicationError); + options.error(); + }); + } else { //assume it returned the creation result + completeEdit(funcResult); + } + + } else { //Assume it's a URL string + + //Make an Ajax call to create record + self._submitFormUsingAjax( + options.url || self.options.actions.updateAction, + $.param(options.record), + function (data) { + completeEdit(data); + }, + function () { + self._showError(self.options.messages.serverCommunicationError); + options.error(); + }); + + } + }, + + /************************************************************************ + * OVERRIDED METHODS * + *************************************************************************/ + + /* Overrides base method to add a 'editing column cell' to header row. + *************************************************************************/ + _addColumnsToHeaderRow: function ($tr) { + base._addColumnsToHeaderRow.apply(this, arguments); + if (this.options.actions.updateAction != undefined) { + $tr.append(this._createEmptyCommandHeader()); + } + }, + + /* Overrides base method to add a 'edit command cell' to a row. + *************************************************************************/ + _addCellsToRowUsingRecord: function ($row) { + var self = this; + base._addCellsToRowUsingRecord.apply(this, arguments); + + if (self.options.actions.updateAction != undefined) { + var $span = $('<span></span>').html(self.options.messages.editRecord); + var $button = $('<button title="' + self.options.messages.editRecord + '"></button>') + .addClass('jtable-command-button jtable-edit-command-button') + .append($span) + .click(function (e) { + e.preventDefault(); + e.stopPropagation(); + self._showEditForm($row); + }); + $('<td></td>') + .addClass('jtable-command-column') + .append($button) + .appendTo($row); + } + }, + + /************************************************************************ + * PRIVATE METHODS * + *************************************************************************/ + + /* Shows edit form for a row. + *************************************************************************/ + _showEditForm: function ($tableRow) { + var self = this; + var record = $tableRow.data('record'); + + //Create edit form + var $editForm = $('<form id="jtable-edit-form" class="jtable-dialog-form jtable-edit-form"></form>'); + + //Create input fields + for (var i = 0; i < self._fieldList.length; i++) { + + var fieldName = self._fieldList[i]; + var field = self.options.fields[fieldName]; + var fieldValue = record[fieldName]; + + if (field.key == true) { + if (field.edit != true) { + //Create hidden field for key + $editForm.append(self._createInputForHidden(fieldName, fieldValue)); + continue; + } else { + //Create a special hidden field for key (since key is be editable) + $editForm.append(self._createInputForHidden('jtRecordKey', fieldValue)); + } + } + + //Do not create element for non-editable fields + if (field.edit == false) { + continue; + } + + //Hidden field + if (field.type == 'hidden') { + $editForm.append(self._createInputForHidden(fieldName, fieldValue)); + continue; + } + + //Create a container div for this input field and add to form + var $fieldContainer = $('<div class="jtable-input-field-container"></div>').appendTo($editForm); + + //Create a label for input + $fieldContainer.append(self._createInputLabelForRecordField(fieldName)); + + //Create input element with it's current value + var currentValue = self._getValueForRecordField(record, fieldName); + $fieldContainer.append( + self._createInputForRecordField({ + fieldName: fieldName, + value: currentValue, + record: record, + formType: 'edit', + form: $editForm + })); + } + + self._makeCascadeDropDowns($editForm, record, 'edit'); + + $editForm.submit(function () { + self._onSaveClickedOnEditForm(); + return false; + }); + + //Open dialog + self._$editingRow = $tableRow; + self._$editDiv.append($editForm).dialog('open'); + self._trigger("formCreated", null, { form: $editForm, formType: 'edit', record: record, row: $tableRow }); + }, + + /* Saves editing form to the server and updates the record on the table. + *************************************************************************/ + _saveEditForm: function ($editForm, $saveButton) { + var self = this; + + var completeEdit = function (data) { + if (data.Result != 'OK') { + self._showError(data.Message); + self._setEnabledOfDialogButton($saveButton, true, self.options.messages.save); + return; + } + + var record = self._$editingRow.data('record'); + + self._updateRecordValuesFromForm(record, $editForm); + self._updateRecordValuesFromServerResponse(record, data); + self._updateRowTexts(self._$editingRow); + + self._$editingRow.attr('data-record-key', self._getKeyValueOfRecord(record)); + + self._onRecordUpdated(self._$editingRow, data); + + if (self.options.animationsEnabled) { + self._showUpdateAnimationForRow(self._$editingRow); + } + + self._$editDiv.dialog("close"); + }; + + + //updateAction may be a function, check if it is + if ($.isFunction(self.options.actions.updateAction)) { + + //Execute the function + var funcResult = self.options.actions.updateAction($editForm.serialize()); + + //Check if result is a jQuery Deferred object + if (self._isDeferredObject(funcResult)) { + //Wait promise + funcResult.done(function (data) { + completeEdit(data); + }).fail(function () { + self._showError(self.options.messages.serverCommunicationError); + self._setEnabledOfDialogButton($saveButton, true, self.options.messages.save); + }); + } else { //assume it returned the creation result + completeEdit(funcResult); + } + + } else { //Assume it's a URL string + + //Make an Ajax call to update record + self._submitFormUsingAjax( + self.options.actions.updateAction, + $editForm.serialize(), + function(data) { + completeEdit(data); + }, + function() { + self._showError(self.options.messages.serverCommunicationError); + self._setEnabledOfDialogButton($saveButton, true, self.options.messages.save); + }); + } + + }, + + /* This method ensures updating of current record with server response, + * if server sends a Record object as response to updateAction. + *************************************************************************/ + _updateRecordValuesFromServerResponse: function (record, serverResponse) { + if (!serverResponse || !serverResponse.Record) { + return; + } + + $.extend(true, record, serverResponse.Record); + }, + + /* Gets text for a field of a record according to it's type. + *************************************************************************/ + _getValueForRecordField: function (record, fieldName) { + var field = this.options.fields[fieldName]; + var fieldValue = record[fieldName]; + if (field.type == 'date') { + return this._getDisplayTextForDateRecordField(field, fieldValue); + } else { + return fieldValue; + } + }, + + /* Updates cells of a table row's text values from row's record values. + *************************************************************************/ + _updateRowTexts: function ($tableRow) { + var record = $tableRow.data('record'); + var $columns = $tableRow.find('td'); + for (var i = 0; i < this._columnList.length; i++) { + var displayItem = this._getDisplayTextForRecordField(record, this._columnList[i]); + if ((displayItem === 0)) displayItem = "0"; + $columns.eq(this._firstDataColumnOffset + i).html(displayItem || ''); + } + + this._onRowUpdated($tableRow); + }, + + /* Shows 'updated' animation for a table row. + *************************************************************************/ + _showUpdateAnimationForRow: function ($tableRow) { + var className = 'jtable-row-updated'; + if (this.options.jqueryuiTheme) { + className = className + ' ui-state-highlight'; + } + + $tableRow.stop(true, true).addClass(className, 'slow', '', function () { + $tableRow.removeClass(className, 5000); + }); + }, + + /************************************************************************ + * EVENT RAISING METHODS * + *************************************************************************/ + + _onRowUpdated: function ($row) { + this._trigger("rowUpdated", null, { row: $row, record: $row.data('record') }); + }, + + _onRecordUpdated: function ($row, data) { + this._trigger("recordUpdated", null, { record: $row.data('record'), row: $row, serverResponse: data }); + } + + }); + +})(jQuery); + + +/************************************************************************ +* DELETION extension for jTable * +*************************************************************************/ +(function ($) { + + //Reference to base object members + var base = { + _create: $.hik.jtable.prototype._create, + _addColumnsToHeaderRow: $.hik.jtable.prototype._addColumnsToHeaderRow, + _addCellsToRowUsingRecord: $.hik.jtable.prototype._addCellsToRowUsingRecord + }; + + //extension members + $.extend(true, $.hik.jtable.prototype, { + + /************************************************************************ + * DEFAULT OPTIONS / EVENTS * + *************************************************************************/ + options: { + + //Options + deleteConfirmation: true, + + //Events + recordDeleted: function (event, data) { }, + + //Localization + messages: { + deleteConfirmation: 'This record will be deleted. Are you sure?', + deleteText: 'Delete', + deleting: 'Deleting', + canNotDeletedRecords: 'Can not delete {0} of {1} records!', + deleteProggress: 'Deleting {0} of {1} records, processing...' + } + }, + + /************************************************************************ + * PRIVATE FIELDS * + *************************************************************************/ + + _$deleteRecordDiv: null, //Reference to the adding new record dialog div (jQuery object) + _$deletingRow: null, //Reference to currently deleting row (jQuery object) + + /************************************************************************ + * CONSTRUCTOR * + *************************************************************************/ + + /* Overrides base method to do deletion-specific constructions. + *************************************************************************/ + _create: function () { + base._create.apply(this, arguments); + this._createDeleteDialogDiv(); + }, + + /* Creates and prepares delete record confirmation dialog div. + *************************************************************************/ + _createDeleteDialogDiv: function () { + var self = this; + + //Check if deleteAction is supplied + if (!self.options.actions.deleteAction) { + return; + } + + //Create div element for delete confirmation dialog + self._$deleteRecordDiv = $('<div><p><span class="ui-icon ui-icon-alert" style="float:left; margin:0 7px 20px 0;"></span><span class="jtable-delete-confirm-message"></span></p></div>').appendTo(self._$mainContainer); + + //Prepare dialog + self._$deleteRecordDiv.dialog({ + autoOpen: false, + show: self.options.dialogShowEffect, + hide: self.options.dialogHideEffect, + modal: true, + title: self.options.messages.areYouSure, + buttons: + [{ //cancel button + text: self.options.messages.cancel, + click: function () { + self._$deleteRecordDiv.dialog("close"); + } + }, {//delete button + id: 'DeleteDialogButton', + text: self.options.messages.deleteText, + click: function () { + + //row maybe removed by another source, if so, do nothing + if (self._$deletingRow.hasClass('jtable-row-removed')) { + self._$deleteRecordDiv.dialog('close'); + return; + } + + var $deleteButton = self._$deleteRecordDiv.parent().find('#DeleteDialogButton'); + self._setEnabledOfDialogButton($deleteButton, false, self.options.messages.deleting); + self._deleteRecordFromServer( + self._$deletingRow, + function () { + self._removeRowsFromTableWithAnimation(self._$deletingRow); + self._$deleteRecordDiv.dialog('close'); + }, + function (message) { //error + self._showError(message); + self._setEnabledOfDialogButton($deleteButton, true, self.options.messages.deleteText); + } + ); + } + }], + close: function () { + var $deleteButton = self._$deleteRecordDiv.parent().find('#DeleteDialogButton'); + self._setEnabledOfDialogButton($deleteButton, true, self.options.messages.deleteText); + } + }); + }, + + /************************************************************************ + * PUBLIC METHODS * + *************************************************************************/ + + /* This method is used to delete one or more rows from server and the table. + *************************************************************************/ + deleteRows: function ($rows) { + var self = this; + + if ($rows.length <= 0) { + self._logWarn('No rows specified to jTable deleteRows method.'); + return; + } + + if (self._isBusy()) { + self._logWarn('Can not delete rows since jTable is busy!'); + return; + } + + //Deleting just one row + if ($rows.length == 1) { + self._deleteRecordFromServer( + $rows, + function () { //success + self._removeRowsFromTableWithAnimation($rows); + }, + function (message) { //error + self._showError(message); + } + ); + + return; + } + + //Deleting multiple rows + self._showBusy(self._formatString(self.options.messages.deleteProggress, 0, $rows.length)); + + //This method checks if deleting of all records is completed + var completedCount = 0; + var isCompleted = function () { + return (completedCount >= $rows.length); + }; + + //This method is called when deleting of all records completed + var completed = function () { + var $deletedRows = $rows.filter('.jtable-row-ready-to-remove'); + if ($deletedRows.length < $rows.length) { + self._showError(self._formatString(self.options.messages.canNotDeletedRecords, $rows.length - $deletedRows.length, $rows.length)); + } + + if ($deletedRows.length > 0) { + self._removeRowsFromTableWithAnimation($deletedRows); + } + + self._hideBusy(); + }; + + //Delete all rows + var deletedCount = 0; + $rows.each(function () { + var $row = $(this); + self._deleteRecordFromServer( + $row, + function () { //success + ++deletedCount; ++completedCount; + $row.addClass('jtable-row-ready-to-remove'); + self._showBusy(self._formatString(self.options.messages.deleteProggress, deletedCount, $rows.length)); + if (isCompleted()) { + completed(); + } + }, + function () { //error + ++completedCount; + if (isCompleted()) { + completed(); + } + } + ); + }); + }, + + /* Deletes a record from the table (optionally from the server also). + *************************************************************************/ + deleteRecord: function (options) { + var self = this; + options = $.extend({ + clientOnly: false, + animationsEnabled: self.options.animationsEnabled, + url: self.options.actions.deleteAction, + success: function () { }, + error: function () { } + }, options); + + if (options.key == undefined) { + self._logWarn('options parameter in deleteRecord method must contain a key property.'); + return; + } + + var $deletingRow = self.getRowByKey(options.key); + if ($deletingRow == null) { + self._logWarn('Can not found any row by key: ' + options.key); + return; + } + + if (options.clientOnly) { + self._removeRowsFromTableWithAnimation($deletingRow, options.animationsEnabled); + options.success(); + return; + } + + self._deleteRecordFromServer( + $deletingRow, + function (data) { //success + self._removeRowsFromTableWithAnimation($deletingRow, options.animationsEnabled); + options.success(data); + }, + function (message) { //error + self._showError(message); + options.error(message); + }, + options.url + ); + }, + + /************************************************************************ + * OVERRIDED METHODS * + *************************************************************************/ + + /* Overrides base method to add a 'deletion column cell' to header row. + *************************************************************************/ + _addColumnsToHeaderRow: function ($tr) { + base._addColumnsToHeaderRow.apply(this, arguments); + if (this.options.actions.deleteAction != undefined) { + $tr.append(this._createEmptyCommandHeader()); + } + }, + + /* Overrides base method to add a 'delete command cell' to a row. + *************************************************************************/ + _addCellsToRowUsingRecord: function ($row) { + base._addCellsToRowUsingRecord.apply(this, arguments); + + var self = this; + if (self.options.actions.deleteAction != undefined) { + var $span = $('<span></span>').html(self.options.messages.deleteText); + var $button = $('<button title="' + self.options.messages.deleteText + '"></button>') + .addClass('jtable-command-button jtable-delete-command-button') + .append($span) + .click(function (e) { + e.preventDefault(); + e.stopPropagation(); + self._deleteButtonClickedForRow($row); + }); + $('<td></td>') + .addClass('jtable-command-column') + .append($button) + .appendTo($row); + } + }, + + /************************************************************************ + * PRIVATE METHODS * + *************************************************************************/ + + /* This method is called when user clicks delete button on a row. + *************************************************************************/ + _deleteButtonClickedForRow: function ($row) { + var self = this; + + var deleteConfirm; + var deleteConfirmMessage = self.options.messages.deleteConfirmation; + + //If options.deleteConfirmation is function then call it + if ($.isFunction(self.options.deleteConfirmation)) { + var data = { row: $row, record: $row.data('record'), deleteConfirm: true, deleteConfirmMessage: deleteConfirmMessage, cancel: false, cancelMessage: null }; + self.options.deleteConfirmation(data); + + //If delete progress is cancelled + if (data.cancel) { + + //If a canlellation reason is specified + if (data.cancelMessage) { + self._showError(data.cancelMessage); //TODO: show warning/stop message instead of error (also show warning/error ui icon)! + } + + return; + } + + deleteConfirmMessage = data.deleteConfirmMessage; + deleteConfirm = data.deleteConfirm; + } else { + deleteConfirm = self.options.deleteConfirmation; + } + + if (deleteConfirm != false) { + //Confirmation + self._$deleteRecordDiv.find('.jtable-delete-confirm-message').html(deleteConfirmMessage); + self._showDeleteDialog($row); + } else { + //No confirmation + self._deleteRecordFromServer( + $row, + function () { //success + self._removeRowsFromTableWithAnimation($row); + }, + function (message) { //error + self._showError(message); + } + ); + } + }, + + /* Shows delete comfirmation dialog. + *************************************************************************/ + _showDeleteDialog: function ($row) { + this._$deletingRow = $row; + this._$deleteRecordDiv.dialog('open'); + }, + + /* Performs an ajax call to server to delete record + * and removes row of the record from table if ajax call success. + *************************************************************************/ + _deleteRecordFromServer: function ($row, success, error, url) { + var self = this; + + var completeDelete = function(data) { + if (data.Result != 'OK') { + $row.data('deleting', false); + if (error) { + error(data.Message); + } + + return; + } + + self._trigger("recordDeleted", null, { record: $row.data('record'), row: $row, serverResponse: data }); + + if (success) { + success(data); + } + }; + + //Check if it is already being deleted right now + if ($row.data('deleting') == true) { + return; + } + + $row.data('deleting', true); + + var postData = {}; + postData[self._keyField] = self._getKeyValueOfRecord($row.data('record')); + + //deleteAction may be a function, check if it is + if (!url && $.isFunction(self.options.actions.deleteAction)) { + + //Execute the function + var funcResult = self.options.actions.deleteAction(postData); + + //Check if result is a jQuery Deferred object + if (self._isDeferredObject(funcResult)) { + //Wait promise + funcResult.done(function (data) { + completeDelete(data); + }).fail(function () { + $row.data('deleting', false); + if (error) { + error(self.options.messages.serverCommunicationError); + } + }); + } else { //assume it returned the deletion result + completeDelete(funcResult); + } + + } else { //Assume it's a URL string + //Make ajax call to delete the record from server + this._ajax({ + url: (url || self.options.actions.deleteAction), + data: postData, + success: function (data) { + completeDelete(data); + }, + error: function () { + $row.data('deleting', false); + if (error) { + error(self.options.messages.serverCommunicationError); + } + } + }); + + } + }, + + /* Removes a row from table after a 'deleting' animation. + *************************************************************************/ + _removeRowsFromTableWithAnimation: function ($rows, animationsEnabled) { + var self = this; + + if (animationsEnabled == undefined) { + animationsEnabled = self.options.animationsEnabled; + } + + if (animationsEnabled) { + var className = 'jtable-row-deleting'; + if (this.options.jqueryuiTheme) { + className = className + ' ui-state-disabled'; + } + + //Stop current animation (if does exists) and begin 'deleting' animation. + $rows.stop(true, true).addClass(className, 'slow', '').promise().done(function () { + self._removeRowsFromTable($rows, 'deleted'); + }); + } else { + self._removeRowsFromTable($rows, 'deleted'); + } + } + + }); + +})(jQuery); + + +/************************************************************************ +* SELECTING extension for jTable * +*************************************************************************/ +(function ($) { + + //Reference to base object members + var base = { + _create: $.hik.jtable.prototype._create, + _addColumnsToHeaderRow: $.hik.jtable.prototype._addColumnsToHeaderRow, + _addCellsToRowUsingRecord: $.hik.jtable.prototype._addCellsToRowUsingRecord, + _onLoadingRecords: $.hik.jtable.prototype._onLoadingRecords, + _onRecordsLoaded: $.hik.jtable.prototype._onRecordsLoaded, + _onRowsRemoved: $.hik.jtable.prototype._onRowsRemoved + }; + + //extension members + $.extend(true, $.hik.jtable.prototype, { + + /************************************************************************ + * DEFAULT OPTIONS / EVENTS * + *************************************************************************/ + options: { + + //Options + selecting: false, + multiselect: false, + selectingCheckboxes: false, + selectOnRowClick: true, + + //Events + selectionChanged: function (event, data) { } + }, + + /************************************************************************ + * PRIVATE FIELDS * + *************************************************************************/ + + _selectedRecordIdsBeforeLoad: null, //This array is used to store selected row Id's to restore them after a page refresh (string array). + _$selectAllCheckbox: null, //Reference to the 'select/deselect all' checkbox (jQuery object) + _shiftKeyDown: false, //True, if shift key is currently down. + + /************************************************************************ + * CONSTRUCTOR * + *************************************************************************/ + + /* Overrides base method to do selecting-specific constructions. + *************************************************************************/ + _create: function () { + if (this.options.selecting && this.options.selectingCheckboxes) { + ++this._firstDataColumnOffset; + this._bindKeyboardEvents(); + } + + //Call base method + base._create.apply(this, arguments); + }, + + /* Registers to keyboard events those are needed for selection + *************************************************************************/ + _bindKeyboardEvents: function () { + var self = this; + //Register to events to set _shiftKeyDown value + $(document) + .keydown(function (event) { + switch (event.which) { + case 16: + self._shiftKeyDown = true; + break; + } + }) + .keyup(function (event) { + switch (event.which) { + case 16: + self._shiftKeyDown = false; + break; + } + }); + }, + + /************************************************************************ + * PUBLIC METHODS * + *************************************************************************/ + + /* Gets jQuery selection for currently selected rows. + *************************************************************************/ + selectedRows: function () { + return this._getSelectedRows(); + }, + + /* Makes row/rows 'selected'. + *************************************************************************/ + selectRows: function ($rows) { + this._selectRows($rows); + this._onSelectionChanged(); //TODO: trigger only if selected rows changes? + }, + + /************************************************************************ + * OVERRIDED METHODS * + *************************************************************************/ + + /* Overrides base method to add a 'select column' to header row. + *************************************************************************/ + _addColumnsToHeaderRow: function ($tr) { + if (this.options.selecting && this.options.selectingCheckboxes) { + if (this.options.multiselect) { + $tr.append(this._createSelectAllHeader()); + } else { + $tr.append(this._createEmptyCommandHeader()); + } + } + + base._addColumnsToHeaderRow.apply(this, arguments); + }, + + /* Overrides base method to add a 'delete command cell' to a row. + *************************************************************************/ + _addCellsToRowUsingRecord: function ($row) { + if (this.options.selecting) { + this._makeRowSelectable($row); + } + + base._addCellsToRowUsingRecord.apply(this, arguments); + }, + + /* Overrides base event to store selection list + *************************************************************************/ + _onLoadingRecords: function () { + if (this.options.selecting) { + this._storeSelectionList(); + } + + base._onLoadingRecords.apply(this, arguments); + }, + + /* Overrides base event to restore selection list + *************************************************************************/ + _onRecordsLoaded: function () { + if (this.options.selecting) { + this._restoreSelectionList(); + } + + base._onRecordsLoaded.apply(this, arguments); + }, + + /* Overrides base event to check is any selected row is being removed. + *************************************************************************/ + _onRowsRemoved: function ($rows, reason) { + if (this.options.selecting && (reason != 'reloading') && ($rows.filter('.jtable-row-selected').length > 0)) { + this._onSelectionChanged(); + } + + base._onRowsRemoved.apply(this, arguments); + }, + + /************************************************************************ + * PRIVATE METHODS * + *************************************************************************/ + + /* Creates a header column to select/deselect all rows. + *************************************************************************/ + _createSelectAllHeader: function () { + var self = this; + + var $columnHeader = $('<th class=""></th>') + .addClass('jtable-command-column-header jtable-column-header-selecting'); + this._jqueryuiThemeAddClass($columnHeader, 'ui-state-default'); + + var $headerContainer = $('<div />') + .addClass('jtable-column-header-container') + .appendTo($columnHeader); + + self._$selectAllCheckbox = $('<input type="checkbox" />') + .appendTo($headerContainer) + .click(function () { + if (self._$tableRows.length <= 0) { + self._$selectAllCheckbox.attr('checked', false); + return; + } + + var allRows = self._$tableBody.find('>tr.jtable-data-row'); + if (self._$selectAllCheckbox.is(':checked')) { + self._selectRows(allRows); + } else { + self._deselectRows(allRows); + } + + self._onSelectionChanged(); + }); + + return $columnHeader; + }, + + /* Stores Id's of currently selected records to _selectedRecordIdsBeforeLoad. + *************************************************************************/ + _storeSelectionList: function () { + var self = this; + + if (!self.options.selecting) { + return; + } + + self._selectedRecordIdsBeforeLoad = []; + self._getSelectedRows().each(function () { + self._selectedRecordIdsBeforeLoad.push(self._getKeyValueOfRecord($(this).data('record'))); + }); + }, + + /* Selects rows whose Id is in _selectedRecordIdsBeforeLoad; + *************************************************************************/ + _restoreSelectionList: function () { + var self = this; + + if (!self.options.selecting) { + return; + } + + var selectedRowCount = 0; + for (var i = 0; i < self._$tableRows.length; ++i) { + var recordId = self._getKeyValueOfRecord(self._$tableRows[i].data('record')); + if ($.inArray(recordId, self._selectedRecordIdsBeforeLoad) > -1) { + self._selectRows(self._$tableRows[i]); + ++selectedRowCount; + } + } + + if (self._selectedRecordIdsBeforeLoad.length > 0 && self._selectedRecordIdsBeforeLoad.length != selectedRowCount) { + self._onSelectionChanged(); + } + + self._selectedRecordIdsBeforeLoad = []; + self._refreshSelectAllCheckboxState(); + }, + + /* Gets all selected rows. + *************************************************************************/ + _getSelectedRows: function () { + return this._$tableBody + .find('>tr.jtable-row-selected'); + }, + + /* Adds selectable feature to a row. + *************************************************************************/ + _makeRowSelectable: function ($row) { + var self = this; + + //Select/deselect on row click + if (self.options.selectOnRowClick) { + $row.click(function () { + self._invertRowSelection($row); + }); + } + + //'select/deselect' checkbox column + if (self.options.selectingCheckboxes) { + var $cell = $('<td></td>').addClass('jtable-selecting-column'); + var $selectCheckbox = $('<input type="checkbox" />').appendTo($cell); + if (!self.options.selectOnRowClick) { + $selectCheckbox.click(function () { + self._invertRowSelection($row); + }); + } + + $row.append($cell); + } + }, + + /* Inverts selection state of a single row. + *************************************************************************/ + _invertRowSelection: function ($row) { + if ($row.hasClass('jtable-row-selected')) { + this._deselectRows($row); + } else { + //Shift key? + if (this._shiftKeyDown) { + var rowIndex = this._findRowIndex($row); + //try to select row and above rows until first selected row + var beforeIndex = this._findFirstSelectedRowIndexBeforeIndex(rowIndex) + 1; + if (beforeIndex > 0 && beforeIndex < rowIndex) { + this._selectRows(this._$tableBody.find('tr').slice(beforeIndex, rowIndex + 1)); + } else { + //try to select row and below rows until first selected row + var afterIndex = this._findFirstSelectedRowIndexAfterIndex(rowIndex) - 1; + if (afterIndex > rowIndex) { + this._selectRows(this._$tableBody.find('tr').slice(rowIndex, afterIndex + 1)); + } else { + //just select this row + this._selectRows($row); + } + } + } else { + this._selectRows($row); + } + } + + this._onSelectionChanged(); + }, + + /* Search for a selected row (that is before given row index) to up and returns it's index + *************************************************************************/ + _findFirstSelectedRowIndexBeforeIndex: function (rowIndex) { + for (var i = rowIndex - 1; i >= 0; --i) { + if (this._$tableRows[i].hasClass('jtable-row-selected')) { + return i; + } + } + + return -1; + }, + + /* Search for a selected row (that is after given row index) to down and returns it's index + *************************************************************************/ + _findFirstSelectedRowIndexAfterIndex: function (rowIndex) { + for (var i = rowIndex + 1; i < this._$tableRows.length; ++i) { + if (this._$tableRows[i].hasClass('jtable-row-selected')) { + return i; + } + } + + return -1; + }, + + /* Makes row/rows 'selected'. + *************************************************************************/ + _selectRows: function ($rows) { + if (!this.options.multiselect) { + this._deselectRows(this._getSelectedRows()); + } + + $rows.addClass('jtable-row-selected'); + this._jqueryuiThemeAddClass($rows, 'ui-state-highlight'); + + if (this.options.selectingCheckboxes) { + $rows.find('>td.jtable-selecting-column >input').prop('checked', true); + } + + this._refreshSelectAllCheckboxState(); + }, + + /* Makes row/rows 'non selected'. + *************************************************************************/ + _deselectRows: function ($rows) { + $rows.removeClass('jtable-row-selected ui-state-highlight'); + if (this.options.selectingCheckboxes) { + $rows.find('>td.jtable-selecting-column >input').prop('checked', false); + } + + this._refreshSelectAllCheckboxState(); + }, + + /* Updates state of the 'select/deselect' all checkbox according to count of selected rows. + *************************************************************************/ + _refreshSelectAllCheckboxState: function () { + if (!this.options.selectingCheckboxes || !this.options.multiselect) { + return; + } + + var totalRowCount = this._$tableRows.length; + var selectedRowCount = this._getSelectedRows().length; + + if (selectedRowCount == 0) { + this._$selectAllCheckbox.prop('indeterminate', false); + this._$selectAllCheckbox.attr('checked', false); + } else if (selectedRowCount == totalRowCount) { + this._$selectAllCheckbox.prop('indeterminate', false); + this._$selectAllCheckbox.attr('checked', true); + } else { + this._$selectAllCheckbox.attr('checked', false); + this._$selectAllCheckbox.prop('indeterminate', true); + } + }, + + /************************************************************************ + * EVENT RAISING METHODS * + *************************************************************************/ + + _onSelectionChanged: function () { + this._trigger("selectionChanged", null, {}); + } + + }); + +})(jQuery); + + +/************************************************************************ +* PAGING extension for jTable * +*************************************************************************/ +(function ($) { + + //Reference to base object members + var base = { + load: $.hik.jtable.prototype.load, + _create: $.hik.jtable.prototype._create, + _setOption: $.hik.jtable.prototype._setOption, + _createRecordLoadUrl: $.hik.jtable.prototype._createRecordLoadUrl, + _createJtParamsForLoading: $.hik.jtable.prototype._createJtParamsForLoading, + _addRowToTable: $.hik.jtable.prototype._addRowToTable, + _addRow: $.hik.jtable.prototype._addRow, + _removeRowsFromTable: $.hik.jtable.prototype._removeRowsFromTable, + _onRecordsLoaded: $.hik.jtable.prototype._onRecordsLoaded + }; + + //extension members + $.extend(true, $.hik.jtable.prototype, { + + /************************************************************************ + * DEFAULT OPTIONS / EVENTS * + *************************************************************************/ + options: { + paging: false, + pageList: 'normal', //possible values: 'minimal', 'normal' + pageSize: 10, + pageSizes: [10, 25, 50, 100, 250, 500], + pageSizeChangeArea: true, + gotoPageArea: 'combobox', //possible values: 'textbox', 'combobox', 'none' + + messages: { + pagingInfo: 'Showing {0}-{1} of {2}', + pageSizeChangeLabel: 'Row count', + gotoPageLabel: 'Go to page' + } + }, + + /************************************************************************ + * PRIVATE FIELDS * + *************************************************************************/ + + _$bottomPanel: null, //Reference to the panel at the bottom of the table (jQuery object) + _$pagingListArea: null, //Reference to the page list area in to bottom panel (jQuery object) + _$pageSizeChangeArea: null, //Reference to the page size change area in to bottom panel (jQuery object) + _$pageInfoSpan: null, //Reference to the paging info area in to bottom panel (jQuery object) + _$gotoPageArea: null, //Reference to 'Go to page' input area in to bottom panel (jQuery object) + _$gotoPageInput: null, //Reference to 'Go to page' input in to bottom panel (jQuery object) + _totalRecordCount: 0, //Total count of records on all pages + _currentPageNo: 1, //Current page number + + /************************************************************************ + * CONSTRUCTOR AND INITIALIZING METHODS * + *************************************************************************/ + + /* Overrides base method to do paging-specific constructions. + *************************************************************************/ + _create: function() { + base._create.apply(this, arguments); + if (this.options.paging) { + this._loadPagingSettings(); + this._createBottomPanel(); + this._createPageListArea(); + this._createGotoPageInput(); + this._createPageSizeSelection(); + } + }, + + /* Loads user preferences for paging. + *************************************************************************/ + _loadPagingSettings: function() { + if (!this.options.saveUserPreferences) { + return; + } + + var pageSize = this._getCookie('page-size'); + if (pageSize) { + this.options.pageSize = this._normalizeNumber(pageSize, 1, 1000000, this.options.pageSize); + } + }, + + /* Creates bottom panel and adds to the page. + *************************************************************************/ + _createBottomPanel: function() { + this._$bottomPanel = $('<div />') + .addClass('jtable-bottom-panel') + .insertAfter(this._$table); + + this._jqueryuiThemeAddClass(this._$bottomPanel, 'ui-state-default'); + + $('<div />').addClass('jtable-left-area').appendTo(this._$bottomPanel); + $('<div />').addClass('jtable-right-area').appendTo(this._$bottomPanel); + }, + + /* Creates page list area. + *************************************************************************/ + _createPageListArea: function() { + this._$pagingListArea = $('<span></span>') + .addClass('jtable-page-list') + .appendTo(this._$bottomPanel.find('.jtable-left-area')); + + this._$pageInfoSpan = $('<span></span>') + .addClass('jtable-page-info') + .appendTo(this._$bottomPanel.find('.jtable-right-area')); + }, + + /* Creates page list change area. + *************************************************************************/ + _createPageSizeSelection: function() { + var self = this; + + if (!self.options.pageSizeChangeArea) { + return; + } + + //Add current page size to page sizes list if not contains it + if (self._findIndexInArray(self.options.pageSize, self.options.pageSizes) < 0) { + self.options.pageSizes.push(parseInt(self.options.pageSize)); + self.options.pageSizes.sort(function(a, b) { return a - b; }); + } + + //Add a span to contain page size change items + self._$pageSizeChangeArea = $('<span></span>') + .addClass('jtable-page-size-change') + .appendTo(self._$bottomPanel.find('.jtable-left-area')); + + //Page size label + self._$pageSizeChangeArea.append('<span>' + self.options.messages.pageSizeChangeLabel + ': </span>'); + + //Page size change combobox + var $pageSizeChangeCombobox = $('<select></select>').appendTo(self._$pageSizeChangeArea); + + //Add page sizes to the combobox + for (var i = 0; i < self.options.pageSizes.length; i++) { + $pageSizeChangeCombobox.append('<option value="' + self.options.pageSizes[i] + '">' + self.options.pageSizes[i] + '</option>'); + } + + //Select current page size + $pageSizeChangeCombobox.val(self.options.pageSize); + + //Change page size on combobox change + $pageSizeChangeCombobox.change(function() { + self._changePageSize(parseInt($(this).val())); + }); + }, + + /* Creates go to page area. + *************************************************************************/ + _createGotoPageInput: function() { + var self = this; + + if (!self.options.gotoPageArea || self.options.gotoPageArea == 'none') { + return; + } + + //Add a span to contain goto page items + this._$gotoPageArea = $('<span></span>') + .addClass('jtable-goto-page') + .appendTo(self._$bottomPanel.find('.jtable-left-area')); + + //Goto page label + this._$gotoPageArea.append('<span>' + self.options.messages.gotoPageLabel + ': </span>'); + + //Goto page input + if (self.options.gotoPageArea == 'combobox') { + + self._$gotoPageInput = $('<select></select>') + .appendTo(this._$gotoPageArea) + .data('pageCount', 1) + .change(function() { + self._changePage(parseInt($(this).val())); + }); + self._$gotoPageInput.append('<option value="1">1</option>'); + + } else { //textbox + + self._$gotoPageInput = $('<input type="text" maxlength="10" value="' + self._currentPageNo + '" />') + .appendTo(this._$gotoPageArea) + .keypress(function(event) { + if (event.which == 13) { //enter + event.preventDefault(); + self._changePage(parseInt(self._$gotoPageInput.val())); + } else if (event.which == 43) { // + + event.preventDefault(); + self._changePage(parseInt(self._$gotoPageInput.val()) + 1); + } else if (event.which == 45) { // - + event.preventDefault(); + self._changePage(parseInt(self._$gotoPageInput.val()) - 1); + } else { + //Allow only digits + var isValid = ( + (47 < event.keyCode && event.keyCode < 58 && event.shiftKey == false && event.altKey == false) + || (event.keyCode == 8) + || (event.keyCode == 9) + ); + + if (!isValid) { + event.preventDefault(); + } + } + }); + + } + }, + + /* Refreshes the 'go to page' input. + *************************************************************************/ + _refreshGotoPageInput: function() { + if (!this.options.gotoPageArea || this.options.gotoPageArea == 'none') { + return; + } + + if (this._totalRecordCount <= 0) { + this._$gotoPageArea.hide(); + } else { + this._$gotoPageArea.show(); + } + + if (this.options.gotoPageArea == 'combobox') { + var oldPageCount = this._$gotoPageInput.data('pageCount'); + var currentPageCount = this._calculatePageCount(); + if (oldPageCount != currentPageCount) { + this._$gotoPageInput.empty(); + + //Skip some pages is there are too many pages + var pageStep = 1; + if (currentPageCount > 10000) { + pageStep = 100; + } else if (currentPageCount > 5000) { + pageStep = 10; + } else if (currentPageCount > 2000) { + pageStep = 5; + } else if (currentPageCount > 1000) { + pageStep = 2; + } + + for (var i = pageStep; i <= currentPageCount; i += pageStep) { + this._$gotoPageInput.append('<option value="' + i + '">' + i + '</option>'); + } + + this._$gotoPageInput.data('pageCount', currentPageCount); + } + } + + //same for 'textbox' and 'combobox' + this._$gotoPageInput.val(this._currentPageNo); + }, + + /************************************************************************ + * OVERRIDED METHODS * + *************************************************************************/ + + /* Overrides load method to set current page to 1. + *************************************************************************/ + load: function() { + this._currentPageNo = 1; + + base.load.apply(this, arguments); + }, + + /* Used to change options dynamically after initialization. + *************************************************************************/ + _setOption: function(key, value) { + base._setOption.apply(this, arguments); + + if (key == 'pageSize') { + this._changePageSize(parseInt(value)); + } + }, + + /* Changes current page size with given value. + *************************************************************************/ + _changePageSize: function(pageSize) { + if (pageSize == this.options.pageSize) { + return; + } + + this.options.pageSize = pageSize; + + //Normalize current page + var pageCount = this._calculatePageCount(); + if (this._currentPageNo > pageCount) { + this._currentPageNo = pageCount; + } + if (this._currentPageNo <= 0) { + this._currentPageNo = 1; + } + + //if user sets one of the options on the combobox, then select it. + var $pageSizeChangeCombobox = this._$bottomPanel.find('.jtable-page-size-change select'); + if ($pageSizeChangeCombobox.length > 0) { + if (parseInt($pageSizeChangeCombobox.val()) != pageSize) { + var selectedOption = $pageSizeChangeCombobox.find('option[value=' + pageSize + ']'); + if (selectedOption.length > 0) { + $pageSizeChangeCombobox.val(pageSize); + } + } + } + + this._savePagingSettings(); + this._reloadTable(); + }, + + /* Saves user preferences for paging + *************************************************************************/ + _savePagingSettings: function() { + if (!this.options.saveUserPreferences) { + return; + } + + this._setCookie('page-size', this.options.pageSize); + }, + + /* Overrides _createRecordLoadUrl method to add paging info to URL. + *************************************************************************/ + _createRecordLoadUrl: function() { + var loadUrl = base._createRecordLoadUrl.apply(this, arguments); + loadUrl = this._addPagingInfoToUrl(loadUrl, this._currentPageNo); + return loadUrl; + }, + + /* Overrides _createJtParamsForLoading method to add paging parameters to jtParams object. + *************************************************************************/ + _createJtParamsForLoading: function () { + var jtParams = base._createJtParamsForLoading.apply(this, arguments); + + if (this.options.paging) { + jtParams.jtStartIndex = (this._currentPageNo - 1) * this.options.pageSize; + jtParams.jtPageSize = this.options.pageSize; + } + + return jtParams; + }, + + /* Overrides _addRowToTable method to re-load table when a new row is created. + * NOTE: THIS METHOD IS DEPRECATED AND WILL BE REMOVED FROM FEATURE RELEASES. + * USE _addRow METHOD. + *************************************************************************/ + _addRowToTable: function ($tableRow, index, isNewRow) { + if (isNewRow && this.options.paging) { + this._reloadTable(); + return; + } + + base._addRowToTable.apply(this, arguments); + }, + + /* Overrides _addRow method to re-load table when a new row is created. + *************************************************************************/ + _addRow: function ($row, options) { + if (options && options.isNewRow && this.options.paging) { + this._reloadTable(); + return; + } + + base._addRow.apply(this, arguments); + }, + + /* Overrides _removeRowsFromTable method to re-load table when a row is removed from table. + *************************************************************************/ + _removeRowsFromTable: function ($rows, reason) { + base._removeRowsFromTable.apply(this, arguments); + + if (this.options.paging) { + if (this._$tableRows.length <= 0 && this._currentPageNo > 1) { + --this._currentPageNo; + } + + this._reloadTable(); + } + }, + + /* Overrides _onRecordsLoaded method to to do paging specific tasks. + *************************************************************************/ + _onRecordsLoaded: function (data) { + if (this.options.paging) { + this._totalRecordCount = data.TotalRecordCount; + this._createPagingList(); + this._createPagingInfo(); + this._refreshGotoPageInput(); + } + + base._onRecordsLoaded.apply(this, arguments); + }, + + /************************************************************************ + * PRIVATE METHODS * + *************************************************************************/ + + /* Adds jtStartIndex and jtPageSize parameters to a URL as query string. + *************************************************************************/ + _addPagingInfoToUrl: function (url, pageNumber) { + if (!this.options.paging) { + return url; + } + + var jtStartIndex = (pageNumber - 1) * this.options.pageSize; + var jtPageSize = this.options.pageSize; + + return (url + (url.indexOf('?') < 0 ? '?' : '&') + 'jtStartIndex=' + jtStartIndex + '&jtPageSize=' + jtPageSize); + }, + + /* Creates and shows the page list. + *************************************************************************/ + _createPagingList: function () { + if (this.options.pageSize <= 0) { + return; + } + + this._$pagingListArea.empty(); + if (this._totalRecordCount <= 0) { + return; + } + + var pageCount = this._calculatePageCount(); + + this._createFirstAndPreviousPageButtons(); + if (this.options.pageList == 'normal') { + this._createPageNumberButtons(this._calculatePageNumbers(pageCount)); + } + this._createLastAndNextPageButtons(pageCount); + this._bindClickEventsToPageNumberButtons(); + }, + + /* Creates and shows previous and first page links. + *************************************************************************/ + _createFirstAndPreviousPageButtons: function () { + var $first = $('<span></span>') + .addClass('jtable-page-number-first') + .html('<<') + .data('pageNumber', 1) + .appendTo(this._$pagingListArea); + + var $previous = $('<span></span>') + .addClass('jtable-page-number-previous') + .html('<') + .data('pageNumber', this._currentPageNo - 1) + .appendTo(this._$pagingListArea); + + this._jqueryuiThemeAddClass($first, 'ui-button ui-state-default', 'ui-state-hover'); + this._jqueryuiThemeAddClass($previous, 'ui-button ui-state-default', 'ui-state-hover'); + + if (this._currentPageNo <= 1) { + $first.addClass('jtable-page-number-disabled'); + $previous.addClass('jtable-page-number-disabled'); + this._jqueryuiThemeAddClass($first, 'ui-state-disabled'); + this._jqueryuiThemeAddClass($previous, 'ui-state-disabled'); + } + }, + + /* Creates and shows next and last page links. + *************************************************************************/ + _createLastAndNextPageButtons: function (pageCount) { + var $next = $('<span></span>') + .addClass('jtable-page-number-next') + .html('>') + .data('pageNumber', this._currentPageNo + 1) + .appendTo(this._$pagingListArea); + var $last = $('<span></span>') + .addClass('jtable-page-number-last') + .html('>>') + .data('pageNumber', pageCount) + .appendTo(this._$pagingListArea); + + this._jqueryuiThemeAddClass($next, 'ui-button ui-state-default', 'ui-state-hover'); + this._jqueryuiThemeAddClass($last, 'ui-button ui-state-default', 'ui-state-hover'); + + if (this._currentPageNo >= pageCount) { + $next.addClass('jtable-page-number-disabled'); + $last.addClass('jtable-page-number-disabled'); + this._jqueryuiThemeAddClass($next, 'ui-state-disabled'); + this._jqueryuiThemeAddClass($last, 'ui-state-disabled'); + } + }, + + /* Creates and shows page number links for given number array. + *************************************************************************/ + _createPageNumberButtons: function (pageNumbers) { + var previousNumber = 0; + for (var i = 0; i < pageNumbers.length; i++) { + //Create "..." between page numbers if needed + if ((pageNumbers[i] - previousNumber) > 1) { + $('<span></span>') + .addClass('jtable-page-number-space') + .html('...') + .appendTo(this._$pagingListArea); + } + + this._createPageNumberButton(pageNumbers[i]); + previousNumber = pageNumbers[i]; + } + }, + + /* Creates a page number link and adds to paging area. + *************************************************************************/ + _createPageNumberButton: function (pageNumber) { + var $pageNumber = $('<span></span>') + .addClass('jtable-page-number') + .html(pageNumber) + .data('pageNumber', pageNumber) + .appendTo(this._$pagingListArea); + + this._jqueryuiThemeAddClass($pageNumber, 'ui-button ui-state-default', 'ui-state-hover'); + + if (this._currentPageNo == pageNumber) { + $pageNumber.addClass('jtable-page-number-active jtable-page-number-disabled'); + this._jqueryuiThemeAddClass($pageNumber, 'ui-state-active'); + } + }, + + /* Calculates total page count according to page size and total record count. + *************************************************************************/ + _calculatePageCount: function () { + var pageCount = Math.floor(this._totalRecordCount / this.options.pageSize); + if (this._totalRecordCount % this.options.pageSize != 0) { + ++pageCount; + } + + return pageCount; + }, + + /* Calculates page numbers and returns an array of these numbers. + *************************************************************************/ + _calculatePageNumbers: function (pageCount) { + if (pageCount <= 4) { + //Show all pages + var pageNumbers = []; + for (var i = 1; i <= pageCount; ++i) { + pageNumbers.push(i); + } + + return pageNumbers; + } else { + //show first three, last three, current, previous and next page numbers + var shownPageNumbers = [1, 2, pageCount - 1, pageCount]; + var previousPageNo = this._normalizeNumber(this._currentPageNo - 1, 1, pageCount, 1); + var nextPageNo = this._normalizeNumber(this._currentPageNo + 1, 1, pageCount, 1); + + this._insertToArrayIfDoesNotExists(shownPageNumbers, previousPageNo); + this._insertToArrayIfDoesNotExists(shownPageNumbers, this._currentPageNo); + this._insertToArrayIfDoesNotExists(shownPageNumbers, nextPageNo); + + shownPageNumbers.sort(function (a, b) { return a - b; }); + return shownPageNumbers; + } + }, + + /* Creates and shows paging informations. + *************************************************************************/ + _createPagingInfo: function () { + if (this._totalRecordCount <= 0) { + this._$pageInfoSpan.empty(); + return; + } + + var startNo = (this._currentPageNo - 1) * this.options.pageSize + 1; + var endNo = this._currentPageNo * this.options.pageSize; + endNo = this._normalizeNumber(endNo, startNo, this._totalRecordCount, 0); + + if (endNo >= startNo) { + var pagingInfoMessage = this._formatString(this.options.messages.pagingInfo, startNo, endNo, this._totalRecordCount); + this._$pageInfoSpan.html(pagingInfoMessage); + } + }, + + /* Binds click events of all page links to change the page. + *************************************************************************/ + _bindClickEventsToPageNumberButtons: function () { + var self = this; + self._$pagingListArea + .find('.jtable-page-number,.jtable-page-number-previous,.jtable-page-number-next,.jtable-page-number-first,.jtable-page-number-last') + .not('.jtable-page-number-disabled') + .click(function (e) { + e.preventDefault(); + self._changePage($(this).data('pageNumber')); + }); + }, + + /* Changes current page to given value. + *************************************************************************/ + _changePage: function (pageNo) { + pageNo = this._normalizeNumber(pageNo, 1, this._calculatePageCount(), 1); + if (pageNo == this._currentPageNo) { + this._refreshGotoPageInput(); + return; + } + + this._currentPageNo = pageNo; + this._reloadTable(); + } + + }); + +})(jQuery); + + +/************************************************************************ +* SORTING extension for jTable * +*************************************************************************/ +(function ($) { + + //Reference to base object members + var base = { + _initializeFields: $.hik.jtable.prototype._initializeFields, + _normalizeFieldOptions: $.hik.jtable.prototype._normalizeFieldOptions, + _createHeaderCellForField: $.hik.jtable.prototype._createHeaderCellForField, + _createRecordLoadUrl: $.hik.jtable.prototype._createRecordLoadUrl, + _createJtParamsForLoading: $.hik.jtable.prototype._createJtParamsForLoading + }; + + //extension members + $.extend(true, $.hik.jtable.prototype, { + + /************************************************************************ + * DEFAULT OPTIONS / EVENTS * + *************************************************************************/ + options: { + sorting: false, + multiSorting: false, + defaultSorting: '' + }, + + /************************************************************************ + * PRIVATE FIELDS * + *************************************************************************/ + + _lastSorting: null, //Last sorting of the table + + /************************************************************************ + * OVERRIDED METHODS * + *************************************************************************/ + + /* Overrides base method to create sorting array. + *************************************************************************/ + _initializeFields: function () { + base._initializeFields.apply(this, arguments); + + this._lastSorting = []; + if (this.options.sorting) { + this._buildDefaultSortingArray(); + } + }, + + /* Overrides _normalizeFieldOptions method to normalize sorting option for fields. + *************************************************************************/ + _normalizeFieldOptions: function (fieldName, props) { + base._normalizeFieldOptions.apply(this, arguments); + props.sorting = (props.sorting != false); + }, + + /* Overrides _createHeaderCellForField to make columns sortable. + *************************************************************************/ + _createHeaderCellForField: function (fieldName, field) { + var $headerCell = base._createHeaderCellForField.apply(this, arguments); + if (this.options.sorting && field.sorting) { + this._makeColumnSortable($headerCell, fieldName); + } + + return $headerCell; + }, + + /* Overrides _createRecordLoadUrl to add sorting specific info to URL. + *************************************************************************/ + _createRecordLoadUrl: function () { + var loadUrl = base._createRecordLoadUrl.apply(this, arguments); + loadUrl = this._addSortingInfoToUrl(loadUrl); + return loadUrl; + }, + + /************************************************************************ + * PRIVATE METHODS * + *************************************************************************/ + + /* Builds the sorting array according to defaultSorting string + *************************************************************************/ + _buildDefaultSortingArray: function () { + var self = this; + + $.each(self.options.defaultSorting.split(","), function (orderIndex, orderValue) { + $.each(self.options.fields, function (fieldName, fieldProps) { + if (fieldProps.sorting) { + var colOffset = orderValue.indexOf(fieldName); + if (colOffset > -1) { + if (orderValue.toUpperCase().indexOf(' DESC', colOffset) > -1) { + self._lastSorting.push({ + fieldName: fieldName, + sortOrder: 'DESC' + }); + } else { + self._lastSorting.push({ + fieldName: fieldName, + sortOrder: 'ASC' + }); + } + } + } + }); + }); + }, + + /* Makes a column sortable. + *************************************************************************/ + _makeColumnSortable: function ($columnHeader, fieldName) { + var self = this; + + $columnHeader + .addClass('jtable-column-header-sortable') + .click(function (e) { + e.preventDefault(); + + if (!self.options.multiSorting || !e.ctrlKey) { + self._lastSorting = []; //clear previous sorting + } + + self._sortTableByColumn($columnHeader); + }); + + //Set default sorting + $.each(this._lastSorting, function (sortIndex, sortField) { + if (sortField.fieldName == fieldName) { + if (sortField.sortOrder == 'DESC') { + $columnHeader.addClass('jtable-column-header-sorted-desc'); + } else { + $columnHeader.addClass('jtable-column-header-sorted-asc'); + } + } + }); + }, + + /* Sorts table according to a column header. + *************************************************************************/ + _sortTableByColumn: function ($columnHeader) { + //Remove sorting styles from all columns except this one + if (this._lastSorting.length == 0) { + $columnHeader.siblings().removeClass('jtable-column-header-sorted-asc jtable-column-header-sorted-desc'); + } + + //If current sorting list includes this column, remove it from the list + for (var i = 0; i < this._lastSorting.length; i++) { + if (this._lastSorting[i].fieldName == $columnHeader.data('fieldName')) { + this._lastSorting.splice(i--, 1); + } + } + + //Sort ASC or DESC according to current sorting state + if ($columnHeader.hasClass('jtable-column-header-sorted-asc')) { + $columnHeader.removeClass('jtable-column-header-sorted-asc').addClass('jtable-column-header-sorted-desc'); + this._lastSorting.push({ + 'fieldName': $columnHeader.data('fieldName'), + sortOrder: 'DESC' + }); + } else { + $columnHeader.removeClass('jtable-column-header-sorted-desc').addClass('jtable-column-header-sorted-asc'); + this._lastSorting.push({ + 'fieldName': $columnHeader.data('fieldName'), + sortOrder: 'ASC' + }); + } + + //Load current page again + this._reloadTable(); + }, + + /* Adds jtSorting parameter to a URL as query string. + *************************************************************************/ + _addSortingInfoToUrl: function (url) { + if (!this.options.sorting || this._lastSorting.length == 0) { + return url; + } + + var sorting = []; + $.each(this._lastSorting, function (idx, value) { + sorting.push(value.fieldName + ' ' + value.sortOrder); + }); + + return (url + (url.indexOf('?') < 0 ? '?' : '&') + 'jtSorting=' + sorting.join(",")); + }, + + /* Overrides _createJtParamsForLoading method to add sorging parameters to jtParams object. + *************************************************************************/ + _createJtParamsForLoading: function () { + var jtParams = base._createJtParamsForLoading.apply(this, arguments); + + if (this.options.sorting && this._lastSorting.length) { + var sorting = []; + $.each(this._lastSorting, function (idx, value) { + sorting.push(value.fieldName + ' ' + value.sortOrder); + }); + + jtParams.jtSorting = sorting.join(","); + } + + return jtParams; + } + + }); + +})(jQuery); + +/************************************************************************ +* DYNAMIC COLUMNS extension for jTable * +* (Show/hide/resize columns) * +*************************************************************************/ +(function ($) { + + //Reference to base object members + var base = { + _create: $.hik.jtable.prototype._create, + _normalizeFieldOptions: $.hik.jtable.prototype._normalizeFieldOptions, + _createHeaderCellForField: $.hik.jtable.prototype._createHeaderCellForField, + _createCellForRecordField: $.hik.jtable.prototype._createCellForRecordField + }; + + //extension members + $.extend(true, $.hik.jtable.prototype, { + + /************************************************************************ + * DEFAULT OPTIONS / EVENTS * + *************************************************************************/ + + options: { + tableId: undefined, + columnResizable: true, + columnSelectable: true + }, + + /************************************************************************ + * PRIVATE FIELDS * + *************************************************************************/ + + _$columnSelectionDiv: null, + _$columnResizeBar: null, + _cookieKeyPrefix: null, + _currentResizeArgs: null, + + /************************************************************************ + * OVERRIDED METHODS * + *************************************************************************/ + + /* Overrides _addRowToTableHead method. + *************************************************************************/ + + _create: function () { + base._create.apply(this, arguments); + + this._createColumnResizeBar(); + this._createColumnSelection(); + + if (this.options.saveUserPreferences) { + this._loadColumnSettings(); + } + + this._normalizeColumnWidths(); + }, + + /* Normalizes some options for a field (sets default values). + *************************************************************************/ + _normalizeFieldOptions: function (fieldName, props) { + base._normalizeFieldOptions.apply(this, arguments); + + //columnResizable + if (this.options.columnResizable) { + props.columnResizable = (props.columnResizable != false); + } + + //visibility + if (!props.visibility) { + props.visibility = 'visible'; + } + }, + + /* Overrides _createHeaderCellForField to make columns dynamic. + *************************************************************************/ + _createHeaderCellForField: function (fieldName, field) { + var $headerCell = base._createHeaderCellForField.apply(this, arguments); + + //Make data columns resizable except the last one + if (this.options.columnResizable && field.columnResizable && (fieldName != this._columnList[this._columnList.length - 1])) { + this._makeColumnResizable($headerCell); + } + + //Hide column if needed + if (field.visibility == 'hidden') { + $headerCell.hide(); + } + + return $headerCell; + }, + + /* Overrides _createHeaderCellForField to decide show or hide a column. + *************************************************************************/ + _createCellForRecordField: function (record, fieldName) { + var $column = base._createCellForRecordField.apply(this, arguments); + + var field = this.options.fields[fieldName]; + if (field.visibility == 'hidden') { + $column.hide(); + } + + return $column; + }, + + /************************************************************************ + * PUBLIC METHODS * + *************************************************************************/ + + /* Changes visibility of a column. + *************************************************************************/ + changeColumnVisibility: function (columnName, visibility) { + this._changeColumnVisibilityInternal(columnName, visibility); + this._normalizeColumnWidths(); + if (this.options.saveUserPreferences) { + this._saveColumnSettings(); + } + }, + + /************************************************************************ + * PRIVATE METHODS * + *************************************************************************/ + + /* Changes visibility of a column. + *************************************************************************/ + _changeColumnVisibilityInternal: function (columnName, visibility) { + //Check if there is a column with given name + var columnIndex = this._columnList.indexOf(columnName); + if (columnIndex < 0) { + this._logWarn('Column "' + columnName + '" does not exist in fields!'); + return; + } + + //Check if visibility value is valid + if (['visible', 'hidden', 'fixed'].indexOf(visibility) < 0) { + this._logWarn('Visibility value is not valid: "' + visibility + '"! Options are: visible, hidden, fixed.'); + return; + } + + //Get the field + var field = this.options.fields[columnName]; + if (field.visibility == visibility) { + return; //No action if new value is same as old one. + } + + //Hide or show the column if needed + var columnIndexInTable = this._firstDataColumnOffset + columnIndex + 1; + if (field.visibility != 'hidden' && visibility == 'hidden') { + this._$table + .find('>thead >tr >th:nth-child(' + columnIndexInTable + '),>tbody >tr >td:nth-child(' + columnIndexInTable + ')') + .hide(); + } else if (field.visibility == 'hidden' && visibility != 'hidden') { + this._$table + .find('>thead >tr >th:nth-child(' + columnIndexInTable + '),>tbody >tr >td:nth-child(' + columnIndexInTable + ')') + .show() + .css('display', 'table-cell'); + } + + field.visibility = visibility; + }, + + /* Prepares dialog to change settings. + *************************************************************************/ + _createColumnSelection: function () { + var self = this; + + //Create a div for dialog and add to container element + this._$columnSelectionDiv = $('<div />') + .addClass('jtable-column-selection-container') + .appendTo(self._$mainContainer); + + this._$table.children('thead').bind('contextmenu', function (e) { + if (!self.options.columnSelectable) { + return; + } + + e.preventDefault(); + + //Make an overlay div to disable page clicks + $('<div />') + .addClass('jtable-contextmenu-overlay') + .click(function () { + $(this).remove(); + self._$columnSelectionDiv.hide(); + }) + .bind('contextmenu', function () { return false; }) + .appendTo(document.body); + + self._fillColumnSelection(); + + //Calculate position of column selection list and show it + + var containerOffset = self._$mainContainer.offset(); + var selectionDivTop = e.pageY - containerOffset.top; + var selectionDivLeft = e.pageX - containerOffset.left; + + var selectionDivMinWidth = 100; //in pixels + var containerWidth = self._$mainContainer.width(); + + //If user clicks right area of header of the table, show list at a little left + if ((containerWidth > selectionDivMinWidth) && (selectionDivLeft > (containerWidth - selectionDivMinWidth))) { + selectionDivLeft = containerWidth - selectionDivMinWidth; + } + + self._$columnSelectionDiv.css({ + left: selectionDivLeft, + top: selectionDivTop, + 'min-width': selectionDivMinWidth + 'px' + }).show(); + }); + }, + + /* Prepares content of settings dialog. + *************************************************************************/ + _fillColumnSelection: function () { + var self = this; + + var $columnsUl = $('<ul></ul>') + .addClass('jtable-column-select-list'); + for (var i = 0; i < this._columnList.length; i++) { + var columnName = this._columnList[i]; + var field = this.options.fields[columnName]; + + //Crete li element + var $columnLi = $('<li></li>').appendTo($columnsUl); + + //Create label for the checkbox + var $label = $('<label for="' + columnName + '"></label>') + .append($('<span>' + (field.title || columnName) + '</span>')) + .appendTo($columnLi); + + //Create checkbox + var $checkbox = $('<input type="checkbox" name="' + columnName + '">') + .prependTo($label) + .click(function () { + var $clickedCheckbox = $(this); + var clickedColumnName = $clickedCheckbox.attr('name'); + var clickedField = self.options.fields[clickedColumnName]; + if (clickedField.visibility == 'fixed') { + return; + } + + self.changeColumnVisibility(clickedColumnName, $clickedCheckbox.is(':checked') ? 'visible' : 'hidden'); + }); + + //Check, if column if shown + if (field.visibility != 'hidden') { + $checkbox.attr('checked', 'checked'); + } + + //Disable, if column is fixed + if (field.visibility == 'fixed') { + $checkbox.attr('disabled', 'disabled'); + } + } + + this._$columnSelectionDiv.html($columnsUl); + }, + + /* creates a vertical bar that is shown while resizing columns. + *************************************************************************/ + _createColumnResizeBar: function () { + this._$columnResizeBar = $('<div />') + .addClass('jtable-column-resize-bar') + .appendTo(this._$mainContainer) + .hide(); + }, + + /* Makes a column sortable. + *************************************************************************/ + _makeColumnResizable: function ($columnHeader) { + var self = this; + + //Create a handler to handle mouse click event + $('<div />') + .addClass('jtable-column-resize-handler') + .appendTo($columnHeader.find('.jtable-column-header-container')) //Append the handler to the column + .mousedown(function (downevent) { //handle mousedown event for the handler + downevent.preventDefault(); + downevent.stopPropagation(); + + var mainContainerOffset = self._$mainContainer.offset(); + + //Get a reference to the next column + var $nextColumnHeader = $columnHeader.nextAll('th.jtable-column-header:visible:first'); + if (!$nextColumnHeader.length) { + return; + } + + //Store some information to be used on resizing + var minimumColumnWidth = 10; //A column's width can not be smaller than 10 pixel. + self._currentResizeArgs = { + currentColumnStartWidth: $columnHeader.outerWidth(), + minWidth: minimumColumnWidth, + maxWidth: $columnHeader.outerWidth() + $nextColumnHeader.outerWidth() - minimumColumnWidth, + mouseStartX: downevent.pageX, + minResizeX: function () { return this.mouseStartX - (this.currentColumnStartWidth - this.minWidth); }, + maxResizeX: function () { return this.mouseStartX + (this.maxWidth - this.currentColumnStartWidth); } + }; + + //Handle mouse move event to move resizing bar + var resizeonmousemove = function (moveevent) { + if (!self._currentResizeArgs) { + return; + } + + var resizeBarX = self._normalizeNumber(moveevent.pageX, self._currentResizeArgs.minResizeX(), self._currentResizeArgs.maxResizeX()); + self._$columnResizeBar.css('left', (resizeBarX - mainContainerOffset.left) + 'px'); + }; + + //Handle mouse up event to finish resizing of the column + var resizeonmouseup = function (upevent) { + if (!self._currentResizeArgs) { + return; + } + + $(document).unbind('mousemove', resizeonmousemove); + $(document).unbind('mouseup', resizeonmouseup); + + self._$columnResizeBar.hide(); + + //Calculate new widths in pixels + var mouseChangeX = upevent.pageX - self._currentResizeArgs.mouseStartX; + var currentColumnFinalWidth = self._normalizeNumber(self._currentResizeArgs.currentColumnStartWidth + mouseChangeX, self._currentResizeArgs.minWidth, self._currentResizeArgs.maxWidth); + var nextColumnFinalWidth = $nextColumnHeader.outerWidth() + (self._currentResizeArgs.currentColumnStartWidth - currentColumnFinalWidth); + + //Calculate widths as percent + var pixelToPercentRatio = $columnHeader.data('width-in-percent') / self._currentResizeArgs.currentColumnStartWidth; + $columnHeader.data('width-in-percent', currentColumnFinalWidth * pixelToPercentRatio); + $nextColumnHeader.data('width-in-percent', nextColumnFinalWidth * pixelToPercentRatio); + + //Set new widths to columns (resize!) + $columnHeader.css('width', $columnHeader.data('width-in-percent') + '%'); + $nextColumnHeader.css('width', $nextColumnHeader.data('width-in-percent') + '%'); + + //Normalize all column widths + self._normalizeColumnWidths(); + + //Finish resizing + self._currentResizeArgs = null; + + //Save current preferences + if (self.options.saveUserPreferences) { + self._saveColumnSettings(); + } + }; + + //Show vertical resize bar + self._$columnResizeBar + .show() + .css({ + top: ($columnHeader.offset().top - mainContainerOffset.top) + 'px', + left: (downevent.pageX - mainContainerOffset.left) + 'px', + height: (self._$table.outerHeight()) + 'px' + }); + + //Bind events + $(document).bind('mousemove', resizeonmousemove); + $(document).bind('mouseup', resizeonmouseup); + }); + }, + + /* Normalizes column widths as percent for current view. + *************************************************************************/ + _normalizeColumnWidths: function () { + + //Set command column width + var commandColumnHeaders = this._$table + .find('>thead th.jtable-command-column-header') + .data('width-in-percent', 1) + .css('width', '1%'); + + //Find data columns + var headerCells = this._$table.find('>thead th.jtable-column-header'); + + //Calculate total width of data columns + var totalWidthInPixel = 0; + headerCells.each(function () { + var $cell = $(this); + if ($cell.is(':visible')) { + totalWidthInPixel += $cell.outerWidth(); + } + }); + + //Calculate width of each column + var columnWidhts = {}; + var availableWidthInPercent = 100.0 - commandColumnHeaders.length; + headerCells.each(function () { + var $cell = $(this); + if ($cell.is(':visible')) { + var fieldName = $cell.data('fieldName'); + var widthInPercent = $cell.outerWidth() * availableWidthInPercent / totalWidthInPixel; + columnWidhts[fieldName] = widthInPercent; + } + }); + + //Set width of each column + headerCells.each(function () { + var $cell = $(this); + if ($cell.is(':visible')) { + var fieldName = $cell.data('fieldName'); + $cell.data('width-in-percent', columnWidhts[fieldName]).css('width', columnWidhts[fieldName] + '%'); + } + }); + }, + + /* Saves field setting to cookie. + * Saved setting will be a string like that: + * fieldName1=visible;23|fieldName2=hidden;17|... + *************************************************************************/ + _saveColumnSettings: function () { + var self = this; + var fieldSettings = ''; + this._$table.find('>thead >tr >th.jtable-column-header').each(function () { + var $cell = $(this); + var fieldName = $cell.data('fieldName'); + var columnWidth = $cell.data('width-in-percent'); + var fieldVisibility = self.options.fields[fieldName].visibility; + var fieldSetting = fieldName + "=" + fieldVisibility + ';' + columnWidth; + fieldSettings = fieldSettings + fieldSetting + '|'; + }); + + this._setCookie('column-settings', fieldSettings.substr(0, fieldSettings.length - 1)); + }, + + /* Loads field settings from cookie that is saved by _saveFieldSettings method. + *************************************************************************/ + _loadColumnSettings: function () { + var self = this; + var columnSettingsCookie = this._getCookie('column-settings'); + if (!columnSettingsCookie) { + return; + } + + var columnSettings = {}; + $.each(columnSettingsCookie.split('|'), function (inx, fieldSetting) { + var splitted = fieldSetting.split('='); + var fieldName = splitted[0]; + var settings = splitted[1].split(';'); + columnSettings[fieldName] = { + columnVisibility: settings[0], + columnWidth: settings[1] + }; + }); + + var headerCells = this._$table.find('>thead >tr >th.jtable-column-header'); + headerCells.each(function () { + var $cell = $(this); + var fieldName = $cell.data('fieldName'); + var field = self.options.fields[fieldName]; + if (columnSettings[fieldName]) { + if (field.visibility != 'fixed') { + self._changeColumnVisibilityInternal(fieldName, columnSettings[fieldName].columnVisibility); + } + + $cell.data('width-in-percent', columnSettings[fieldName].columnWidth).css('width', columnSettings[fieldName].columnWidth + '%'); + } + }); + } + + }); + +})(jQuery); + + +/************************************************************************ +* MASTER/CHILD tables extension for jTable * +*************************************************************************/ +(function ($) { + + //Reference to base object members + var base = { + _removeRowsFromTable: $.hik.jtable.prototype._removeRowsFromTable + }; + + //extension members + $.extend(true, $.hik.jtable.prototype, { + + /************************************************************************ + * DEFAULT OPTIONS / EVENTS * + *************************************************************************/ + options: { + openChildAsAccordion: false + }, + + /************************************************************************ + * PUBLIC METHODS * + *************************************************************************/ + + /* Creates and opens a new child table for given row. + *************************************************************************/ + openChildTable: function ($row, tableOptions, opened) { + var self = this; + + //Apply theming as same as parent table unless explicitily set + if (tableOptions.jqueryuiTheme == undefined) { + tableOptions.jqueryuiTheme = self.options.jqueryuiTheme; + } + + //Show close button as default + tableOptions.showCloseButton = (tableOptions.showCloseButton != false); + + //Close child table when close button is clicked (default behavior) + if (tableOptions.showCloseButton && !tableOptions.closeRequested) { + tableOptions.closeRequested = function () { + self.closeChildTable($row); + }; + } + + //If accordion style, close open child table (if it does exists) + if (self.options.openChildAsAccordion) { + $row.siblings('.jtable-data-row').each(function () { + self.closeChildTable($(this)); + }); + } + + //Close child table for this row and open new one for child table + self.closeChildTable($row, function () { + var $childRowColumn = self.getChildRow($row).children('td').empty(); + var $childTableContainer = $('<div />') + .addClass('jtable-child-table-container') + .appendTo($childRowColumn); + $childRowColumn.data('childTable', $childTableContainer); + $childTableContainer.jtable(tableOptions); + self.openChildRow($row); + $childTableContainer.hide().slideDown('fast', function () { + if (opened) { + opened({ + childTable: $childTableContainer + }); + } + }); + }); + }, + + /* Closes child table for given row. + *************************************************************************/ + closeChildTable: function ($row, closed) { + var self = this; + + var $childRowColumn = this.getChildRow($row).children('td'); + var $childTable = $childRowColumn.data('childTable'); + if (!$childTable) { + if (closed) { + closed(); + } + + return; + } + + $childRowColumn.data('childTable', null); + $childTable.slideUp('fast', function () { + $childTable.jtable('destroy'); + $childTable.remove(); + self.closeChildRow($row); + if (closed) { + closed(); + } + }); + }, + + /* Returns a boolean value indicates that if a child row is open for given row. + *************************************************************************/ + isChildRowOpen: function ($row) { + return (this.getChildRow($row).is(':visible')); + }, + + /* Gets child row for given row, opens it if it's closed (Creates if needed). + *************************************************************************/ + getChildRow: function ($row) { + return $row.data('childRow') || this._createChildRow($row); + }, + + /* Creates and opens child row for given row. + *************************************************************************/ + openChildRow: function ($row) { + var $childRow = this.getChildRow($row); + if (!$childRow.is(':visible')) { + $childRow.show(); + } + + return $childRow; + }, + + /* Closes child row if it's open. + *************************************************************************/ + closeChildRow: function ($row) { + var $childRow = this.getChildRow($row); + if ($childRow.is(':visible')) { + $childRow.hide(); + } + }, + + /************************************************************************ + * OVERRIDED METHODS * + *************************************************************************/ + + /* Overrides _removeRowsFromTable method to remove child rows of deleted rows. + *************************************************************************/ + _removeRowsFromTable: function ($rows, reason) { + //var self = this; + + if (reason == 'deleted') { + $rows.each(function () { + var $row = $(this); + var $childRow = $row.data('childRow'); + if ($childRow) { + //self.closeChildTable($row); //Removed since it causes "Uncaught Error: cannot call methods on jtable prior to initialization; attempted to call method 'destroy'" + $childRow.remove(); + } + }); + } + + base._removeRowsFromTable.apply(this, arguments); + }, + + /************************************************************************ + * PRIVATE METHODS * + *************************************************************************/ + + /* Creates a child row for a row, hides and returns it. + *************************************************************************/ + _createChildRow: function ($row) { + var totalColumnCount = this._$table.find('thead th').length; + var $childRow = $('<tr></tr>') + .addClass('jtable-child-row') + .append('<td colspan="' + totalColumnCount + '"></td>'); + $row.after($childRow); + $row.data('childRow', $childRow); + $childRow.hide(); + return $childRow; + } + + }); + +})(jQuery); + diff --git a/lib/extensions/jquery.jtable.record-actions.js b/lib/extensions/jquery.jtable.record-actions.js new file mode 100644 index 0000000..5f221c2 --- /dev/null +++ b/lib/extensions/jquery.jtable.record-actions.js @@ -0,0 +1,147 @@ +/************************************************************************ +* RECORD-ACTIONS extension for jTable * +*************************************************************************/ +(function ($) { + + //Reference to base object members + var base = { + _initializeFields: $.hik.jtable.prototype._initializeFields, + _onRecordsLoaded: $.hik.jtable.prototype._onRecordsLoaded + }; + + //extension members + $.extend(true, $.hik.jtable.prototype, { + + /************************************************************************ + * OVERRIDED METHODS * + *************************************************************************/ + + /* Overrides base method to create record-actions field type. + *************************************************************************/ + _initializeFields: function () { + base._initializeFields.apply(this, arguments); + + var self = this; + + self._extraFieldTypes.push({ + type:'record-actions', + creator: function(record, field){ + return self._createRecordActionsDropdown(record, field); + } + }); + }, + + /* Overrides base method to handle dropdown menu overflow. + *************************************************************************/ + _onRecordsLoaded: function () { + base._onRecordsLoaded.apply(this, arguments); + + var self = this; + self._$tableBody.find('div.dropdown').on('show.bs.dropdown', function (e) { + var $this = $(this); + + if (!$this.data('_tether')) { + var $dropdownButton = $this.find('.dropdown-toggle'); + var $dropdownMenu = $this.find('.dropdown-menu'); + + $dropdownMenu.css({ + 'display': 'block' + }); + + $this.data('_tether', new Tether({ + element: $dropdownMenu[0], + target: $dropdownButton[0], + attachment: 'top left', + targetAttachment: 'bottom left', + constraints: [{ + to: 'window', + attachment: 'together', + pin: true + }] + })); + } + + var $dropdownMenu = $($this.data('_tether').element); + $dropdownMenu.css({ + 'display': 'block' + }); + }).on('hidden.bs.dropdown', function (e) { + var $this = $(this); + var $dropdownMenu = $($this.data('_tether').element); + $dropdownMenu.css({ + 'display': 'none' + }); + }); + }, + + /************************************************************************ + * PRIVATE METHODS * + *************************************************************************/ + + /* Builds the dropdown actions button according to field definition + *************************************************************************/ + _createRecordActionsDropdown: function(record, field){ + var self = this; + var $dropdownContainer = $('<div></div>') + .addClass('btn-group') + .addClass('dropdown'); + + var $dropdownButton = $('<button></button>') + .html(field.text) + .addClass('dropdown-toggle') + .attr('data-toggle','dropdown') + .attr('aria-haspopup','true') + .attr('aria-expanded','true'); + + if(field.cssClass){ + $dropdownButton.addClass(field.cssClass); + } + + var $dropdownItemsContainer = $('<ul></ul>').addClass('dropdown-menu'); + for (var i = 0; i < field.items.length; i++) { + var fieldItem = field.items[i]; + + if(fieldItem.visible && !fieldItem.visible({record: record})){ + continue; + } + + var $dropdownItem = self._createDropdownItem(record, fieldItem); + + if(fieldItem.enabled && !fieldItem.enabled({ record: record })){ + $dropdownItem.addClass('disabled'); + } + + $dropdownItem.appendTo($dropdownItemsContainer); + } + + if($dropdownItemsContainer.find('li').length > 0){ + $dropdownItemsContainer.appendTo($dropdownContainer); + $dropdownButton.appendTo($dropdownContainer); + } + + return $dropdownContainer; + }, + + _createDropdownItem: function(record, fieldItem){ + var $li = $('<li></li>'); + var $a = $('<a></a>'); + + if(fieldItem.text){ + $a.html(fieldItem.text); + } + + if(fieldItem.action && (fieldItem.enabled && fieldItem.enabled({ record: record }))) { + $a.click(function(){ + fieldItem.action({ + record: record + }); + }); + } + + $a.appendTo($li); + return $li; + } + + }); + +})(jQuery);
\ No newline at end of file |