PageRenderTime 91ms CodeModel.GetById 33ms RepoModel.GetById 0ms app.codeStats 0ms

/files/backbone.paginator/0.8/backbone.paginator.js

https://gitlab.com/Mirros/jsdelivr
JavaScript | 1050 lines | 695 code | 188 blank | 167 comment | 163 complexity | d95877dc0b0f20821ba60db3cfd7d176 MD5 | raw file
  1. /*! backbone.paginator - v0.8.0 - 6/14/2013
  2. * http://github.com/addyosmani/backbone.paginator
  3. * Copyright (c) 2013 Addy Osmani; Licensed MIT */
  4. /*globals Backbone:true, _:true, jQuery:true*/
  5. Backbone.Paginator = (function ( Backbone, _, $ ) {
  6. "use strict";
  7. var bbVer = _.map(Backbone.VERSION.split('.'), function(digit) {
  8. return parseInt(digit, 10);
  9. });
  10. var Paginator = {};
  11. Paginator.version = "0.8.0";
  12. // @name: clientPager
  13. //
  14. // @tagline: Paginator for client-side data
  15. //
  16. // @description:
  17. // This paginator is responsible for providing pagination
  18. // and sort capabilities for a single payload of data
  19. // we wish to paginate by the UI for easier browsering.
  20. //
  21. Paginator.clientPager = Backbone.Collection.extend({
  22. // DEFAULTS FOR SORTING & FILTERING
  23. useDiacriticsPlugin: true, // use diacritics plugin if available
  24. useLevenshteinPlugin: true, // use levenshtein plugin if available
  25. sortColumn: "",
  26. sortDirection: "desc",
  27. lastSortColumn: "",
  28. fieldFilterRules: [],
  29. lastFieldFilterRules: [],
  30. filterFields: "",
  31. filterExpression: "",
  32. lastFilterExpression: "",
  33. //DEFAULT PAGINATOR UI VALUES
  34. defaults_ui: {
  35. firstPage: 0,
  36. currentPage: 1,
  37. perPage: 5,
  38. totalPages: 10,
  39. pagesInRange: 4
  40. },
  41. // Default values used when sorting and/or filtering.
  42. initialize: function(){
  43. //LISTEN FOR ADD & REMOVE EVENTS THEN REMOVE MODELS FROM ORGINAL MODELS
  44. this.on('add', this.addModel, this);
  45. this.on('remove', this.removeModel, this);
  46. // SET DEFAULT VALUES (ALLOWS YOU TO POPULATE PAGINATOR MAUNALLY)
  47. this.setDefaults();
  48. },
  49. setDefaults: function() {
  50. // SET DEFAULT UI SETTINGS
  51. var options = _.defaults(this.paginator_ui, this.defaults_ui);
  52. //UPDATE GLOBAL UI SETTINGS
  53. _.defaults(this, options);
  54. },
  55. addModel: function(model) {
  56. this.origModels.push(model);
  57. },
  58. removeModel: function(model) {
  59. var index = _.indexOf(this.origModels, model);
  60. this.origModels.splice(index, 1);
  61. },
  62. sync: function ( method, model, options ) {
  63. var self = this;
  64. // SET DEFAULT VALUES
  65. this.setDefaults();
  66. // Some values could be functions, let's make sure
  67. // to change their scope too and run them
  68. var queryAttributes = {};
  69. _.each(_.result(self, "server_api"), function(value, key){
  70. if( _.isFunction(value) ) {
  71. value = _.bind(value, self);
  72. value = value();
  73. }
  74. queryAttributes[key] = value;
  75. });
  76. var queryOptions = _.clone(self.paginator_core);
  77. _.each(queryOptions, function(value, key){
  78. if( _.isFunction(value) ) {
  79. value = _.bind(value, self);
  80. value = value();
  81. }
  82. queryOptions[key] = value;
  83. });
  84. // Create default values if no others are specified
  85. queryOptions = _.defaults(queryOptions, {
  86. timeout: 25000,
  87. cache: false,
  88. type: 'GET',
  89. dataType: 'jsonp'
  90. });
  91. queryOptions = _.extend(queryOptions, {
  92. data: decodeURIComponent($.param(queryAttributes)),
  93. processData: false,
  94. url: _.result(queryOptions, 'url')
  95. }, options);
  96. var promiseSuccessFormat = !(bbVer[0] === 0 &&
  97. bbVer[1] === 9 &&
  98. bbVer[2] === 10);
  99. var success = queryOptions.success;
  100. queryOptions.success = function ( resp, status, xhr ) {
  101. if ( success ) {
  102. // This is to keep compatibility with Backbone 0.9.10
  103. if (promiseSuccessFormat) {
  104. success( resp, status, xhr );
  105. } else {
  106. success( model, resp, queryOptions );
  107. }
  108. }
  109. if ( model && model.trigger ) {
  110. model.trigger( 'sync', model, resp, queryOptions );
  111. }
  112. };
  113. var error = queryOptions.error;
  114. queryOptions.error = function ( xhr ) {
  115. if ( error ) {
  116. error( model, xhr, queryOptions );
  117. }
  118. if ( model && model.trigger ) {
  119. model.trigger( 'error', model, xhr, queryOptions );
  120. }
  121. };
  122. var xhr = queryOptions.xhr = Backbone.ajax( queryOptions );
  123. if ( model && model.trigger ) {
  124. model.trigger('request', model, xhr, queryOptions);
  125. }
  126. return xhr;
  127. },
  128. nextPage: function (options) {
  129. if(this.currentPage < this.information.totalPages) {
  130. this.currentPage = ++this.currentPage;
  131. this.pager(options);
  132. }
  133. },
  134. previousPage: function (options) {
  135. if(this.currentPage > 1) {
  136. this.currentPage = --this.currentPage;
  137. this.pager(options);
  138. }
  139. },
  140. goTo: function ( page, options ) {
  141. if(page !== undefined){
  142. this.currentPage = parseInt(page, 10);
  143. this.pager(options);
  144. }
  145. },
  146. howManyPer: function ( perPage ) {
  147. if(perPage !== undefined){
  148. var lastPerPage = this.perPage;
  149. this.perPage = parseInt(perPage, 10);
  150. this.currentPage = Math.ceil( ( lastPerPage * ( this.currentPage - 1 ) + 1 ) / perPage);
  151. this.pager();
  152. }
  153. },
  154. // setSort is used to sort the current model. After
  155. // passing 'column', which is the model's field you want
  156. // to filter and 'direction', which is the direction
  157. // desired for the ordering ('asc' or 'desc'), pager()
  158. // and info() will be called automatically.
  159. setSort: function ( column, direction ) {
  160. if(column !== undefined && direction !== undefined){
  161. this.lastSortColumn = this.sortColumn;
  162. this.sortColumn = column;
  163. this.sortDirection = direction;
  164. this.pager();
  165. this.info();
  166. }
  167. },
  168. // setFieldFilter is used to filter each value of each model
  169. // according to `rules` that you pass as argument.
  170. // Example: You have a collection of books with 'release year' and 'author'.
  171. // You can filter only the books that were released between 1999 and 2003
  172. // And then you can add another `rule` that will filter those books only to
  173. // authors who's name start with 'A'.
  174. setFieldFilter: function ( fieldFilterRules ) {
  175. if( !_.isEmpty( fieldFilterRules ) ) {
  176. this.lastFieldFilterRules = this.fieldFilterRules;
  177. this.fieldFilterRules = fieldFilterRules;
  178. this.pager();
  179. this.info();
  180. // if all the filters are removed, we should save the last filter
  181. // and then let the list reset to it's original state.
  182. } else {
  183. this.lastFieldFilterRules = this.fieldFilterRules;
  184. this.fieldFilterRules = '';
  185. this.pager();
  186. this.info();
  187. }
  188. },
  189. // doFakeFieldFilter can be used to get the number of models that will remain
  190. // after calling setFieldFilter with a filter rule(s)
  191. doFakeFieldFilter: function ( rules ) {
  192. if( !_.isEmpty( rules ) ) {
  193. var testModels = this.origModels;
  194. if (testModels === undefined) {
  195. testModels = this.models;
  196. }
  197. testModels = this._fieldFilter(testModels, rules);
  198. // To comply with current behavior, also filter by any previously defined setFilter rules.
  199. if ( this.filterExpression !== "" ) {
  200. testModels = this._filter(testModels, this.filterFields, this.filterExpression);
  201. }
  202. // Return size
  203. return testModels.length;
  204. }
  205. },
  206. // setFilter is used to filter the current model. After
  207. // passing 'fields', which can be a string referring to
  208. // the model's field, an array of strings representing
  209. // each of the model's fields or an object with the name
  210. // of the model's field(s) and comparing options (see docs)
  211. // you wish to filter by and
  212. // 'filter', which is the word or words you wish to
  213. // filter by, pager() and info() will be called automatically.
  214. setFilter: function ( fields, filter ) {
  215. if( fields !== undefined && filter !== undefined ){
  216. this.filterFields = fields;
  217. this.lastFilterExpression = this.filterExpression;
  218. this.filterExpression = filter;
  219. this.pager();
  220. this.info();
  221. }
  222. },
  223. // doFakeFilter can be used to get the number of models that will
  224. // remain after calling setFilter with a `fields` and `filter` args.
  225. doFakeFilter: function ( fields, filter ) {
  226. if( fields !== undefined && filter !== undefined ){
  227. var testModels = this.origModels;
  228. if (testModels === undefined) {
  229. testModels = this.models;
  230. }
  231. // To comply with current behavior, first filter by any previously defined setFieldFilter rules.
  232. if ( !_.isEmpty( this.fieldFilterRules ) ) {
  233. testModels = this._fieldFilter(testModels, this.fieldFilterRules);
  234. }
  235. testModels = this._filter(testModels, fields, filter);
  236. // Return size
  237. return testModels.length;
  238. }
  239. },
  240. // pager is used to sort, filter and show the data
  241. // you expect the library to display.
  242. pager: function (options) {
  243. var self = this,
  244. disp = this.perPage,
  245. start = (self.currentPage - 1) * disp,
  246. stop = start + disp;
  247. // Saving the original models collection is important
  248. // as we could need to sort or filter, and we don't want
  249. // to loose the data we fetched from the server.
  250. if (self.origModels === undefined) {
  251. self.origModels = self.models;
  252. }
  253. self.models = self.origModels.slice();
  254. // Check if sorting was set using setSort.
  255. if ( this.sortColumn !== "" ) {
  256. self.models = self._sort(self.models, this.sortColumn, this.sortDirection);
  257. }
  258. // Check if field-filtering was set using setFieldFilter
  259. if ( !_.isEmpty( this.fieldFilterRules ) ) {
  260. self.models = self._fieldFilter(self.models, this.fieldFilterRules);
  261. }
  262. // Check if filtering was set using setFilter.
  263. if ( this.filterExpression !== "" ) {
  264. self.models = self._filter(self.models, this.filterFields, this.filterExpression);
  265. }
  266. // If the sorting or the filtering was changed go to the first page
  267. if ( this.lastSortColumn !== this.sortColumn || this.lastFilterExpression !== this.filterExpression || !_.isEqual(this.fieldFilterRules, this.lastFieldFilterRules) ) {
  268. start = 0;
  269. stop = start + disp;
  270. self.currentPage = 1;
  271. this.lastSortColumn = this.sortColumn;
  272. this.lastFieldFilterRules = this.fieldFilterRules;
  273. this.lastFilterExpression = this.filterExpression;
  274. }
  275. // We need to save the sorted and filtered models collection
  276. // because we'll use that sorted and filtered collection in info().
  277. self.sortedAndFilteredModels = self.models.slice();
  278. self.info();
  279. self.reset(self.models.slice(start, stop));
  280. // This is somewhat of a hack to get all the nextPage, prevPage, and goTo methods
  281. // to work with a success callback (as in the requestPager). Realistically there is no failure case here,
  282. // but maybe we could catch exception and trigger a failure callback?
  283. _.result(options, 'success');
  284. },
  285. // The actual place where the collection is sorted.
  286. // Check setSort for arguments explicacion.
  287. _sort: function ( models, sort, direction ) {
  288. models = models.sort(function (a, b) {
  289. var ac = a.get(sort),
  290. bc = b.get(sort);
  291. if ( _.isUndefined(ac) || _.isUndefined(bc) || ac === null || bc === null ) {
  292. return 0;
  293. } else {
  294. /* Make sure that both ac and bc are lowercase strings.
  295. * .toString() first so we don't have to worry if ac or bc
  296. * have other String-only methods.
  297. */
  298. ac = ac.toString().toLowerCase();
  299. bc = bc.toString().toLowerCase();
  300. }
  301. if (direction === 'desc') {
  302. // We need to know if there aren't any non-number characters
  303. // and that there are numbers-only characters and maybe a dot
  304. // if we have a float.
  305. // Oh, also a '-' for negative numbers!
  306. if((!ac.match(/[^\-\d\.]/) && ac.match(/-?[\d\.]+/)) &&
  307. (!bc.match(/[^\-\d\.]/) && bc.match(/-?[\d\.]+/))){
  308. if( (ac - 0) < (bc - 0) ) {
  309. return 1;
  310. }
  311. if( (ac - 0) > (bc - 0) ) {
  312. return -1;
  313. }
  314. } else {
  315. if (ac < bc) {
  316. return 1;
  317. }
  318. if (ac > bc) {
  319. return -1;
  320. }
  321. }
  322. } else {
  323. //Same as the regexp check in the 'if' part.
  324. if((!ac.match(/[^\-\d\.]/) && ac.match(/-?[\d\.]+/)) &&
  325. (!bc.match(/[^\-\d\.]/) && bc.match(/-?[\d\.]+/))){
  326. if( (ac - 0) < (bc - 0) ) {
  327. return -1;
  328. }
  329. if( (ac - 0) > (bc - 0) ) {
  330. return 1;
  331. }
  332. } else {
  333. if (ac < bc) {
  334. return -1;
  335. }
  336. if (ac > bc) {
  337. return 1;
  338. }
  339. }
  340. }
  341. if (a.cid && b.cid){
  342. var aId = a.cid,
  343. bId = b.cid;
  344. if (aId < bId) {
  345. return -1;
  346. }
  347. if (aId > bId) {
  348. return 1;
  349. }
  350. }
  351. return 0;
  352. });
  353. return models;
  354. },
  355. // The actual place where the collection is field-filtered.
  356. // Check setFieldFilter for arguments explicacion.
  357. _fieldFilter: function( models, rules ) {
  358. // Check if there are any rules
  359. if ( _.isEmpty(rules) ) {
  360. return models;
  361. }
  362. var filteredModels = [];
  363. // Iterate over each rule
  364. _.each(models, function(model){
  365. var should_push = true;
  366. // Apply each rule to each model in the collection
  367. _.each(rules, function(rule){
  368. // Don't go inside the switch if we're already sure that the model won't be included in the results
  369. if( !should_push ){
  370. return false;
  371. }
  372. should_push = false;
  373. // The field's value will be passed to a custom function, which should
  374. // return true (if model should be included) or false (model should be ignored)
  375. if(rule.type === "function"){
  376. var f = _.wrap(rule.value, function(func){
  377. return func( model.get(rule.field) );
  378. });
  379. if( f() ){
  380. should_push = true;
  381. }
  382. // The field's value is required to be non-empty
  383. }else if(rule.type === "required"){
  384. if( !_.isEmpty( model.get(rule.field).toString() ) ) {
  385. should_push = true;
  386. }
  387. // The field's value is required to be greater tan N (numbers only)
  388. }else if(rule.type === "min"){
  389. if( !_.isNaN( Number( model.get(rule.field) ) ) &&
  390. !_.isNaN( Number( rule.value ) ) &&
  391. Number( model.get(rule.field) ) >= Number( rule.value ) ) {
  392. should_push = true;
  393. }
  394. // The field's value is required to be smaller tan N (numbers only)
  395. }else if(rule.type === "max"){
  396. if( !_.isNaN( Number( model.get(rule.field) ) ) &&
  397. !_.isNaN( Number( rule.value ) ) &&
  398. Number( model.get(rule.field) ) <= Number( rule.value ) ) {
  399. should_push = true;
  400. }
  401. // The field's value is required to be between N and M (numbers only)
  402. }else if(rule.type === "range"){
  403. if( !_.isNaN( Number( model.get(rule.field) ) ) &&
  404. _.isObject( rule.value ) &&
  405. !_.isNaN( Number( rule.value.min ) ) &&
  406. !_.isNaN( Number( rule.value.max ) ) &&
  407. Number( model.get(rule.field) ) >= Number( rule.value.min ) &&
  408. Number( model.get(rule.field) ) <= Number( rule.value.max ) ) {
  409. should_push = true;
  410. }
  411. // The field's value is required to be more than N chars long
  412. }else if(rule.type === "minLength"){
  413. if( model.get(rule.field).toString().length >= rule.value ) {
  414. should_push = true;
  415. }
  416. // The field's value is required to be no more than N chars long
  417. }else if(rule.type === "maxLength"){
  418. if( model.get(rule.field).toString().length <= rule.value ) {
  419. should_push = true;
  420. }
  421. // The field's value is required to be more than N chars long and no more than M chars long
  422. }else if(rule.type === "rangeLength"){
  423. if( _.isObject( rule.value ) &&
  424. !_.isNaN( Number( rule.value.min ) ) &&
  425. !_.isNaN( Number( rule.value.max ) ) &&
  426. model.get(rule.field).toString().length >= rule.value.min &&
  427. model.get(rule.field).toString().length <= rule.value.max ) {
  428. should_push = true;
  429. }
  430. // The field's value is required to be equal to one of the values in rules.value
  431. }else if(rule.type === "oneOf"){
  432. if( _.isArray( rule.value ) &&
  433. _.include( rule.value, model.get(rule.field) ) ) {
  434. should_push = true;
  435. }
  436. // The field's value is required to be equal to the value in rules.value
  437. }else if(rule.type === "equalTo"){
  438. if( rule.value === model.get(rule.field) ) {
  439. should_push = true;
  440. }
  441. }else if(rule.type === "containsAllOf"){
  442. if( _.isArray( rule.value ) &&
  443. _.isArray(model.get(rule.field)) &&
  444. _.intersection( rule.value, model.get(rule.field)).length === rule.value.length) {
  445. should_push = true;
  446. }
  447. // The field's value is required to match the regular expression
  448. }else if(rule.type === "pattern"){
  449. if( model.get(rule.field).toString().match(rule.value) ) {
  450. should_push = true;
  451. }
  452. //Unknown type
  453. }else{
  454. should_push = false;
  455. }
  456. });
  457. if( should_push ){
  458. filteredModels.push(model);
  459. }
  460. });
  461. return filteredModels;
  462. },
  463. // The actual place where the collection is filtered.
  464. // Check setFilter for arguments explicacion.
  465. _filter: function ( models, fields, filter ) {
  466. // For example, if you had a data model containing cars like { color: '', description: '', hp: '' },
  467. // your fields was set to ['color', 'description', 'hp'] and your filter was set
  468. // to "Black Mustang 300", the word "Black" will match all the cars that have black color, then
  469. // "Mustang" in the description and then the HP in the 'hp' field.
  470. // NOTE: "Black Musta 300" will return the same as "Black Mustang 300"
  471. // We accept fields to be a string, an array or an object
  472. // but if string or array is passed we need to convert it
  473. // to an object.
  474. var self = this;
  475. var obj_fields = {};
  476. if( _.isString( fields ) ) {
  477. obj_fields[fields] = {cmp_method: 'regexp'};
  478. }else if( _.isArray( fields ) ) {
  479. _.each(fields, function(field){
  480. obj_fields[field] = {cmp_method: 'regexp'};
  481. });
  482. }else{
  483. _.each(fields, function( cmp_opts, field ) {
  484. obj_fields[field] = _.defaults(cmp_opts, { cmp_method: 'regexp' });
  485. });
  486. }
  487. fields = obj_fields;
  488. //Remove diacritic characters if diacritic plugin is loaded
  489. if( _.has(Backbone.Paginator, 'removeDiacritics') && self.useDiacriticsPlugin ){
  490. filter = Backbone.Paginator.removeDiacritics(filter);
  491. }
  492. // 'filter' can be only a string.
  493. // If 'filter' is string we need to convert it to
  494. // a regular expression.
  495. // For example, if 'filter' is 'black dog' we need
  496. // to find every single word, remove duplicated ones (if any)
  497. // and transform the result to '(black|dog)'
  498. if( filter === '' || !_.isString(filter) ) {
  499. return models;
  500. } else {
  501. var words = _.map(filter.match(/\w+/ig), function(element) { return element.toLowerCase(); });
  502. var pattern = "(" + _.uniq(words).join("|") + ")";
  503. var regexp = new RegExp(pattern, "igm");
  504. }
  505. var filteredModels = [];
  506. // We need to iterate over each model
  507. _.each( models, function( model ) {
  508. var matchesPerModel = [];
  509. // and over each field of each model
  510. _.each( fields, function( cmp_opts, field ) {
  511. var value = model.get( field );
  512. if( value ) {
  513. // The regular expression we created earlier let's us detect if a
  514. // given string contains each and all of the words in the regular expression
  515. // or not, but in both cases match() will return an array containing all
  516. // the words it matched.
  517. var matchesPerField = [];
  518. if( _.has(Backbone.Paginator, 'removeDiacritics') && self.useDiacriticsPlugin ){
  519. value = Backbone.Paginator.removeDiacritics(value.toString());
  520. }else{
  521. value = value.toString();
  522. }
  523. // Levenshtein cmp
  524. if( cmp_opts.cmp_method === 'levenshtein' && _.has(Backbone.Paginator, 'levenshtein') && self.useLevenshteinPlugin ) {
  525. var distance = Backbone.Paginator.levenshtein(value, filter);
  526. _.defaults(cmp_opts, { max_distance: 0 });
  527. if( distance <= cmp_opts.max_distance ) {
  528. matchesPerField = _.uniq(words);
  529. }
  530. // Default (RegExp) cmp
  531. }else{
  532. matchesPerField = value.match( regexp );
  533. }
  534. matchesPerField = _.map(matchesPerField, function(match) {
  535. return match.toString().toLowerCase();
  536. });
  537. _.each(matchesPerField, function(match){
  538. matchesPerModel.push(match);
  539. });
  540. }
  541. });
  542. // We just need to check if the returned array contains all the words in our
  543. // regex, and if it does, it means that we have a match, so we should save it.
  544. matchesPerModel = _.uniq( _.without(matchesPerModel, "") );
  545. if( _.isEmpty( _.difference(words, matchesPerModel) ) ) {
  546. filteredModels.push(model);
  547. }
  548. });
  549. return filteredModels;
  550. },
  551. // You shouldn't need to call info() as this method is used to
  552. // calculate internal data as first/prev/next/last page...
  553. info: function () {
  554. var self = this,
  555. info = {},
  556. totalRecords = (self.sortedAndFilteredModels) ? self.sortedAndFilteredModels.length : self.length,
  557. totalPages = Math.ceil(totalRecords / self.perPage);
  558. info = {
  559. totalUnfilteredRecords: self.origModels.length,
  560. totalRecords: totalRecords,
  561. currentPage: self.currentPage,
  562. perPage: this.perPage,
  563. totalPages: totalPages,
  564. lastPage: totalPages,
  565. previous: false,
  566. next: false,
  567. startRecord: totalRecords === 0 ? 0 : (self.currentPage - 1) * this.perPage + 1,
  568. endRecord: Math.min(totalRecords, self.currentPage * this.perPage)
  569. };
  570. if (self.currentPage > 1) {
  571. info.previous = self.currentPage - 1;
  572. }
  573. if (self.currentPage < info.totalPages) {
  574. info.next = self.currentPage + 1;
  575. }
  576. info.pageSet = self.setPagination(info);
  577. self.information = info;
  578. return info;
  579. },
  580. // setPagination also is an internal function that shouldn't be called directly.
  581. // It will create an array containing the pages right before and right after the
  582. // actual page.
  583. setPagination: function ( info ) {
  584. var pages = [], i = 0, l = 0;
  585. // How many adjacent pages should be shown on each side?
  586. var ADJACENTx2 = this.pagesInRange * 2,
  587. LASTPAGE = Math.ceil(info.totalRecords / info.perPage);
  588. if (LASTPAGE > 1) {
  589. // not enough pages to bother breaking it up
  590. if (LASTPAGE <= (1 + ADJACENTx2)) {
  591. for (i = 1, l = LASTPAGE; i <= l; i++) {
  592. pages.push(i);
  593. }
  594. }
  595. // enough pages to hide some
  596. else {
  597. //close to beginning; only hide later pages
  598. if (info.currentPage <= (this.pagesInRange + 1)) {
  599. for (i = 1, l = 2 + ADJACENTx2; i < l; i++) {
  600. pages.push(i);
  601. }
  602. }
  603. // in middle; hide some front and some back
  604. else if (LASTPAGE - this.pagesInRange > info.currentPage && info.currentPage > this.pagesInRange) {
  605. for (i = info.currentPage - this.pagesInRange; i <= info.currentPage + this.pagesInRange; i++) {
  606. pages.push(i);
  607. }
  608. }
  609. // close to end; only hide early pages
  610. else {
  611. for (i = LASTPAGE - ADJACENTx2; i <= LASTPAGE; i++) {
  612. pages.push(i);
  613. }
  614. }
  615. }
  616. }
  617. return pages;
  618. },
  619. bootstrap: function(options) {
  620. _.extend(this, options);
  621. this.goTo(1);
  622. this.info();
  623. return this;
  624. }
  625. });
  626. // function aliasing
  627. Paginator.clientPager.prototype.prevPage = Paginator.clientPager.prototype.previousPage;
  628. // Helper function to generate rejected Deferred
  629. var reject = function () {
  630. var response = new $.Deferred();
  631. response.reject();
  632. return response.promise();
  633. };
  634. // @name: requestPager
  635. //
  636. // Paginator for server-side data being requested from a backend/API
  637. //
  638. // @description:
  639. // This paginator is responsible for providing pagination
  640. // and sort capabilities for requests to a server-side
  641. // data service (e.g an API)
  642. //
  643. Paginator.requestPager = Backbone.Collection.extend({
  644. sync: function ( method, model, options ) {
  645. var self = this;
  646. self.setDefaults();
  647. // Some values could be functions, let's make sure
  648. // to change their scope too and run them
  649. var queryAttributes = {};
  650. _.each(_.result(self, "server_api"), function(value, key){
  651. if( _.isFunction(value) ) {
  652. value = _.bind(value, self);
  653. value = value();
  654. }
  655. queryAttributes[key] = value;
  656. });
  657. var queryOptions = _.clone(self.paginator_core);
  658. _.each(queryOptions, function(value, key){
  659. if( _.isFunction(value) ) {
  660. value = _.bind(value, self);
  661. value = value();
  662. }
  663. queryOptions[key] = value;
  664. });
  665. // Create default values if no others are specified
  666. queryOptions = _.defaults(queryOptions, {
  667. timeout: 25000,
  668. cache: false,
  669. type: 'GET',
  670. dataType: 'jsonp'
  671. });
  672. // Allows the passing in of {data: {foo: 'bar'}} at request time to overwrite server_api defaults
  673. if( options.data ){
  674. options.data = decodeURIComponent($.param(_.extend(queryAttributes,options.data)));
  675. }else{
  676. options.data = decodeURIComponent($.param(queryAttributes));
  677. }
  678. queryOptions = _.extend(queryOptions, {
  679. data: decodeURIComponent($.param(queryAttributes)),
  680. processData: false,
  681. url: _.result(queryOptions, 'url')
  682. }, options);
  683. var promiseSuccessFormat = !(bbVer[0] === 0 &&
  684. bbVer[1] === 9 &&
  685. bbVer[2] === 10);
  686. var success = queryOptions.success;
  687. queryOptions.success = function ( resp, status, xhr ) {
  688. if ( success ) {
  689. // This is to keep compatibility with Backbone 0.9.10
  690. if (promiseSuccessFormat) {
  691. success( resp, status, xhr );
  692. } else {
  693. success( model, resp, queryOptions );
  694. }
  695. }
  696. if (bbVer[0] < 1 && model && model.trigger ) {
  697. model.trigger( 'sync', model, resp, queryOptions );
  698. }
  699. };
  700. var error = queryOptions.error;
  701. queryOptions.error = function ( xhr ) {
  702. if ( error ) {
  703. error( xhr );
  704. }
  705. if ( model && model.trigger ) {
  706. model.trigger( 'error', model, xhr, queryOptions );
  707. }
  708. };
  709. var xhr = queryOptions.xhr = Backbone.ajax( queryOptions );
  710. if ( model && model.trigger ) {
  711. model.trigger('request', model, xhr, queryOptions);
  712. }
  713. return xhr;
  714. },
  715. setDefaults: function() {
  716. var self = this;
  717. // Create default values if no others are specified
  718. _.defaults(self.paginator_ui, {
  719. firstPage: 0,
  720. currentPage: 1,
  721. perPage: 5,
  722. totalPages: 10,
  723. pagesInRange: 4
  724. });
  725. // Change scope of 'paginator_ui' object values
  726. _.each(self.paginator_ui, function(value, key) {
  727. if (_.isUndefined(self[key])) {
  728. self[key] = self.paginator_ui[key];
  729. }
  730. });
  731. },
  732. requestNextPage: function ( options ) {
  733. if ( this.currentPage !== undefined ) {
  734. this.currentPage += 1;
  735. return this.pager( options );
  736. } else {
  737. return reject();
  738. }
  739. },
  740. requestPreviousPage: function ( options ) {
  741. if ( this.currentPage !== undefined ) {
  742. this.currentPage -= 1;
  743. return this.pager( options );
  744. } else {
  745. return reject();
  746. }
  747. },
  748. updateOrder: function ( column, options ) {
  749. if (column !== undefined) {
  750. this.sortField = column;
  751. return this.pager( options );
  752. } else {
  753. return reject();
  754. }
  755. },
  756. goTo: function ( page, options ) {
  757. if ( page !== undefined ) {
  758. this.currentPage = parseInt(page, 10);
  759. return this.pager( options );
  760. } else {
  761. return reject();
  762. }
  763. },
  764. howManyPer: function ( count, options ) {
  765. if ( count !== undefined ) {
  766. this.currentPage = this.firstPage;
  767. this.perPage = count;
  768. return this.pager( options );
  769. } else {
  770. return reject();
  771. }
  772. },
  773. info: function () {
  774. var info = {
  775. // If parse() method is implemented and totalRecords is set to the length
  776. // of the records returned, make it available. Else, default it to 0
  777. totalRecords: this.totalRecords || 0,
  778. currentPage: this.currentPage,
  779. firstPage: this.firstPage,
  780. totalPages: Math.ceil(this.totalRecords / this.perPage),
  781. lastPage: this.totalPages, // should use totalPages in template
  782. perPage: this.perPage,
  783. previous:false,
  784. next:false
  785. };
  786. if (this.currentPage > 1) {
  787. info.previous = this.currentPage - 1;
  788. }
  789. if (this.currentPage < info.totalPages) {
  790. info.next = this.currentPage + 1;
  791. }
  792. // left around for backwards compatibility
  793. info.hasNext = info.next;
  794. info.hasPrevious = info.next;
  795. info.pageSet = this.setPagination(info);
  796. this.information = info;
  797. return info;
  798. },
  799. setPagination: function ( info ) {
  800. var pages = [], i = 0, l = 0;
  801. // How many adjacent pages should be shown on each side?
  802. var ADJACENTx2 = this.pagesInRange * 2,
  803. LASTPAGE = Math.ceil(info.totalRecords / info.perPage);
  804. if (LASTPAGE > 1) {
  805. // not enough pages to bother breaking it up
  806. if (LASTPAGE <= (1 + ADJACENTx2)) {
  807. for (i = 1, l = LASTPAGE; i <= l; i++) {
  808. pages.push(i);
  809. }
  810. }
  811. // enough pages to hide some
  812. else {
  813. //close to beginning; only hide later pages
  814. if (info.currentPage <= (this.pagesInRange + 1)) {
  815. for (i = 1, l = 2 + ADJACENTx2; i < l; i++) {
  816. pages.push(i);
  817. }
  818. }
  819. // in middle; hide some front and some back
  820. else if (LASTPAGE - this.pagesInRange > info.currentPage && info.currentPage > this.pagesInRange) {
  821. for (i = info.currentPage - this.pagesInRange; i <= info.currentPage + this.pagesInRange; i++) {
  822. pages.push(i);
  823. }
  824. }
  825. // close to end; only hide early pages
  826. else {
  827. for (i = LASTPAGE - ADJACENTx2; i <= LASTPAGE; i++) {
  828. pages.push(i);
  829. }
  830. }
  831. }
  832. }
  833. return pages;
  834. },
  835. // fetches the latest results from the server
  836. pager: function ( options ) {
  837. if ( !_.isObject(options) ) {
  838. options = {};
  839. }
  840. return this.fetch( options );
  841. },
  842. url: function(){
  843. // Expose url parameter enclosed in this.paginator_core.url to properly
  844. // extend Collection and allow Collection CRUD
  845. if(this.paginator_core !== undefined && this.paginator_core.url !== undefined){
  846. return this.paginator_core.url;
  847. } else {
  848. return null;
  849. }
  850. },
  851. bootstrap: function(options) {
  852. _.extend(this, options);
  853. this.setDefaults();
  854. this.info();
  855. return this;
  856. }
  857. });
  858. // function aliasing
  859. Paginator.requestPager.prototype.nextPage = Paginator.requestPager.prototype.requestNextPage;
  860. Paginator.requestPager.prototype.prevPage = Paginator.requestPager.prototype.requestPreviousPage;
  861. return Paginator;
  862. }( Backbone, _, jQuery ));