PageRenderTime 52ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/SingularityUI/vendor/scripts/backbone.paginator-0.9.0.js

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