1/** 2 * @namespace A unique namespace for the AJAX Solr library. 3 */ 4AjaxSolr = function () {}; 5 6/** 7 * @namespace Baseclass for all classes 8 * @see https://github.com/documentcloud/backbone/blob/51eed189bf4d25877be4acdf51e0a4c6039583c5/backbone.js#L243 9 */ 10AjaxSolr.Class = function(attributes) { 11 AjaxSolr.extend(this, attributes); 12}; 13 14/** 15 * A class 'extends' itself into a subclass. 16 * 17 * @static 18 * @param protoProps The properties of the subclass. 19 * @returns A function that represents the subclass. 20 * @see https://github.com/documentcloud/backbone/blob/51eed189bf4d25877be4acdf51e0a4c6039583c5/backbone.js#L1516 21 */ 22AjaxSolr.Class.extend = function (protoProps, staticProps) { 23 var parent = this; 24 var child; 25 26 // The constructor function for the new subclass is either defined by you 27 // (the "constructor" property in your `extend` definition), or defaulted 28 // by us to simply call the parent's constructor. 29 if (protoProps && Object.prototype.hasOwnProperty.call(protoProps, 'constructor')) { 30 child = protoProps.constructor; 31 } else { 32 child = function(){ return parent.apply(this, arguments); }; 33 } 34 35 // Add static properties to the constructor function, if supplied.
36 AjaxSolr.extend(child, parent, staticProps); 37 38 // Set the prototype chain to inherit from `parent`, without calling 39 // `parent`'s constructor function. 40 var Surrogate = function(){ this.constructor = child; }; 41 Surrogate.prototype = parent.prototype; 42 child.prototype = new Surrogate; 43 44 // Add prototype properties (instance properties) to the subclass, 45 // if supplied. 46 if (protoProps) AjaxSolr.extend(child.prototype, protoProps); 47 48 // Set a convenience property in case the parent's prototype is needed 49 // later. 50 child.__super__ = parent.prototype; 51 52 return child; 53}; 54 55/** 56 * @static 57 * @see https://github.com/documentcloud/underscore/blob/7342e289aa9d91c5aacfb3662ea56e7a6d081200/underscore.js#L789 58*/ 59AjaxSolr.extend = function (child) { 60 // From _.extend 61 var obj = Array.prototype.slice.call(arguments, 1); 62 63 // From _.extend 64 var iterator = function(source) { 65 if (source) { 66 for (var prop in source) { 67 child[prop] = source[prop]; 68 } 69 } 70 }; 71 72 // From _.each 73 if (obj == null) return; 74 if (Array.prototype.forEach && obj.forEach === Array.prototype.forEach) { 75 obj.forEach(iterator); 76 } else if (obj.length === +obj.length) { 77 for (var i = 0, l = obj.length; i < l; i++) { 78 iterator.call(undefined, obj[i], i, obj); 79 } 80 } else { 81 for (var key in obj) { 82 if (Object.prototype.hasOwnProperty.call(obj, key)) { 83 iterator.call(undefined, obj[key], key, obj); 84 } 85 } 86 } 87 88 return child; 89}; 90 91/** 92 * @static 93 * @param value A value. 94 * @param array An array. 95 * @returns {Boolean} Whether value exists in the array. 96 */ 97AjaxSolr.inArray = function (value, array) { 98 if (array) { 99 for (var i = 0, l = array.length; i < l; i++) { 100 if (AjaxSolr.equals(array[i], value)) { 101 return i; 102 } 103 } 104 } 105 return -1; 106}; 107 108/** 109 * @static 110 * @param foo A value. 111 * @param bar A value. 112 * @returns {Boolean} Whether the two given values are equal. 113 */ 114AjaxSolr.equals = function (foo, bar) { 115 if (AjaxSolr.isArray(foo) && AjaxSolr.isArray(bar)) { 116 if (foo.length !== bar.length) { 117 return false; 118 } 119 for (var i = 0, l = foo.length; i < l; i++) { 120 if (foo[i] !== bar[i]) { 121 return false; 122 } 123 } 124 return true; 125 } 126 else if (AjaxSolr.isRegExp(foo) && AjaxSolr.isString(bar)) { 127 return bar.match(foo); 128 } 129 else if (AjaxSolr.isRegExp(bar) && AjaxSolr.isString(foo)) { 130 return foo.match(bar); 131 } 132 else { 133 return foo === bar; 134 } 135}; 136 137/** 138 * Can't use toString.call(obj) === "[object Array]", as it may return 139 * "[xpconnect wrapped native prototype]", which is undesirable. 140 * 141 * @static 142 * @see http://thinkweb2.com/projects/prototype/instanceof-considered-harmful-or-how-to-write-a-robust-isarray/ 143 * @see http://ajax.googleapis.com/ajax/libs/prototype/1.6.0.3/prototype.js 144 */ 145AjaxSolr.isArray = function (obj) { 146 return obj != null && typeof obj == 'object' && 'splice' in obj && 'join' in obj; 147}; 148 149/** 150 * @param obj Any object. 151 * @returns {Boolean} Whether the object is a RegExp object. 152 */ 153AjaxSolr.isRegExp = function (obj) { 154 return obj != null && (typeof obj == 'object' || typeof obj == 'function') && 'ignoreCase' in obj; 155}; 156 157/** 158 * @param obj Any object. 159 * @returns {Boolean} Whether the object is a String object. 160 */ 161AjaxSolr.isString = function (obj) { 162 return obj != null && typeof obj == 'string'; 163}; 164(function (callback) { 165 if (typeof define === 'function' && define.amd) { 166 define(['core/Core'], callback); 167 } 168 else { 169 callback(); 170 } 171}(function () { 172 173/** 174 * The Manager acts as the controller in a Model-View-Controller framework. All 175 * public calls should be performed on the manager object. 176 * 177 * @param properties A map of fields to set. Refer to the list of public fields. 178 * @class AbstractManager 179 */ 180AjaxSolr.AbstractManager = AjaxSolr.Class.extend( 181 /** @lends AjaxSolr.AbstractManager.prototype */ 182 { 183 /** 184 * @param {Object} [attributes] 185 * @param {String} [attributes.solrUrl] The fully-qualified URL of the Solr 186 * application. You must include the trailing slash. Do not include the path 187 * to any Solr servlet. Defaults to "http://localhost:8983/solr/" 188 * @param {String} [attributes.proxyUrl] If we want to proxy queries through a 189 * script, rather than send queries to Solr directly, set this field to the 190 * fully-qualified URL of the script. 191 * @param {String} [attributes.servlet] The default Solr servlet. You may 192 * prepend the servlet with a core if using multiple cores. Defaults to 193 * "servlet". 194 */ 195 constructor: function (attributes) { 196 AjaxSolr.extend(this, { 197 solrUrl: 'http://localhost:8983/solr/', 198 proxyUrl: null, 199 servlet: 'select', 200 // The most recent response from Solr. 201 response: {}, 202 // A collection of all registered widgets. 203 widgets: {}, 204 // The parameter store for the manager and its widgets. 205 store: null, 206 // Whether <tt>init()</tt> has been called yet. 207 initialized: false 208 }, attributes); 209 }, 210 211 /** 212 * An abstract hook for child implementations. 213 * 214 * <p>This method should be called after the store and the widgets have been 215 * added. It should initialize the widgets and the store, and do any other 216 * one-time initializations, e.g., perform the first request to Solr.</p> 217 * 218 * <p>If no store has been set, it sets the store to the basic <tt> 219 * AjaxSolr.ParameterStore</tt>.</p> 220 */ 221 init: function () { 222 this.initialized = true; 223 if (this.store === null) { 224 this.setStore(new AjaxSolr.ParameterStore()); 225 } 226 this.store.load(false); 227 for (var widgetId in this.widgets) { 228 this.widgets[widgetId].init(); 229 }
230 this.store.init(); 231 }, 232 233 /** 234 * Set the manager's parameter store. 235 * 236 * @param {AjaxSolr.ParameterStore} store 237 */ 238 setStore: function (store) { 239 store.manager = this; 240 this.store = store; 241 }, 242 243 /** 244 * Adds a widget to the manager. 245 * 246 * @param {AjaxSolr.AbstractWidget} widget 247 */ 248 addWidget: function (widget) { 249 widget.manager = this; 250 this.widgets[widget.id] = widget; 251 }, 252 253 /** 254 * Stores the Solr parameters to be sent to Solr and sends a request to Solr. 255 * 256 * @param {Boolean} [start] The Solr start offset parameter. 257 * @param {String} [servlet] The Solr servlet to send the request to. 258 */ 259 doRequest: function (start, servlet) { 260 if (this.initialized === false) { 261 this.init(); 262 } 263 // Allow non-pagination widgets to reset the offset parameter. 264 if (start !== undefined) { 265 this.store.get('start').val(start); 266 } 267 if (servlet === undefined) { 268 servlet = this.servlet; 269 } 270 271 this.store.save(); 272 273 for (var widgetId in this.widgets) { 274 this.widgets[widgetId].beforeRequest(); 275 } 276 277 this.executeRequest(servlet); 278 }, 279 280 /** 281 * An abstract hook for child implementations. 282 * 283 * <p>Sends the request to Solr, i.e. to <code>this.solrUrl</code> or <code> 284 * this.proxyUrl</code>, and receives Solr's response. It should pass Solr's 285 * response to <code>handleResponse()</code> for handling.</p> 286 * 287 * <p>See <tt>managers/Manager.jquery.js</tt> for a jQuery implementation.</p> 288 * 289 * @param {String} servlet The Solr servlet to send the request to. 290 * @param {String} string The query string of the request. If not set, it 291 * should default to <code>this.store.string()</code> 292 * @throws If not defined in child implementation. 293 */ 294 executeRequest: function (servlet, string) { 295 throw 'Abstract method executeRequest must be overridden in a subclass.'; 296 }, 297 298 /** 299 * This method is executed after the Solr response data arrives. Allows each 300 * widget to handle Solr's response separately. 301 * 302 * @param {Object} data The Solr response. 303 */ 304 handleResponse: function (data) { 305 this.response = data; 306 307 for (var widgetId in this.widgets) {
308 this.widgets[widgetId].afterRequest(); 309 } 310 }, 311 312 /** 313 * This method is executed if Solr encounters an error. 314 * 315 * @param {String} message An error message. 316 */ 317 handleError: function (message) { 318 window.console && console.log && console.log(message); 319 } 320}); 321 322})); 323(function (callback) { 324 if (typeof define === 'function' && define.amd) { 325 define(['core/Core', 'core/Parameter'], callback); 326 } 327 else { 328 callback(); 329 } 330}(function () { 331 332/** 333 * The ParameterStore, as its name suggests, stores Solr parameters. Widgets 334 * expose some of these parameters to the user. Whenever the user changes the 335 * values of these parameters, the state of the application changes. In order to 336 * allow the user to move back and forth between these states with the browser's 337 * Back and Forward buttons, and to bookmark these states, each state needs to 338 * be stored. The easiest method is to store the exposed parameters in the URL 339 * hash (see the <tt>ParameterHashStore</tt> class). However, you may implement 340 * your own storage method by extending this class. 341 * 342 * <p>For a list of possible parameters, please consult the links below.</p> 343 * 344 * @see http://wiki.apache.org/solr/CoreQueryParameters 345 * @see http://wiki.apache.org/solr/CommonQueryParameters 346 * @see http://wiki.apache.org/solr/SimpleFacetParameters 347 * @see http://wiki.apache.org/solr/HighlightingParameters 348 * @see http://wiki.apache.org/solr/MoreLikeThis 349 * @see http://wiki.apache.org/solr/SpellCheckComponent 350 * @see http://wiki.apache.org/solr/StatsComponent 351 * @see http://wiki.apache.org/solr/TermsComponent 352 * @see http://wiki.apache.org/solr/TermVectorComponent 353 * @see http://wiki.apache.org/solr/LocalParams 354 * 355 * @param properties A map of fields to set. Refer to the list of public fields. 356 * @class ParameterStore 357 */ 358AjaxSolr.ParameterStore = AjaxSolr.Class.extend( 359 /** @lends AjaxSolr.ParameterStore.prototype */ 360 { 361 constructor: function (attributes) { 362 /** 363 * @param {Object} [attributes] 364 * @param {String[]} [attributes.exposed] The names of the exposed 365 * parameters. Any parameters that your widgets expose to the user, 366 * directly or indirectly, should be listed here. 367 */ 368 AjaxSolr.extend(this, { 369 exposed: [], 370 // The Solr parameters. 371 params: {}, 372 // A reference to the parameter store's manager. 373 manager: null 374 }, attributes); 375 }, 376 377 /** 378 * An abstract hook for child implementations. 379 * 380 * <p>This method should do any necessary one-time initializations.</p> 381 */ 382 init: function () {}, 383 384 /** 385 * Some Solr parameters may be specified multiple times. It is easiest to 386 * hard-code a list of such parameters. You may change the list by passing 387 * <code>{ multiple: /pattern/ }</code> as an argument to the constructor of 388 * this class or one of its children, e.g.: 389 * 390 * <p><code>new ParameterStore({ multiple: /pattern/ })</code> 391 * 392 * @param {String} name The name of the parameter. 393 * @returns {Boolean} Whether the parameter may be specified multiple times. 394 * @see http://lucene.apache.org/solr/api/org/apache/solr/handler/DisMaxRequestHandler.html 395 */ 396 isMultiple: function (name) { 397 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)$/); 398 }, 399 400 /** 401 * Returns a parameter. If the parameter doesn't exist, creates it. 402 * 403 * @param {String} name The name of the parameter. 404 * @returns {AjaxSolr.Parameter|AjaxSolr.Parameter[]} The parameter. 405 */ 406 get: function (name) { 407 if (this.params[name] === undefined) { 408 var param = new AjaxSolr.Parameter({ name: name }); 409 if (this.isMultiple(name)) { 410 this.params[name] = [ param ]; 411 } 412 else { 413 this.params[name] = param; 414 } 415 } 416 return this.params[name]; 417 }, 418 419 /** 420 * If the parameter may be specified multiple times, returns the values of 421 * all identically-named parameters. If the parameter may be specified only 422 * once, returns the value of that parameter. 423 * 424 * @param {String} name The name of the parameter. 425 * @returns {String[]|Number[]} The value(s) of the parameter. 426 */ 427 values: function (name) { 428 if (this.params[name] !== undefined) { 429 if (this.isMultiple(name)) { 430 var values = []; 431 for (var i = 0, l = this.params[name].length; i < l; i++) { 432 values.push(this.params[name][i].val()); 433 } 434 return values; 435 } 436 else { 437 return [ this.params[name].val() ]; 438 } 439 } 440 return []; 441 }, 442 443 /** 444 * If the parameter may be specified multiple times, adds the given parameter 445 * to the list of identically-named parameters, unless one already exists with 446 * the same value. If it may be specified only once, replaces the parameter. 447 * 448 * @param {String} name The name of the parameter. 449 * @param {AjaxSolr.Parameter} [param] The parameter. 450 * @returns {AjaxSolr.Parameter|Boolean} The parameter, or false. 451 */ 452 add: function (name, param) { 453 if (param === undefined) { 454 param = new AjaxSolr.Parameter({ name: name }); 455 } 456 if (this.isMultiple(name)) { 457 if (this.params[name] === undefined) { 458 this.params[name] = [ param ]; 459 } 460 else { 461 if (AjaxSolr.inArray(param.val(), this.values(name)) == -1) { 462 this.params[name].push(param); 463 } 464 else { 465 return false;
466 } 467 } 468 } 469 else { 470 this.params[name] = param; 471 } 472 return param; 473 }, 474 475 /** 476 * Deletes a parameter. 477 * 478 * @param {String} name The name of the parameter. 479 * @param {Number} [index] The index of the parameter. 480 */ 481 remove: function (name, index) { 482 if (index === undefined) { 483 delete this.params[name]; 484 } 485 else { 486 this.params[name].splice(index, 1); 487 if (this.params[name].length == 0) { 488 delete this.params[name]; 489 } 490 } 491 }, 492 493 /** 494 * Finds all parameters with matching values. 495 * 496 * @param {String} name The name of the parameter. 497 * @param {String|Number|String[]|Number[]|RegExp} value The value. 498 * @returns {String|Number[]} The indices of the parameters found. 499 */ 500 find: function (name, value) { 501 if (this.params[name] !== undefined) { 502 if (this.isMultiple(name)) { 503 var indices = []; 504 for (var i = 0, l = this.params[name].length; i < l; i++) { 505 if (AjaxSolr.equals(this.params[name][i].val(), value)) { 506 indices.push(i); 507 } 508 } 509 return indices.length ? indices : false; 510 } 511 else { 512 if (AjaxSolr.equals(this.params[name].val(), value)) { 513 return name; 514 } 515 } 516 } 517 return false; 518 }, 519 520 /** 521 * If the parameter may be specified multiple times, creates a parameter using 522 * the given name and value, and adds it to the list of identically-named 523 * parameters, unless one already exists with the same value. If it may be 524 * specified only once, replaces the parameter. 525 * 526 * @param {String} name The name of the parameter. 527 * @param {String|Number|String[]|Number[]} value The value. 528 * @param {Object} [locals] The parameter's local parameters. 529 * @returns {AjaxSolr.Parameter|Boolean} The parameter, or false. 530 */ 531 addByValue: function (name, value, locals) { 532 if (locals === undefined) { 533 locals = {}; 534 } 535 if (this.isMultiple(name) && AjaxSolr.isArray(value)) { 536 var ret = []; 537 for (var i = 0, l = value.length; i < l; i++) { 538 ret.push(this.add(name, new AjaxSolr.Parameter({ name: name, value: value[i], locals: locals }))); 539 } 540 return ret; 541 } 542 else { 543 return this.add(name, new AjaxSolr.Parameter({ name: name, value: value, locals: locals })); 544 } 545 }, 546 547 /** 548 * Deletes any parameter with a matching value. 549 * 550 * @param {String} name The name of the parameter. 551 * @param {String|Number|String[]|Number[]|RegExp} value The value. 552 * @returns {String|Number[]} The indices deleted. 553 */ 554 removeByValue: function (name, value) { 555 var indices = this.find(name, value); 556 if (indices) { 557 if (AjaxSolr.isArray(indices)) { 558 for (var i = indices.length - 1; i >= 0; i--) { 559 this.remove(name, indices[i]); 560 } 561 } 562 else { 563 this.remove(indices); 564 } 565 } 566 return indices; 567 }, 568 569 /** 570 * Returns the Solr parameters as a query string. 571 * 572 * <p>IE6 calls the default toString() if you write <tt>store.toString() 573 * </tt>. So, we need to choose another name for toString().</p> 574 */ 575 string: function () { 576 var params = [], string; 577 for (var name in this.params) { 578 if (this.isMultiple(name)) { 579 for (var i = 0, l = this.params[name].length; i < l; i++) { 580 string = this.params[name][i].string(); 581 if (string) { 582 params.push(string); 583 } 584 } 585 } 586 else { 587 string = this.params[name].string(); 588 if (string) { 589 params.push(string); 590 } 591 } 592 } 593 return params.join('&'); 594 }, 595 596 /** 597 * Parses a query string into Solr parameters. 598 * 599 * @param {String} str The string to parse. 600 */ 601 parseString: function (str) { 602 var pairs = str.split('&'); 603 for (var i = 0, l = pairs.length; i < l; i++) { 604 if (pairs[i]) { // ignore leading, trailing, and consecutive &'s 605 var param = new AjaxSolr.Parameter(); 606 param.parseString(pairs[i]); 607 this.add(param.name, param); 608 } 609 } 610 }, 611 612 /** 613 * Returns the exposed parameters as a query string. 614 * 615 * @returns {String} A string representation of the exposed parameters. 616 */ 617 exposedString: function () { 618 var params = [], string; 619 for (var i = 0, l = this.exposed.length; i < l; i++) { 620 if (this.params[this.exposed[i]] !== undefined) { 621 if (this.isMultiple(this.exposed[i])) { 622 for (var j = 0, m = this.params[this.exposed[i]].length; j < m; j++) {
623 string = this.params[this.exposed[i]][j].string(); 624 if (string) { 625 params.push(string); 626 } 627 } 628 } 629 else { 630 string = this.params[this.exposed[i]].string(); 631 if (string) { 632 params.push(string); 633 } 634 } 635 } 636 } 637 return params.join('&'); 638 }, 639 640 /** 641 * Resets the values of the exposed parameters. 642 */ 643 exposedReset: function () { 644 for (var i = 0, l = this.exposed.length; i < l; i++) { 645 this.remove(this.exposed[i]); 646 } 647 }, 648 649 /** 650 * Loads the values of exposed parameters from persistent storage. It is 651 * necessary, in most cases, to reset the values of exposed parameters before 652 * setting the parameters to the values in storage. This is to ensure that a 653 * parameter whose name is not present in storage is properly reset. 654 * 655 * @param {Boolean} [reset=true] Whether to reset the exposed parameters. 656 * before loading new values from persistent storage. Default: true. 657 */ 658 load: function (reset) { 659 if (reset === undefined) { 660 reset = true; 661 } 662 if (reset) { 663 this.exposedReset(); 664 } 665 this.parseString(this.storedString()); 666 }, 667 668 /** 669 * An abstract hook for child implementations. 670 * 671 * <p>Stores the values of the exposed parameters in persistent storage. This 672 * method should usually be called before each Solr request.</p> 673 */ 674 save: function () {}, 675 676 /** 677 * An abstract hook for child implementations. 678 * 679 * <p>Returns the string to parse from persistent storage.</p> 680 * 681 * @returns {String} The string from persistent storage. 682 */ 683 storedString: function () { 684 return ''; 685 } 686}); 687 688})); 689(function (callback) { 690 if (typeof define === 'function' && define.amd) { 691 define(['core/Core'], callback); 692 } 693 else { 694 callback(); 695 } 696}(function () { 697 698/** 699 * Represents a Solr parameter. 700 * 701 * @param properties A map of fields to set. Refer to the list of public fields. 702 * @class Parameter 703 */ 704AjaxSolr.Parameter = AjaxSolr.Class.extend( 705 /** @lends AjaxSolr.Parameter.prototype */ 706 { 707 /** 708 * @param {Object} attributes 709 * @param {String} attributes.name The parameter's name. 710 * @param {String} [attributes.value] The parameter's value. 711 * @param {Object} [attributes.local] The parameter's local parameters. 712 */ 713 constructor: function (attributes) { 714 AjaxSolr.extend(this, { 715 name: null, 716 value: null, 717 locals: {} 718 }, attributes); 719 }, 720 721 /** 722 * Returns the value. If called with an argument, sets the value. 723 * 724 * @param {String|Number|String[]|Number[]} [value] The value to set. 725 * @returns The value. 726 */ 727 val: function (value) { 728 if (value === undefined) { 729 return this.value; 730 } 731 else { 732 this.value = value; 733 } 734 }, 735 736 /** 737 * Returns the value of a local parameter. If called with a second argument, 738 * sets the value of a local parameter. 739 * 740 * @param {String} name The name of the local parameter. 741 * @param {String|Number|String[]|Number[]} [value] The value to set. 742 * @returns The value. 743 */ 744 local: function (name, value) { 745 if (value === undefined) { 746 return this.locals[name]; 747 } 748 else { 749 this.locals[name] = value; 750 } 751 }, 752 753 /** 754 * Deletes a local parameter. 755 * 756 * @param {String} name The name of the local parameter. 757 */ 758 remove: function (name) { 759 delete this.locals[name]; 760 }, 761 762 /** 763 * Returns the Solr parameter as a query string key-value pair. 764 * 765 * <p>IE6 calls the default toString() if you write <tt>store.toString() 766 * </tt>. So, we need to choose another name for toString().</p> 767 */ 768 string: function () { 769 var pairs = []; 770 771 for (var name in this.locals) { 772 if (this.locals[name]) { 773 pairs.push(name + '=' + encodeURIComponent(this.locals[name])); 774 } 775 } 776 777 var prefix = pairs.length ? '{!' + pairs.join('%20') + '}' : ''; 778 779 if (this.value) { 780 return this.name + '=' + prefix + this.valueString(this.value); 781 } 782 // For dismax request handlers, if the q parameter has local params, the 783 // q parameter must be set to a non-empty value. In case the q parameter 784 // has local params but is empty, use the q.alt parameter, which accepts 785 // wildcards. 786 else if (this.name == 'q' && prefix) { 787 return 'q.alt=' + prefix + encodeURIComponent('*:*'); 788 } 789 else { 790 return ''; 791 } 792 }, 793 794 /** 795 * Parses a string formed by calling string(). 796 * 797 * @param {String} str The string to parse. 798 */ 799 parseString: function (str) { 800 var param = str.match(/^([^=]+)=(?:\{!([^\}]*)\})?(.*)$/); 801 if (param) { 802 var matches; 803 804 while (matches = /([^\s=]+)=(\S*)/g.exec(decodeURIComponent(param[2]))) { 805 this.locals[matches[1]] = decodeURIComponent(matches[2]); 806 param[2] = param[2].replace(matches[0], ''); // Safari's exec seems not to do this on its own 807 } 808 809 if (param[1] == 'q.alt') { 810 this.name = 'q'; 811 // if q.alt is present, assume it is because q was empty, as above 812 } 813 else { 814 this.name = param[1]; 815 this.value = this.parseValueString(param[3]); 816 } 817 } 818 }, 819 820 /** 821 * Returns the value as a URL-encoded string. 822 * 823 * @private 824 * @param {String|Number|String[]|Number[]} value The value. 825 * @returns {String} The URL-encoded string. 826 */ 827 valueString: function (value) { 828 value = AjaxSolr.isArray(value) ? value.join(',') : value; 829 return encodeURIComponent(value); 830 }, 831 832 /** 833 * Parses a URL-encoded string to return the value. 834 * 835 * @private 836 * @param {String} str The URL-encoded string. 837 * @returns {Array} The value. 838 */ 839 parseValueString: function (str) { 840 str = decodeURIComponent(str); 841 return str.indexOf(',') == -1 ? str : str.split(','); 842 } 843}); 844 845/** 846 * Escapes a value, to be used in, for example, an fq parameter. Surrounds
847 * strings containing spaces or colons in double quotes. 848 * 849 * @public 850 * @param {String|Number} value The value. 851 * @returns {String} The escaped value. 852 */ 853AjaxSolr.Parameter.escapeValue = function (value) { 854 // If the field value has a space, colon, quotation mark or forward slash 855 // in it, wrap it in quotes, unless it is a range query or it is already 856 // wrapped in quotes. 857 if (value.match(/[ :\/"]/) && !value.match(/[\[\{]\S+ TO \S+[\]\}]/) && !value.match(/^["\(].*["\)]$/)) { 858 return '"' + value.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"'; 859 } 860 return value; 861} 862 863})); 864(function (callback) { 865 if (typeof define === 'function' && define.amd) { 866 define(['core/Core'], callback); 867 } 868 else { 869 callback(); 870 } 871}(function () { 872 873/** 874 * Baseclass for all widgets. 875 * 876 * Provides abstract hooks for child classes. 877 * 878 * @param properties A map of fields to set. May be new or public fields. 879 * @class AbstractWidget 880 */ 881AjaxSolr.AbstractWidget = AjaxSolr.Class.extend( 882 /** @lends AjaxSolr.AbstractWidget.prototype */ 883 { 884 /** 885 * @param {Object} attributes 886 * @param {String} attributes.id A unique identifier of this widget. 887 * @param {String} [attributes.target] The CSS selector for this widget's 888 * target HTML element, e.g. a specific <tt>div</tt> or <tt>ul</tt>. A 889 * Widget is usually implemented to perform all its UI changes relative to 890 * its target HTML element. 891 * @param {Number} [attributes.start] The offset parameter. Set this field to 892 * make the widget reset the offset parameter to the given value on each 893 * request. 894 * @param {String} [attributes.servlet] The Solr servlet for this widget. You 895 * may prepend the servlet with a core if using multiple cores. If none is 896 * set, it will default to the manager's servlet. 897 */ 898 constructor: function (attributes) { 899 AjaxSolr.extend(this, { 900 id: null, 901 target: null, 902 start: undefined, 903 servlet: undefined, 904 // A reference to the widget's manager. 905 manager: null 906 }, attributes); 907 }, 908 909 /** 910 * An abstract hook for child implementations. 911 * 912 * <p>This method should do any necessary one-time initializations.</p> 913 */ 914 init: function () {}, 915 916 /** 917 * An abstract hook for child implementations. 918 * 919 * <p>This method is executed before the Solr request is sent.</p> 920 */ 921 beforeRequest: function () {}, 922 923 /** 924 * An abstract hook for child implementations. 925 * 926 * <p>This method is executed after the Solr response is received.</p> 927 */ 928 afterRequest: function () {}, 929 930 /** 931 * A proxy to the manager's doRequest method. 932 * 933 * @param {Boolean} [start] The Solr start offset parameter. 934 * @param {String} [servlet] The Solr servlet to send the request to. 935 */ 936 doRequest: function (start, servlet) { 937 this.manager.doRequest(start || this.start, servlet || this.servlet); 938 } 939}); 940 941})); 942(function (callback) { 943 if (typeof define === 'function' && define.amd) { 944 define(['core/AbstractWidget', 'core/Parameter'], callback); 945 } 946 else { 947 callback(); 948 } 949}(function () { 950 951/** 952 * Baseclass for all facet widgets. 953 * 954 * @class AbstractFacetWidget 955 * @augments AjaxSolr.AbstractWidget 956 */ 957AjaxSolr.AbstractFacetWidget = AjaxSolr.AbstractWidget.extend( 958 /** @lends AjaxSolr.AbstractFacetWidget.prototype */ 959 { 960 /** 961 * @param {Object} attributes 962 * @param {String} attributes.field The field to facet on. 963 * @param {Number} [attributes.start] This widget will by default set the 964 * offset parameter to 0 on each request. 965 * @param {Boolean} [attributes.multivalue] Set to <tt>false</tt> to force a 966 * single "fq" parameter for this widget. Defaults to <tt>true</tt>. 967 */ 968 constructor: function (attributes) { 969 AjaxSolr.AbstractFacetWidget.__super__.constructor.apply(this, arguments); 970 AjaxSolr.extend(this, { 971 start: 0, 972 field: null, 973 multivalue: true 974 }, attributes); 975 }, 976 977 init: function () { 978 this.initStore(); 979 }, 980 981 /** 982 * Add facet parameters to the parameter store. 983 */ 984 initStore: function () { 985 /* http://wiki.apache.org/solr/SimpleFacetParameters */ 986 var parameters = [ 987 'facet.prefix', 988 'facet.sort', 989 'facet.limit', 990 'facet.offset', 991 'facet.mincount', 992 'facet.missing', 993 'facet.method', 994 'facet.enum.cache.minDf' 995 ]; 996 997 this.manager.store.addByValue('facet', true); 998 999 // Set facet.field, facet.date or facet.range to truthy values to add 1000 // related per-field parameters to the parameter store. 1001 if (this['facet.field'] !== undefined) { 1002 this.manager.store.addByValue('facet.field', this.field); 1003 } 1004 else if (this['facet.date'] !== undefined) { 1005 this.manager.store.addByValue('facet.date', this.field); 1006 parameters = parameters.concat([ 1007 'facet.date.start', 1008 'facet.date.end', 1009 'facet.date.gap', 1010 'facet.date.hardend', 1011 'facet.date.other', 1012 'facet.date.include' 1013 ]); 1014 } 1015 else if (this['facet.range'] !== undefined) { 1016 this.manager.store.addByValue('facet.range', this.field); 1017 parameters = parameters.concat([ 1018 'facet.range.start', 1019 'facet.range.end', 1020 'facet.range.gap', 1021 'facet.range.hardend', 1022 'facet.range.other', 1023 'facet.range.include' 1024 ]); 1025 } 1026 1027 for (var i = 0, l = parameters.length; i < l; i++) { 1028 if (this[parameters[i]] !== undefined) { 1029 this.manager.store.addByValue('f.' + this.field + '.' + parameters[i], this[parameters[i]]); 1030 } 1031 } 1032 }, 1033 1034 /** 1035 * @returns {Boolean} Whether any filter queries have been set using this 1036 * widget's facet field. 1037 */ 1038 isEmpty: function () { 1039 return !this.manager.store.find('fq', new RegExp('^-?' + this.field + ':')); 1040 }, 1041 1042 /** 1043 * Sets the filter query. 1044 * 1045 * @returns {Boolean} Whether the selection changed. 1046 */ 1047 set: function (value) { 1048 return this.changeSelection(function () { 1049 var a = this.manager.store.removeByValue('fq', new RegExp('^-?' + this.field + ':')), 1050 b = this.manager.store.addByValue('fq', this.fq(value)); 1051 return a || b; 1052 }); 1053 }, 1054 1055 /** 1056 * Adds a filter query. 1057 * 1058 * @returns {Boolean} Whether a filter query was added. 1059 */ 1060 add: function (value) { 1061 return this.changeSelection(function () { 1062 return this.manager.store.addByValue('fq', this.fq(value)); 1063 }); 1064 }, 1065 1066 /** 1067 * Removes a filter query. 1068 * 1069 * @returns {Boolean} Whether a filter query was removed. 1070 */ 1071 remove: function (value) { 1072 return this.changeSelection(function () { 1073 return this.manager.store.removeByValue('fq', this.fq(value)); 1074 }); 1075 }, 1076 1077 /** 1078 * Removes all filter queries using the widget's facet field. 1079 * 1080 * @returns {Boolean} Whether a filter query was removed. 1081 */ 1082 clear: function () { 1083 return this.changeSelection(function () { 1084 return this.manager.store.removeByValue('fq', new RegExp('^-?' + this.field + ':')); 1085 }); 1086 }, 1087 1088 /** 1089 * Helper for selection functions. 1090 * 1091 * @param {Function} Selection function to call. 1092 * @returns {Boolean} Whether the selection changed. 1093 */ 1094 changeSelection: function (func) { 1095 changed = func.apply(this); 1096 if (changed) { 1097 this.afterChangeSelection(); 1098 } 1099 return changed; 1100 }, 1101 1102 /** 1103 * An abstract hook for child implementations. 1104 * 1105 * <p>This method is executed after the filter queries change.</p> 1106 */ 1107 afterChangeSelection: function () {}, 1108 1109 /** 1110 * One of "facet.field", "facet.date" or "facet.range" must be set on the 1111 * widget in order to determine where the facet counts are stored. 1112 * 1113 * @returns {Array} An array of objects with the properties <tt>facet</tt> and 1114 * <tt>count</tt>, e.g <tt>
1114{ facet: 'facet', count: 1 }</tt>. 1115 */ 1116 getFacetCounts: function () { 1117 var property; 1118 if (this['facet.field'] !== undefined) { 1119 property = 'facet_fields'; 1120 } 1121 else if (this['facet.date'] !== undefined) { 1122 property = 'facet_dates'; 1123 } 1124 else if (this['facet.range'] !== undefined) { 1125 property = 'facet_ranges'; 1126 } 1127 if (property !== undefined) { 1128 switch (this.manager.store.get('json.nl').val()) { 1129 case 'map': 1130 return this.getFacetCountsMap(property); 1131 case 'arrarr': 1132 return this.getFacetCountsArrarr(property); 1133 default: 1134 return this.getFacetCountsFlat(property); 1135 } 1136 } 1137 throw 'Cannot get facet counts unless one of the following properties is set to "true" on widget "' + this.id + '": "facet.field", "facet.date", or "facet.range".'; 1138 }, 1139 1140 /** 1141 * Used if the facet counts are represented as a JSON object. 1142 * 1143 * @param {String} property "facet_fields", "facet_dates", or "facet_ranges". 1144 * @returns {Array} An array of objects with the properties <tt>facet</tt> and 1145 * <tt>count</tt>, e.g <tt>{ facet: 'facet', count: 1 }</tt>. 1146 */ 1147 getFacetCountsMap: function (property) { 1148 var counts = []; 1149 for (var facet in this.manager.response.facet_counts[property][this.field]) { 1150 counts.push({ 1151 facet: facet, 1152 count: parseInt(this.manager.response.facet_counts[property][this.field][facet]) 1153 }); 1154 } 1155 return counts; 1156 }, 1157 1158 /** 1159 * Used if the facet counts are represented as an array of two-element arrays. 1160 * 1161 * @param {String} property "facet_fields", "facet_dates", or "facet_ranges". 1162 * @returns {Array} An array of objects with the properties <tt>facet</tt> and 1163 * <tt>count</tt>, e.g <tt>{ facet: 'facet', count: 1 }</tt>. 1164 */ 1165 getFacetCountsArrarr: function (property) { 1166 var counts = []; 1167 for (var i = 0, l = this.manager.response.facet_counts[property][this.field].length; i < l; i++) { 1168 counts.push({ 1169 facet: this.manager.response.facet_counts[property][this.field][i][0], 1170 count: parseInt(this.manager.response.facet_counts[property][this.field][i][1]) 1171 }); 1172 } 1173 return counts; 1174 }, 1175 1176 /** 1177 * Used if the facet counts are represented as a flat array. 1178 * 1179 * @param {String} property "facet_fields", "facet_dates", or "facet_ranges". 1180 * @returns {Array} An array of objects with the properties <tt>facet</tt> and 1181 * <tt>count</tt>, e.g <tt>{ facet: 'facet', count: 1 }</tt>. 1182 */ 1183 getFacetCountsFlat: function (property) { 1184 var counts = []; 1185 for (var i = 0, l = this.manager.response.facet_counts[property][this.field].length; i < l; i += 2) { 1186 counts.push({ 1187 facet: this.manager.response.facet_counts[property][this.field][i], 1188 count: parseInt(this.manager.response.facet_counts[property][this.field][i+1]) 1189 }); 1190 } 1191 return counts; 1192 }, 1193 1194 /** 1195 * @param {String} value The value. 1196 * @returns {Function} Sends a request to Solr if it successfully adds a 1197 * filter query with the given value. 1198 */ 1199 clickHandler: function (value) { 1200 var self = this, meth = this.multivalue ? 'add' : 'set'; 1201 return function () { 1202 if (self[meth].call(self, value)) { 1203 self.doRequest(); 1204 } 1205 return false; 1206 } 1207 }, 1208 1209 /** 1210 * @param {String} value The value. 1211 * @returns {Function} Sends a request to Solr if it successfully removes a 1212 * filter query with the given value. 1213 */ 1214 unclickHandler: function (value) { 1215 var self = this; 1216 return function () { 1217 if (self.remove(value)) { 1218 self.doRequest(); 1219 } 1220 return false; 1221 } 1222 }, 1223 1224 /** 1225 * @param {String} value The facet value. 1226 * @param {Boolean} exclude Whether to exclude this fq parameter value. 1227 * @returns {String} An fq parameter value. 1228 */ 1229 fq: function (value, exclude) { 1230 return (exclude ? '-' : '') + this.field + ':' + AjaxSolr.Parameter.escapeValue(value); 1231 } 1232}); 1233 1234})); 1235(function (callback) { 1236 if (typeof define === 'function' && define.amd) { 1237 define(['core/AbstractWidget'], callback); 1238 } 1239 else { 1240 callback(); 1241 } 1242}(function () { 1243 1244/** 1245 * Offers an interface to the local parameters used by the Spatial Solr plugin. 1246 * 1247 * @see http://www.jteam.nl/news/spatialsolr 1248 * 1249 * @class AbstractSpatialWidget 1250 * @augments AjaxSolr.AbstractWidget 1251 */ 1252AjaxSolr.AbstractSpatialWidget = AjaxSolr.AbstractWidget.extend( 1253 /** @lends AjaxSolr.AbstractSpatialWidget.prototype */ 1254 { 1255 /** 1256 * Sets the Spatial Solr local parameters. 1257 * 1258 * @param {Object}
1258 params The local parameters to set. 1259 * @param {Number} params.lat Latitude of the center of the search area. 1260 * @param {Number} params.lng Longitude of the center of the search area. 1261 * @param {Number} params.radius Radius of the search area. 1262 * @param {String} [params.unit] Unit the distances should be calculated in: 1263 * "km" or "miles". 1264 * @param {String} [params.calc] <tt>GeoDistanceCalculator</tt> that will be 1265 * used to calculate the distances. "arc" for 1266 * <tt>ArchGeoDistanceCalculator</tt> and "plane" for 1267 * <tt>PlaneGeoDistanceCalculator</tt>. 1268 * @param {Number} [params.threadCount] Number of threads that will be used 1269 * by the <tt>ThreadedDistanceFilter</tt>. 1270 */ 1271 set: function (params) { 1272 this.manager.store.get('q').local('type', 'spatial'); 1273 this.manager.store.get('q').local('lat', params.lat); 1274 this.manager.store.get('q').local('long', params.lng); 1275 this.manager.store.get('q').local('radius', params.radius); 1276 if (params.unit !== undefined) { 1277 this.manager.store.get('q').local('unit', params.unit); 1278 } 1279 if (params.calc !== undefined) { 1280 this.manager.store.get('q').local('calc', params.calc); 1281 } 1282 if (params.threadCount !== undefined) { 1283 this.manager.store.get('q').local('threadCount', params.threadCount); 1284 } 1285 }, 1286 1287 /** 1288 * Removes the Spatial Solr local parameters. 1289 */ 1290 clear: function () { 1291 this.manager.store.get('q').remove('type'); 1292 this.manager.store.get('q').remove('lat'); 1293 this.manager.store.get('q').remove('long'); 1294 this.manager.store.get('q').remove('radius'); 1295 this.manager.store.get('q').remove('unit'); 1296 this.manager.store.get('q').remove('calc'); 1297 this.manager.store.get('q').remove('threadCount'); 1298 } 1299}); 1300 1301})); 1302(function (callback) { 1303 if (typeof define === 'function' && define.amd) { 1304 define(['core/AbstractWidget'], callback); 1305 } 1306 else { 1307 callback(); 1308 } 1309}(function () { 1310 1311/** 1312 * Interacts with Solr's SpellCheckComponent. 1313 * 1314 * @see http://wiki.apache.org/solr/SpellCheckComponent 1315 * 1316 * @class AbstractSpellcheckWidget 1317 * @augments AjaxSolr.AbstractWidget 1318 */ 1319AjaxSolr.AbstractSpellcheckWidget = AjaxSolr.AbstractWidget.extend( 1320 /** @lends AjaxSolr.AbstractSpellcheckWidget.prototype */ 1321 { 1322 constructor: function (attributes) { 1323 AjaxSolr.AbstractSpellcheckWidget.__super__.constructor.apply(this, arguments); 1324 AjaxSolr.extend(this, { 1325 // The suggestions. 1326 suggestions: {} 1327 }, attributes); 1328 }, 1329 1330 /** 1331 * Uses the top suggestion for each word to return a suggested query. 1332 * 1333 * @returns {String} A suggested query. 1334 */ 1335 suggestion: function () { 1336 var suggestion = this.manager.response.responseHeader.params['spellcheck.q']; 1337 for (var word in this.suggestions) { 1338 suggestion = suggestion.replace(new RegExp(word, 'g'), this.suggestions[word][0]); 1339 } 1340 return suggestion; 1341 }, 1342 1343 beforeRequest: function () { 1344 if (this.manager.store.get('spellcheck').val() && this.manager.store.get('q').val()) { 1345 this.manager.store.get('spellcheck.q').val(this.manager.store.get('q').val()); 1346 } 1347 else { 1348 this.manager.store.remove('spellcheck.q'); 1349 } 1350 }, 1351 1352 afterRequest: function () { 1353 this.suggestions = {}; 1354 1355 if (this.manager.response.spellcheck && this.manager.response.spellcheck.suggestions) { 1356 var suggestions = this.manager.response.spellcheck.suggestions, 1357 empty = true; 1358 1359 for (var word in suggestions) { 1360 if (word == 'collation' || word == 'correctlySpelled') continue; 1361 1362 this.suggestions[word] = []; 1363 for (var i = 0, l = suggestions[word].suggestion.length; i < l; i++) { 1364 if (this.manager.response.responseHeader.params['spellcheck.extendedResults']) { 1365 this.suggestions[word].push(suggestions[word].suggestion[i].word); 1366 } 1367 else { 1368 this.suggestions[word].push(suggestions[word].suggestion[i]); 1369 } 1370 } 1371 empty = false; 1372 } 1373 1374 if (!empty) { 1375 this.handleSuggestions(this.manager.response); 1376 } 1377 } 1378 }, 1379 1380 /** 1381 * An abstract hook for child implementations. 1382 * 1383 * <p>Allow the child to handle the suggestions without parsing the response.</p> 1384 */ 1385 handleSuggestions: function () {} 1386}); 1387 1388})); 1389(function (callback) { 1390 if (typeof define === 'function' && define.amd) { 1391 define(['core/AbstractWidget'], callback); 1392 } 1393 else { 1394 callback(); 1395 } 1396}(function () { 1397 1398/** 1399 * Baseclass for all free-text widgets. 1400 * 1401 * @class AbstractTextWidget 1402 * @augments AjaxSolr.AbstractWidget 1403 */ 1404AjaxSolr.AbstractTextWidget = AjaxSolr.AbstractWidget.extend( 1405 /** @lends AjaxSolr.AbstractTextWidget.prototype */ 1406 { 1407 /** 1408 * @param {Object} [attributes] 1409 * @param {Number} [attributes.start] This widget will by default set the 1410 * offset parameter to 0 on each request. 1411 */ 1412 constructor: function (attributes) { 1413 AjaxSolr.AbstractTextWidget.__super__.constructor.apply(this, arguments); 1414 AjaxSolr.extend(this, { 1415 start: 0 1416 }, attributes); 1417 }, 1418 1419 /** 1420 * Sets the main Solr query to the given string. 1421 * 1422 * @param {String} q The new Solr query. 1423 * @returns {Boolean} Whether the selection changed. 1424 */ 1425 set: function (q) { 1426 return this.changeSelection(function () { 1427 this.manager.store.get('q').val(q); 1428 }); 1429 }, 1430 1431 /** 1432 * Sets the main Solr query to the empty string. 1433 * 1434 * @returns {Boolean} Whether the selection changed. 1435 */ 1436 clear: function () { 1437 return this.changeSelection(function () { 1438 this.manager.store.remove('q'); 1439 }); 1440 }, 1441 1442 /** 1443 * Helper for selection functions. 1444 * 1445 * @param {Function} Selection function to call. 1446 * @returns {Boolean} Whether the selection changed. 1447 */ 1448 changeSelection: function (func) { 1449 var before = this.manager.store.get('q').val(); 1450 func.apply(this); 1451 var after = this.manager.store.get('q').val(); 1452 if (after !== before) { 1453 this.afterChangeSelection(after); 1454 } 1455 return after !== before; 1456 }, 1457 1458 /** 1459 * An abstract hook for child implementations. 1460 * 1461 * <p>This method is executed after the main Solr query changes.</p> 1462 * 1463 * @param {String} value The current main Solr query. 1464 */ 1465 afterChangeSelection: function (value) {}, 1466 1467 /** 1468 * Returns a function to unset the main Solr query. 1469 * 1470 * @returns {Function} 1471 */ 1472 unclickHandler: function () { 1473 var self = this; 1474 return function () { 1475 if (self.clear()) { 1476 self.doRequest(); 1477 } 1478 return false;
1479 } 1480 }, 1481 1482 /** 1483 * Returns a function to set the main Solr query. 1484 * 1485 * @param {String} value The new Solr query. 1486 * @returns {Function} 1487 */ 1488 clickHandler: function (q) { 1489 var self = this; 1490 return function () { 1491 if (self.set(q)) { 1492 self.doRequest(); 1493 } 1494 return false; 1495 } 1496 } 1497}); 1498 1499})); 1500(function (callback) { 1501 if (typeof define === 'function' && define.amd) { 1502 define(['core/ParameterStore'], callback); 1503 } 1504 else { 1505 callback(); 1506 } 1507}(function () { 1508 1509/** 1510 * A parameter store that stores the values of exposed parameters in the URL 1511 * hash to maintain the application's state. 1512 * 1513 * <p>The ParameterHashStore observes the hash for changes and loads Solr 1514 * parameters from the hash if it observes a change or if the hash is empty. 1515 * The onhashchange event is used if the browser supports it.</p> 1516 * 1517 * <p>Configure the manager with:</p> 1518 * 1519 * @class ParameterHashStore 1520 * @augments AjaxSolr.ParameterStore 1521 * @see https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange 1522 */ 1523AjaxSolr.ParameterHashStore = AjaxSolr.ParameterStore.extend( 1524 /** @lends AjaxSolr.ParameterHashStore.prototype */ 1525 { 1526 /** 1527 * @param {Object} [attributes] 1528 * @param {Number} [attributes.interval] The interval in milliseconds to use 1529 * in <tt>setInterval()</tt>. Do not set the interval too low as you may set 1530 * up a race condition. Defaults to 250. 1531 */ 1532 constructor: function (attributes) { 1533 AjaxSolr.ParameterHashStore.__super__.constructor.apply(this, arguments); 1534 AjaxSolr.extend(this, { 1535 interval: 250, 1536 // Reference to the setInterval() function. 1537 intervalId: null, 1538 // A local copy of the URL hash, so we can detect changes to it. 1539 hash: '' 1540 }, attributes); 1541 }, 1542 1543 /** 1544 * If loading and saving the hash take longer than <tt>interval</tt>, we'll 1545 * hit a race condition. However, this should never happen. 1546 */ 1547 init: function () { 1548 if (this.exposed.length) { 1549 // Check if the browser supports the onhashchange event. IE 8 and 9 in compatibility mode 1550 // incorrectly report support for onhashchange. 1551 if ('onhashchange' in window && (!document.documentMode || document.documentMode > 7)) { 1552 if (window.addEventListener) { 1553 window.addEventListener('hashchange', this.intervalFunction(this), false); 1554 } 1555 else if (window.attachEvent) { 1556 window.attachEvent('onhashchange', this.intervalFunction(this)); 1557 } 1558 else { 1559 window.onhashchange = this.intervalFunction(this); 1560 } 1561 } 1562 else { 1563 this.intervalId = window.setInterval(this.intervalFunction(this), this.interval); 1564 } 1565 } 1566 }, 1567 1568 /** 1569 * Stores the values of the exposed parameters in both the local hash and the 1570 * URL hash. No other code should be made to change these two values. 1571 */ 1572 save: function () { 1573 this.hash = this.exposedString(); 1574 if (this.storedString()) { 1575 // Make a new history entry. 1576 window.location.hash = this.hash; 1577 } 1578 else { 1579 // Replace the old history entry. 1580 window.location.replace(window.location.href.replace('#', '') + '#' + this.hash); 1581 } 1582 }, 1583 1584 /** 1585 * @see ParameterStore#storedString() 1586 */ 1587 storedString: function () { 1588 // Some browsers automatically unescape characters in the hash, others 1589 // don't. Fortunately, all leave window.location.href alone. So, use that. 1590 var index = window.location.href.indexOf('#'); 1591 if (index == -1) { 1592 return ''; 1593 } 1594 else { 1595 return window.location.href.substr(index + 1); 1596 } 1597 }, 1598 1599 /** 1600 * Checks the hash for changes, and loads Solr parameters from the hash and 1601 * sends a request to Solr if it observes a change or if the hash is empty 1602 */ 1603 intervalFunction: function (self) { 1604 return function () { 1605 // Support the back/forward buttons. If the hash changes, do a request. 1606 var hash = self.storedString(); 1607 if (self.hash != hash && decodeURIComponent(self.hash) != decodeURIComponent(hash)) { 1608 self.load(); 1609 self.manager.doRequest(); 1610 } 1611 } 1612 } 1613}); 1614 1615})); 1616(function (callback) { 1617 if (typeof define === 'function' && define.amd) { 1618 define(['core/AbstractManager'], callback); 1619 } 1620 else { 1621 callback(); 1622 } 1623}(function () { 1624 1625/** 1626 * @see http://wiki.apache.org/solr/SolJSON#JSON_specific_parameters 1627 * @class Manager 1628 * @augments AjaxSolr.AbstractManager 1629 */ 1630AjaxSolr.Manager = AjaxSolr.AbstractManager.extend( 1631 /** @lends AjaxSolr.Manager.prototype */ 1632 { 1633 executeRequest: function (servlet, string, handler, errorHandler) { 1634 var self = this, 1635 options = {dataType: 'json'}; 1636 string = string || this.store.string(); 1637 handler = handler || function (data) { 1638 self.handleResponse(data); 1639 }; 1640 errorHandler = errorHandler || function (jqXHR, textStatus, errorThrown) { 1641 self.handleError(textStatus + ', ' + errorThrown); 1642 }; 1643 if (this.proxyUrl) { 1644 options.url = this.proxyUrl; 1645 options.data = {query: string}; 1646 options.type = 'POST'; 1647 } 1648 else { 1649 options.url = this.solrUrl + servlet + '?' + string + '&wt=json&json.wrf=?'; 1650 } 1651 jQuery.ajax(options).done(handler).fail(errorHandler); 1652 } 1653}); 1654 1655})); 1656(function ($) { 1657 1658/** 1659 * A <i>term</i> autocomplete search box, using jQueryUI.autocomplete. This 1660 * implementation uses Solr's facet.prefix technique. This technique benefits 1661 * from honoring the filter query state and by being able to put words prior to 1662 * the last one the user is typing into a filter query as well to get even more 1663 * relevant completion suggestions. 1664 * 1665 * Index instructions:
1666 * 1. Put a facet warming query into Solr's "firstSearcher" in solrconfig.xml, 1667 * for the target field. 1668 * 2. Use appropriate text analysis to include a tokenizer (not keyword) and do 1669 * <i>not</i> do stemming or else you will see stems suggested. A 'light' 1670 * stemmer may produce acceptable stems. 1671 * 3. If you are auto-completing in a search box that would normally be using 1672 * the dismax query parser AND your qf parameter references more than one field, 1673 * then you might want to use a catch-all search field to autocomplete on. 1674 * 1675 * For large indexes, another implementation approach like the Suggester feature 1676 * or TermsComponent might be better than a faceting approach. 1677 * 1678 * Other types of autocomplete (a.k.a. suggest) are "search-results", 1679 * "query-log", and "facet-value". This widget does term autocompletion. 1680 * 1681 * @author David Smiley <david.w.smiley at gmail.com> 1682 */ 1683AjaxSolr.AutocompleteTermWidget = AjaxSolr.AbstractTextWidget.extend( 1684 /** @lends AjaxSolr.AutocompleteTermWidget.prototype */ 1685 { 1686 /** 1687 * @param {Object} attributes 1688 * @param {String} attributes.field The Solr field to autocomplete indexed 1689 * terms from. 1690 * @param {Boolean} [attributes.tokenized] Whether the underlying field is 1691 * tokenized. This component will take words before the last word 1692 * (whitespace separated) and generate a filter query for those words, while 1693 * only the last word will be used for facet.prefix. For field-value 1694 * completion (on just one field) or query log completion, you would have a 1695 * non-tokenized field to complete against. Defaults to <tt>true</tt>. 1696 * @param {Boolean} [attributes.lowercase] Indicates whether to lowercase the 1697 * facet.prefix value. Defaults to <tt>true</tt>. 1698 * @param {Number} [attributes.limit] The maximum number of results to show. 1699 * Defaults to 10. 1700 * @param {Number} [attributes.minLength] The minimum number of characters 1701 * required to show suggestions. Defaults to 2. 1702 * @param {String} [attributes.servlet] The URL path that follows the solr 1703 * webapp, for use in auto-complete queries. If not specified, the manager's 1704 * servlet property will be used. You may prepend the servlet with a core if 1705 * using multiple cores. It is a good idea to use a non-default one to 1706 * differentiate these requests in server logs and Solr statistics. 1707 */ 1708 constructor: function (attributes) { 1709 AjaxSolr.AutocompleteTermWidget.__super__.constructor.apply(this, arguments); 1710 AjaxSolr.extend(this, { 1711 field: null, 1712 tokenized: true, 1713 lowercase: true, 1714 limit: 10, 1715 minLength: 2, 1716 servlet: null 1717 }, attributes); 1718 }, 1719 1720 init: function () { 1721 var self = this; 1722 1723 if (!this.field) { 1724 throw '"field" must be set on AutocompleteTermWidget.'; 1725 } 1726 this.servlet = this.servlet || this.manager.servlet; 1727 1728 $(this.target).find('input').bind('keydown', function (e) { 1729 if (e.which == 13) { 1730 var q = $(this).val(); 1731 if (self.set(q)) { 1732 self.doRequest(); 1733 } 1734 } 1735 }); 1736 1737 $(this.target).find('input').autocomplete({ 1738 source: function (request, response) { // note: must always call response() 1739 // If term ends with a space: 1740 if (request.term.charAt(request.term.length - 1).replace(/^ +/, '').replace(/ +$/, '') == '') { 1741 response(); 1742 return; 1743 } 1744 1745 var term = request.term, 1746 facetPrefix = term, // before the last word (if we tokenize) 1747 fq = '', 1748 store = new AjaxSolr.ParameterStore(); 1749 1750 store.addByValue('fq', self.manager.store.values('fq')); 1751 1752 if (self.tokenized) { 1753 // Split out the last word of the term from the words before it. 1754 var lastSpace = term.lastIndexOf(' '); 1755 if (lastSpace > -1) { 1756 fq = term.substring(0, lastSpace); 1757 facetPrefix = term.substring(lastSpace + 1); 1758 store.addByValue('fq', '{!dismax qf=' + self.field + '}' + fq); 1759 } 1760 } 1761 if (self.lowercase) { 1762 facetPrefix = facetPrefix.toLowerCase(); 1763 } 1764 1765 store.addByValue('facet.field', self.field); 1766 store.addByValue('facet.limit', self.limit); 1767 store.addByValue('facet.prefix', facetPrefix); 1768 1769 self.manager.executeRequest(self.servlet, 'json.nl=arrarr&q=*:*&rows=0&facet=true&facet.mincount=1&' + store.string(), function (data) { 1770 response($.map(data.facet_counts.facet_fields[self.field], function (term) { 1771 var q = (fq + ' ' + term[0]).replace(/^ +/, '').replace(/ +$/, ''); 1772 return { 1773 label: q + ' (' + term[1] + ')', 1774 value: q 1775 } 1776 })); 1777 }); 1778 }, 1779 minLength: this.minLength, 1780 select: function(event, ui) { 1781 if (self.set(ui.item.value)) { 1782 self.doRequest(); 1783 } 1784 } 1785 }); 1786 } 1787}); 1788 1789})(jQuery); 1790(function (callback) { 1791 if (typeof define === 'function' && define.amd) { 1792 define(['core/AbstractWidget'], callback); 1793 } 1794 else { 1795 callback(); 1796 } 1797}(function () { 1798 1799(function ($) { 1800 1801/** 1802 * A pager widget for jQuery. 1803 * 1804 * <p>Heavily inspired by the Ruby on Rails will_paginate gem.</p> 1805 * 1806 * @expects this.target to be a list. 1807 * @class PagerWidget 1808 * @augments AjaxSolr.AbstractWidget 1809 * @todo Don't use the manager to send the request. Request only the results, 1810 * not the facets. Update only itself and the results widget. 1811 */ 1812AjaxSolr.PagerWidget = AjaxSolr.AbstractWidget.extend( 1813 /** @lends AjaxSolr.PagerWidget.prototype */ 1814 { 1815 /** 1816 * @param {Object} [attributes] 1817 * @param {Number} [attributes.innerWindow] How many links are shown around 1818 * the current page. Defaults to 4. 1819 * @param {Number} [attributes.outerWindow] How many links are around the 1820 * first and the last page. Defaults to 1. 1821 * @param {String} [attributes.prevLabel] The previous page link label. 1822 * Defaults to "« Previous". 1823 * @param {String} [attributes.nextLabel] The next page link label. Defaults 1824 * to "Next »". 1825 * @param {String} [attributes.separator] Separator between pagination links. 1826 * Defaults to " ". 1827 */ 1828 constructor: function (attributes) { 1829 AjaxSolr.PagerWidget.__super__.constructor.apply(this, arguments); 1830 AjaxSolr.extend(this, { 1831 innerWindow: 4, 1832 outerWindow: 1, 1833 prevLabel: '« Previous', 1834 nextLabel: 'Next »', 1835 separator: ' ', 1836 // The current page number. 1837 currentPage: null, 1838 // The total number of pages. 1839 totalPages: null 1840 }, attributes); 1841 }, 1842 1843 /** 1844 * @returns {String} The gap in page links, which is represented by: 1845 * <span class="pager-gap">…</span> 1846 */ 1847 gapMarker: function () { 1848 return '<span class="pager-gap">…</span>'; 1849 }, 1850 1851 /** 1852 * @returns {Array} The links for the visible page numbers. 1853 */ 1854 windowedLinks: function () { 1855 var links = []; 1856 1857 var prev = null; 1858 1859 visible = this.visiblePageNumbers(); 1860 for (var i = 0, l = visible.length; i < l; i++) { 1861 if (prev && visible[i] > prev + 1) links.push(this.gapMarker()); 1862 links.push(this.pageLinkOrSpan(visible[i], [ 'pager-current' ])); 1863 prev = visible[i]; 1864 } 1865 1866 return links; 1867 }, 1868 1869 /** 1870 * @returns {Array} The visible page numbers according to the window options. 1871 */ 1872 visiblePageNumbers: function () { 1873 var windowFrom = this.currentPage - this.innerWindow; 1874 var windowTo = this.currentPage + this.innerWindow; 1875 1876 // If the window is truncated on one side, make the other side longer 1877 if (windowTo > this.totalPages) { 1878 windowFrom = Math.max(0, windowFrom - (windowTo - this.totalPages)); 1879 windowTo = this.totalPages; 1880 } 1881 if (windowFrom < 1) { 1882 windowTo = Math.min(this.totalPages, windowTo + (1 - windowFrom)); 1883 windowFrom = 1; 1884 } 1885 1886 var visible = []; 1887 1888 // Always show the first page 1889 visible.push(1); 1890 // Don't add inner window pages twice 1891 for (var i = 2; i <= Math.min(1 + this.outerWindow, windowFrom - 1); i++) { 1892 visible.push(i); 1893 } 1894 // If the gap is just one page, close the gap
1895 if (1 + this.outerWindow == windowFrom - 2) { 1896 visible.push(windowFrom - 1); 1897 } 1898 // Don't add the first or last page twice 1899 for (var i = Math.max(2, windowFrom); i <= Math.min(windowTo, this.totalPages - 1); i++) { 1900 visible.push(i); 1901 } 1902 // If the gap is just one page, close the gap 1903 if (this.totalPages - this.outerWindow == windowTo + 2) { 1904 visible.push(windowTo + 1); 1905 } 1906 // Don't add inner window pages twice 1907 for (var i = Math.max(this.totalPages - this.outerWindow, windowTo + 1); i < this.totalPages; i++) { 1908 visible.push(i); 1909 } 1910 // Always show the last page, unless it's the first page 1911 if (this.totalPages > 1) { 1912 visible.push(this.totalPages); 1913 } 1914 1915 return visible; 1916 }, 1917 1918 /** 1919 * @param {Number} page A page number. 1920 * @param {String} classnames CSS classes to add to the page link. 1921 * @param {String} text The inner HTML of the page link (optional). 1922 * @returns The link or span for the given page. 1923 */ 1924 pageLinkOrSpan: function (page, classnames, text) { 1925 text = text || page; 1926 1927 if (page && page != this.currentPage) { 1928 return $('<a href="#"></a>').html(text).attr('rel', this.relValue(page)).addClass(classnames[1]).click(this.clickHandler(page)); 1929 } 1930 else { 1931 return $('<span></span>').html(text).addClass(classnames.join(' ')); 1932 } 1933 }, 1934 1935 /** 1936 * @param {Number} page A page number. 1937 * @returns {Function} The click handler for the page link. 1938 */ 1939 clickHandler: function (page) { 1940 var self = this; 1941 return function () { 1942 self.manager.store.get('start').val((page - 1) * self.perPage()); 1943 self.doRequest(); 1944 return false; 1945 } 1946 }, 1947 1948 /** 1949 * @param {Number} page A page number. 1950 * @returns {String} The <tt>rel</tt> attribute for the page link. 1951 */ 1952 relValue: function (page) { 1953 switch (page) { 1954 case this.previousPage(): 1955 return 'prev' + (page == 1 ? 'start' : ''); 1956 case this.nextPage(): 1957 return 'next'; 1958 case 1: 1959 return 'start'; 1960 default: 1961 return ''; 1962 } 1963 }, 1964 1965 /** 1966 * @returns {Number} The page number of the previous page or null if no previous page. 1967 */ 1968 previousPage: function () { 1969 return this.currentPage > 1 ? (this.currentPage - 1) : null; 1970 }, 1971 1972 /** 1973 * @returns {Number} The page number of the next page or null if no next page. 1974 */ 1975 nextPage: function () { 1976 return this.currentPage < this.totalPages ? (this.currentPage + 1) : null; 1977 }, 1978 1979 /** 1980 * An abstract hook for child implementations. 1981 * 1982 * @param {Number} perPage The number of items shown per results page. 1983 * @param {Number} offset The index in the result set of the first document to render. 1984 * @param {Number} total The total number of documents in the result set. 1985 */ 1986 renderHeader: function (perPage, offset, total) {}, 1987 1988 /** 1989 * Render the pagination links. 1990 * 1991 * @param {Array} links The links for the visible page numbers. 1992 */ 1993 renderLinks: function (links) { 1994 if (this.totalPages) { 1995 links.unshift(this.pageLinkOrSpan(this.previousPage(), [ 'pager-disabled', 'pager-prev' ], this.prevLabel)); 1996 links.push(this.pageLinkOrSpan(this.nextPage(), [ 'pager-disabled', 'pager-next' ], this.nextLabel)); 1997 1998 var $target = $(this.target); 1999 $target.empty(); 2000 2001 for (var i = 0, l = links.length; i < l; i++) { 2002 var $li = $('<li></li>'); 2003 if (this.separator && i > 0) { 2004 $li.append(this.separator); 2005 } 2006 $target.append($li.append(links[i])); 2007 } 2008 } 2009 }, 2010 2011 /** 2012 * @returns {Number} The number of results to display per page. 2013 */ 2014 perPage: function () { 2015 return parseInt(this.manager.response.responseHeader && this.manager.response.responseHeader.params && this.manager.response.responseHeader.params.rows || this.manager.store.get('rows').val() || 10); 2016 }, 2017 2018 /** 2019 * @returns {Number} The Solr offset parameter's value. 2020 */ 2021 getOffset: function () { 2022 return parseInt(this.manager.response.responseHeader && this.manager.response.responseHeader.params && this.manager.response.responseHeader.params.start || this.manager.store.get('start').val() || 0); 2023 }, 2024 2025 afterRequest: function () { 2026 var perPage = this.perPage(); 2027 var offset = this.getOffset(); 2028 var total = parseInt(this.manager.response.response.numFound); 2029 2030 // Normalize the offset to a multiple of perPage. 2031 offset = offset - offset % perPage; 2032
2033 this.currentPage = Math.ceil((offset + 1) / perPage); 2034 this.totalPages = Math.ceil(total / perPage); 2035 2036 $(this.target).empty(); 2037 2038 this.renderLinks(this.windowedLinks()); 2039 this.renderHeader(perPage, offset, total); 2040 } 2041}); 2042 2043})(jQuery); 2044 2045})); 2046;(function(history) { 2047 /** 2048 * A parameter store that stores the values of exposed parameters in the URL via History.js 2049 * to maintain the application's state. This uses the HTML5 History API for newer browsers, and 2050 * falls back to using the hash in older browsers. Don't forget to add the following (or similar) 2051 * inside your <tt>head</tt> tag: 2052 * 2053 * <pre> 2054 * <script src="history.js/scripts/bundled/html4+html5/jquery.history.js"></script> 2055 * </pre> 2056 * 2057 * Configure the manager with: 2058 * 2059 * <pre> 2060 * Manager.setStore(new AjaxSolr.ParameterHistoryStore()); 2061 * </pre> 2062 * 2063 * @class ParameterHistoryStore 2064 * @augments AjaxSolr.ParameterStore 2065 * @see https://github.com/browserstate/history.js 2066 * @see http://www.whatwg.org/specs/web-apps/current-work/multipage/history.html 2067 */ 2068 AjaxSolr.ParameterHistoryStore = AjaxSolr.ParameterStore.extend( 2069 /** @lends AjaxSolr.ParameterHistoryStore.prototype */ 2070 { 2071 init: function () { 2072 if (this.exposed.length) { 2073 if (!history) { 2074 throw 'ParameterHistoryStore requires History.js'; 2075 } 2076 2077 history.Adapter.bind(window, 'statechange', this.stateChangeFunction(this)); 2078 } 2079 }, 2080 2081 /** 2082 * Stores the values of the exposed parameters in both the local hash and History.js 2083 * No other code should be made to change these two values. 2084 */ 2085 save: function () { 2086 this.hash = this.exposedString(); 2087 history.pushState({ params: this.hash }, null, '?' + this.hash); 2088 }, 2089 2090 /** 2091 * @see ParameterStore#storedString() 2092 */ 2093 storedString: function () { 2094 var state = history.getState(); 2095 2096 // Load the state from the History object. 2097 if (state.data && state.data.params) { 2098 return state.data.params; 2099 } 2100 2101 // If initial load, load the state from the URL. 2102 var url = state.cleanUrl, index = url.indexOf('?'); 2103 if (index == -1) { 2104 return ''; 2105 } 2106 else { 2107 return url.substr(index + 1); 2108 } 2109 }, 2110 2111 /** 2112 * Called when History.js detects a state change. Checks if state is different to previous state, 2113 * and if so, sends a request to Solr. This needs to check if the state has changed since it also 2114 * gets called when we call pushState above. 2115 */ 2116 stateChangeFunction: function (self) { 2117 return function () { 2118 var hash = self.storedString(); 2119 2120 if (self.hash != hash) { 2121 self.load(); 2122 self.manager.doRequest(); 2123 } 2124 } 2125 } 2126 }); 2127})(window.History); 2128/** 2129 * A parameter store that stores the values of exposed parameters using the YUI 2130 * History Manager to maintain the application's state. Don't forget to add the 2131 * following inside your <tt>head</tt> tag: 2132 * 2133 * <pre> 2134 * <script src="http://yui.yahooapis.com/2.9.0/build/yahoo/yahoo-min.js"></script> 2135 * <script src="http://yui.yahooapis.com/2.9.0/build/event/event-min.js"></script> 2136 * <script src="http://yui.yahooapis.com/2.9.0/build/history/history-min.js"></script> 2137 * </pre> 2138 * 2139 * And the following inside your <tt>body</tt> tag: 2140 * 2141 * <pre> 2142 * <iframe id="yui-history-iframe" src="path-to-existing-asset" style="position:absolute;top:0;left:0;width:1px;height:1px;visibility:hidden"></iframe> 2143 * <input id="yui-history-field" type="hidden"> 2144 * </pre> 2145 * 2146 * Configure the manager with: 2147 * 2148 * <pre> 2149 * Manager.setStore(new AjaxSolr.ParameterYUIStore()); 2150 * </pre> 2151 * 2152 * @see http://developer.yahoo.com/yui/history/ 2153 * @class ParameterYUIStore 2154 * @augments AjaxSolr.ParameterStore 2155 */ 2156AjaxSolr.ParameterYUIStore = AjaxSolr.ParameterStore.extend( 2157 /** @lends AjaxSolr.ParameterYUIStore.prototype */ 2158 { 2159 /** 2160 * @param {Object} [attributes] 2161 * @param {String} [attributes.module] The name of the YUI History Manager 2162 * module to use for the parameter store. Defaults to "q". 2163 */ 2164 constructor: function (attributes) { 2165 AjaxSolr.ParameterYUIStore.__super__.constructor.apply(this, arguments); 2166 AjaxSolr.extend(this, { 2167 module: 'q',
2168 // Whether the YUI History Manager is initialized. 2169 initialized: false, 2170 // Whether the parameter store is curring loading state. 2171 loading: false, 2172 // Whether the parameter store is curring saving state. 2173 saving: false 2174 }, attributes); 2175 }, 2176 2177 /** 2178 * Initializes the YUI History Manager. 2179 */ 2180 init: function () { 2181 if (this.exposed.length) { 2182 var self = this; 2183 YAHOO.util.History.register(this.module, YAHOO.util.History.getBookmarkedState(this.module) || this.exposedString(), function () { 2184 if (!self.saving) { 2185 self.loading = true; 2186 self.load(); 2187 self.manager.doRequest(); 2188 self.loading = false; 2189 } 2190 }); 2191 YAHOO.util.History.onReady(function () { 2192 self.initialized = true; 2193 self.load(); 2194 self.manager.doRequest(); 2195 }); 2196 YAHOO.util.History.initialize('yui-history-field', 'yui-history-iframe'); 2197 } 2198 }, 2199 2200 /** 2201 * Stores the values of the exposed parameters in the YUI History Manager. 2202 */ 2203 save: function () { 2204 if (!self.loading) { 2205 this.saving = true; 2206 YAHOO.util.History.navigate(this.module, this.exposedString()); 2207 this.saving = false; 2208 } 2209 }, 2210 2211 /** 2212 * @see ParameterStore#storedString() 2213 */ 2214 storedString: function () { 2215 return this.initialized ? YAHOO.util.History.getCurrentState(this.module) : this.exposedString(); 2216 } 2217});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.