notch8_sunspot_autocomplete 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. data/LICENSE +7 -0
  2. data/README.rdoc +95 -0
  3. data/Rakefile +37 -0
  4. data/VERSION +1 -0
  5. data/lib/autocomplete_view_helpers.rb +126 -0
  6. data/lib/notch8_sunspot_autocomplete.rb +3 -0
  7. data/lib/sunspot_autocomplete.rb +38 -0
  8. data/notch8_sunspot_autocomplete.gemspec +78 -0
  9. data/rdoc/classes/AutocompleteViewHelpers.html +322 -0
  10. data/rdoc/classes/Sunspot.html +111 -0
  11. data/rdoc/classes/Sunspot/Type.html +112 -0
  12. data/rdoc/classes/Sunspot/Type/AutocompleteType.html +117 -0
  13. data/rdoc/classes/Sunspot/Type/AutosuggestType.html +117 -0
  14. data/rdoc/created.rid +1 -0
  15. data/rdoc/files/README.html +238 -0
  16. data/rdoc/files/README_rdoc.html +238 -0
  17. data/rdoc/files/lib/autocomplete_view_helpers_rb.html +236 -0
  18. data/rdoc/files/lib/sunspot_autocomplete_rb.html +101 -0
  19. data/rdoc/fr_class_index.html +31 -0
  20. data/rdoc/fr_file_index.html +29 -0
  21. data/rdoc/fr_method_index.html +28 -0
  22. data/rdoc/index.html +24 -0
  23. data/rdoc/rdoc-style.css +208 -0
  24. data/tasks/tasks.rake +10 -0
  25. data/test/sunspot_autocomplete_test.rb +21 -0
  26. data/test/test_helper.rb +3 -0
  27. data/vendor/assets/javascripts/jquery.js +6240 -0
  28. data/vendor/assets/javascripts/solr-autocomplete/ajax-solr/core/AbstractManager.js +182 -0
  29. data/vendor/assets/javascripts/solr-autocomplete/ajax-solr/core/Core.js +226 -0
  30. data/vendor/assets/javascripts/solr-autocomplete/ajax-solr/core/Parameter.js +161 -0
  31. data/vendor/assets/javascripts/solr-autocomplete/ajax-solr/core/ParameterStore.js +354 -0
  32. data/vendor/assets/javascripts/solr-autocomplete/ajax-solr/managers/Manager.jquery.js +20 -0
  33. data/vendor/assets/javascripts/solr-autocomplete/jquery-autocomplete/indicator.gif +0 -0
  34. data/vendor/assets/javascripts/solr-autocomplete/jquery-autocomplete/jquery.autocomplete.css +49 -0
  35. data/vendor/assets/javascripts/solr-autocomplete/jquery-autocomplete/jquery.autocomplete.js +867 -0
  36. metadata +114 -0
