/Demo/Knockout.Extensions.Demo.Web/Scripts/knockout.bindings.dataTables.js

https://bitbucket.org/grid13/knockout.extensions · JavaScript · 307 lines · 165 code · 47 blank · 95 comment · 54 complexity · 47fa9da151968a478dce1355b0795892 MD5 · raw file

  1. /// <reference path="_references.js" />
  2. /**
  3. * A KnockoutJs binding handler for the html tables javascript library DataTables.
  4. *
  5. * File: knockout.bindings.dataTables.js
  6. * Author: Lucas Martin
  7. * License: Creative Commons Attribution 3.0 Unported License. http://creativecommons.org/licenses/by/3.0/
  8. *
  9. * Copyright 2011, All Rights Reserved, Cognitive Shift http://www.cogshift.com
  10. *
  11. * For more information about KnockoutJs or DataTables, see http://www.knockoutjs.com and http://www.datatables.com for details.
  12. */
  13. (function () {
  14. var _onInitialisingEventName = "ko_bindingHandlers_dataTable_onInitialising",
  15. _dataTablesInstanceDataKey = "ko_bindingHandlers_dataTable_Instance";
  16. ko.bindingHandlers['dataTable'] = {
  17. options: {},
  18. addOnInitListener: function (handler) {
  19. /// <Summary>
  20. /// Registers a event handler that fires when the Data Table is being initialised.
  21. /// </Summary>
  22. $(document).bind(_onInitialisingEventName, handler);
  23. },
  24. removeOnInitListener: function (handler) {
  25. /// <Summary>
  26. /// Unregisters an event handler to the onInitialising event.
  27. /// </Summary>
  28. $(document).unbind(_onInitialisingEventName, handler);
  29. },
  30. init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
  31. var binding = ko.utils.unwrapObservable(valueAccessor());
  32. var options = $.extend(true, {}, ko.bindingHandlers['dataTable'].options);
  33. // If the table has already been initialised, exit now. Sometimes knockout.js invokes the init function of a binding handler in particular
  34. // situations twice for a given element.
  35. if (getDataTableInstance(element))
  36. return;
  37. // ** Initialise the DataTables options object with the data-bind settings **
  38. // Clone the options object found in the data bindings. This object will form the base for the DataTable initialisation object.
  39. if (binding.options)
  40. options = $.extend(options, binding.options);
  41. // Define the tables columns.
  42. if (binding.columns && binding.columns.length) {
  43. options.aoColumns = [];
  44. ko.utils.arrayForEach(binding.columns, function (col) {
  45. if (typeof col == "string") {
  46. col = { mDataProp: col }
  47. }
  48. options.aoColumns.push(col);
  49. })
  50. }
  51. // Support for computed template name and templates that change
  52. var rowTemplate = ko.utils.unwrapObservable(binding.rowTemplate);
  53. if (ko.isObservable(binding.rowTemplate)) {
  54. binding.rowTemplate.subscribe(function (value) {
  55. rowTemplate = value;
  56. getDataTableInstance(element).fnDraw();
  57. });
  58. }
  59. // Register the row template to be used with the DataTable.
  60. if (binding.rowTemplate && binding.rowTemplate != '') {
  61. // Intercept the fnRowCallback function.
  62. options.fnRowCallback = cog.utils.intercept(options.fnRowCallback || function (row) { return row; }, function (row, data, displayIndex, displayIndexFull, next) {
  63. // Render the row template for this row.
  64. ko.renderTemplate(rowTemplate, bindingContext.createChildContext(data), null, row, "replaceChildren");
  65. return next(row, data, displayIndex, displayIndexFull);
  66. });
  67. }
  68. // Set the data source of the DataTable.
  69. if (binding.dataSource) {
  70. var dataSource = ko.utils.unwrapObservable(binding.dataSource);
  71. // If the data source is a function that gets the data for us...
  72. if (typeof dataSource == 'function' && dataSource.length == 2) {
  73. // Register a fnServerData callback which calls the data source function when the DataTable requires data.
  74. options.fnServerData = function (source, criteria, callback) {
  75. dataSource.call(viewModel, convertDataCriteria(criteria), function (result) {
  76. callback({
  77. aaData: ko.utils.unwrapObservable(result.Data),
  78. iTotalRecords: ko.utils.unwrapObservable(result.TotalRecords),
  79. iTotalDisplayRecords: ko.utils.unwrapObservable(result.DisplayedRecords)
  80. });
  81. });
  82. }
  83. // In this data source scenario, we are relying on the server processing.
  84. options.bProcessing = true;
  85. options.bServerSide = true;
  86. }
  87. // If the data source is a javascript array...
  88. else if (dataSource instanceof Array) {
  89. // Set the initial datasource of the table.
  90. options.aaData = ko.utils.unwrapObservable(binding.dataSource);
  91. // If the data source is a knockout observable array...
  92. if (ko.isObservable(binding.dataSource)) {
  93. // Subscribe to the dataSource observable. This callback will fire whenever items are added to
  94. // and removed from the data source.
  95. binding.dataSource.subscribe(function (newItems) {
  96. // ** Redraw table **
  97. var dataTable = $(element).dataTable();
  98. setDataTableInstanceOnBinding(dataTable, binding.table);
  99. // Get a list of rows in the DataTable.
  100. var tableRows = dataTable.fnGetNodes();
  101. // If the table contains rows...
  102. if (tableRows.length) {
  103. // Clear the datatable of rows, and if there are no items to display
  104. // in newItems, force the fnClearTables call to rerender the table (because
  105. // the call to fnAddData with a newItems.length == 0 wont rerender the table).
  106. dataTable.fnClearTable(newItems.length == 0);
  107. }
  108. // Unwrap the items in the data source if required.
  109. var unwrappedItems = [];
  110. ko.utils.arrayForEach(newItems, function (item) {
  111. unwrappedItems.push(ko.utils.unwrapObservable(item));
  112. });
  113. // Add the new data back into the data table.
  114. dataTable.fnAddData(unwrappedItems);
  115. // Unregister each of the table rows from knockout.
  116. // NB: This must be called after fnAddData and fnClearTable are called because we want to allow
  117. // DataTables to fire it's draw callbacks with the table's rows in their original state. Calling
  118. // this any earlier will modify the tables rows, which may cause issues with third party plugins that
  119. // use the data table.
  120. ko.utils.arrayForEach(tableRows, function (tableRow) { ko.cleanNode(tableRow); });
  121. });
  122. }
  123. }
  124. // If the dataSource was not a function that retrieves data, or a javascript object array containing data.
  125. else {
  126. throw 'The dataSource defined must either be a javascript object array, or a function that takes special parameters.';
  127. }
  128. }
  129. // If no fnRowCallback has been registered in the DataTable's options, then register the default fnRowCallback.
  130. // This default fnRowCallback function is called for every row in the data source. The intention of this callback
  131. // is to build a table row that is bound it's associated record in the data source via knockout js.
  132. if (!binding.rowTemplate || binding.rowTemplate == '') {
  133. options.fnRowCallback = cog.utils.intercept(options.fnRowCallback || function (row) { return row; }, function (row, srcData, displayIndex, displayIndexFull, next) {
  134. var columns = this.fnSettings().aoColumns
  135. // Empty the row that has been build by the DataTable of any child elements.
  136. var destRow = $(row);
  137. destRow.empty();
  138. // For each column in the data table...
  139. ko.utils.arrayForEach(columns, function (column) {
  140. var columnName = column.mDataProp;
  141. // Create a new cell.
  142. var newCell = $("<td></td>");
  143. // Insert the cell in the current row.
  144. destRow.append(newCell);
  145. // bind the cell to the observable in the current data row.
  146. var accesor = eval("srcData['" + columnName.replace(".", "']['") + "']");
  147. ko.applyBindingsToNode(newCell[0], { text: accesor }, bindingContext.createChildContext(srcData));
  148. });
  149. return next(destRow[0], srcData, displayIndex, displayIndexFull);
  150. });
  151. }
  152. // Before the table has it's rows rendered, we want to scan the table for elements with knockout bindings
  153. // and bind them to the current binding context. This is so you can bind elements like the header row of the
  154. // table to observables your view model. Ideally, it would be great to call ko.applyBindingsToNode here,
  155. // but when we initialise the table with dataTables, it seems dataTables recreates the elements in the table
  156. // during it's initialisation proccess, killing any knockout bindings you apply before initialisation. Instead,
  157. // we mark the elements to bind here with the ko-bind class so we can recognise the elements after the table has been initialised,
  158. // for binding.
  159. $(element).find("[data-bind]").each(function (i, childElement) {
  160. $(childElement).addClass("ko-bind");
  161. });
  162. // Fire the onInitialising event to allow the options object to be globally edited before the dataTables table is initialised. This
  163. // gives third party javascript the ability to apply any additional settings to the dataTable before load.
  164. $(document).trigger(_onInitialisingEventName, { options: options });
  165. var dataTable = $(element).dataTable(options);
  166. setDataTableInstanceOnBinding(dataTable, binding.table);
  167. setDataTableInstance(element, dataTable);
  168. // Apply bindings to those elements that were marked for binding. See comments above.
  169. $(element).find(".ko-bind").each(function (e, childElement) {
  170. ko.applyBindingsToNode(childElement, null, bindingContext);
  171. $(childElement).removeClass("ko-bind");
  172. });
  173. // Tell knockout that the control rendered by this binding is capable of managing the binding of it's descendent elements.
  174. // This is crucial, otherwise knockout will attempt to rebind elements that have been printed by the row template.
  175. return { controlsDescendantBindings: true };
  176. },
  177. getDataTableInstance: function (element) {
  178. return getDataTableInstance(element);
  179. }
  180. };
  181. //// This function transforms the data format that DataTables uses to transfer paging and sorting information to the server
  182. //// to something that is a little easier to work with on the server side. The resulting object should look something like
  183. //// this in C#
  184. //public class DataGridCriteria
  185. //{
  186. // public int RecordsToTake { get; set; }
  187. // public int RecordsToSkip { get; set; }
  188. // public string GlobalSearchText { get; set; }
  189. // public ICollection<DataGridColumnCriteria> Columns { get; set; }
  190. //}
  191. //public class DataGridColumnCriteria
  192. //{
  193. // public string ColumnName { get; set; }
  194. // public bool IsSorted { get; set; }
  195. // public int SortOrder { get; set; }
  196. // public string SearchText { get; set; }
  197. // public bool IsSearchable { get; set; }
  198. // public SortDirection SortDirection { get; set; }
  199. //}
  200. //public enum SortDirection
  201. //{
  202. // Ascending,
  203. // Descending
  204. //}
  205. function convertDataCriteria (srcOptions) {
  206. var getColIndex = function (name) {
  207. var matches = name.match("\\d+");
  208. if (matches && matches.length)
  209. return matches[0];
  210. return null;
  211. }
  212. var destOptions = { Columns: [] };
  213. // Figure out how many columns in in the data table.
  214. for (var i = 0; i < srcOptions.length; i++) {
  215. if (srcOptions[i].name == "iColumns") {
  216. for (var j = 0; j < srcOptions[i].value; j++)
  217. destOptions.Columns.push(new Object());
  218. break;
  219. }
  220. }
  221. ko.utils.arrayForEach(srcOptions, function (item) {
  222. var colIndex = getColIndex(item.name);
  223. if (item.name == "iDisplayStart")
  224. destOptions.RecordsToSkip = item.value;
  225. else if (item.name == "iDisplayLength")
  226. destOptions.RecordsToTake = item.value;
  227. else if (item.name == "sSearch")
  228. destOptions.GlobalSearchText = item.value;
  229. else if (cog.utils.string.startsWith(item.name, "bSearchable_"))
  230. destOptions.Columns[colIndex].IsSearchable = item.value;
  231. else if (cog.utils.string.startsWith(item.name, "sSearch_"))
  232. destOptions.Columns[colIndex].SearchText = item.value;
  233. else if (cog.utils.string.startsWith(item.name, "mDataProp_"))
  234. destOptions.Columns[colIndex].ColumnName = item.value;
  235. else if (cog.utils.string.startsWith(item.name, "iSortCol_")) {
  236. destOptions.Columns[item.value].IsSorted = true;
  237. destOptions.Columns[item.value].SortOrder = colIndex;
  238. var sortOrder = ko.utils.arrayFilter(srcOptions, function (item) {
  239. return item.name == "sSortDir_" + colIndex;
  240. });
  241. if (sortOrder.length && sortOrder[0].value == "desc")
  242. destOptions.Columns[item.value].SortDirection = "Descending";
  243. else
  244. destOptions.Columns[item.value].SortDirection = "Ascending";
  245. }
  246. });
  247. return destOptions;
  248. }
  249. function getDataTableInstance(element) {
  250. return $(element).data(_dataTablesInstanceDataKey);
  251. }
  252. function setDataTableInstance(element, dataTable) {
  253. $(element).data(_dataTablesInstanceDataKey, dataTable);
  254. }
  255. function setDataTableInstanceOnBinding(dataTable, binding) {
  256. if(binding && ko.isObservable(binding)) {
  257. binding(dataTable);
  258. }
  259. }
  260. })();