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