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