PageRenderTime 58ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/ajax/libs/backbone.paginator/0.6/backbone.paginator.js

https://bitbucket.org/kolbyjAFK/cdnjs
JavaScript | 1046 lines | 694 code | 187 blank | 165 comment | 160 complexity | 291d826b01678c2334d73f79bc9a53b0 MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception
  1. /*! backbone.paginator - v0.6.0 - 3/15/2013
  2. * http://github.com/addyosmani/backbone.paginator
  3. * Copyright (c) 2013 Addy Osmani; Licensed MIT */
  4. Backbone.Paginator = (function ( Backbone, _, $ ) {
  5. "use strict";
  6. var Paginator = {};
  7. Paginator.version = "0.6.0";
  8. // @name: clientPager
  9. //
  10. // @tagline: Paginator for client-side data
  11. //
  12. // @description:
  13. // This paginator is responsible for providing pagination
  14. // and sort capabilities for a single payload of data
  15. // we wish to paginate by the UI for easier browsering.
  16. //
  17. Paginator.clientPager = Backbone.Collection.extend({
  18. // DEFAULTS FOR SORTING & FILTERING
  19. useDiacriticsPlugin: true, // use diacritics plugin if available
  20. useLevenshteinPlugin: true, // use levenshtein plugin if available
  21. sortColumn: "",
  22. sortDirection: "desc",
  23. lastSortColumn: "",
  24. fieldFilterRules: [],
  25. lastFieldFilterRules: [],
  26. filterFields: "",
  27. filterExpression: "",
  28. lastFilterExpression: "",
  29. //DEFAULT PAGINATOR UI VALUES
  30. defaults_ui: {
  31. firstPage: 0,
  32. currentPage: 1,
  33. perPage: 5,
  34. totalPages: 10,
  35. pagesInRange: 4
  36. },
  37. // Default values used when sorting and/or filtering.
  38. initialize: function(){
  39. //LISTEN FOR ADD & REMOVE EVENTS THEN REMOVE MODELS FROM ORGINAL MODELS
  40. this.on('add', this.addModel, this);
  41. this.on('remove', this.removeModel, this);
  42. // SET DEFAULT VALUES (ALLOWS YOU TO POPULATE PAGINATOR MAUNALLY)
  43. this.setDefaults();
  44. },
  45. setDefaults: function() {
  46. // SET DEFAULT UI SETTINGS
  47. var options = _.defaults(this.paginator_ui, this.defaults_ui);
  48. //UPDATE GLOBAL UI SETTINGS
  49. _.defaults(this, options);
  50. },
  51. addModel: function(model) {
  52. this.origModels.push(model);
  53. },
  54. removeModel: function(model) {
  55. var index = _.indexOf(this.origModels, model);
  56. this.origModels.splice(index, 1);
  57. },
  58. sync: function ( method, model, options ) {
  59. var self = this;
  60. // SET DEFAULT VALUES
  61. this.setDefaults();
  62. // Some values could be functions, let's make sure
  63. // to change their scope too and run them
  64. var queryAttributes = {};
  65. _.each(_.result(self, "server_api"), function(value, key){
  66. if( _.isFunction(value) ) {
  67. value = _.bind(value, self);
  68. value = value();
  69. }
  70. queryAttributes[key] = value;
  71. });
  72. var queryOptions = _.clone(self.paginator_core);
  73. _.each(queryOptions, function(value, key){
  74. if( _.isFunction(value) ) {
  75. value = _.bind(value, self);
  76. value = value();
  77. }
  78. queryOptions[key] = value;
  79. });
  80. // Create default values if no others are specified
  81. queryOptions = _.defaults(queryOptions, {
  82. timeout: 25000,
  83. cache: false,
  84. type: 'GET',
  85. dataType: 'jsonp'
  86. });
  87. queryOptions = _.extend(queryOptions, {
  88. data: decodeURIComponent($.param(queryAttributes)),
  89. processData: false,
  90. url: _.result(queryOptions, 'url')
  91. }, options);
  92. var bbVer = Backbone.VERSION.split('.');
  93. var oldSuccessFormat = (parseInt(bbVer[0], 10) === 0 &&
  94. parseInt(bbVer[1], 10) === 9 &&
  95. parseInt(bbVer[2], 10) <= 9);
  96. var success = queryOptions.success;
  97. queryOptions.success = function ( resp, status, xhr ) {
  98. if ( success ) {
  99. // This is to keep compatibility with Backbone older than 0.9.10
  100. if (oldSuccessFormat) {
  101. success( resp, status, xhr );
  102. } else {
  103. success( model, resp, queryOptions );
  104. }
  105. }
  106. if ( model && model.trigger ) {
  107. model.trigger( 'sync', model, resp, queryOptions );
  108. }
  109. };
  110. var error = queryOptions.error;
  111. queryOptions.error = function ( xhr ) {
  112. if ( error ) {
  113. error( model, xhr, queryOptions );
  114. }
  115. if ( model && model.trigger ) {
  116. model.trigger( 'error', model, xhr, queryOptions );
  117. }
  118. };
  119. var xhr = queryOptions.xhr = $.ajax( queryOptions );
  120. if ( model && model.trigger ) {
  121. model.trigger('request', model, xhr, queryOptions);
  122. }
  123. return xhr;
  124. },
  125. nextPage: function (options) {
  126. if(this.currentPage < this.information.totalPages) {
  127. this.currentPage = ++this.currentPage;
  128. this.pager(options);
  129. }
  130. },
  131. previousPage: function (options) {
  132. if(this.currentPage > 1) {
  133. this.currentPage = --this.currentPage;
  134. this.pager(options);
  135. }
  136. },
  137. goTo: function ( page, options ) {
  138. if(page !== undefined){
  139. this.currentPage = parseInt(page, 10);
  140. this.pager(options);
  141. }
  142. },
  143. howManyPer: function ( perPage ) {
  144. if(perPage !== undefined){
  145. var lastPerPage = this.perPage;
  146. this.perPage = parseInt(perPage, 10);
  147. this.currentPage = Math.ceil( ( lastPerPage * ( this.currentPage - 1 ) + 1 ) / perPage);
  148. this.pager();
  149. }
  150. },
  151. // setSort is used to sort the current model. After
  152. // passing 'column', which is the model's field you want
  153. // to filter and 'direction', which is the direction
  154. // desired for the ordering ('asc' or 'desc'), pager()
  155. // and info() will be called automatically.
  156. setSort: function ( column, direction ) {
  157. if(column !== undefined && direction !== undefined){
  158. this.lastSortColumn = this.sortColumn;
  159. this.sortColumn = column;
  160. this.sortDirection = direction;
  161. this.pager();
  162. this.info();
  163. }
  164. },
  165. // setFieldFilter is used to filter each value of each model
  166. // according to `rules` that you pass as argument.
  167. // Example: You have a collection of books with 'release year' and 'author'.
  168. // You can filter only the books that were released between 1999 and 2003
  169. // And then you can add another `rule` that will filter those books only to
  170. // authors who's name start with 'A'.
  171. setFieldFilter: function ( fieldFilterRules ) {
  172. if( !_.isEmpty( fieldFilterRules ) ) {
  173. this.lastFieldFilterRules = this.fieldFilterRules;
  174. this.fieldFilterRules = fieldFilterRules;
  175. this.pager();
  176. this.info();
  177. // if all the filters are removed, we should save the last filter
  178. // and then let the list reset to it's original state.
  179. } else {
  180. this.lastFieldFilterRules = this.fieldFilterRules;
  181. this.fieldFilterRules = '';
  182. this.pager();
  183. this.info();
  184. }
  185. },
  186. // doFakeFieldFilter can be used to get the number of models that will remain
  187. // after calling setFieldFilter with a filter rule(s)
  188. doFakeFieldFilter: function ( rules ) {
  189. if( !_.isEmpty( rules ) ) {
  190. var testModels = this.origModels;
  191. if (testModels === undefined) {
  192. testModels = this.models;
  193. }
  194. testModels = this._fieldFilter(testModels, rules);
  195. // To comply with current behavior, also filter by any previously defined setFilter rules.
  196. if ( this.filterExpression !== "" ) {
  197. testModels = this._filter(testModels, this.filterFields, this.filterExpression);
  198. }
  199. // Return size
  200. return testModels.length;
  201. }
  202. },
  203. // setFilter is used to filter the current model. After
  204. // passing 'fields', which can be a string referring to
  205. // the model's field, an array of strings representing
  206. // each of the model's fields or an object with the name
  207. // of the model's field(s) and comparing options (see docs)
  208. // you wish to filter by and
  209. // 'filter', which is the word or words you wish to
  210. // filter by, pager() and info() will be called automatically.
  211. setFilter: function ( fields, filter ) {
  212. if( fields !== undefined && filter !== undefined ){
  213. this.filterFields = fields;
  214. this.lastFilterExpression = this.filterExpression;
  215. this.filterExpression = filter;
  216. this.pager();
  217. this.info();
  218. }
  219. },
  220. // doFakeFilter can be used to get the number of models that will
  221. // remain after calling setFilter with a `fields` and `filter` args.
  222. doFakeFilter: function ( fields, filter ) {
  223. if( fields !== undefined && filter !== undefined ){
  224. var testModels = this.origModels;
  225. if (testModels === undefined) {
  226. testModels = this.models;
  227. }
  228. // To comply with current behavior, first filter by any previously defined setFieldFilter rules.
  229. if ( !_.isEmpty( this.fieldFilterRules ) ) {
  230. testModels = this._fieldFilter(testModels, this.fieldFilterRules);
  231. }
  232. testModels = this._filter(testModels, fields, filter);
  233. // Return size
  234. return testModels.length;
  235. }
  236. },
  237. // pager is used to sort, filter and show the data
  238. // you expect the library to display.
  239. pager: function (options) {
  240. var self = this,
  241. disp = this.perPage,
  242. start = (self.currentPage - 1) * disp,
  243. stop = start + disp;
  244. // Saving the original models collection is important
  245. // as we could need to sort or filter, and we don't want
  246. // to loose the data we fetched from the server.
  247. if (self.origModels === undefined) {
  248. self.origModels = self.models;
  249. }
  250. self.models = self.origModels.slice();
  251. // Check if sorting was set using setSort.
  252. if ( this.sortColumn !== "" ) {
  253. self.models = self._sort(self.models, this.sortColumn, this.sortDirection);
  254. }
  255. // Check if field-filtering was set using setFieldFilter
  256. if ( !_.isEmpty( this.fieldFilterRules ) ) {
  257. self.models = self._fieldFilter(self.models, this.fieldFilterRules);
  258. }
  259. // Check if filtering was set using setFilter.
  260. if ( this.filterExpression !== "" ) {
  261. self.models = self._filter(self.models, this.filterFields, this.filterExpression);
  262. }
  263. // If the sorting or the filtering was changed go to the first page
  264. if ( this.lastSortColumn !== this.sortColumn || this.lastFilterExpression !== this.filterExpression || !_.isEqual(this.fieldFilterRules, this.lastFieldFilterRules) ) {
  265. start = 0;
  266. stop = start + disp;
  267. self.currentPage = 1;
  268. this.lastSortColumn = this.sortColumn;
  269. this.lastFieldFilterRules = this.fieldFilterRules;
  270. this.lastFilterExpression = this.filterExpression;
  271. }
  272. // We need to save the sorted and filtered models collection
  273. // because we'll use that sorted and filtered collection in info().
  274. self.sortedAndFilteredModels = self.models.slice();
  275. self.info();
  276. self.reset(self.models.slice(start, stop));
  277. // This is somewhat of a hack to get all the nextPage, prevPage, and goTo methods
  278. // to work with a success callback (as in the requestPager). Realistically there is no failure case here,
  279. // but maybe we could catch exception and trigger a failure callback?
  280. _.result(options, 'success');
  281. },
  282. // The actual place where the collection is sorted.
  283. // Check setSort for arguments explicacion.
  284. _sort: function ( models, sort, direction ) {
  285. models = models.sort(function (a, b) {
  286. var ac = a.get(sort),
  287. bc = b.get(sort);
  288. if ( _.isUndefined(ac) || _.isUndefined(bc) || ac === null || bc === null ) {
  289. return 0;
  290. } else {
  291. /* Make sure that both ac and bc are lowercase strings.
  292. * .toString() first so we don't have to worry if ac or bc
  293. * have other String-only methods.
  294. */
  295. ac = ac.toString().toLowerCase();
  296. bc = bc.toString().toLowerCase();
  297. }
  298. if (direction === 'desc') {
  299. // We need to know if there aren't any non-number characters
  300. // and that there are numbers-only characters and maybe a dot
  301. // if we have a float.
  302. // Oh, also a '-' for negative numbers!
  303. if((!ac.match(/[^\-\d\.]/) && ac.match(/-?[\d\.]+/)) &&
  304. (!bc.match(/[^\-\d\.]/) && bc.match(/-?[\d\.]+/))
  305. ){
  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. ){
  325. if( (ac - 0) < (bc - 0) ) {
  326. return -1;
  327. }
  328. if( (ac - 0) > (bc - 0) ) {
  329. return 1;
  330. }
  331. } else {
  332. if (ac < bc) {
  333. return -1;
  334. }
  335. if (ac > bc) {
  336. return 1;
  337. }
  338. }
  339. }
  340. if (a.cid && b.cid){
  341. var aId = a.cid,
  342. bId = b.cid;
  343. if (aId < bId) {
  344. return -1;
  345. }
  346. if (aId > bId) {
  347. return 1;
  348. }
  349. }
  350. return 0;
  351. });
  352. return models;
  353. },
  354. // The actual place where the collection is field-filtered.
  355. // Check setFieldFilter for arguments explicacion.
  356. _fieldFilter: function( models, rules ) {
  357. // Check if there are any rules
  358. if ( _.isEmpty(rules) ) {
  359. return models;
  360. }
  361. var filteredModels = [];
  362. // Iterate over each rule
  363. _.each(models, function(model){
  364. var should_push = true;
  365. // Apply each rule to each model in the collection
  366. _.each(rules, function(rule){
  367. // Don't go inside the switch if we're already sure that the model won't be included in the results
  368. if( !should_push ){
  369. return false;
  370. }
  371. should_push = false;
  372. // The field's value will be passed to a custom function, which should
  373. // return true (if model should be included) or false (model should be ignored)
  374. if(rule.type === "function"){
  375. var f = _.wrap(rule.value, function(func){
  376. return func( model.get(rule.field) );
  377. });
  378. if( f() ){
  379. should_push = true;
  380. }
  381. // The field's value is required to be non-empty
  382. }else if(rule.type === "required"){
  383. if( !_.isEmpty( model.get(rule.field).toString() ) ) {
  384. should_push = true;
  385. }
  386. // The field's value is required to be greater tan N (numbers only)
  387. }else if(rule.type === "min"){
  388. if( !_.isNaN( Number( model.get(rule.field) ) ) &&
  389. !_.isNaN( Number( rule.value ) ) &&
  390. Number( model.get(rule.field) ) >= Number( rule.value ) ) {
  391. should_push = true;
  392. }
  393. // The field's value is required to be smaller tan N (numbers only)
  394. }else if(rule.type === "max"){
  395. if( !_.isNaN( Number( model.get(rule.field) ) ) &&
  396. !_.isNaN( Number( rule.value ) ) &&
  397. Number( model.get(rule.field) ) <= Number( rule.value ) ) {
  398. should_push = true;
  399. }
  400. // The field's value is required to be between N and M (numbers only)
  401. }else if(rule.type === "range"){
  402. if( !_.isNaN( Number( model.get(rule.field) ) ) &&
  403. _.isObject( rule.value ) &&
  404. !_.isNaN( Number( rule.value.min ) ) &&
  405. !_.isNaN( Number( rule.value.max ) ) &&
  406. Number( model.get(rule.field) ) >= Number( rule.value.min ) &&
  407. Number( model.get(rule.field) ) <= Number( rule.value.max ) ) {
  408. should_push = true;
  409. }
  410. // The field's value is required to be more than N chars long
  411. }else if(rule.type === "minLength"){
  412. if( model.get(rule.field).toString().length >= rule.value ) {
  413. should_push = true;
  414. }
  415. // The field's value is required to be no more than N chars long
  416. }else if(rule.type === "maxLength"){
  417. if( model.get(rule.field).toString().length <= rule.value ) {
  418. should_push = true;
  419. }
  420. // The field's value is required to be more than N chars long and no more than M chars long
  421. }else if(rule.type === "rangeLength"){
  422. if( _.isObject( rule.value ) &&
  423. !_.isNaN( Number( rule.value.min ) ) &&
  424. !_.isNaN( Number( rule.value.max ) ) &&
  425. model.get(rule.field).toString().length >= rule.value.min &&
  426. model.get(rule.field).toString().length <= rule.value.max ) {
  427. should_push = true;
  428. }
  429. // The field's value is required to be equal to one of the values in rules.value
  430. }else if(rule.type === "oneOf"){
  431. if( _.isArray( rule.value ) &&
  432. _.include( rule.value, model.get(rule.field) ) ) {
  433. should_push = true;
  434. }
  435. // The field's value is required to be equal to the value in rules.value
  436. }else if(rule.type === "equalTo"){
  437. if( rule.value === model.get(rule.field) ) {
  438. should_push = true;
  439. }
  440. }else if(rule.type === "containsAllOf"){
  441. if( _.isArray( rule.value ) &&
  442. _.isArray(model.get(rule.field)) &&
  443. _.intersection( rule.value, model.get(rule.field)).length === rule.value.length
  444. ) {
  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. // @name: requestPager
  629. //
  630. // Paginator for server-side data being requested from a backend/API
  631. //
  632. // @description:
  633. // This paginator is responsible for providing pagination
  634. // and sort capabilities for requests to a server-side
  635. // data service (e.g an API)
  636. //
  637. Paginator.requestPager = Backbone.Collection.extend({
  638. sync: function ( method, model, options ) {
  639. var self = this;
  640. self.setDefaults();
  641. // Some values could be functions, let's make sure
  642. // to change their scope too and run them
  643. var queryAttributes = {};
  644. _.each(_.result(self, "server_api"), function(value, key){
  645. if( _.isFunction(value) ) {
  646. value = _.bind(value, self);
  647. value = value();
  648. }
  649. queryAttributes[key] = value;
  650. });
  651. var queryOptions = _.clone(self.paginator_core);
  652. _.each(queryOptions, function(value, key){
  653. if( _.isFunction(value) ) {
  654. value = _.bind(value, self);
  655. value = value();
  656. }
  657. queryOptions[key] = value;
  658. });
  659. // Create default values if no others are specified
  660. queryOptions = _.defaults(queryOptions, {
  661. timeout: 25000,
  662. cache: false,
  663. type: 'GET',
  664. dataType: 'jsonp'
  665. });
  666. // Allows the passing in of {data: {foo: 'bar'}} at request time to overwrite server_api defaults
  667. if( options.data ){
  668. options.data = decodeURIComponent($.param(_.extend(queryAttributes,options.data)));
  669. }else{
  670. options.data = decodeURIComponent($.param(queryAttributes));
  671. }
  672. queryOptions = _.extend(queryOptions, {
  673. data: decodeURIComponent($.param(queryAttributes)),
  674. processData: false,
  675. url: _.result(queryOptions, 'url')
  676. }, options);
  677. var bbVer = Backbone.VERSION.split('.');
  678. var oldSuccessFormat = (parseInt(bbVer[0], 10) === 0 &&
  679. parseInt(bbVer[1], 10) === 9 &&
  680. parseInt(bbVer[2], 10) <= 9);
  681. var success = queryOptions.success;
  682. queryOptions.success = function ( resp, status, xhr ) {
  683. if ( success ) {
  684. // This is to keep compatibility with Backbone older than 0.9.10
  685. if (oldSuccessFormat) {
  686. success( resp, status, xhr );
  687. } else {
  688. success( model, resp, queryOptions );
  689. }
  690. }
  691. if ( model && model.trigger ) {
  692. model.trigger( 'sync', model, resp, queryOptions );
  693. }
  694. };
  695. var error = queryOptions.error;
  696. queryOptions.error = function ( xhr ) {
  697. if ( error ) {
  698. error( model, xhr, queryOptions );
  699. }
  700. if ( model && model.trigger ) {
  701. model.trigger( 'error', model, xhr, queryOptions );
  702. }
  703. };
  704. var xhr = queryOptions.xhr = $.ajax( queryOptions );
  705. if ( model && model.trigger ) {
  706. model.trigger('request', model, xhr, queryOptions);
  707. }
  708. return xhr;
  709. },
  710. setDefaults: function() {
  711. var self = this;
  712. // Create default values if no others are specified
  713. _.defaults(self.paginator_ui, {
  714. firstPage: 0,
  715. currentPage: 1,
  716. perPage: 5,
  717. totalPages: 10,
  718. pagesInRange: 4
  719. });
  720. // Change scope of 'paginator_ui' object values
  721. _.each(self.paginator_ui, function(value, key) {
  722. if (_.isUndefined(self[key])) {
  723. self[key] = self.paginator_ui[key];
  724. }
  725. });
  726. },
  727. requestNextPage: function ( options ) {
  728. if ( this.currentPage !== undefined ) {
  729. this.currentPage += 1;
  730. return this.pager( options );
  731. } else {
  732. var response = new $.Deferred();
  733. response.reject();
  734. return response.promise();
  735. }
  736. },
  737. requestPreviousPage: function ( options ) {
  738. if ( this.currentPage !== undefined ) {
  739. this.currentPage -= 1;
  740. return this.pager( options );
  741. } else {
  742. var response = new $.Deferred();
  743. response.reject();
  744. return response.promise();
  745. }
  746. },
  747. updateOrder: function ( column ) {
  748. if (column !== undefined) {
  749. this.sortField = column;
  750. this.pager();
  751. }
  752. },
  753. goTo: function ( page, options ) {
  754. if ( page !== undefined ) {
  755. this.currentPage = parseInt(page, 10);
  756. return this.pager( options );
  757. } else {
  758. var response = new $.Deferred();
  759. response.reject();
  760. return response.promise();
  761. }
  762. },
  763. howManyPer: function ( count ) {
  764. if( count !== undefined ){
  765. this.currentPage = this.firstPage;
  766. this.perPage = count;
  767. this.pager();
  768. }
  769. },
  770. info: function () {
  771. var info = {
  772. // If parse() method is implemented and totalRecords is set to the length
  773. // of the records returned, make it available. Else, default it to 0
  774. totalRecords: this.totalRecords || 0,
  775. currentPage: this.currentPage,
  776. firstPage: this.firstPage,
  777. totalPages: Math.ceil(this.totalRecords / this.perPage),
  778. lastPage: this.totalPages, // should use totalPages in template
  779. perPage: this.perPage,
  780. previous:false,
  781. next:false
  782. };
  783. if (this.currentPage > 1) {
  784. info.previous = this.currentPage - 1;
  785. }
  786. if (this.currentPage < info.totalPages) {
  787. info.next = this.currentPage + 1;
  788. }
  789. // left around for backwards compatibility
  790. info.hasNext = info.next;
  791. info.hasPrevious = info.next;
  792. info.pageSet = this.setPagination(info);
  793. this.information = info;
  794. return info;
  795. },
  796. setPagination: function ( info ) {
  797. var pages = [], i = 0, l = 0;
  798. // How many adjacent pages should be shown on each side?
  799. var ADJACENTx2 = this.pagesInRange * 2,
  800. LASTPAGE = Math.ceil(info.totalRecords / info.perPage);
  801. if (LASTPAGE > 1) {
  802. // not enough pages to bother breaking it up
  803. if (LASTPAGE <= (1 + ADJACENTx2)) {
  804. for (i = 1, l = LASTPAGE; i <= l; i++) {
  805. pages.push(i);
  806. }
  807. }
  808. // enough pages to hide some
  809. else {
  810. //close to beginning; only hide later pages
  811. if (info.currentPage <= (this.pagesInRange + 1)) {
  812. for (i = 1, l = 2 + ADJACENTx2; i < l; i++) {
  813. pages.push(i);
  814. }
  815. }
  816. // in middle; hide some front and some back
  817. else if (LASTPAGE - this.pagesInRange > info.currentPage && info.currentPage > this.pagesInRange) {
  818. for (i = info.currentPage - this.pagesInRange; i <= info.currentPage + this.pagesInRange; i++) {
  819. pages.push(i);
  820. }
  821. }
  822. // close to end; only hide early pages
  823. else {
  824. for (i = LASTPAGE - ADJACENTx2; i <= LASTPAGE; i++) {
  825. pages.push(i);
  826. }
  827. }
  828. }
  829. }
  830. return pages;
  831. },
  832. // fetches the latest results from the server
  833. pager: function ( options ) {
  834. if ( !_.isObject(options) ) {
  835. options = {};
  836. }
  837. return this.fetch( options );
  838. },
  839. url: function(){
  840. // Expose url parameter enclosed in this.paginator_core.url to properly
  841. // extend Collection and allow Collection CRUD
  842. if(this.paginator_core !== undefined && this.paginator_core.url !== undefined){
  843. return this.paginator_core.url;
  844. } else {
  845. return null;
  846. }
  847. },
  848. bootstrap: function(options) {
  849. _.extend(this, options);
  850. this.setDefaults();
  851. this.info();
  852. return this;
  853. }
  854. });
  855. // function aliasing
  856. Paginator.requestPager.prototype.nextPage = Paginator.requestPager.prototype.requestNextPage;
  857. Paginator.requestPager.prototype.prevPage = Paginator.requestPager.prototype.requestPreviousPage;
  858. return Paginator;
  859. }( Backbone, _, jQuery ));