1 // $Id$
  2 
  3 (function ($) {
  4 
  5 /**
  6  * A pager widget for jQuery.
  7  *
  8  * <p>Heavily inspired by the Ruby on Rails will_paginate gem.</p>
  9  *
 10  * @expects this.target to be a list.
 11  * @class PagerWidget
 12  * @augments AjaxSolr.AbstractWidget
 13  */
 14 AjaxSolr.PagerWidget = AjaxSolr.AbstractWidget.extend(
 15   /** @lends AjaxSolr.PagerWidget.prototype */
 16   {
 17   /**
 18    * How many links are shown around the current page.
 19    *
 20    * @field
 21    * @public
 22    * @type Number
 23    * @default 4
 24    */
 25   innerWindow: 4,
 26 
 27   /**
 28    * How many links are around the first and the last page.
 29    *
 30    * @field
 31    * @public
 32    * @type Number
 33    * @default 1
 34    */
 35   outerWindow: 1,
 36 
 37   /**
 38    * The previous page link label.
 39    *
 40    * @field
 41    * @public
 42    * @type String
 43    * @default "« previous"
 44    */
 45   prevLabel: '« Previous',
 46 
 47   /**
 48    * The next page link label.
 49    *
 50    * @field
 51    * @public
 52    * @type String
 53    * @default "next »"
 54    */
 55   nextLabel: 'Next »',
 56 
 57   /**
 58    * Separator between pagination links.
 59    *
 60    * @field
 61    * @public
 62    * @type String
 63    * @default ""
 64    */
 65   separator: ' ',
 66 
 67   /**
 68    * The current page number.
 69    *
 70    * @field
 71    * @private
 72    * @type Number
 73    */
 74   currentPage: null,
 75 
 76   /**
 77    * The total number of pages.
 78    *
 79    * @field
 80    * @private
 81    * @type Number
 82    */
 83   totalPages: null,
 84 
 85   /**
 86    * @returns {String} The gap in page links, which is represented by:
 87    *   <span class="pager-gap">…</span>
 88    */
 89   gapMarker: function () {
 90     return '<span class="pager-gap">…</span>';
 91   },
 92 
 93   /**
 94    * @returns {Array} The links for the visible page numbers.
 95    */
 96   windowedLinks: function () {
 97     var links = [];
 98 
 99     var prev = null;
100 
101     visible = this.visiblePageNumbers();
102     for (var i = 0, l = visible.length; i < l; i++) {
103       if (prev && visible[i] > prev + 1) links.push(this.gapMarker());
104       links.push(this.pageLinkOrSpan(visible[i], [ 'pager-current' ]));
105       prev = visible[i];
106     }
107 
108     return links;
109   },
110 
111   /**
112    * @returns {Array} The visible page numbers according to the window options.
113    */ 
114   visiblePageNumbers: function () {
115     var windowFrom = this.currentPage - this.innerWindow;
116     var windowTo = this.currentPage + this.innerWindow;
117 
118     // If the window is truncated on one side, make the other side longer
119     if (windowTo > this.totalPages) {
120       windowFrom = Math.max(0, windowFrom - (windowTo - this.totalPages));
121       windowTo = this.totalPages;
122     }
123     if (windowFrom < 1) {
124       windowTo = Math.min(this.totalPages, windowTo + (1 - windowFrom));
125       windowFrom = 1;
126     }
127 
128     var visible = [];
129 
130     // Always show the first page
131     visible.push(1);
132     // Don't add inner window pages twice
133     for (var i = 2; i <= Math.min(1 + this.outerWindow, windowFrom - 1); i++) {
134       visible.push(i);
135     }
136     // If the gap is just one page, close the gap
137     if (1 + this.outerWindow == windowFrom - 2) {
138       visible.push(windowFrom - 1);
139     }
140     // Don't add the first or last page twice
141     for (var i = Math.max(2, windowFrom); i <= Math.min(windowTo, this.totalPages - 1); i++) {
142       visible.push(i);
143     }
144     // If the gap is just one page, close the gap
145     if (this.totalPages - this.outerWindow == windowTo + 2) {
146       visible.push(windowTo + 1);
147     }
148     // Don't add inner window pages twice
149     for (var i = Math.max(this.totalPages - this.outerWindow, windowTo + 1); i < this.totalPages; i++) {
150       visible.push(i);
151     }
152     // Always show the last page, unless it's the first page
153     if (this.totalPages > 1) {
154       visible.push(this.totalPages);
155     }
156 
157     return visible;
158   },
159 
160   /**
161    * @param {Number} page A page number.
162    * @param {String} classnames CSS classes to add to the page link.
163    * @param {String} text The inner HTML of the page link (optional).
164    * @returns The link or span for the given page.
165    */
166   pageLinkOrSpan: function (page, classnames, text) {
167     text = text || page;
168 
169     if (page && page != this.currentPage) {
170       return $('<a href="#"/>').html(text).attr('rel', this.relValue(page)).addClass(classnames[1]).click(this.clickHandler(page));
171     }
172     else {
173       return $('<span/>').html(text).addClass(classnames.join(' '));
174     }
175   },
176 
177   /**
178    * @param {Number} page A page number.
179    * @returns {Function} The click handler for the page link.
180    */
181   clickHandler: function (page) {
182     var self = this;
183     return function () {
184       self.manager.store.get('start').val((page - 1) * (self.manager.response.responseHeader.params.rows || 10));
185       self.manager.doRequest();
186       return false;
187     }
188   },
189 
190   /**
191    * @param {Number} page A page number.
192    * @returns {String} The <tt>rel</tt> attribute for the page link.
193    */
194   relValue: function (page) {
195     switch (page) {
196       case this.previousPage():
197         return 'prev' + (page == 1 ? 'start' : '');
198       case this.nextPage():
199         return 'next';
200       case 1:
201         return 'start';
202       default: 
203         return '';
204     }
205   },
206 
207   /**
208    * @returns {Number} The page number of the previous page or null if no previous page.
209    */
210   previousPage: function () {
211     return this.currentPage > 1 ? (this.currentPage - 1) : null;
212   },
213 
214   /**
215    * @returns {Number} The page number of the next page or null if no next page.
216    */
217   nextPage: function () {
218     return this.currentPage < this.totalPages ? (this.currentPage + 1) : null;
219   },
220 
221   /**
222    * An abstract hook for child implementations.
223    *
224    * @param {Number} perPage The number of items shown per results page.
225    * @param {Number} offset The index in the result set of the first document to render.
226    * @param {Number} total The total number of documents in the result set.
227    */
228   renderHeader: function (perPage, offset, total) {},
229 
230   /**
231    * Render the pagination links.
232    *
233    * @param {Array} links The links for the visible page numbers.
234    */
235   renderLinks: function (links) {
236     if (this.totalPages) {
237       links.unshift(this.pageLinkOrSpan(this.previousPage(), [ 'pager-disabled', 'pager-prev' ], this.prevLabel));
238       links.push(this.pageLinkOrSpan(this.nextPage(), [ 'pager-disabled', 'pager-next' ], this.nextLabel));
239       AjaxSolr.theme('list_items', this.target, links, this.separator);
240     }
241   },
242 
243   afterRequest: function () {
244     var perPage = parseInt(this.manager.response.responseHeader.params.rows || 10);
245     var offset = parseInt(this.manager.response.responseHeader.params.start || 0);
246     var total = parseInt(this.manager.response.response.numFound);
247 
248     // Normalize the offset to a multiple of perPage.
249     offset = offset - offset % perPage;
250 
251     this.currentPage = Math.ceil((offset + 1) / perPage);
252     this.totalPages = Math.ceil(total / perPage);
253 
254     $(this.target).empty();
255 
256     this.renderLinks(this.windowedLinks());
257     this.renderHeader(perPage, offset, total);
258   }
259 });
260 
261 })(jQuery);
262