http://git-wip-us.apache.org/repos/asf/chukwa/blob/0cad3aae/src/main/web/hicc/ajax-solr/core/ParameterStore.js ---------------------------------------------------------------------- diff --git a/src/main/web/hicc/ajax-solr/core/ParameterStore.js b/src/main/web/hicc/ajax-solr/core/ParameterStore.js new file mode 100644 index 0000000..cb53db6 --- /dev/null +++ b/src/main/web/hicc/ajax-solr/core/ParameterStore.js @@ -0,0 +1,366 @@ +(function (callback) { + if (typeof define === 'function' && define.amd) { + define(['core/Core', 'core/Parameter'], callback); + } + else { + callback(); + } +}(function () { + +/** + * The ParameterStore, as its name suggests, stores Solr parameters. Widgets + * expose some of these parameters to the user. Whenever the user changes the + * values of these parameters, the state of the application changes. In order to + * allow the user to move back and forth between these states with the browser's + * Back and Forward buttons, and to bookmark these states, each state needs to + * be stored. The easiest method is to store the exposed parameters in the URL + * hash (see the <tt>ParameterHashStore</tt> class). However, you may implement + * your own storage method by extending this class. + * + * <p>For a list of possible parameters, please consult the links below.</p> + * + * @see http://wiki.apache.org/solr/CoreQueryParameters + * @see http://wiki.apache.org/solr/CommonQueryParameters + * @see http://wiki.apache.org/solr/SimpleFacetParameters + * @see http://wiki.apache.org/solr/HighlightingParameters + * @see http://wiki.apache.org/solr/MoreLikeThis + * @see http://wiki.apache.org/solr/SpellCheckComponent + * @see http://wiki.apache.org/solr/StatsComponent + * @see http://wiki.apache.org/solr/TermsComponent + * @see http://wiki.apache.org/solr/TermVectorComponent + * @see http://wiki.apache.org/solr/LocalParams + * + * @param properties A map of fields to set. Refer to the list of public fields. + * @class ParameterStore + */ +AjaxSolr.ParameterStore = AjaxSolr.Class.extend( + /** @lends AjaxSolr.ParameterStore.prototype */ + { + constructor: function (attributes) { + /** + * @param {Object} [attributes] + * @param {String[]} [attributes.exposed] The names of the exposed + * parameters. Any parameters that your widgets expose to the user, + * directly or indirectly, should be listed here. + */ + AjaxSolr.extend(this, { + exposed: [], + // The Solr parameters. + params: {}, + // A reference to the parameter store's manager. + manager: null + }, attributes); + }, + + /** + * An abstract hook for child implementations. + * + * <p>This method should do any necessary one-time initializations.</p> + */ + init: function () {}, + + /** + * Some Solr parameters may be specified multiple times. It is easiest to + * hard-code a list of such parameters. You may change the list by passing + * <code>{ multiple: /pattern/ }</code> as an argument to the constructor of + * this class or one of its children, e.g.: + * + * <p><code>new ParameterStore({ multiple: /pattern/ })</code> + * + * @param {String} name The name of the parameter. + * @returns {Boolean} Whether the parameter may be specified multiple times. + * @see http://lucene.apache.org/solr/api/org/apache/solr/handler/DisMaxRequestHandler.html + */ + isMultiple: function (name) { + return name.match(/^(?:bf|bq|facet\.date|facet\.date\.other|facet\.date\.include|facet\.field|facet\.pivot|facet\.range|facet\.range\.other|facet\.range\.include|facet\.query|fq|group\.field|group\.func|group\.query|pf|qf)$/); + }, + + /** + * Returns a parameter. If the parameter doesn't exist, creates it. + * + * @param {String} name The name of the parameter. + * @returns {AjaxSolr.Parameter|AjaxSolr.Parameter[]} The parameter. + */ + get: function (name) { + if (this.params[name] === undefined) { + var param = new AjaxSolr.Parameter({ name: name }); + if (this.isMultiple(name)) { + this.params[name] = [ param ]; + } + else { + this.params[name] = param; + } + } + return this.params[name]; + }, + + /** + * If the parameter may be specified multiple times, returns the values of + * all identically-named parameters. If the parameter may be specified only + * once, returns the value of that parameter. + * + * @param {String} name The name of the parameter. + * @returns {String[]|Number[]} The value(s) of the parameter. + */ + values: function (name) { + if (this.params[name] !== undefined) { + if (this.isMultiple(name)) { + var values = []; + for (var i = 0, l = this.params[name].length; i < l; i++) { + values.push(this.params[name][i].val()); + } + return values; + } + else { + return [ this.params[name].val() ]; + } + } + return []; + }, + + /** + * If the parameter may be specified multiple times, adds the given parameter + * to the list of identically-named parameters, unless one already exists with + * the same value. If it may be specified only once, replaces the parameter. + * + * @param {String} name The name of the parameter. + * @param {AjaxSolr.Parameter} [param] The parameter. + * @returns {AjaxSolr.Parameter|Boolean} The parameter, or false. + */ + add: function (name, param) { + if (param === undefined) { + param = new AjaxSolr.Parameter({ name: name }); + } + if (this.isMultiple(name)) { + if (this.params[name] === undefined) { + this.params[name] = [ param ]; + } + else { + if (AjaxSolr.inArray(param.val(), this.values(name)) == -1) { + this.params[name].push(param); + } + else { + return false; + } + } + } + else { + this.params[name] = param; + } + return param; + }, + + /** + * Deletes a parameter. + * + * @param {String} name The name of the parameter. + * @param {Number} [index] The index of the parameter. + */ + remove: function (name, index) { + if (index === undefined) { + delete this.params[name]; + } + else { + this.params[name].splice(index, 1); + if (this.params[name].length == 0) { + delete this.params[name]; + } + } + }, + + /** + * Finds all parameters with matching values. + * + * @param {String} name The name of the parameter. + * @param {String|Number|String[]|Number[]|RegExp} value The value. + * @returns {String|Number[]} The indices of the parameters found. + */ + find: function (name, value) { + if (this.params[name] !== undefined) { + if (this.isMultiple(name)) { + var indices = []; + for (var i = 0, l = this.params[name].length; i < l; i++) { + if (AjaxSolr.equals(this.params[name][i].val(), value)) { + indices.push(i); + } + } + return indices.length ? indices : false; + } + else { + if (AjaxSolr.equals(this.params[name].val(), value)) { + return name; + } + } + } + return false; + }, + + /** + * If the parameter may be specified multiple times, creates a parameter using + * the given name and value, and adds it to the list of identically-named + * parameters, unless one already exists with the same value. If it may be + * specified only once, replaces the parameter. + * + * @param {String} name The name of the parameter. + * @param {String|Number|String[]|Number[]} value The value. + * @param {Object} [locals] The parameter's local parameters. + * @returns {AjaxSolr.Parameter|Boolean} The parameter, or false. + */ + addByValue: function (name, value, locals) { + if (locals === undefined) { + locals = {}; + } + if (this.isMultiple(name) && AjaxSolr.isArray(value)) { + var ret = []; + for (var i = 0, l = value.length; i < l; i++) { + ret.push(this.add(name, new AjaxSolr.Parameter({ name: name, value: value[i], locals: locals }))); + } + return ret; + } + else { + return this.add(name, new AjaxSolr.Parameter({ name: name, value: value, locals: locals })); + } + }, + + /** + * Deletes any parameter with a matching value. + * + * @param {String} name The name of the parameter. + * @param {String|Number|String[]|Number[]|RegExp} value The value. + * @returns {String|Number[]} The indices deleted. + */ + removeByValue: function (name, value) { + var indices = this.find(name, value); + if (indices) { + if (AjaxSolr.isArray(indices)) { + for (var i = indices.length - 1; i >= 0; i--) { + this.remove(name, indices[i]); + } + } + else { + this.remove(indices); + } + } + return indices; + }, + + /** + * Returns the Solr parameters as a query string. + * + * <p>IE6 calls the default toString() if you write <tt>store.toString() + * </tt>. So, we need to choose another name for toString().</p> + */ + string: function () { + var params = [], string; + for (var name in this.params) { + if (this.isMultiple(name)) { + for (var i = 0, l = this.params[name].length; i < l; i++) { + string = this.params[name][i].string(); + if (string) { + params.push(string); + } + } + } + else { + string = this.params[name].string(); + if (string) { + params.push(string); + } + } + } + return params.join('&'); + }, + + /** + * Parses a query string into Solr parameters. + * + * @param {String} str The string to parse. + */ + parseString: function (str) { + var pairs = str.split('&'); + for (var i = 0, l = pairs.length; i < l; i++) { + if (pairs[i]) { // ignore leading, trailing, and consecutive &'s + var param = new AjaxSolr.Parameter(); + param.parseString(pairs[i]); + this.add(param.name, param); + } + } + }, + + /** + * Returns the exposed parameters as a query string. + * + * @returns {String} A string representation of the exposed parameters. + */ + exposedString: function () { + var params = [], string; + for (var i = 0, l = this.exposed.length; i < l; i++) { + if (this.params[this.exposed[i]] !== undefined) { + if (this.isMultiple(this.exposed[i])) { + for (var j = 0, m = this.params[this.exposed[i]].length; j < m; j++) { + string = this.params[this.exposed[i]][j].string(); + if (string) { + params.push(string); + } + } + } + else { + string = this.params[this.exposed[i]].string(); + if (string) { + params.push(string); + } + } + } + } + return params.join('&'); + }, + + /** + * Resets the values of the exposed parameters. + */ + exposedReset: function () { + for (var i = 0, l = this.exposed.length; i < l; i++) { + this.remove(this.exposed[i]); + } + }, + + /** + * Loads the values of exposed parameters from persistent storage. It is + * necessary, in most cases, to reset the values of exposed parameters before + * setting the parameters to the values in storage. This is to ensure that a + * parameter whose name is not present in storage is properly reset. + * + * @param {Boolean} [reset=true] Whether to reset the exposed parameters. + * before loading new values from persistent storage. Default: true. + */ + load: function (reset) { + if (reset === undefined) { + reset = true; + } + if (reset) { + this.exposedReset(); + } + this.parseString(this.storedString()); + }, + + /** + * An abstract hook for child implementations. + * + * <p>Stores the values of the exposed parameters in persistent storage. This + * method should usually be called before each Solr request.</p> + */ + save: function () {}, + + /** + * An abstract hook for child implementations. + * + * <p>Returns the string to parse from persistent storage.</p> + * + * @returns {String} The string from persistent storage. + */ + storedString: function () { + return ''; + } +}); + +}));
http://git-wip-us.apache.org/repos/asf/chukwa/blob/0cad3aae/src/main/web/hicc/ajax-solr/managers/Manager.jquery.js ---------------------------------------------------------------------- diff --git a/src/main/web/hicc/ajax-solr/managers/Manager.jquery.js b/src/main/web/hicc/ajax-solr/managers/Manager.jquery.js new file mode 100644 index 0000000..03ec766 --- /dev/null +++ b/src/main/web/hicc/ajax-solr/managers/Manager.jquery.js @@ -0,0 +1,40 @@ +(function (callback) { + if (typeof define === 'function' && define.amd) { + define(['core/AbstractManager'], callback); + } + else { + callback(); + } +}(function () { + +/** + * @see http://wiki.apache.org/solr/SolJSON#JSON_specific_parameters + * @class Manager + * @augments AjaxSolr.AbstractManager + */ +AjaxSolr.Manager = AjaxSolr.AbstractManager.extend( + /** @lends AjaxSolr.Manager.prototype */ + { + executeRequest: function (servlet, string, handler, errorHandler) { + var self = this, + options = {dataType: 'json'}; + string = string || this.store.string(); + handler = handler || function (data) { + self.handleResponse(data); + }; + errorHandler = errorHandler || function (jqXHR, textStatus, errorThrown) { + self.handleError(textStatus + ', ' + errorThrown); + }; + if (this.proxyUrl) { + options.url = this.proxyUrl; + options.data = {query: string}; + options.type = 'POST'; + } + else { + options.url = this.solrUrl + servlet + '?' + string + '&wt=json&json.wrf=?'; + } + jQuery.ajax(options).done(handler).fail(errorHandler); + } +}); + +})); http://git-wip-us.apache.org/repos/asf/chukwa/blob/0cad3aae/src/main/web/hicc/ajax-solr/widgets/ParameterHistoryStore.js ---------------------------------------------------------------------- diff --git a/src/main/web/hicc/ajax-solr/widgets/ParameterHistoryStore.js b/src/main/web/hicc/ajax-solr/widgets/ParameterHistoryStore.js new file mode 100644 index 0000000..accc9de --- /dev/null +++ b/src/main/web/hicc/ajax-solr/widgets/ParameterHistoryStore.js @@ -0,0 +1,82 @@ +;(function(history) { + /** + * A parameter store that stores the values of exposed parameters in the URL via History.js + * to maintain the application's state. This uses the HTML5 History API for newer browsers, and + * falls back to using the hash in older browsers. Don't forget to add the following (or similar) + * inside your <tt>head</tt> tag: + * + * <pre> + * <script src="history.js/scripts/bundled/html4+html5/jquery.history.js"></script> + * </pre> + * + * Configure the manager with: + * + * <pre> + * Manager.setStore(new AjaxSolr.ParameterHistoryStore()); + * </pre> + * + * @class ParameterHistoryStore + * @augments AjaxSolr.ParameterStore + * @see https://github.com/browserstate/history.js + * @see http://www.whatwg.org/specs/web-apps/current-work/multipage/history.html + */ + AjaxSolr.ParameterHistoryStore = AjaxSolr.ParameterStore.extend( + /** @lends AjaxSolr.ParameterHistoryStore.prototype */ + { + init: function () { + if (this.exposed.length) { + if (!history) { + throw 'ParameterHistoryStore requires History.js'; + } + + history.Adapter.bind(window, 'statechange', this.stateChangeFunction(this)); + } + }, + + /** + * Stores the values of the exposed parameters in both the local hash and History.js + * No other code should be made to change these two values. + */ + save: function () { + this.hash = this.exposedString(); + history.pushState({ params: this.hash }, null, '?' + this.hash); + }, + + /** + * @see ParameterStore#storedString() + */ + storedString: function () { + var state = history.getState(); + + // Load the state from the History object. + if (state.data && state.data.params) { + return state.data.params; + } + + // If initial load, load the state from the URL. + var url = state.cleanUrl, index = url.indexOf('?'); + if (index == -1) { + return ''; + } + else { + return url.substr(index + 1); + } + }, + + /** + * Called when History.js detects a state change. Checks if state is different to previous state, + * and if so, sends a request to Solr. This needs to check if the state has changed since it also + * gets called when we call pushState above. + */ + stateChangeFunction: function (self) { + return function () { + var hash = self.storedString(); + + if (self.hash != hash) { + self.load(); + self.manager.doRequest(); + } + } + } + }); +})(window.History); http://git-wip-us.apache.org/repos/asf/chukwa/blob/0cad3aae/src/main/web/hicc/ajax-solr/widgets/ParameterYUIStore.js ---------------------------------------------------------------------- diff --git a/src/main/web/hicc/ajax-solr/widgets/ParameterYUIStore.js b/src/main/web/hicc/ajax-solr/widgets/ParameterYUIStore.js new file mode 100644 index 0000000..7ee377d --- /dev/null +++ b/src/main/web/hicc/ajax-solr/widgets/ParameterYUIStore.js @@ -0,0 +1,90 @@ +/** + * A parameter store that stores the values of exposed parameters using the YUI + * History Manager to maintain the application's state. Don't forget to add the + * following inside your <tt>head</tt> tag: + * + * <pre> + * <script src="http://yui.yahooapis.com/2.9.0/build/yahoo/yahoo-min.js"></script> + * <script src="http://yui.yahooapis.com/2.9.0/build/event/event-min.js"></script> + * <script src="http://yui.yahooapis.com/2.9.0/build/history/history-min.js"></script> + * </pre> + * + * And the following inside your <tt>body</tt> tag: + * + * <pre> + * <iframe id="yui-history-iframe" src="path-to-existing-asset" style="position:absolute;top:0;left:0;width:1px;height:1px;visibility:hidden"></iframe> + * <input id="yui-history-field" type="hidden"> + * </pre> + * + * Configure the manager with: + * + * <pre> + * Manager.setStore(new AjaxSolr.ParameterYUIStore()); + * </pre> + * + * @see http://developer.yahoo.com/yui/history/ + * @class ParameterYUIStore + * @augments AjaxSolr.ParameterStore + */ +AjaxSolr.ParameterYUIStore = AjaxSolr.ParameterStore.extend( + /** @lends AjaxSolr.ParameterYUIStore.prototype */ + { + /** + * @param {Object} [attributes] + * @param {String} [attributes.module] The name of the YUI History Manager + * module to use for the parameter store. Defaults to "q". + */ + constructor: function (attributes) { + AjaxSolr.ParameterYUIStore.__super__.constructor.apply(this, arguments); + AjaxSolr.extend(this, { + module: 'q', + // Whether the YUI History Manager is initialized. + initialized: false, + // Whether the parameter store is curring loading state. + loading: false, + // Whether the parameter store is curring saving state. + saving: false + }, attributes); + }, + + /** + * Initializes the YUI History Manager. + */ + init: function () { + if (this.exposed.length) { + var self = this; + YAHOO.util.History.register(this.module, YAHOO.util.History.getBookmarkedState(this.module) || this.exposedString(), function () { + if (!self.saving) { + self.loading = true; + self.load(); + self.manager.doRequest(); + self.loading = false; + } + }); + YAHOO.util.History.onReady(function () { + self.initialized = true; + self.load(); + self.manager.doRequest(); + }); + YAHOO.util.History.initialize('yui-history-field', 'yui-history-iframe'); + } + }, + + /** + * Stores the values of the exposed parameters in the YUI History Manager. + */ + save: function () { + if (!self.loading) { + this.saving = true; + YAHOO.util.History.navigate(this.module, this.exposedString()); + this.saving = false; + } + }, + + /** + * @see ParameterStore#storedString() + */ + storedString: function () { + return this.initialized ? YAHOO.util.History.getCurrentState(this.module) : this.exposedString(); + } +}); http://git-wip-us.apache.org/repos/asf/chukwa/blob/0cad3aae/src/main/web/hicc/ajax-solr/widgets/jquery/AutocompleteTermWidget.js ---------------------------------------------------------------------- diff --git a/src/main/web/hicc/ajax-solr/widgets/jquery/AutocompleteTermWidget.js b/src/main/web/hicc/ajax-solr/widgets/jquery/AutocompleteTermWidget.js new file mode 100644 index 0000000..1afa34c --- /dev/null +++ b/src/main/web/hicc/ajax-solr/widgets/jquery/AutocompleteTermWidget.js @@ -0,0 +1,134 @@ +(function ($) { + +/** + * A <i>term</i> autocomplete search box, using jQueryUI.autocomplete. This + * implementation uses Solr's facet.prefix technique. This technique benefits + * from honoring the filter query state and by being able to put words prior to + * the last one the user is typing into a filter query as well to get even more + * relevant completion suggestions. + * + * Index instructions: + * 1. Put a facet warming query into Solr's "firstSearcher" in solrconfig.xml, + * for the target field. + * 2. Use appropriate text analysis to include a tokenizer (not keyword) and do + * <i>not</i> do stemming or else you will see stems suggested. A 'light' + * stemmer may produce acceptable stems. + * 3. If you are auto-completing in a search box that would normally be using + * the dismax query parser AND your qf parameter references more than one field, + * then you might want to use a catch-all search field to autocomplete on. + * + * For large indexes, another implementation approach like the Suggester feature + * or TermsComponent might be better than a faceting approach. + * + * Other types of autocomplete (a.k.a. suggest) are "search-results", + * "query-log", and "facet-value". This widget does term autocompletion. + * + * @author David Smiley <david.w.smiley at gmail.com> + */ +AjaxSolr.AutocompleteTermWidget = AjaxSolr.AbstractTextWidget.extend( + /** @lends AjaxSolr.AutocompleteTermWidget.prototype */ + { + /** + * @param {Object} attributes + * @param {String} attributes.field The Solr field to autocomplete indexed + * terms from. + * @param {Boolean} [attributes.tokenized] Whether the underlying field is + * tokenized. This component will take words before the last word + * (whitespace separated) and generate a filter query for those words, while + * only the last word will be used for facet.prefix. For field-value + * completion (on just one field) or query log completion, you would have a + * non-tokenized field to complete against. Defaults to <tt>true</tt>. + * @param {Boolean} [attributes.lowercase] Indicates whether to lowercase the + * facet.prefix value. Defaults to <tt>true</tt>. + * @param {Number} [attributes.limit] The maximum number of results to show. + * Defaults to 10. + * @param {Number} [attributes.minLength] The minimum number of characters + * required to show suggestions. Defaults to 2. + * @param {String} [attributes.servlet] The URL path that follows the solr + * webapp, for use in auto-complete queries. If not specified, the manager's + * servlet property will be used. You may prepend the servlet with a core if + * using multiple cores. It is a good idea to use a non-default one to + * differentiate these requests in server logs and Solr statistics. + */ + constructor: function (attributes) { + AjaxSolr.AutocompleteTermWidget.__super__.constructor.apply(this, arguments); + AjaxSolr.extend(this, { + field: null, + tokenized: true, + lowercase: true, + limit: 10, + minLength: 2, + servlet: null + }, attributes); + }, + + init: function () { + var self = this; + + if (!this.field) { + throw '"field" must be set on AutocompleteTermWidget.'; + } + this.servlet = this.servlet || this.manager.servlet; + + $(this.target).find('input').bind('keydown', function (e) { + if (e.which == 13) { + var q = $(this).val(); + if (self.set(q)) { + self.doRequest(); + } + } + }); + + $(this.target).find('input').autocomplete({ + source: function (request, response) { // note: must always call response() + // If term ends with a space: + if (request.term.charAt(request.term.length - 1).replace(/^ +/, '').replace(/ +$/, '') == '') { + response(); + return; + } + + var term = request.term, + facetPrefix = term, // before the last word (if we tokenize) + fq = '', + store = new AjaxSolr.ParameterStore(); + + store.addByValue('fq', self.manager.store.values('fq')); + + if (self.tokenized) { + // Split out the last word of the term from the words before it. + var lastSpace = term.lastIndexOf(' '); + if (lastSpace > -1) { + fq = term.substring(0, lastSpace); + facetPrefix = term.substring(lastSpace + 1); + store.addByValue('fq', '{!dismax qf=' + self.field + '}' + fq); + } + } + if (self.lowercase) { + facetPrefix = facetPrefix.toLowerCase(); + } + + store.addByValue('facet.field', self.field); + store.addByValue('facet.limit', self.limit); + store.addByValue('facet.prefix', facetPrefix); + + self.manager.executeRequest(self.servlet, 'json.nl=arrarr&q=*:*&rows=0&facet=true&facet.mincount=1&' + store.string(), function (data) { + response($.map(data.facet_counts.facet_fields[self.field], function (term) { + var q = (fq + ' ' + term[0]).replace(/^ +/, '').replace(/ +$/, ''); + return { + label: q + ' (' + term[1] + ')', + value: q + } + })); + }); + }, + minLength: this.minLength, + select: function(event, ui) { + if (self.set(ui.item.value)) { + self.doRequest(); + } + } + }); + } +}); + +})(jQuery); http://git-wip-us.apache.org/repos/asf/chukwa/blob/0cad3aae/src/main/web/hicc/ajax-solr/widgets/jquery/PagerWidget.js ---------------------------------------------------------------------- diff --git a/src/main/web/hicc/ajax-solr/widgets/jquery/PagerWidget.js b/src/main/web/hicc/ajax-solr/widgets/jquery/PagerWidget.js new file mode 100644 index 0000000..768ec79 --- /dev/null +++ b/src/main/web/hicc/ajax-solr/widgets/jquery/PagerWidget.js @@ -0,0 +1,256 @@ +(function (callback) { + if (typeof define === 'function' && define.amd) { + define(['core/AbstractWidget'], callback); + } + else { + callback(); + } +}(function () { + +(function ($) { + +/** + * A pager widget for jQuery. + * + * <p>Heavily inspired by the Ruby on Rails will_paginate gem.</p> + * + * @expects this.target to be a list. + * @class PagerWidget + * @augments AjaxSolr.AbstractWidget + * @todo Don't use the manager to send the request. Request only the results, + * not the facets. Update only itself and the results widget. + */ +AjaxSolr.PagerWidget = AjaxSolr.AbstractWidget.extend( + /** @lends AjaxSolr.PagerWidget.prototype */ + { + /** + * @param {Object} [attributes] + * @param {Number} [attributes.innerWindow] How many links are shown around + * the current page. Defaults to 4. + * @param {Number} [attributes.outerWindow] How many links are around the + * first and the last page. Defaults to 1. + * @param {String} [attributes.prevLabel] The previous page link label. + * Defaults to "« Previous". + * @param {String} [attributes.nextLabel] The next page link label. Defaults + * to "Next »". + * @param {String} [attributes.separator] Separator between pagination links. + * Defaults to " ". + */ + constructor: function (attributes) { + AjaxSolr.PagerWidget.__super__.constructor.apply(this, arguments); + AjaxSolr.extend(this, { + innerWindow: 4, + outerWindow: 1, + prevLabel: '« Previous', + nextLabel: 'Next »', + separator: ' ', + // The current page number. + currentPage: null, + // The total number of pages. + totalPages: null + }, attributes); + }, + + /** + * @returns {String} The gap in page links, which is represented by: + * <span class="pager-gap">…</span> + */ + gapMarker: function () { + return '<span class="pager-gap">…</span>'; + }, + + /** + * @returns {Array} The links for the visible page numbers. + */ + windowedLinks: function () { + var links = []; + + var prev = null; + + visible = this.visiblePageNumbers(); + for (var i = 0, l = visible.length; i < l; i++) { + if (prev && visible[i] > prev + 1) links.push(this.gapMarker()); + links.push(this.pageLinkOrSpan(visible[i], [ 'pager-current' ])); + prev = visible[i]; + } + + return links; + }, + + /** + * @returns {Array} The visible page numbers according to the window options. + */ + visiblePageNumbers: function () { + var windowFrom = this.currentPage - this.innerWindow; + var windowTo = this.currentPage + this.innerWindow; + + // If the window is truncated on one side, make the other side longer + if (windowTo > this.totalPages) { + windowFrom = Math.max(0, windowFrom - (windowTo - this.totalPages)); + windowTo = this.totalPages; + } + if (windowFrom < 1) { + windowTo = Math.min(this.totalPages, windowTo + (1 - windowFrom)); + windowFrom = 1; + } + + var visible = []; + + // Always show the first page + visible.push(1); + // Don't add inner window pages twice + for (var i = 2; i <= Math.min(1 + this.outerWindow, windowFrom - 1); i++) { + visible.push(i); + } + // If the gap is just one page, close the gap + if (1 + this.outerWindow == windowFrom - 2) { + visible.push(windowFrom - 1); + } + // Don't add the first or last page twice + for (var i = Math.max(2, windowFrom); i <= Math.min(windowTo, this.totalPages - 1); i++) { + visible.push(i); + } + // If the gap is just one page, close the gap + if (this.totalPages - this.outerWindow == windowTo + 2) { + visible.push(windowTo + 1); + } + // Don't add inner window pages twice + for (var i = Math.max(this.totalPages - this.outerWindow, windowTo + 1); i < this.totalPages; i++) { + visible.push(i); + } + // Always show the last page, unless it's the first page + if (this.totalPages > 1) { + visible.push(this.totalPages); + } + + return visible; + }, + + /** + * @param {Number} page A page number. + * @param {String} classnames CSS classes to add to the page link. + * @param {String} text The inner HTML of the page link (optional). + * @returns The link or span for the given page. + */ + pageLinkOrSpan: function (page, classnames, text) { + text = text || page; + + if (page && page != this.currentPage) { + return $('<a href="#"></a>').html(text).attr('rel', this.relValue(page)).addClass(classnames[1]).click(this.clickHandler(page)); + } + else { + return $('<span></span>').html(text).addClass(classnames.join(' ')); + } + }, + + /** + * @param {Number} page A page number. + * @returns {Function} The click handler for the page link. + */ + clickHandler: function (page) { + var self = this; + return function () { + self.manager.store.get('start').val((page - 1) * self.perPage()); + self.doRequest(); + return false; + } + }, + + /** + * @param {Number} page A page number. + * @returns {String} The <tt>rel</tt> attribute for the page link. + */ + relValue: function (page) { + switch (page) { + case this.previousPage(): + return 'prev' + (page == 1 ? 'start' : ''); + case this.nextPage(): + return 'next'; + case 1: + return 'start'; + default: + return ''; + } + }, + + /** + * @returns {Number} The page number of the previous page or null if no previous page. + */ + previousPage: function () { + return this.currentPage > 1 ? (this.currentPage - 1) : null; + }, + + /** + * @returns {Number} The page number of the next page or null if no next page. + */ + nextPage: function () { + return this.currentPage < this.totalPages ? (this.currentPage + 1) : null; + }, + + /** + * An abstract hook for child implementations. + * + * @param {Number} perPage The number of items shown per results page. + * @param {Number} offset The index in the result set of the first document to render. + * @param {Number} total The total number of documents in the result set. + */ + renderHeader: function (perPage, offset, total) {}, + + /** + * Render the pagination links. + * + * @param {Array} links The links for the visible page numbers. + */ + renderLinks: function (links) { + if (this.totalPages) { + links.unshift(this.pageLinkOrSpan(this.previousPage(), [ 'pager-disabled', 'pager-prev' ], this.prevLabel)); + links.push(this.pageLinkOrSpan(this.nextPage(), [ 'pager-disabled', 'pager-next' ], this.nextLabel)); + + var $target = $(this.target); + $target.empty(); + + for (var i = 0, l = links.length; i < l; i++) { + var $li = $('<li></li>'); + if (this.separator && i > 0) { + $li.append(this.separator); + } + $target.append($li.append(links[i])); + } + } + }, + + /** + * @returns {Number} The number of results to display per page. + */ + perPage: function () { + return parseInt(this.manager.response.responseHeader && this.manager.response.responseHeader.params && this.manager.response.responseHeader.params.rows || this.manager.store.get('rows').val() || 10); + }, + + /** + * @returns {Number} The Solr offset parameter's value. + */ + getOffset: function () { + return parseInt(this.manager.response.responseHeader && this.manager.response.responseHeader.params && this.manager.response.responseHeader.params.start || this.manager.store.get('start').val() || 0); + }, + + afterRequest: function () { + var perPage = this.perPage(); + var offset = this.getOffset(); + var total = parseInt(this.manager.response.response.numFound); + + // Normalize the offset to a multiple of perPage. + offset = offset - offset % perPage; + + this.currentPage = Math.ceil((offset + 1) / perPage); + this.totalPages = Math.ceil(total / perPage); + + $(this.target).empty(); + + this.renderLinks(this.windowedLinks()); + this.renderHeader(perPage, offset, total); + } +}); + +})(jQuery); + +}));
