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