PageRenderTime 53ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/public/vendor/backbone-paginator/backbone.paginator.js

https://github.com/bozz/cashflow
JavaScript | 757 lines | 483 code | 141 blank | 133 comment | 105 complexity | 716e2fc754ab9055e766e81c9ab308a4 MD5 | raw file
  1. /*! backbone.paginator - v0.1.54 - 6/10/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.15";
  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. // Default values used when sorting and/or filtering.
  19. initialize: function(){
  20. this.useDiacriticsPlugin = true; // use diacritics plugin if available
  21. this.useLevenshteinPlugin = true; // use levenshtein plugin if available
  22. this.sortColumn = "";
  23. this.sortDirection = "desc";
  24. this.lastSortColumn = "";
  25. this.fieldFilterRules = [];
  26. this.lastFieldFilterRiles = [];
  27. this.filterFields = "";
  28. this.filterExpression = "";
  29. this.lastFilterExpression = "";
  30. },
  31. sync: function ( method, model, options ) {
  32. var self = this;
  33. // Create default values if no others are specified
  34. _.defaults(self.paginator_ui, {
  35. firstPage: 0,
  36. currentPage: 1,
  37. perPage: 5,
  38. totalPages: 10
  39. });
  40. // Change scope of 'paginator_ui' object values
  41. _.each(self.paginator_ui, function(value, key) {
  42. if( _.isUndefined(self[key]) ) {
  43. self[key] = self.paginator_ui[key];
  44. }
  45. });
  46. // Some values could be functions, let's make sure
  47. // to change their scope too and run them
  48. var queryAttributes = {};
  49. _.each(self.server_api, function(value, key){
  50. if( _.isFunction(value) ) {
  51. value = _.bind(value, self);
  52. }
  53. queryAttributes[key] = value;
  54. });
  55. var queryOptions = _.clone(self.paginator_core);
  56. _.each(queryOptions, function(value, key){
  57. if( _.isFunction(value) ) {
  58. value = _.bind(value, self);
  59. }
  60. queryOptions[key] = value;
  61. });
  62. // Create default values if no others are specified
  63. queryOptions = _.defaults(queryOptions, {
  64. timeout: 25000,
  65. cache: false,
  66. type: 'GET',
  67. dataType: 'jsonp'
  68. });
  69. queryOptions = _.extend(queryOptions, {
  70. jsonpCallback: 'callback',
  71. data: decodeURIComponent($.param(queryAttributes)),
  72. processData: false,
  73. url: _.result(queryOptions, 'url')
  74. }, options);
  75. return $.ajax( queryOptions );
  76. },
  77. nextPage: function () {
  78. this.currentPage = ++this.currentPage;
  79. this.pager();
  80. },
  81. previousPage: function () {
  82. this.currentPage = --this.currentPage || 1;
  83. this.pager();
  84. },
  85. goTo: function ( page ) {
  86. if(page !== undefined){
  87. this.currentPage = parseInt(page, 10);
  88. this.pager();
  89. }
  90. },
  91. howManyPer: function ( perPage ) {
  92. if(perPage !== undefined){
  93. var lastPerPage = this.perPage;
  94. this.perPage = parseInt(perPage, 10);
  95. this.currentPage = Math.ceil( ( lastPerPage * ( this.currentPage - 1 ) + 1 ) / perPage);
  96. this.pager();
  97. }
  98. },
  99. // setSort is used to sort the current model. After
  100. // passing 'column', which is the model's field you want
  101. // to filter and 'direction', which is the direction
  102. // desired for the ordering ('asc' or 'desc'), pager()
  103. // and info() will be called automatically.
  104. setSort: function ( column, direction ) {
  105. if(column !== undefined && direction !== undefined){
  106. this.lastSortColumn = this.sortColumn;
  107. this.sortColumn = column;
  108. this.sortDirection = direction;
  109. this.pager();
  110. this.info();
  111. }
  112. },
  113. // setFieldFilter is used to filter each value of each model
  114. // according to `rules` that you pass as argument.
  115. // Example: You have a collection of books with 'release year' and 'author'.
  116. // You can filter only the books that were released between 1999 and 2003
  117. // And then you can add another `rule` that will filter those books only to
  118. // authors who's name start with 'A'.
  119. setFieldFilter: function ( fieldFilterRules ) {
  120. if( !_.isEmpty( fieldFilterRules ) ) {
  121. this.lastFieldFilterRiles = this.fieldFilterRules;
  122. this.fieldFilterRules = fieldFilterRules;
  123. this.pager();
  124. this.info();
  125. }
  126. },
  127. // setFilter is used to filter the current model. After
  128. // passing 'fields', which can be a string referring to
  129. // the model's field, an array of strings representing
  130. // each of the model's fields or an object with the name
  131. // of the model's field(s) and comparing options (see docs)
  132. // you wish to filter by and
  133. // 'filter', which is the word or words you wish to
  134. // filter by, pager() and info() will be called automatically.
  135. setFilter: function ( fields, filter ) {
  136. if( fields !== undefined && filter !== undefined ){
  137. this.filterFields = fields;
  138. this.lastFilterExpression = this.filterExpression;
  139. this.filterExpression = filter;
  140. this.pager();
  141. this.info();
  142. }
  143. },
  144. // pager is used to sort, filter and show the data
  145. // you expect the library to display.
  146. pager: function () {
  147. var self = this,
  148. disp = this.perPage,
  149. start = (self.currentPage - 1) * disp,
  150. stop = start + disp;
  151. // Saving the original models collection is important
  152. // as we could need to sort or filter, and we don't want
  153. // to loose the data we fetched from the server.
  154. if (self.origModels === undefined) {
  155. self.origModels = self.models;
  156. }
  157. self.models = self.origModels;
  158. // Check if sorting was set using setSort.
  159. if ( this.sortColumn !== "" ) {
  160. self.models = self._sort(self.models, this.sortColumn, this.sortDirection);
  161. }
  162. // Check if field-filtering was set using setFieldFilter
  163. if ( !_.isEmpty( this.fieldFilterRules ) ) {
  164. self.models = self._fieldFilter(self.models, this.fieldFilterRules);
  165. }
  166. // Check if filtering was set using setFilter.
  167. if ( this.filterExpression !== "" ) {
  168. self.models = self._filter(self.models, this.filterFields, this.filterExpression);
  169. }
  170. // If the sorting or the filtering was changed go to the first page
  171. if ( this.lastSortColumn !== this.sortColumn || this.lastFilterExpression !== this.filterExpression || !_.isEqual(this.fieldFilterRules, this.lastFieldFilterRiles) ) {
  172. start = 0;
  173. stop = start + disp;
  174. self.currentPage = 1;
  175. this.lastSortColumn = this.sortColumn;
  176. this.lastFieldFilterRiles = this.fieldFilterRules;
  177. this.lastFilterExpression = this.filterExpression;
  178. }
  179. // We need to save the sorted and filtered models collection
  180. // because we'll use that sorted and filtered collection in info().
  181. self.sortedAndFilteredModels = self.models;
  182. self.reset(self.models.slice(start, stop));
  183. },
  184. // The actual place where the collection is sorted.
  185. // Check setSort for arguments explicacion.
  186. _sort: function ( models, sort, direction ) {
  187. models = models.sort(function (a, b) {
  188. var ac = a.get(sort),
  189. bc = b.get(sort);
  190. if ( !ac || !bc ) {
  191. return 0;
  192. } else {
  193. /* Make sure that both ac and bc are lowercase strings.
  194. * .toString() first so we don't have to worry if ac or bc
  195. * have other String-only methods.
  196. */
  197. ac = ac.toString().toLowerCase();
  198. bc = bc.toString().toLowerCase();
  199. }
  200. if (direction === 'desc') {
  201. // We need to know if there aren't any non-number characters
  202. // and that there are numbers-only characters and maybe a dot
  203. // if we have a float.
  204. if((!ac.match(/[^\d\.]/) && ac.match(/[\d\.]*/)) &&
  205. (!bc.match(/[^\d\.]/) && bc.match(/[\d\.]*/))
  206. ){
  207. if( (ac - 0) < (bc - 0) ) {
  208. return 1;
  209. }
  210. if( (ac - 0) > (bc - 0) ) {
  211. return -1;
  212. }
  213. } else {
  214. if (ac < bc) {
  215. return 1;
  216. }
  217. if (ac > bc) {
  218. return -1;
  219. }
  220. }
  221. } else {
  222. //Same as the regexp check in the 'if' part.
  223. if((!ac.match(/[^\d\.]/) && ac.match(/[\d\.]*/)) &&
  224. (!bc.match(/[^\d\.]/) && bc.match(/[\d\.]*/))
  225. ){
  226. if( (ac - 0) < (bc - 0) ) {
  227. return -1;
  228. }
  229. if( (ac - 0) > (bc - 0) ) {
  230. return 1;
  231. }
  232. } else {
  233. if (ac < bc) {
  234. return -1;
  235. }
  236. if (ac > bc) {
  237. return 1;
  238. }
  239. }
  240. }
  241. return 0;
  242. });
  243. return models;
  244. },
  245. // The actual place where the collection is field-filtered.
  246. // Check setFieldFilter for arguments explicacion.
  247. _fieldFilter: function( models, rules ) {
  248. // Check if there are any rules
  249. if ( _.isEmpty(rules) ) {
  250. return models;
  251. }
  252. var filteredModels = [];
  253. // Iterate over each rule
  254. _.each(models, function(model){
  255. var should_push = true;
  256. // Apply each rule to each model in the collection
  257. _.each(rules, function(rule){
  258. // Don't go inside the switch if we're already sure that the model won't be included in the results
  259. if( !should_push ){
  260. return false;
  261. }
  262. should_push = false;
  263. // The field's value will be passed to a custom function, which should
  264. // return true (if model should be included) or false (model should be ignored)
  265. if(rule.type === "function"){
  266. var f = _.wrap(rule.value, function(func){
  267. return func( model.get(rule.field) );
  268. });
  269. if( f() ){
  270. should_push = true;
  271. }
  272. // The field's value is required to be non-empty
  273. }else if(rule.type === "required"){
  274. if( !_.isEmpty( model.get(rule.field).toString() ) ) {
  275. should_push = true;
  276. }
  277. // The field's value is required to be greater tan N (numbers only)
  278. }else if(rule.type === "min"){
  279. if( !_.isNaN( Number( model.get(rule.field) ) ) &&
  280. !_.isNaN( Number( rule.value ) ) &&
  281. Number( model.get(rule.field) ) >= Number( rule.value ) ) {
  282. should_push = true;
  283. }
  284. // The field's value is required to be smaller tan N (numbers only)
  285. }else if(rule.type === "max"){
  286. if( !_.isNaN( Number( model.get(rule.field) ) ) &&
  287. !_.isNaN( Number( rule.value ) ) &&
  288. Number( model.get(rule.field) ) <= Number( rule.value ) ) {
  289. should_push = true;
  290. }
  291. // The field's value is required to be between N and M (numbers only)
  292. }else if(rule.type === "range"){
  293. if( !_.isNaN( Number( model.get(rule.field) ) ) &&
  294. _.isObject( rule.value ) &&
  295. !_.isNaN( Number( rule.value.min ) ) &&
  296. !_.isNaN( Number( rule.value.max ) ) &&
  297. Number( model.get(rule.field) ) >= Number( rule.value.min ) &&
  298. Number( model.get(rule.field) ) <= Number( rule.value.max ) ) {
  299. should_push = true;
  300. }
  301. // The field's value is required to be more than N chars long
  302. }else if(rule.type === "minLength"){
  303. if( model.get(rule.field).toString().length >= rule.value ) {
  304. should_push = true;
  305. }
  306. // The field's value is required to be no more than N chars long
  307. }else if(rule.type === "maxLength"){
  308. if( model.get(rule.field).toString().length <= rule.value ) {
  309. should_push = true;
  310. }
  311. // The field's value is required to be more than N chars long and no more than M chars long
  312. }else if(rule.type === "rangeLength"){
  313. if( _.isObject( rule.value ) &&
  314. !_.isNaN( Number( rule.value.min ) ) &&
  315. !_.isNaN( Number( rule.value.max ) ) &&
  316. model.get(rule.field).toString().length >= rule.value.min &&
  317. model.get(rule.field).toString().length <= rule.value.max ) {
  318. should_push = true;
  319. }
  320. // The field's value is required to be equal to one of the values in rules.value
  321. }else if(rule.type === "oneOf"){
  322. if( _.isArray( rule.value ) &&
  323. _.include( rule.value, model.get(rule.field) ) ) {
  324. should_push = true;
  325. }
  326. // The field's value is required to be equal to the value in rules.value
  327. }else if(rule.type === "equalTo"){
  328. if( rule.value === model.get(rule.field) ) {
  329. should_push = true;
  330. }
  331. // The field's value is required to match the regular expression
  332. }else if(rule.type === "pattern"){
  333. if( model.get(rule.field).toString().match(rule.value) ) {
  334. should_push = true;
  335. }
  336. //Unknown type
  337. }else{
  338. should_push = false;
  339. }
  340. });
  341. if( should_push ){
  342. filteredModels.push(model);
  343. }
  344. });
  345. return filteredModels;
  346. },
  347. // The actual place where the collection is filtered.
  348. // Check setFilter for arguments explicacion.
  349. _filter: function ( models, fields, filter ) {
  350. // For example, if you had a data model containing cars like { color: '', description: '', hp: '' },
  351. // your fields was set to ['color', 'description', 'hp'] and your filter was set
  352. // to "Black Mustang 300", the word "Black" will match all the cars that have black color, then
  353. // "Mustang" in the description and then the HP in the 'hp' field.
  354. // NOTE: "Black Musta 300" will return the same as "Black Mustang 300"
  355. // We accept fields to be a string, an array or an object
  356. // but if string or array is passed we need to convert it
  357. // to an object.
  358. var self = this;
  359. var obj_fields = {};
  360. if( _.isString( fields ) ) {
  361. obj_fields[fields] = {cmp_method: 'regexp'};
  362. }else if( _.isArray( fields ) ) {
  363. _.each(fields, function(field){
  364. obj_fields[field] = {cmp_method: 'regexp'};
  365. });
  366. }else{
  367. _.each(fields, function( cmp_opts, field ) {
  368. obj_fields[field] = _.defaults(cmp_opts, { cmp_method: 'regexp' });
  369. });
  370. }
  371. fields = obj_fields;
  372. //Remove diacritic characters if diacritic plugin is loaded
  373. if( _.has(Backbone.Paginator, 'removeDiacritics') && self.useDiacriticsPlugin ){
  374. filter = Backbone.Paginator.removeDiacritics(filter);
  375. }
  376. // 'filter' can be only a string.
  377. // If 'filter' is string we need to convert it to
  378. // a regular expression.
  379. // For example, if 'filter' is 'black dog' we need
  380. // to find every single word, remove duplicated ones (if any)
  381. // and transform the result to '(black|dog)'
  382. if( filter === '' || !_.isString(filter) ) {
  383. return models;
  384. } else {
  385. var words = filter.match(/\w+/ig);
  386. var pattern = "(" + _.uniq(words).join("|") + ")";
  387. var regexp = new RegExp(pattern, "igm");
  388. }
  389. var filteredModels = [];
  390. // We need to iterate over each model
  391. _.each( models, function( model ) {
  392. var matchesPerModel = [];
  393. // and over each field of each model
  394. _.each( fields, function( cmp_opts, field ) {
  395. var value = model.get( field );
  396. if( value ) {
  397. // The regular expression we created earlier let's us detect if a
  398. // given string contains each and all of the words in the regular expression
  399. // or not, but in both cases match() will return an array containing all
  400. // the words it matched.
  401. var matchesPerField = [];
  402. if( _.has(Backbone.Paginator, 'removeDiacritics') && self.useDiacriticsPlugin ){
  403. value = Backbone.Paginator.removeDiacritics(value.toString());
  404. }else{
  405. value = value.toString();
  406. }
  407. // Levenshtein cmp
  408. if( cmp_opts.cmp_method === 'levenshtein' && _.has(Backbone.Paginator, 'levenshtein') && self.useLevenshteinPlugin ) {
  409. var distance = Backbone.Paginator.levenshtein(value, filter);
  410. _.defaults(cmp_opts, { max_distance: 0 });
  411. if( distance <= cmp_opts.max_distance ) {
  412. matchesPerField = _.uniq(words);
  413. }
  414. // Default (RegExp) cmp
  415. }else{
  416. matchesPerField = value.match( regexp );
  417. }
  418. matchesPerField = _.map(matchesPerField, function(match) {
  419. return match.toString().toLowerCase();
  420. });
  421. _.each(matchesPerField, function(match){
  422. matchesPerModel.push(match);
  423. });
  424. }
  425. });
  426. // We just need to check if the returned array contains all the words in our
  427. // regex, and if it does, it means that we have a match, so we should save it.
  428. matchesPerModel = _.uniq( _.without(matchesPerModel, "") );
  429. if( _.isEmpty( _.difference(words, matchesPerModel) ) ) {
  430. filteredModels.push(model);
  431. }
  432. });
  433. return filteredModels;
  434. },
  435. // You shouldn't need to call info() as this method is used to
  436. // calculate internal data as first/prev/next/last page...
  437. info: function () {
  438. var self = this,
  439. info = {},
  440. totalRecords = (self.sortedAndFilteredModels) ? self.sortedAndFilteredModels.length : self.length,
  441. totalPages = Math.ceil(totalRecords / self.perPage);
  442. info = {
  443. totalUnfilteredRecords: self.origModels.length,
  444. totalRecords: totalRecords,
  445. currentPage: self.currentPage,
  446. perPage: this.perPage,
  447. totalPages: totalPages,
  448. lastPage: totalPages,
  449. previous: false,
  450. next: false,
  451. startRecord: totalRecords === 0 ? 0 : (self.currentPage - 1) * this.perPage + 1,
  452. endRecord: Math.min(totalRecords, self.currentPage * this.perPage)
  453. };
  454. if (self.currentPage > 1) {
  455. info.prev = self.currentPage - 1;
  456. }
  457. if (self.currentPage < info.totalPages) {
  458. info.next = self.currentPage + 1;
  459. }
  460. info.pageSet = self.setPagination(info);
  461. self.information = info;
  462. return info;
  463. },
  464. // setPagination also is an internal function that shouldn't be called directly.
  465. // It will create an array containing the pages right before and right after the
  466. // actual page.
  467. setPagination: function ( info ) {
  468. var pages = [], i = 0, l = 0;
  469. // How many adjacent pages should be shown on each side?
  470. var ADJACENT = 3,
  471. ADJACENTx2 = ADJACENT * 2,
  472. LASTPAGE = Math.ceil(info.totalRecords / info.perPage),
  473. LPM1 = -1;
  474. if (LASTPAGE > 1) {
  475. // not enough pages to bother breaking it up
  476. if (LASTPAGE < (7 + ADJACENTx2)) {
  477. for (i = 1, l = LASTPAGE; i <= l; i++) {
  478. pages.push(i);
  479. }
  480. }
  481. // enough pages to hide some
  482. else if (LASTPAGE > (5 + ADJACENTx2)) {
  483. //close to beginning; only hide later pages
  484. if (info.currentPage < (1 + ADJACENTx2)) {
  485. for (i = 1, l = 4 + ADJACENTx2; i < l; i++) {
  486. pages.push(i);
  487. }
  488. }
  489. // in middle; hide some front and some back
  490. else if (LASTPAGE - ADJACENTx2 > info.currentPage && info.currentPage > ADJACENTx2) {
  491. for (i = info.currentPage - ADJACENT; i <= info.currentPage + ADJACENT; i++) {
  492. pages.push(i);
  493. }
  494. }
  495. // close to end; only hide early pages
  496. else {
  497. for (i = LASTPAGE - (2 + ADJACENTx2); i <= LASTPAGE; i++) {
  498. pages.push(i);
  499. }
  500. }
  501. }
  502. }
  503. return pages;
  504. }
  505. });
  506. // @name: requestPager
  507. //
  508. // Paginator for server-side data being requested from a backend/API
  509. //
  510. // @description:
  511. // This paginator is responsible for providing pagination
  512. // and sort capabilities for requests to a server-side
  513. // data service (e.g an API)
  514. //
  515. Paginator.requestPager = Backbone.Collection.extend({
  516. sync: function ( method, model, options ) {
  517. var self = this;
  518. // Create default values if no others are specified
  519. _.defaults(self.paginator_ui, {
  520. firstPage: 0,
  521. currentPage: 1,
  522. perPage: 5,
  523. totalPages: 10
  524. });
  525. // Change scope of 'paginator_ui' object values
  526. _.each(self.paginator_ui, function(value, key) {
  527. if( _.isUndefined(self[key]) ) {
  528. self[key] = self.paginator_ui[key];
  529. }
  530. });
  531. // Some values could be functions, let's make sure
  532. // to change their scope too and run them
  533. var queryAttributes = {};
  534. _.each(self.server_api, function(value, key){
  535. if( _.isFunction(value) ) {
  536. value = _.bind(value, self);
  537. }
  538. queryAttributes[key] = value;
  539. });
  540. var queryOptions = _.clone(self.paginator_core);
  541. _.each(queryOptions, function(value, key){
  542. if( _.isFunction(value) ) {
  543. value = _.bind(value, self);
  544. }
  545. queryOptions[key] = value;
  546. });
  547. // Create default values if no others are specified
  548. queryOptions = _.defaults(queryOptions, {
  549. timeout: 25000,
  550. cache: false,
  551. type: 'GET',
  552. dataType: 'jsonp'
  553. });
  554. queryOptions = _.extend(queryOptions, {
  555. jsonpCallback: 'callback',
  556. data: decodeURIComponent($.param(queryAttributes)),
  557. processData: false,
  558. url: _.result(queryOptions, 'url')
  559. }, options);
  560. return $.ajax( queryOptions );
  561. },
  562. requestNextPage: function () {
  563. if ( this.currentPage !== undefined ) {
  564. this.currentPage += 1;
  565. this.pager();
  566. }
  567. },
  568. requestPreviousPage: function () {
  569. if ( this.currentPage !== undefined ) {
  570. this.currentPage -= 1;
  571. this.pager();
  572. }
  573. },
  574. updateOrder: function ( column ) {
  575. if (column !== undefined) {
  576. this.sortField = column;
  577. this.pager();
  578. }
  579. },
  580. goTo: function ( page ) {
  581. if(page !== undefined){
  582. this.currentPage = parseInt(page, 10);
  583. this.pager();
  584. }
  585. },
  586. howManyPer: function ( count ) {
  587. if( count !== undefined ){
  588. this.currentPage = this.firstPage;
  589. this.perPage = count;
  590. this.pager();
  591. }
  592. },
  593. sort: function () {
  594. //assign to as needed.
  595. },
  596. info: function () {
  597. var info = {
  598. // If parse() method is implemented and totalRecords is set to the length
  599. // of the records returned, make it available. Else, default it to 0
  600. totalRecords: this.totalRecords || 0,
  601. currentPage: this.currentPage,
  602. firstPage: this.firstPage,
  603. totalPages: this.totalPages,
  604. lastPage: this.totalPages,
  605. perPage: this.perPage
  606. };
  607. this.information = info;
  608. return info;
  609. },
  610. // fetches the latest results from the server
  611. pager: function () {
  612. this.fetch({});
  613. }
  614. });
  615. return Paginator;
  616. }( Backbone, _, jQuery ));