PageRenderTime 79ms CodeModel.GetById 40ms RepoModel.GetById 0ms app.codeStats 0ms

/web/js/libs/backbone/backbone.paginator.js

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