bootstrap-table-pipeline.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. (function (global, factory) {
  2. if (typeof define === "function" && define.amd) {
  3. define([], factory);
  4. } else if (typeof exports !== "undefined") {
  5. factory();
  6. } else {
  7. var mod = {
  8. exports: {}
  9. };
  10. factory();
  11. global.bootstrapTablePipeline = mod.exports;
  12. }
  13. })(this, function () {
  14. 'use strict';
  15. /**
  16. * @author doug-the-guy
  17. * @version v1.0.0
  18. *
  19. * Boostrap Table Pipeline
  20. * -----------------------
  21. *
  22. * This plugin enables client side data caching for server side requests which will
  23. * eliminate the need to issue a new request every page change. This will allow
  24. * for a performance balance for a large data set between returning all data at once
  25. * (client side paging) and a new server side request (server side paging).
  26. *
  27. * There are two new options:
  28. * - usePipeline: enables this feature
  29. * - pipelineSize: the size of each cache window
  30. *
  31. * The size of the pipeline must be evenly divisible by the current page size. This is
  32. * assured by rounding up to the nearest evenly divisible value. For example, if
  33. * the pipeline size is 4990 and the current page size is 25, then pipeline size will
  34. * be dynamically set to 5000.
  35. *
  36. * The cache windows are computed based on the pipeline size and the total number of rows
  37. * returned by the server side query. For example, with pipeline size 500 and total rows
  38. * 1300, the cache windows will be:
  39. *
  40. * [{'lower': 0, 'upper': 499}, {'lower': 500, 'upper': 999}, {'lower': 1000, 'upper': 1499}]
  41. *
  42. * Using the limit (i.e. the pipelineSize) and offset parameters, the server side request
  43. * **MUST** return only the data in the requested cache window **AND** the total number of rows.
  44. * To wit, the server side code must use the offset and limit parameters to prepare the response
  45. * data.
  46. *
  47. * On a page change, the new offset is checked if it is within the current cache window. If so,
  48. * the requested page data is returned from the cached data set. Otherwise, a new server side
  49. * request will be issued for the new cache window.
  50. *
  51. * The current cached data is only invalidated on these events:
  52. * * sorting
  53. * * searching
  54. * * page size change
  55. * * page change moves into a new cache window
  56. *
  57. * There are two new events:
  58. * - cached-data-hit.bs.table: issued when cached data is used on a page change
  59. * - cached-data-reset.bs.table: issued when the cached data is invalidated and a
  60. * new server side request is issued
  61. *
  62. **/
  63. (function ($) {
  64. 'use strict';
  65. var Utils = $.fn.bootstrapTable.utils;
  66. $.extend($.fn.bootstrapTable.defaults, {
  67. usePipeline: false,
  68. pipelineSize: 1000,
  69. onCachedDataHit: function onCachedDataHit(data) {
  70. return false;
  71. },
  72. onCachedDataReset: function onCachedDataReset(data) {
  73. return false;
  74. }
  75. });
  76. $.extend($.fn.bootstrapTable.Constructor.EVENTS, {
  77. 'cached-data-hit.bs.table': 'onCachedDataHit',
  78. 'cached-data-reset.bs.table': 'onCachedDataReset'
  79. });
  80. var BootstrapTable = $.fn.bootstrapTable.Constructor,
  81. _init = BootstrapTable.prototype.init,
  82. _initServer = BootstrapTable.prototype.initServer,
  83. _onSearch = BootstrapTable.prototype.onSearch,
  84. _onSort = BootstrapTable.prototype.onSort,
  85. _onPageListChange = BootstrapTable.prototype.onPageListChange;
  86. BootstrapTable.prototype.init = function () {
  87. // needs to be called before initServer()
  88. this.initPipeline();
  89. _init.apply(this, Array.prototype.slice.apply(arguments));
  90. };
  91. BootstrapTable.prototype.initPipeline = function () {
  92. this.cacheRequestJSON = {};
  93. this.cacheWindows = [];
  94. this.currWindow = 0;
  95. this.resetCache = true;
  96. };
  97. BootstrapTable.prototype.onSearch = function (event) {
  98. /* force a cache reset on search */
  99. if (this.options.usePipeline) {
  100. this.resetCache = true;
  101. }
  102. _onSearch.apply(this, Array.prototype.slice.apply(arguments));
  103. };
  104. BootstrapTable.prototype.onSort = function (event) {
  105. /* force a cache reset on sort */
  106. if (this.options.usePipeline) {
  107. this.resetCache = true;
  108. }
  109. _onSort.apply(this, Array.prototype.slice.apply(arguments));
  110. };
  111. BootstrapTable.prototype.onPageListChange = function (event) {
  112. /* rebuild cache window on page size change */
  113. var target = $(event.currentTarget);
  114. var newPageSize = parseInt(target.text());
  115. this.options.pipelineSize = this.calculatePipelineSize(this.options.pipelineSize, newPageSize);
  116. this.resetCache = true;
  117. _onPageListChange.apply(this, Array.prototype.slice.apply(arguments));
  118. };
  119. BootstrapTable.prototype.calculatePipelineSize = function (pipelineSize, pageSize) {
  120. /* calculate pipeline size by rounding up to the nearest value evenly divisible
  121. * by the pageSize */
  122. if (pageSize == 0) return 0;
  123. return Math.ceil(pipelineSize / pageSize) * pageSize;
  124. };
  125. BootstrapTable.prototype.setCacheWindows = function () {
  126. /* set cache windows based on the total number of rows returned by server side
  127. * request and the pipelineSize */
  128. this.cacheWindows = [];
  129. var numWindows = this.options.totalRows / this.options.pipelineSize;
  130. for (var i = 0; i <= numWindows; i++) {
  131. var b = i * this.options.pipelineSize;
  132. this.cacheWindows[i] = { 'lower': b, 'upper': b + this.options.pipelineSize - 1 };
  133. }
  134. };
  135. BootstrapTable.prototype.setCurrWindow = function (offset) {
  136. /* set the current cache window index, based on where the current offset falls */
  137. this.currWindow = 0;
  138. for (var i = 0; i < this.cacheWindows.length; i++) {
  139. if (this.cacheWindows[i].lower <= offset && offset <= this.cacheWindows[i].upper) {
  140. this.currWindow = i;
  141. break;
  142. }
  143. }
  144. };
  145. BootstrapTable.prototype.drawFromCache = function (offset, limit) {
  146. /* draw rows from the cache using offset and limit */
  147. var res = $.extend(true, {}, this.cacheRequestJSON);
  148. var drawStart = offset - this.cacheWindows[this.currWindow].lower;
  149. var drawEnd = drawStart + limit;
  150. res.rows = res.rows.slice(drawStart, drawEnd);
  151. return res;
  152. };
  153. BootstrapTable.prototype.initServer = function (silent, query, url) {
  154. /* determine if requested data is in cache (on paging) or if
  155. * a new ajax request needs to be issued (sorting, searching, paging
  156. * moving outside of cached data, page size change)
  157. * initial version of this extension will entirely override base initServer
  158. **/
  159. var data = {};
  160. var index = this.header.fields.indexOf(this.options.sortName);
  161. var params = {
  162. searchText: this.searchText,
  163. sortName: this.options.sortName,
  164. sortOrder: this.options.sortOrder
  165. };
  166. var request = null;
  167. if (this.header.sortNames[index]) {
  168. params.sortName = this.header.sortNames[index];
  169. }
  170. if (this.options.pagination && this.options.sidePagination === 'server') {
  171. params.pageSize = this.options.pageSize === this.options.formatAllRows() ? this.options.totalRows : this.options.pageSize;
  172. params.pageNumber = this.options.pageNumber;
  173. }
  174. if (!(url || this.options.url) && !this.options.ajax) {
  175. return;
  176. }
  177. var useAjax = true;
  178. if (this.options.queryParamsType === 'limit') {
  179. params = {
  180. searchText: params.searchText,
  181. sortName: params.sortName,
  182. sortOrder: params.sortOrder
  183. };
  184. if (this.options.pagination && this.options.sidePagination === 'server') {
  185. params.limit = this.options.pageSize === this.options.formatAllRows() ? this.options.totalRows : this.options.pageSize;
  186. params.offset = (this.options.pageSize === this.options.formatAllRows() ? this.options.totalRows : this.options.pageSize) * (this.options.pageNumber - 1);
  187. if (this.options.usePipeline) {
  188. // if cacheWindows is empty, this is the initial request
  189. if (!this.cacheWindows.length) {
  190. useAjax = true;
  191. params.drawOffset = params.offset;
  192. // cache exists: determine if the page request is entirely within the current cached window
  193. } else {
  194. var w = this.cacheWindows[this.currWindow];
  195. // case 1: reset cache but stay within current window (e.g. column sort)
  196. // case 2: move outside of the current window (e.g. search or paging)
  197. // since each cache window is aligned with the current page size
  198. // checking if params.offset is outside the current window is sufficient.
  199. // need to requery for preceding or succeeding cache window
  200. // also handle case
  201. if (this.resetCache || params.offset < w.lower || params.offset > w.upper) {
  202. useAjax = true;
  203. this.setCurrWindow(params.offset);
  204. // store the relative offset for drawing the page data afterwards
  205. params.drawOffset = params.offset;
  206. // now set params.offset to the lower bound of the new cache window
  207. // the server will return that whole cache window
  208. params.offset = this.cacheWindows[this.currWindow].lower;
  209. // within current cache window
  210. } else {
  211. useAjax = false;
  212. }
  213. }
  214. } else {
  215. if (params.limit === 0) {
  216. delete params.limit;
  217. }
  218. }
  219. }
  220. }
  221. // force an ajax call - this is on search, sort or page size change
  222. if (this.resetCache) {
  223. useAjax = true;
  224. this.resetCache = false;
  225. }
  226. if (this.options.usePipeline && useAjax) {
  227. /* in this scenario limit is used on the server to get the cache window
  228. * and drawLimit is used to get the page data afterwards */
  229. params.drawLimit = params.limit;
  230. params.limit = this.options.pipelineSize;
  231. }
  232. // cached results can be used
  233. if (!useAjax) {
  234. var res = this.drawFromCache(params.offset, params.limit);
  235. this.load(res);
  236. this.trigger('load-success', res);
  237. this.trigger('cached-data-hit', res);
  238. return;
  239. }
  240. // cached results can't be used
  241. // continue base initServer code
  242. if (!$.isEmptyObject(this.filterColumnsPartial)) {
  243. params.filter = JSON.stringify(this.filterColumnsPartial, null);
  244. }
  245. data = Utils.calculateObjectValue(this.options, this.options.queryParams, [params], data);
  246. $.extend(data, query || {});
  247. // false to stop request
  248. if (data === false) {
  249. return;
  250. }
  251. if (!silent) {
  252. this.$tableLoading.show();
  253. }
  254. var self = this;
  255. request = $.extend({}, Utils.calculateObjectValue(null, this.options.ajaxOptions), {
  256. type: this.options.method,
  257. url: url || this.options.url,
  258. data: this.options.contentType === 'application/json' && this.options.method === 'post' ? JSON.stringify(data) : data,
  259. cache: this.options.cache,
  260. contentType: this.options.contentType,
  261. dataType: this.options.dataType,
  262. success: function success(res) {
  263. res = Utils.calculateObjectValue(self.options, self.options.responseHandler, [res], res);
  264. // cache results if using pipelining
  265. if (self.options.usePipeline) {
  266. // store entire request in cache
  267. self.cacheRequestJSON = $.extend(true, {}, res);
  268. // this gets set in load() also but needs to be set before
  269. // setting cacheWindows
  270. self.options.totalRows = res[self.options.totalField];
  271. // if this is a search, potentially less results will be returned
  272. // so cache windows need to be rebuilt. Otherwise it
  273. // will come out the same
  274. self.setCacheWindows();
  275. self.setCurrWindow(params.drawOffset);
  276. // just load data for the page
  277. res = self.drawFromCache(params.drawOffset, params.drawLimit);
  278. self.trigger('cached-data-reset', res);
  279. }
  280. self.load(res);
  281. self.trigger('load-success', res);
  282. if (!silent) self.$tableLoading.hide();
  283. },
  284. error: function error(res) {
  285. var data = [];
  286. if (self.options.sidePagination === 'server') {
  287. data = {};
  288. data[self.options.totalField] = 0;
  289. data[self.options.dataField] = [];
  290. }
  291. self.load(data);
  292. self.trigger('load-error', res.status, res);
  293. if (!silent) self.$tableLoading.hide();
  294. }
  295. });
  296. if (this.options.ajax) {
  297. Utils.calculateObjectValue(this, this.options.ajax, [request], null);
  298. } else {
  299. if (this._xhr && this._xhr.readyState !== 4) {
  300. this._xhr.abort();
  301. }
  302. this._xhr = $.ajax(request);
  303. }
  304. };
  305. $.fn.bootstrapTable.methods.push();
  306. })(jQuery);
  307. });