PageRenderTime 53ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/com.poetry/WebContent/js/backbone.paginator.js

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