PageRenderTime 54ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 1ms

/ajax/libs/backbone.paginator/0.7/backbone.paginator.js

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