@@ -0,0 +1,354 @@
1
+ // $Id$
2
+
3
+ /**
4
+ * The ParameterStore, as its name suggests, stores Solr parameters. Widgets
5
+ * expose some of these parameters to the user. Whenever the user changes the
6
+ * values of these parameters, the state of the application changes. In order to
7
+ * allow the user to move back and forth between these states with the browser's
8
+ * Back and Forward buttons, and to bookmark these states, each state needs to
9
+ * be stored. The easiest method is to store the exposed parameters in the URL
10
+ * hash (see the <tt>ParameterHashStore</tt> class). However, you may implement
11
+ * your own storage method by extending this class.
12
+ *
13
+ * <p>For a list of possible parameters, please consult the links below.</p>
14
+ *
15
+ * @see http://wiki.apache.org/solr/CoreQueryParameters
16
+ * @see http://wiki.apache.org/solr/CommonQueryParameters
17
+ * @see http://wiki.apache.org/solr/SimpleFacetParameters
18
+ * @see http://wiki.apache.org/solr/HighlightingParameters
19
+ * @see http://wiki.apache.org/solr/MoreLikeThis
20
+ * @see http://wiki.apache.org/solr/SpellCheckComponent
21
+ * @see http://wiki.apache.org/solr/StatsComponent
22
+ * @see http://wiki.apache.org/solr/TermsComponent
23
+ * @see http://wiki.apache.org/solr/TermVectorComponent
24
+ * @see http://wiki.apache.org/solr/LocalParams
25
+ *
26
+ * @param properties A map of fields to set. Refer to the list of public fields.
27
+ * @class ParameterStore
28
+ */
29
+ AjaxSolr.ParameterStore = AjaxSolr.Class.extend(
30
+ /** @lends AjaxSolr.ParameterStore.prototype */
31
+ {
32
+ /**
33
+ * The names of the exposed parameters. Any parameters that your widgets
34
+ * expose to the user, directly or indirectly, should be listed here.
35
+ *
36
+ * @field
37
+ * @public
38
+ * @type String[]
39
+ * @default []
40
+ */
41
+ exposed: [],
42
+
43
+ /**
44
+ * The Solr parameters.
45
+ *
46
+ * @field
47
+ * @private
48
+ * @type Object
49
+ * @default {}
50
+ */
51
+ params: {},
52
+
53
+ /**
54
+ * A reference to the parameter store's manager. For internal use only.
55
+ *
56
+ * @field
57
+ * @private
58
+ * @type AjaxSolr.AbstractManager
59
+ */
60
+ manager: null,
61
+
62
+ /**
63
+ * An abstract hook for child implementations.
64
+ *
65
+ * <p>This method should do any necessary one-time initializations.</p>
66
+ */
67
+ init: function () {},
68
+
69
+ /**
70
+ * Some Solr parameters may be specified multiple times. It is easiest to
71
+ * hard-code a list of such parameters. You may change the list by passing
72
+ * <code>{ multiple: /pattern/ }</code> as an argument to the constructor of
73
+ * this class or one of its children, e.g.:
74
+ *
75
+ * <p><code>new ParameterStore({ multiple: /pattern/ })</code>
76
+ *
77
+ * @param {String} name The name of the parameter.
78
+ * @returns {Boolean} Whether the parameter may be specified multiple times.
79
+ */
80
+ isMultiple: function (name) {
81
+ return name.match(/^(?:bf|bq|facet\.date|facet\.date\.other|facet\.field|facet\.query|fq|pf|qf)$/);
82
+ },
83
+
84
+ /**
85
+ * Returns a parameter. If the parameter doesn't exist, creates it.
86
+ *
87
+ * @param {String} name The name of the parameter.
88
+ * @returns {AjaxSolr.Parameter|AjaxSolr.Parameter[]} The parameter.
89
+ */
90
+ get: function (name) {
91
+ if (this.params[name] === undefined) {
92
+ var param = new AjaxSolr.Parameter({ name: name });
93
+ if (this.isMultiple(name)) {
94
+ this.params[name] = [ param ];
95
+ }
96
+ else {
97
+ this.params[name] = param;
98
+ }
99
+ }
100
+ return this.params[name];
101
+ },
102
+
103
+ /**
104
+ * If the parameter may be specified multiple times, returns the values of
105
+ * all identically-named parameters. If the parameter may be specified only
106
+ * once, returns the value of that parameter.
107
+ *
108
+ * @param {String} name The name of the parameter.
109
+ * @returns {String[]|Number[]} The value(s) of the parameter.
110
+ */
111
+ values: function (name) {
112
+ if (this.params[name] !== undefined) {
113
+ if (this.isMultiple(name)) {
114
+ var values = [];
115
+ for (var i = 0, l = this.params[name].length; i < l; i++) {
116
+ values.push(this.params[name][i].val());
117
+ }
118
+ return values;
119
+ }
120
+ else {
121
+ return [ this.params[name].val() ];
122
+ }
123
+ }
124
+ return [];
125
+ },
126
+
127
+ /**
128
+ * If the parameter may be specified multiple times, adds the given parameter
129
+ * to the list of identically-named parameters, unless one already exists with
130
+ * the same value. If it may be specified only once, replaces the parameter.
131
+ *
132
+ * @param {String} name The name of the parameter.
133
+ * @param {AjaxSolr.Parameter} [param] The parameter.
134
+ * @returns {AjaxSolr.Parameter|Boolean} The parameter, or false.
135
+ */
136
+ add: function (name, param) {
137
+ if (param === undefined) {
138
+ param = new AjaxSolr.Parameter({ name: name });
139
+ }
140
+ if (this.isMultiple(name)) {
141
+ if (this.params[name] === undefined) {
142
+ this.params[name] = [ param ];
143
+ }
144
+ else {
145
+ if (AjaxSolr.inArray(param.val(), this.values(name)) == -1) {
146
+ this.params[name].push(param);
147
+ }
148
+ else {
149
+ return false;
150
+ }
151
+ }
152
+ }
153
+ else {
154
+ this.params[name] = param;
155
+ }
156
+ return param;
157
+ },
158
+
159
+ /**
160
+ * Deletes a parameter.
161
+ *
162
+ * @param {String} name The name of the parameter.
163
+ * @param {Number} [index] The index of the parameter.
164
+ */
165
+ remove: function (name, index) {
166
+ if (index === undefined) {
167
+ delete this.params[name];
168
+ }
169
+ else {
170
+ this.params[name].splice(index, 1);
171
+ if (this.params[name].length == 0) {
172
+ delete this.params[name];
173
+ }
174
+ }
175
+ },
176
+
177
+ /**
178
+ * Finds all parameters with matching values.
179
+ *
180
+ * @param {String} name The name of the parameter.
181
+ * @param {String|Number|String[]|Number[]|RegExp} value The value.
182
+ * @returns {String|Number[]} The indices of the parameters found.
183
+ */
184
+ find: function (name, value) {
185
+ if (this.params[name] !== undefined) {
186
+ if (this.isMultiple(name)) {
187
+ var indices = [];
188
+ for (var i = 0, l = this.params[name].length; i < l; i++) {
189
+ if (AjaxSolr.equals(this.params[name][i].val(), value)) {
190
+ indices.push(i);
191
+ }
192
+ }
193
+ return indices.length ? indices : false;
194
+ }
195
+ else {
196
+ if (AjaxSolr.equals(this.params[name].val(), value)) {
197
+ return name;
198
+ }
199
+ }
200
+ }
201
+ return false;
202
+ },
203
+
204
+ /**
205
+ * If the parameter may be specified multiple times, creates a parameter using
206
+ * the given name and value, and adds it to the list of identically-named
207
+ * parameters, unless one already exists with the same value. If it may be
208
+ * specified only once, replaces the parameter.
209
+ *
210
+ * @param {String} name The name of the parameter.
211
+ * @param {String|Number|String[]|Number[]} value The value.
212
+ * @returns {AjaxSolr.Parameter|Boolean} The parameter, or false.
213
+ */
214
+ addByValue: function (name, value) {
215
+ if (this.isMultiple(name) && AjaxSolr.isArray(value)) {
216
+ var ret = [];
217
+ for (var i = 0, l = value.length; i < l; i++) {
218
+ ret.push(this.add(name, new AjaxSolr.Parameter({ name: name, value: value[i] })));
219
+ }
220
+ return ret;
221
+ }
222
+ else {
223
+ return this.add(name, new AjaxSolr.Parameter({ name: name, value: value }))
224
+ }
225
+ },
226
+
227
+ /**
228
+ * Deletes any parameter with a matching value.
229
+ *
230
+ * @param {String} name The name of the parameter.
231
+ * @param {String|Number|String[]|Number[]|RegExp} value The value.
232
+ * @returns {String|Number[]} The indices deleted.
233
+ */
234
+ removeByValue: function (name, value) {
235
+ var indices = this.find(name, value);
236
+ if (indices) {
237
+ if (AjaxSolr.isArray(indices)) {
238
+ for (var i = indices.length - 1; i >= 0; i--) {
239
+ this.remove(name, indices[i]);
240
+ }
241
+ }
242
+ else {
243
+ this.remove(indices);
244
+ }
245
+ }
246
+ return indices;
247
+ },
248
+
249
+ /**
250
+ * Returns the Solr parameters as a query string.
251
+ *
252
+ * <p>IE6 calls the default toString() if you write <tt>store.toString()
253
+ * </tt>. So, we need to choose another name for toString().</p>
254
+ */
255
+ string: function () {
256
+ var params = [];
257
+ for (var name in this.params) {
258
+ if (this.isMultiple(name)) {
259
+ for (var i = 0, l = this.params[name].length; i < l; i++) {
260
+ params.push(this.params[name][i].string());
261
+ }
262
+ }
263
+ else {
264
+ params.push(this.params[name].string());
265
+ }
266
+ }
267
+ return AjaxSolr.compact(params).join('&');
268
+ },
269
+
270
+ /**
271
+ * Parses a query string into Solr parameters.
272
+ *
273
+ * @param {String} str The string to parse.
274
+ */
275
+ parseString: function (str) {
276
+ var pairs = str.split('&');
277
+ for (var i = 0, l = pairs.length; i < l; i++) {
278
+ if (pairs[i]) { // ignore leading, trailing, and consecutive &'s
279
+ var param = new AjaxSolr.Parameter();
280
+ param.parseString(pairs[i]);
281
+ this.add(param.name, param);
282
+ }
283
+ }
284
+ },
285
+
286
+ /**
287
+ * Returns the exposed parameters as a query string.
288
+ *
289
+ * @returns {String} A string representation of the exposed parameters.
290
+ */
291
+ exposedString: function () {
292
+ var params = [];
293
+ for (var i = 0, l = this.exposed.length; i < l; i++) {
294
+ if (this.params[this.exposed[i]] !== undefined) {
295
+ if (this.isMultiple(this.exposed[i])) {
296
+ for (var j = 0, m = this.params[this.exposed[i]].length; j < m; j++) {
297
+ params.push(this.params[this.exposed[i]][j].string());
298
+ }
299
+ }
300
+ else {
301
+ params.push(this.params[this.exposed[i]].string());
302
+ }
303
+ }
304
+ }
305
+ return AjaxSolr.compact(params).join('&');
306
+ },
307
+
308
+ /**
309
+ * Resets the values of the exposed parameters.
310
+ */
311
+ exposedReset: function () {
312
+ for (var i = 0, l = this.exposed.length; i < l; i++) {
313
+ this.remove(this.exposed[i]);
314
+ }
315
+ },
316
+
317
+ /**
318
+ * Loads the values of exposed parameters from persistent storage. It is
319
+ * necessary, in most cases, to reset the values of exposed parameters before
320
+ * setting the parameters to the values in storage. This is to ensure that a
321
+ * parameter whose name is not present in storage is properly reset.
322
+ *
323
+ * @param {Boolean} [reset=true] Whether to reset the exposed parameters.
324
+ * before loading new values from persistent storage. Default: true.
325
+ */
326
+ load: function (reset) {
327
+ if (reset === undefined) {
328
+ reset = true;
329
+ }
330
+ if (reset) {
331
+ this.exposedReset();
332
+ }
333
+ this.parseString(this.storedString());
334
+ },
335
+
336
+ /**
337
+ * An abstract hook for child implementations.
338
+ *
339
+ * <p>Stores the values of the exposed parameters in persistent storage. This
340
+ * method should usually be called before each Solr request.</p>
341
+ */
342
+ save: function () {},
343
+
344
+ /**
345
+ * An abstract hook for child implementations.
346
+ *
347
+ * <p>Returns the string to parse from persistent storage.</p>
348
+ *
349
+ * @returns {String} The string from persistent storage.
350
+ */
351
+ storedString: function () {
352
+ return '';
353
+ }
354
+ });
@@ -0,0 +1,20 @@
1
+ // $Id$
2
+
3
+ /**
4
+ * @see http://wiki.apache.org/solr/SolJSON#JSON_specific_parameters
5
+ * @class Manager
6
+ * @augments AjaxSolr.AbstractManager
7
+ */
8
+ AjaxSolr.Manager = AjaxSolr.AbstractManager.extend(
9
+ /** @lends AjaxSolr.Manager.prototype */
10
+ {
11
+ executeRequest: function (servlet) {
12
+ var self = this;
13
+ if (this.proxyUrl) {
14
+ jQuery.post(this.proxyUrl, { query: this.store.string() }, function (data) { self.handleResponse(data); }, 'json');
15
+ }
16
+ else {
17
+ jQuery.getJSON(this.solrUrl + servlet + '?' + this.store.string() + '&wt=json&json.wrf=?', {}, function (data) { self.handleResponse(data); });
18
+ }
19
+ }
20
+ });
@@ -0,0 +1,49 @@
1
+ .ac_results {
2
+ padding: 0px;
3
+ border: 1px solid black;
4
+ background-color: white;
5
+ overflow: hidden;
6
+ z-index: 99999;
7
+ text-align: left;
8
+ }
9
+
10
+ .ac_results ul {
11
+ width: 100%;
12
+ list-style-position: outside;
13
+ list-style: none;
14
+ padding: 0;
15
+ margin: 0;
16
+ }
17
+
18
+ .ac_results li {
19
+ margin: 0px;
20
+ padding: 2px 5px;
21
+ cursor: default;
22
+ display: block;
23
+ /*
24
+ if width will be 100% horizontal scrollbar will apear
25
+ when scroll mode will be used
26
+ */
27
+ /*width: 100%;*/
28
+ font: menu;
29
+ font-size: 12px;
30
+ /*
31
+ it is very important, if line-height not setted or setted
32
+ in relative units scroll will be broken in firefox
33
+ */
34
+ line-height: 16px;
35
+ overflow: hidden;
36
+ }
37
+
38
+ .ac_loading {
39
+ background: white url('indicator.gif') right center no-repeat;
40
+ }
41
+
42
+ .ac_odd {
43
+ background-color: #eee;
44
+ }
45
+
46
+ .ac_over {
47
+ background-color: #0A246A;
48
+ color: white;
49
+ }
@@ -0,0 +1,867 @@
1
+ /*
2
+ * This part of the library is extended from jQuery Autocomplete plugin by Jörn Zaefferer
3
+ */
4
+
5
+
6
+ jQuery.extend({
7
+
8
+ get: function( url, data, callback, type ) {
9
+ // shift arguments if data argument was omited
10
+ if ( jQuery.isFunction( data ) ) {
11
+ type = type || callback;
12
+ callback = data;
13
+ data = null;
14
+ }
15
+
16
+ return jQuery.ajax({
17
+ type: "GET",
18
+ url: url,
19
+ data: data,
20
+ success: callback,
21
+ dataType: type,
22
+ jsonpCallback: "applyAjaxSolrCallback"
23
+ });
24
+ }
25
+ });
26
+
27
+
28
+ ;(function($) {
29
+
30
+ $.fn.extend({
31
+ autocomplete: function(urlOrData, fieldName, options) {
32
+ var isUrl = typeof urlOrData == "string";
33
+ options = $.extend({}, $.Autocompleter.defaults, {
34
+ url: isUrl ? urlOrData : null,
35
+ data: isUrl ? null : urlOrData,
36
+ delay: isUrl ? $.Autocompleter.defaults.delay : 10,
37
+ max: options && !options.scroll ? 10 : 150
38
+ }, options);
39
+
40
+ // if highlight is set to false, replace it with a do-nothing function
41
+ options.highlight = options.highlight || function(value) { return value; };
42
+
43
+ // if the formatMatch option is not specified, then use formatItem for backwards compatibility
44
+ options.formatMatch = options.formatMatch || options.formatItem;
45
+
46
+ return this.each(function() {
47
+ new $.Autocompleter(this, fieldName, options);
48
+ });
49
+ },
50
+ autosuggest: function(urlOrData, fieldName, options){
51
+ options = $.extend({}, {suggest: true}, options);
52
+ this.autocomplete(urlOrData, fieldName, options)
53
+ },
54
+ result: function(handler) {
55
+ return this.bind("result", handler);
56
+ },
57
+ search: function(handler) {
58
+ return this.trigger("search", [handler]);
59
+ },
60
+ flushCache: function() {
61
+ return this.trigger("flushCache");
62
+ },
63
+ setOptions: function(options){
64
+ return this.trigger("setOptions", [options]);
65
+ },
66
+ unautocomplete: function() {
67
+ return this.trigger("unautocomplete");
68
+ },
69
+ ajaxSolrCallback: null,
70
+ ajaxSolrFieldName: null
71
+ });
72
+
73
+ $.Autocompleter = function(input, fieldName, options) {
74
+
75
+ var KEY = {
76
+ UP: 38,
77
+ DOWN: 40,
78
+ DEL: 46,
79
+ TAB: 9,
80
+ RETURN: 13,
81
+ ESC: 27,
82
+ COMMA: 188,
83
+ PAGEUP: 33,
84
+ PAGEDOWN: 34,
85
+ BACKSPACE: 8
86
+ };
87
+
88
+ // Create $ object for input element
89
+ var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass);
90
+
91
+ var timeout;
92
+ var previousValue = "";
93
+ var cache = $.Autocompleter.Cache(options);
94
+ var hasFocus = 0;
95
+ var lastKeyPressCode;
96
+ var config = {
97
+ mouseDownOnSelect: false
98
+ };
99
+ var select = $.Autocompleter.Select(options, input, selectCurrent, config);
100
+
101
+ var blockSubmit;
102
+
103
+ var fieldName = fieldName;
104
+ var suggest = options.suggest;
105
+
106
+ var ajaxSolrManager = new AjaxSolr.Manager({
107
+ solrUrl: options.url
108
+ });
109
+ ajaxSolrManager.init();
110
+
111
+ // prevent form submit in opera when selecting with return key
112
+ $.browser.opera && $(input.form).bind("submit.autocomplete", function() {
113
+ if (blockSubmit) {
114
+ blockSubmit = false;
115
+ return false;
116
+ }
117
+ });
118
+
119
+ // only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all
120
+ $input.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function(event) {
121
+ // a keypress means the input has focus
122
+ // avoids issue where input had focus before the autocomplete was applied
123
+ hasFocus = 1;
124
+ // track last key pressed
125
+ lastKeyPressCode = event.keyCode;
126
+ switch(event.keyCode) {
127
+
128
+ case KEY.UP:
129
+ event.preventDefault();
130
+ if ( select.visible() ) {
131
+ select.prev();
132
+ } else {
133
+ onChange(0, true);
134
+ }
135
+ break;
136
+
137
+ case KEY.DOWN:
138
+ event.preventDefault();
139
+ if ( select.visible() ) {
140
+ select.next();
141
+ } else {
142
+ onChange(0, true);
143
+ }
144
+ break;
145
+
146
+ case KEY.PAGEUP:
147
+ event.preventDefault();
148
+ if ( select.visible() ) {
149
+ select.pageUp();
150
+ } else {
151
+ onChange(0, true);
152
+ }
153
+ break;
154
+
155
+ case KEY.PAGEDOWN:
156
+ event.preventDefault();
157
+ if ( select.visible() ) {
158
+ select.pageDown();
159
+ } else {
160
+ onChange(0, true);
161
+ }
162
+ break;
163
+
164
+ // matches also semicolon
165
+ case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA:
166
+ case KEY.TAB:
167
+ case KEY.RETURN:
168
+ if( selectCurrent() ) {
169
+ // stop default to prevent a form submit, Opera needs special handling
170
+ event.preventDefault();
171
+ blockSubmit = true;
172
+ return false;
173
+ }
174
+ break;
175
+
176
+ case KEY.ESC:
177
+ select.hide();
178
+ break;
179
+
180
+ default:
181
+ clearTimeout(timeout);
182
+ timeout = setTimeout(onChange, options.delay);
183
+ break;
184
+ }
185
+ }).focus(function(){
186
+ // track whether the field has focus, we shouldn't process any
187
+ // results if the field no longer has focus
188
+ hasFocus++;
189
+ }).blur(function() {
190
+ hasFocus = 0;
191
+ if (!config.mouseDownOnSelect) {
192
+ hideResults();
193
+ }
194
+ }).click(function() {
195
+ // show select when clicking in a focused field
196
+ if ( hasFocus++ > 1 && !select.visible() ) {
197
+ onChange(0, true);
198
+ }
199
+ }).bind("search", function() {
200
+ // TODO why not just specifying both arguments?
201
+ var fn = (arguments.length > 1) ? arguments[1] : null;
202
+ function findValueCallback(q, data) {
203
+ var result;
204
+ if( data && data.length ) {
205
+ for (var i=0; i < data.length; i++) {
206
+ if( data[i].result.toLowerCase() == q.toLowerCase() ) {
207
+ result = data[i];
208
+ break;
209
+ }
210
+ }
211
+ }
212
+ if( typeof fn == "function" ) fn(result);
213
+ else $input.trigger("result", result && [result.data, result.value]);
214
+ }
215
+ $.each(trimWords($input.val()), function(i, value) {
216
+ request(value, findValueCallback, findValueCallback);
217
+ });
218
+ }).bind("flushCache", function() {
219
+ cache.flush();
220
+ }).bind("setOptions", function() {
221
+ $.extend(options, arguments[1]);
222
+ // if we've updated the data, repopulate
223
+ if ( "data" in arguments[1] )
224
+ cache.populate();
225
+ }).bind("unautocomplete", function() {
226
+ select.unbind();
227
+ $input.unbind();
228
+ $(input.form).unbind(".autocomplete");
229
+ });
230
+
231
+
232
+ function selectCurrent() {
233
+ var selected = select.selected();
234
+ if( !selected )
235
+ return false;
236
+
237
+ var v = selected.result;
238
+ previousValue = v;
239
+
240
+ if ( options.multiple ) {
241
+ var words = trimWords($input.val());
242
+ if ( words.length > 1 ) {
243
+ var seperator = options.multipleSeparator.length;
244
+ var cursorAt = $(input).selection().start;
245
+ var wordAt, progress = 0;
246
+ $.each(words, function(i, word) {
247
+ progress += word.length;
248
+ if (cursorAt <= progress) {
249
+ wordAt = i;
250
+ return false;
251
+ }
252
+ progress += seperator;
253
+ });
254
+ words[wordAt] = v;
255
+ // TODO this should set the cursor to the right position, but it gets overriden somewhere
256
+ //$.Autocompleter.Selection(input, progress + seperator, progress + seperator);
257
+ v = words.join( options.multipleSeparator );
258
+ }
259
+ v += options.multipleSeparator;
260
+ }
261
+
262
+ $input.val(v);
263
+ hideResultsNow();
264
+ $input.trigger("result", [selected.data, selected.value]);
265
+ return true;
266
+ }
267
+
268
+ function onChange(crap, skipPrevCheck) {
269
+ if( lastKeyPressCode == KEY.DEL ) {
270
+ select.hide();
271
+ return;
272
+ }
273
+
274
+ var currentValue = $input.val();
275
+
276
+ if ( !skipPrevCheck && currentValue == previousValue )
277
+ return;
278
+
279
+ previousValue = currentValue;
280
+
281
+ currentValue = lastWord(currentValue);
282
+ if ( currentValue.length >= options.minChars) {
283
+ $input.addClass(options.loadingClass);
284
+ if (!options.matchCase)
285
+ currentValue = currentValue.toLowerCase();
286
+ request(currentValue, receiveData, hideResultsNow);
287
+ } else {
288
+ stopLoading();
289
+ select.hide();
290
+ }
291
+ };
292
+
293
+ function trimWords(value) {
294
+ if (!value)
295
+ return [""];
296
+ if (!options.multiple)
297
+ return [$.trim(value)];
298
+ return $.map(value.split(options.multipleSeparator), function(word) {
299
+ return $.trim(value).length ? $.trim(word) : null;
300
+ });
301
+ }
302
+
303
+ function lastWord(value) {
304
+ if ( !options.multiple )
305
+ return value;
306
+ var words = trimWords(value);
307
+ if (words.length == 1)
308
+ return words[0];
309
+ var cursorAt = $(input).selection().start;
310
+ if (cursorAt == value.length) {
311
+ words = trimWords(value)
312
+ } else {
313
+ words = trimWords(value.replace(value.substring(cursorAt), ""));
314
+ }
315
+ return words[words.length - 1];
316
+ }
317
+
318
+ // fills in the input box w/the first match (assumed to be the best match)
319
+ // q: the term entered
320
+ // sValue: the first matching result
321
+ function autoFill(q, sValue){
322
+ // autofill in the complete box w/the first match as long as the user hasn't entered in more data
323
+ // if the last user key pressed was backspace, don't autofill
324
+ if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) {
325
+ // fill in the value (keep the case the user has typed)
326
+ $input.val($input.val() + sValue.substring(lastWord(previousValue).length));
327
+ // select the portion of the value not typed by the user (so the next character will erase)
328
+ $(input).selection(previousValue.length, previousValue.length + sValue.length);
329
+ }
330
+ };
331
+
332
+ function hideResults() {
333
+ clearTimeout(timeout);
334
+ timeout = setTimeout(hideResultsNow, 200);
335
+ };
336
+
337
+ function hideResultsNow() {
338
+ var wasVisible = select.visible();
339
+ select.hide();
340
+ clearTimeout(timeout);
341
+ stopLoading();
342
+ if (options.mustMatch) {
343
+ // call search and run callback
344
+ $input.search(
345
+ function (result){
346
+ // if no value found, clear the input box
347
+ if( !result ) {
348
+ if (options.multiple) {
349
+ var words = trimWords($input.val()).slice(0, -1);
350
+ $input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") );
351
+ }
352
+ else {
353
+ $input.val( "" );
354
+ $input.trigger("result", null);
355
+ }
356
+ }
357
+ }
358
+ );
359
+ }
360
+ };
361
+
362
+ function receiveData(q, data) {
363
+ var parsed = options.parse && options.parse(data) || parse(data);
364
+ cache.add(q, parsed);
365
+ if ( parsed && parsed.length && hasFocus ) {
366
+ stopLoading();
367
+ select.display(parsed, q);
368
+ autoFill(q, parsed[0].value);
369
+ select.show();
370
+ } else {
371
+ hideResultsNow();
372
+ }
373
+ };
374
+
375
+ function request(term, success, failure) {
376
+ if (!options.matchCase)
377
+ term = term.toLowerCase();
378
+ var data = cache.load(term);
379
+ // recieve the cached data
380
+ if (data && data.length) {
381
+ success(term, data);
382
+ // if an AJAX url has been supplied, try loading the data now
383
+ } else if( (typeof options.url == "string") && (options.url.length > 0) ){
384
+
385
+ var extraParams = {
386
+ timestamp: +new Date()
387
+ };
388
+ $.each(options.extraParams, function(key, param) {
389
+ extraParams[key] = typeof param == "function" ? param() : param;
390
+ });
391
+
392
+ // work out what fields to request from solr. the one
393
+ // required field is the value used for the autocomplete
394
+ // while the resultKey is optional and allows the result
395
+ // to be identified by other means e.g. primary key
396
+ var fl = solrFieldName();
397
+ if (options.resultKey)
398
+ fl = [fl, options.resultKey].join(" ");
399
+
400
+ ajaxSolrManager.store.addByValue('q', makeQuery(lastWord(term)));
401
+ ajaxSolrManager.store.addByValue('indent', 'on');
402
+ ajaxSolrManager.store.addByValue('sort', 'score desc');
403
+ ajaxSolrManager.store.addByValue('fl', fl);
404
+
405
+ $.ajaxSolrCallback = success;
406
+ $.ajaxSolrFieldName = solrFieldName();
407
+
408
+ ajaxSolrManager.doRequest();
409
+
410
+ } else {
411
+ // if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match
412
+ select.emptyList();
413
+ failure(term);
414
+ }
415
+ };
416
+
417
+ function solrFieldName(){
418
+ return fieldName + (suggest ? '_as' : '_ac' )
419
+ };
420
+
421
+ function makeQuery(phrase){
422
+ if (suggest){
423
+ return phrase.trim().split(/\s+/).map(function(word){
424
+ return solrFieldName() + ":" + word;
425
+ }).join (" AND ");
426
+ }else{
427
+ return solrFieldName() + ":" + phrase.trim().split(/\s+/).join("\\ ");
428
+ }
429
+ };
430
+
431
+ function parse(data) {
432
+ var parsed = [];
433
+ var rows = data.split("\n");
434
+ for (var i=0; i < rows.length; i++) {
435
+ var row = $.trim(rows[i]);
436
+ if (row) {
437
+ row = row.split("|");
438
+ parsed[parsed.length] = {
439
+ data: row,
440
+ value: row[0],
441
+ result: options.formatResult && options.formatResult(row, row[0]) || row[0]
442
+ };
443
+ }
444
+ }
445
+ return parsed;
446
+ };
447
+
448
+ function stopLoading() {
449
+ $input.removeClass(options.loadingClass);
450
+ };
451
+
452
+ };
453
+
454
+ $.Autocompleter.defaults = {
455
+ inputClass: "ac_input",
456
+ resultsClass: "ac_results",
457
+ loadingClass: "ac_loading",
458
+ minChars: 1,
459
+ delay: 400,
460
+ matchCase: false,
461
+ matchSubset: true,
462
+ matchContains: false,
463
+ cacheLength: 10,
464
+ max: 100,
465
+ mustMatch: false,
466
+ extraParams: {},
467
+ selectFirst: true,
468
+ formatItem: function(row) { return row[0]; },
469
+ formatMatch: null,
470
+ autoFill: false,
471
+ width: 0,
472
+ multiple: false,
473
+ multipleSeparator: ", ",
474
+ highlight: function(value, term) {
475
+ return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong class='highlight'>$1</strong>");
476
+ },
477
+ scroll: true,
478
+ scrollHeight: 180,
479
+ suggest: false
480
+ };
481
+
482
+ $.Autocompleter.Cache = function(options) {
483
+
484
+ var data = {};
485
+ var length = 0;
486
+
487
+ function matchSubset(s, sub) {
488
+ if (!options.matchCase)
489
+ s = s.toLowerCase();
490
+ var i = s.indexOf(sub);
491
+ if (options.matchContains == "word"){
492
+ i = s.toLowerCase().search("\\b" + sub.toLowerCase());
493
+ }
494
+ if (i == -1) return false;
495
+ return i == 0 || options.matchContains;
496
+ };
497
+
498
+ function add(q, value) {
499
+ if (length > options.cacheLength){
500
+ flush();
501
+ }
502
+ if (!data[q]){
503
+ length++;
504
+ }
505
+ data[q] = value;
506
+ }
507
+
508
+ function populate(){
509
+ if( !options.data ) return false;
510
+ // track the matches
511
+ var stMatchSets = {},
512
+ nullData = 0;
513
+
514
+ // no url was specified, we need to adjust the cache length to make sure it fits the local data store
515
+ if( !options.url ) options.cacheLength = 1;
516
+
517
+ // track all options for minChars = 0
518
+ stMatchSets[""] = [];
519
+
520
+ // loop through the array and create a lookup structure
521
+ for ( var i = 0, ol = options.data.length; i < ol; i++ ) {
522
+ var rawValue = options.data[i];
523
+ // if rawValue is a string, make an array otherwise just reference the array
524
+ rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue;
525
+
526
+ var value = options.formatMatch(rawValue, i+1, options.data.length);
527
+ if ( value === false )
528
+ continue;
529
+
530
+ var firstChar = value.charAt(0).toLowerCase();
531
+ // if no lookup array for this character exists, look it up now
532
+ if( !stMatchSets[firstChar] )
533
+ stMatchSets[firstChar] = [];
534
+
535
+ // if the match is a string
536
+ var row = {
537
+ value: value,
538
+ data: rawValue,
539
+ result: options.formatResult && options.formatResult(rawValue) || value
540
+ };
541
+
542
+ // push the current match into the set list
543
+ stMatchSets[firstChar].push(row);
544
+
545
+ // keep track of minChars zero items
546
+ if ( nullData++ < options.max ) {
547
+ stMatchSets[""].push(row);
548
+ }
549
+ };
550
+
551
+ // add the data items to the cache
552
+ $.each(stMatchSets, function(i, value) {
553
+ // increase the cache size
554
+ options.cacheLength++;
555
+ // add to the cache
556
+ add(i, value);
557
+ });
558
+ }
559
+
560
+ // populate any existing data
561
+ setTimeout(populate, 25);
562
+
563
+ function flush(){
564
+ data = {};
565
+ length = 0;
566
+ }
567
+
568
+ return {
569
+ flush: flush,
570
+ add: add,
571
+ populate: populate,
572
+ load: function(q) {
573
+ if (!options.cacheLength || !length)
574
+ return null;
575
+ /*
576
+ * if dealing w/local data and matchContains than we must make sure
577
+ * to loop through all the data collections looking for matches
578
+ */
579
+ if( !options.url && options.matchContains ){
580
+ // track all matches
581
+ var csub = [];
582
+ // loop through all the data grids for matches
583
+ for( var k in data ){
584
+ // don't search through the stMatchSets[""] (minChars: 0) cache
585
+ // this prevents duplicates
586
+ if( k.length > 0 ){
587
+ var c = data[k];
588
+ $.each(c, function(i, x) {
589
+ // if we've got a match, add it to the array
590
+ if (matchSubset(x.value, q)) {
591
+ csub.push(x);
592
+ }
593
+ });
594
+ }
595
+ }
596
+ return csub;
597
+ } else
598
+ // if the exact item exists, use it
599
+ if (data[q]){
600
+ return data[q];
601
+ } else
602
+ if (options.matchSubset) {
603
+ for (var i = q.length - 1; i >= options.minChars; i--) {
604
+ var c = data[q.substr(0, i)];
605
+ if (c) {
606
+ var csub = [];
607
+ $.each(c, function(i, x) {
608
+ if (matchSubset(x.value, q)) {
609
+ csub[csub.length] = x;
610
+ }
611
+ });
612
+ return csub;
613
+ }
614
+ }
615
+ }
616
+ return null;
617
+ }
618
+ };
619
+ };
620
+
621
+ $.Autocompleter.Select = function (options, input, select, config) {
622
+ var CLASSES = {
623
+ ACTIVE: "ac_over"
624
+ };
625
+
626
+ var listItems,
627
+ active = -1,
628
+ data,
629
+ term = "",
630
+ needsInit = true,
631
+ element,
632
+ list;
633
+
634
+ // Create results
635
+ function init() {
636
+ if (!needsInit)
637
+ return;
638
+ element = $("<div/>")
639
+ .hide()
640
+ .addClass(options.resultsClass)
641
+ .css("position", "absolute")
642
+ .appendTo(document.body);
643
+
644
+ list = $("<ul/>").appendTo(element).mouseover( function(event) {
645
+ if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') {
646
+ active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event));
647
+ $(target(event)).addClass(CLASSES.ACTIVE);
648
+ }
649
+ }).click(function(event) {
650
+ $(target(event)).addClass(CLASSES.ACTIVE);
651
+ select();
652
+ // TODO provide option to avoid setting focus again after selection? useful for cleanup-on-focus
653
+ input.focus();
654
+ return false;
655
+ }).mousedown(function() {
656
+ config.mouseDownOnSelect = true;
657
+ }).mouseup(function() {
658
+ config.mouseDownOnSelect = false;
659
+ });
660
+
661
+ if( options.width > 0 ) {
662
+ element.css("width", options.width);
663
+ list.css("width", options.width);
664
+ }
665
+
666
+ needsInit = false;
667
+ }
668
+
669
+ function target(event) {
670
+ var element = event.target;
671
+ while(element && element.tagName != "LI")
672
+ element = element.parentNode;
673
+ // more fun with IE, sometimes event.target is empty, just ignore it then
674
+ if(!element)
675
+ return [];
676
+ return element;
677
+ }
678
+
679
+ function moveSelect(step) {
680
+ listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE);
681
+ movePosition(step);
682
+ var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE);
683
+ if(options.scroll) {
684
+ var offset = 0;
685
+ listItems.slice(0, active).each(function() {
686
+ offset += this.offsetHeight;
687
+ });
688
+ if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) {
689
+ list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight());
690
+ } else if(offset < list.scrollTop()) {
691
+ list.scrollTop(offset);
692
+ }
693
+ }
694
+ };
695
+
696
+ function movePosition(step) {
697
+ active += step;
698
+ if (active < 0) {
699
+ active = listItems.size() - 1;
700
+ } else if (active >= listItems.size()) {
701
+ active = 0;
702
+ }
703
+ }
704
+
705
+ function limitNumberOfItems(available) {
706
+ return options.max && options.max < available
707
+ ? options.max
708
+ : available;
709
+ }
710
+
711
+ function fillList() {
712
+ list.empty();
713
+ var max = limitNumberOfItems(data.length);
714
+ for (var i=0; i < max; i++) {
715
+ if (!data[i]) { continue; }
716
+ var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term);
717
+ if ( formatted === false ) { continue; }
718
+ str = options.highlight(formatted, $(input).val());
719
+ // /* DEBUG */ console.log(input, $(input).val());
720
+ var li = $("<li/>").html( str ).addClass(i % 2 == 0 ? "ac_even" : "ac_odd").appendTo(list)[0];
721
+ $.data(li, "ac_data", data[i]);
722
+ }
723
+ listItems = list.find("li");
724
+ if ( options.selectFirst ) {
725
+ listItems.slice(0, 1).addClass(CLASSES.ACTIVE);
726
+ active = 0;
727
+ }
728
+ // apply bgiframe if available
729
+ if ( $.fn.bgiframe )
730
+ list.bgiframe();
731
+ }
732
+
733
+ return {
734
+ display: function(d, q) {
735
+ init();
736
+ data = d;
737
+ term = q;
738
+ fillList();
739
+ },
740
+ next: function() {
741
+ moveSelect(1);
742
+ },
743
+ prev: function() {
744
+ moveSelect(-1);
745
+ },
746
+ pageUp: function() {
747
+ if (active != 0 && active - 8 < 0) {
748
+ moveSelect( -active );
749
+ } else {
750
+ moveSelect(-8);
751
+ }
752
+ },
753
+ pageDown: function() {
754
+ if (active != listItems.size() - 1 && active + 8 > listItems.size()) {
755
+ moveSelect( listItems.size() - 1 - active );
756
+ } else {
757
+ moveSelect(8);
758
+ }
759
+ },
760
+ hide: function() {
761
+ element && element.hide();
762
+ listItems && listItems.removeClass(CLASSES.ACTIVE);
763
+ active = -1;
764
+ },
765
+ visible : function() {
766
+ return element && element.is(":visible");
767
+ },
768
+ current: function() {
769
+ return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]);
770
+ },
771
+ show: function() {
772
+ var offset = $(input).offset();
773
+ element.css({
774
+ width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(),
775
+ top: offset.top + input.offsetHeight,
776
+ left: offset.left
777
+ }).show();
778
+ if(options.scroll) {
779
+ list.scrollTop(0);
780
+ list.css({
781
+ maxHeight: options.scrollHeight,
782
+ overflow: 'auto'
783
+ });
784
+
785
+ if($.browser.msie && typeof document.body.style.maxHeight === "undefined") {
786
+ var listHeight = 0;
787
+ listItems.each(function() {
788
+ listHeight += this.offsetHeight;
789
+ });
790
+ var scrollbarsVisible = listHeight > options.scrollHeight;
791
+ list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight );
792
+ if (!scrollbarsVisible) {
793
+ // IE doesn't recalculate width when scrollbar disappears
794
+ listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) );
795
+ }
796
+ }
797
+
798
+ }
799
+ },
800
+ selected: function() {
801
+ var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);
802
+ return selected && selected.length && $.data(selected[0], "ac_data");
803
+ },
804
+ emptyList: function (){
805
+ list && list.empty();
806
+ },
807
+ unbind: function() {
808
+ element && element.remove();
809
+ }
810
+ };
811
+ };
812
+
813
+ $.fn.selection = function(start, end) {
814
+ if (start !== undefined) {
815
+ return this.each(function() {
816
+ if( this.createTextRange ){
817
+ var selRange = this.createTextRange();
818
+ if (end === undefined || start == end) {
819
+ selRange.move("character", start);
820
+ selRange.select();
821
+ } else {
822
+ selRange.collapse(true);
823
+ selRange.moveStart("character", start);
824
+ selRange.moveEnd("character", end);
825
+ selRange.select();
826
+ }
827
+ } else if( this.setSelectionRange ){
828
+ this.setSelectionRange(start, end);
829
+ } else if( this.selectionStart ){
830
+ this.selectionStart = start;
831
+ this.selectionEnd = end;
832
+ }
833
+ });
834
+ }
835
+ var field = this[0];
836
+ if ( field.createTextRange ) {
837
+ var range = document.selection.createRange(),
838
+ orig = field.value,
839
+ teststring = "<->",
840
+ textLength = range.text.length;
841
+ range.text = teststring;
842
+ var caretAt = field.value.indexOf(teststring);
843
+ field.value = orig;
844
+ this.selection(caretAt, caretAt + textLength);
845
+ return {
846
+ start: caretAt,
847
+ end: caretAt + textLength
848
+ }
849
+ } else if( field.selectionStart !== undefined ){
850
+ return {
851
+ start: field.selectionStart,
852
+ end: field.selectionEnd
853
+ }
854
+ }
855
+ };
856
+
857
+ })(jQuery);
858
+
859
+ //// callback is not encapsulated in any objects
860
+ //// to avoid the (.) anomaly in the url
861
+ var applyAjaxSolrCallback = function(result){
862
+ var items = "";
863
+ $(result.response.docs).each(function(index, doc){
864
+ items = items + doc[$.ajaxSolrFieldName] + "\n";
865
+ });
866
+ $.ajaxSolrCallback(result.responseHeader.params.q, items);
867
+ };