(function (global, factory) { if (typeof define === "function" && define.amd) { define([], factory); } else if (typeof exports !== "undefined") { factory(); } else { var mod = { exports: {} }; factory(); global.bootstrapTablePipeline = mod.exports; } })(this, function () { 'use strict'; /** * @author doug-the-guy * @version v1.0.0 * * Boostrap Table Pipeline * ----------------------- * * This plugin enables client side data caching for server side requests which will * eliminate the need to issue a new request every page change. This will allow * for a performance balance for a large data set between returning all data at once * (client side paging) and a new server side request (server side paging). * * There are two new options: * - usePipeline: enables this feature * - pipelineSize: the size of each cache window * * The size of the pipeline must be evenly divisible by the current page size. This is * assured by rounding up to the nearest evenly divisible value. For example, if * the pipeline size is 4990 and the current page size is 25, then pipeline size will * be dynamically set to 5000. * * The cache windows are computed based on the pipeline size and the total number of rows * returned by the server side query. For example, with pipeline size 500 and total rows * 1300, the cache windows will be: * * [{'lower': 0, 'upper': 499}, {'lower': 500, 'upper': 999}, {'lower': 1000, 'upper': 1499}] * * Using the limit (i.e. the pipelineSize) and offset parameters, the server side request * **MUST** return only the data in the requested cache window **AND** the total number of rows. * To wit, the server side code must use the offset and limit parameters to prepare the response * data. * * On a page change, the new offset is checked if it is within the current cache window. If so, * the requested page data is returned from the cached data set. Otherwise, a new server side * request will be issued for the new cache window. * * The current cached data is only invalidated on these events: * * sorting * * searching * * page size change * * page change moves into a new cache window * * There are two new events: * - cached-data-hit.bs.table: issued when cached data is used on a page change * - cached-data-reset.bs.table: issued when the cached data is invalidated and a * new server side request is issued * **/ (function ($) { 'use strict'; var Utils = $.fn.bootstrapTable.utils; $.extend($.fn.bootstrapTable.defaults, { usePipeline: false, pipelineSize: 1000, onCachedDataHit: function onCachedDataHit(data) { return false; }, onCachedDataReset: function onCachedDataReset(data) { return false; } }); $.extend($.fn.bootstrapTable.Constructor.EVENTS, { 'cached-data-hit.bs.table': 'onCachedDataHit', 'cached-data-reset.bs.table': 'onCachedDataReset' }); var BootstrapTable = $.fn.bootstrapTable.Constructor, _init = BootstrapTable.prototype.init, _initServer = BootstrapTable.prototype.initServer, _onSearch = BootstrapTable.prototype.onSearch, _onSort = BootstrapTable.prototype.onSort, _onPageListChange = BootstrapTable.prototype.onPageListChange; BootstrapTable.prototype.init = function () { // needs to be called before initServer() this.initPipeline(); _init.apply(this, Array.prototype.slice.apply(arguments)); }; BootstrapTable.prototype.initPipeline = function () { this.cacheRequestJSON = {}; this.cacheWindows = []; this.currWindow = 0; this.resetCache = true; }; BootstrapTable.prototype.onSearch = function (event) { /* force a cache reset on search */ if (this.options.usePipeline) { this.resetCache = true; } _onSearch.apply(this, Array.prototype.slice.apply(arguments)); }; BootstrapTable.prototype.onSort = function (event) { /* force a cache reset on sort */ if (this.options.usePipeline) { this.resetCache = true; } _onSort.apply(this, Array.prototype.slice.apply(arguments)); }; BootstrapTable.prototype.onPageListChange = function (event) { /* rebuild cache window on page size change */ var target = $(event.currentTarget); var newPageSize = parseInt(target.text()); this.options.pipelineSize = this.calculatePipelineSize(this.options.pipelineSize, newPageSize); this.resetCache = true; _onPageListChange.apply(this, Array.prototype.slice.apply(arguments)); }; BootstrapTable.prototype.calculatePipelineSize = function (pipelineSize, pageSize) { /* calculate pipeline size by rounding up to the nearest value evenly divisible * by the pageSize */ if (pageSize == 0) return 0; return Math.ceil(pipelineSize / pageSize) * pageSize; }; BootstrapTable.prototype.setCacheWindows = function () { /* set cache windows based on the total number of rows returned by server side * request and the pipelineSize */ this.cacheWindows = []; var numWindows = this.options.totalRows / this.options.pipelineSize; for (var i = 0; i <= numWindows; i++) { var b = i * this.options.pipelineSize; this.cacheWindows[i] = { 'lower': b, 'upper': b + this.options.pipelineSize - 1 }; } }; BootstrapTable.prototype.setCurrWindow = function (offset) { /* set the current cache window index, based on where the current offset falls */ this.currWindow = 0; for (var i = 0; i < this.cacheWindows.length; i++) { if (this.cacheWindows[i].lower <= offset && offset <= this.cacheWindows[i].upper) { this.currWindow = i; break; } } }; BootstrapTable.prototype.drawFromCache = function (offset, limit) { /* draw rows from the cache using offset and limit */ var res = $.extend(true, {}, this.cacheRequestJSON); var drawStart = offset - this.cacheWindows[this.currWindow].lower; var drawEnd = drawStart + limit; res.rows = res.rows.slice(drawStart, drawEnd); return res; }; BootstrapTable.prototype.initServer = function (silent, query, url) { /* determine if requested data is in cache (on paging) or if * a new ajax request needs to be issued (sorting, searching, paging * moving outside of cached data, page size change) * initial version of this extension will entirely override base initServer **/ var data = {}; var index = this.header.fields.indexOf(this.options.sortName); var params = { searchText: this.searchText, sortName: this.options.sortName, sortOrder: this.options.sortOrder }; var request = null; if (this.header.sortNames[index]) { params.sortName = this.header.sortNames[index]; } if (this.options.pagination && this.options.sidePagination === 'server') { params.pageSize = this.options.pageSize === this.options.formatAllRows() ? this.options.totalRows : this.options.pageSize; params.pageNumber = this.options.pageNumber; } if (!(url || this.options.url) && !this.options.ajax) { return; } var useAjax = true; if (this.options.queryParamsType === 'limit') { params = { searchText: params.searchText, sortName: params.sortName, sortOrder: params.sortOrder }; if (this.options.pagination && this.options.sidePagination === 'server') { params.limit = this.options.pageSize === this.options.formatAllRows() ? this.options.totalRows : this.options.pageSize; params.offset = (this.options.pageSize === this.options.formatAllRows() ? this.options.totalRows : this.options.pageSize) * (this.options.pageNumber - 1); if (this.options.usePipeline) { // if cacheWindows is empty, this is the initial request if (!this.cacheWindows.length) { useAjax = true; params.drawOffset = params.offset; // cache exists: determine if the page request is entirely within the current cached window } else { var w = this.cacheWindows[this.currWindow]; // case 1: reset cache but stay within current window (e.g. column sort) // case 2: move outside of the current window (e.g. search or paging) // since each cache window is aligned with the current page size // checking if params.offset is outside the current window is sufficient. // need to requery for preceding or succeeding cache window // also handle case if (this.resetCache || params.offset < w.lower || params.offset > w.upper) { useAjax = true; this.setCurrWindow(params.offset); // store the relative offset for drawing the page data afterwards params.drawOffset = params.offset; // now set params.offset to the lower bound of the new cache window // the server will return that whole cache window params.offset = this.cacheWindows[this.currWindow].lower; // within current cache window } else { useAjax = false; } } } else { if (params.limit === 0) { delete params.limit; } } } } // force an ajax call - this is on search, sort or page size change if (this.resetCache) { useAjax = true; this.resetCache = false; } if (this.options.usePipeline && useAjax) { /* in this scenario limit is used on the server to get the cache window * and drawLimit is used to get the page data afterwards */ params.drawLimit = params.limit; params.limit = this.options.pipelineSize; } // cached results can be used if (!useAjax) { var res = this.drawFromCache(params.offset, params.limit); this.load(res); this.trigger('load-success', res); this.trigger('cached-data-hit', res); return; } // cached results can't be used // continue base initServer code if (!$.isEmptyObject(this.filterColumnsPartial)) { params.filter = JSON.stringify(this.filterColumnsPartial, null); } data = Utils.calculateObjectValue(this.options, this.options.queryParams, [params], data); $.extend(data, query || {}); // false to stop request if (data === false) { return; } if (!silent) { this.$tableLoading.show(); } var self = this; request = $.extend({}, Utils.calculateObjectValue(null, this.options.ajaxOptions), { type: this.options.method, url: url || this.options.url, data: this.options.contentType === 'application/json' && this.options.method === 'post' ? JSON.stringify(data) : data, cache: this.options.cache, contentType: this.options.contentType, dataType: this.options.dataType, success: function success(res) { res = Utils.calculateObjectValue(self.options, self.options.responseHandler, [res], res); // cache results if using pipelining if (self.options.usePipeline) { // store entire request in cache self.cacheRequestJSON = $.extend(true, {}, res); // this gets set in load() also but needs to be set before // setting cacheWindows self.options.totalRows = res[self.options.totalField]; // if this is a search, potentially less results will be returned // so cache windows need to be rebuilt. Otherwise it // will come out the same self.setCacheWindows(); self.setCurrWindow(params.drawOffset); // just load data for the page res = self.drawFromCache(params.drawOffset, params.drawLimit); self.trigger('cached-data-reset', res); } self.load(res); self.trigger('load-success', res); if (!silent) self.$tableLoading.hide(); }, error: function error(res) { var data = []; if (self.options.sidePagination === 'server') { data = {}; data[self.options.totalField] = 0; data[self.options.dataField] = []; } self.load(data); self.trigger('load-error', res.status, res); if (!silent) self.$tableLoading.hide(); } }); if (this.options.ajax) { Utils.calculateObjectValue(this, this.options.ajax, [request], null); } else { if (this._xhr && this._xhr.readyState !== 4) { this._xhr.abort(); } this._xhr = $.ajax(request); } }; $.fn.bootstrapTable.methods.push(); })(jQuery); });