PageRenderTime 49ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/js/lib/backbone.paginator.js

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