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