source: gs3-extensions/solr/trunk/src/conf/velocity/jquery.autocomplete.js@ 29135

Last change on this file since 29135 was 29135, checked in by ak19, 10 years ago

Part of port from lucene3.3.0 to lucene4.7.2. Solr related. conf and lib folders for solr4.7.2.

File size: 19.5 KB
Line 
1/*
2 * Autocomplete - jQuery plugin 1.1pre
3 *
4 * Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer
5 *
6 * Dual licensed under the MIT and GPL licenses:
7 * http://www.opensource.org/licenses/mit-license.php
8 * http://www.gnu.org/licenses/gpl.html
9 *
10 * Revision: $Id: jquery.autocomplete.js 5785 2008-07-12 10:37:33Z joern.zaefferer $
11 *
12 */
13
14;(function($) {
15
16$.fn.extend({
17 autocomplete: function(urlOrData, options) {
18 var isUrl = typeof urlOrData == "string";
19 options = $.extend({}, $.Autocompleter.defaults, {
20 url: isUrl ? urlOrData : null,
21 data: isUrl ? null : urlOrData,
22 delay: isUrl ? $.Autocompleter.defaults.delay : 10,
23 max: options && !options.scroll ? 10 : 150
24 }, options);
25
26 // if highlight is set to false, replace it with a do-nothing function
27 options.highlight = options.highlight || function(value) { return value; };
28
29 // if the formatMatch option is not specified, then use formatItem for backwards compatibility
30 options.formatMatch = options.formatMatch || options.formatItem;
31
32 return this.each(function() {
33 new $.Autocompleter(this, options);
34 });
35 },
36 result: function(handler) {
37 return this.bind("result", handler);
38 },
39 search: function(handler) {
40 return this.trigger("search", [handler]);
41 },
42 flushCache: function() {
43 return this.trigger("flushCache");
44 },
45 setOptions: function(options){
46 return this.trigger("setOptions", [options]);
47 },
48 unautocomplete: function() {
49 return this.trigger("unautocomplete");
50 }
51});
52
53$.Autocompleter = function(input, options) {
54
55 var KEY = {
56 UP: 38,
57 DOWN: 40,
58 DEL: 46,
59 TAB: 9,
60 RETURN: 13,
61 ESC: 27,
62 COMMA: 188,
63 PAGEUP: 33,
64 PAGEDOWN: 34,
65 BACKSPACE: 8
66 };
67
68 // Create $ object for input element
69 var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass);
70
71 var timeout;
72 var previousValue = "";
73 var cache = $.Autocompleter.Cache(options);
74 var hasFocus = 0;
75 var lastKeyPressCode;
76 var config = {
77 mouseDownOnSelect: false
78 };
79 var select = $.Autocompleter.Select(options, input, selectCurrent, config);
80
81 var blockSubmit;
82
83 // prevent form submit in opera when selecting with return key
84 $.browser.opera && $(input.form).bind("submit.autocomplete", function() {
85 if (blockSubmit) {
86 blockSubmit = false;
87 return false;
88 }
89 });
90
91 // only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all
92 $input.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function(event) {
93 // track last key pressed
94 lastKeyPressCode = event.keyCode;
95 switch(event.keyCode) {
96
97 case KEY.UP:
98 event.preventDefault();
99 if ( select.visible() ) {
100 select.prev();
101 } else {
102 onChange(0, true);
103 }
104 break;
105
106 case KEY.DOWN:
107 event.preventDefault();
108 if ( select.visible() ) {
109 select.next();
110 } else {
111 onChange(0, true);
112 }
113 break;
114
115 case KEY.PAGEUP:
116 event.preventDefault();
117 if ( select.visible() ) {
118 select.pageUp();
119 } else {
120 onChange(0, true);
121 }
122 break;
123
124 case KEY.PAGEDOWN:
125 event.preventDefault();
126 if ( select.visible() ) {
127 select.pageDown();
128 } else {
129 onChange(0, true);
130 }
131 break;
132
133 // matches also semicolon
134 case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA:
135 case KEY.TAB:
136 case KEY.RETURN:
137 if( selectCurrent() ) {
138 // stop default to prevent a form submit, Opera needs special handling
139 event.preventDefault();
140 blockSubmit = true;
141 return false;
142 }
143 break;
144
145 case KEY.ESC:
146 select.hide();
147 break;
148
149 default:
150 clearTimeout(timeout);
151 timeout = setTimeout(onChange, options.delay);
152 break;
153 }
154 }).focus(function(){
155 // track whether the field has focus, we shouldn't process any
156 // results if the field no longer has focus
157 hasFocus++;
158 }).blur(function() {
159 hasFocus = 0;
160 if (!config.mouseDownOnSelect) {
161 hideResults();
162 }
163 }).click(function() {
164 // show select when clicking in a focused field
165 if ( hasFocus++ > 1 && !select.visible() ) {
166 onChange(0, true);
167 }
168 }).bind("search", function() {
169 // TODO why not just specifying both arguments?
170 var fn = (arguments.length > 1) ? arguments[1] : null;
171 function findValueCallback(q, data) {
172 var result;
173 if( data && data.length ) {
174 for (var i=0; i < data.length; i++) {
175 if( data[i].result.toLowerCase() == q.toLowerCase() ) {
176 result = data[i];
177 break;
178 }
179 }
180 }
181 if( typeof fn == "function" ) fn(result);
182 else $input.trigger("result", result && [result.data, result.value]);
183 }
184 $.each(trimWords($input.val()), function(i, value) {
185 request(value, findValueCallback, findValueCallback);
186 });
187 }).bind("flushCache", function() {
188 cache.flush();
189 }).bind("setOptions", function() {
190 $.extend(options, arguments[1]);
191 // if we've updated the data, repopulate
192 if ( "data" in arguments[1] )
193 cache.populate();
194 }).bind("unautocomplete", function() {
195 select.unbind();
196 $input.unbind();
197 $(input.form).unbind(".autocomplete");
198 });
199
200
201 function selectCurrent() {
202 var selected = select.selected();
203 if( !selected )
204 return false;
205
206 var v = selected.result;
207 previousValue = v;
208
209 if ( options.multiple ) {
210 var words = trimWords($input.val());
211 if ( words.length > 1 ) {
212 v = words.slice(0, words.length - 1).join( options.multipleSeparator ) + options.multipleSeparator + v;
213 }
214 v += options.multipleSeparator;
215 }
216
217 $input.val(v);
218 hideResultsNow();
219 $input.trigger("result", [selected.data, selected.value]);
220 return true;
221 }
222
223 function onChange(crap, skipPrevCheck) {
224 if( lastKeyPressCode == KEY.DEL ) {
225 select.hide();
226 return;
227 }
228
229 var currentValue = $input.val();
230
231 if ( !skipPrevCheck && currentValue == previousValue )
232 return;
233
234 previousValue = currentValue;
235
236 currentValue = lastWord(currentValue);
237 if ( currentValue.length >= options.minChars) {
238 $input.addClass(options.loadingClass);
239 if (!options.matchCase)
240 currentValue = currentValue.toLowerCase();
241 request(currentValue, receiveData, hideResultsNow);
242 } else {
243 stopLoading();
244 select.hide();
245 }
246 };
247
248 function trimWords(value) {
249 if ( !value ) {
250 return [""];
251 }
252 var words = value.split( options.multipleSeparator );
253 var result = [];
254 $.each(words, function(i, value) {
255 if ( $.trim(value) )
256 result[i] = $.trim(value);
257 });
258 return result;
259 }
260
261 function lastWord(value) {
262 if ( !options.multiple )
263 return value;
264 var words = trimWords(value);
265 return words[words.length - 1];
266 }
267
268 // fills in the input box w/the first match (assumed to be the best match)
269 // q: the term entered
270 // sValue: the first matching result
271 function autoFill(q, sValue){
272 // autofill in the complete box w/the first match as long as the user hasn't entered in more data
273 // if the last user key pressed was backspace, don't autofill
274 if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) {
275 // fill in the value (keep the case the user has typed)
276 $input.val($input.val() + sValue.substring(lastWord(previousValue).length));
277 // select the portion of the value not typed by the user (so the next character will erase)
278 $.Autocompleter.Selection(input, previousValue.length, previousValue.length + sValue.length);
279 }
280 };
281
282 function hideResults() {
283 clearTimeout(timeout);
284 timeout = setTimeout(hideResultsNow, 200);
285 };
286
287 function hideResultsNow() {
288 var wasVisible = select.visible();
289 select.hide();
290 clearTimeout(timeout);
291 stopLoading();
292 if (options.mustMatch) {
293 // call search and run callback
294 $input.search(
295 function (result){
296 // if no value found, clear the input box
297 if( !result ) {
298 if (options.multiple) {
299 var words = trimWords($input.val()).slice(0, -1);
300 $input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") );
301 }
302 else
303 $input.val( "" );
304 }
305 }
306 );
307 }
308 if (wasVisible)
309 // position cursor at end of input field
310 $.Autocompleter.Selection(input, input.value.length, input.value.length);
311 };
312
313 function receiveData(q, data) {
314 if ( data && data.length && hasFocus ) {
315 stopLoading();
316 select.display(data, q);
317 autoFill(q, data[0].value);
318 select.show();
319 } else {
320 hideResultsNow();
321 }
322 };
323
324 function request(term, success, failure) {
325 if (!options.matchCase)
326 term = term.toLowerCase();
327 var data = cache.load(term);
328 data = null; // Avoid buggy cache and go to Solr every time
329 // recieve the cached data
330 if (data && data.length) {
331 success(term, data);
332 // if an AJAX url has been supplied, try loading the data now
333 } else if( (typeof options.url == "string") && (options.url.length > 0) ){
334
335 var extraParams = {
336 timestamp: +new Date()
337 };
338 $.each(options.extraParams, function(key, param) {
339 extraParams[key] = typeof param == "function" ? param() : param;
340 });
341
342 $.ajax({
343 // try to leverage ajaxQueue plugin to abort previous requests
344 mode: "abort",
345 // limit abortion to this input
346 port: "autocomplete" + input.name,
347 dataType: options.dataType,
348 url: options.url,
349 data: $.extend({
350 q: lastWord(term),
351 limit: options.max
352 }, extraParams),
353 success: function(data) {
354 var parsed = options.parse && options.parse(data) || parse(data);
355 cache.add(term, parsed);
356 success(term, parsed);
357 }
358 });
359 } else {
360 // if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match
361 select.emptyList();
362 failure(term);
363 }
364 };
365
366 function parse(data) {
367 var parsed = [];
368 var rows = data.split("\n");
369 for (var i=0; i < rows.length; i++) {
370 var row = $.trim(rows[i]);
371 if (row) {
372 row = row.split("|");
373 parsed[parsed.length] = {
374 data: row,
375 value: row[0],
376 result: options.formatResult && options.formatResult(row, row[0]) || row[0]
377 };
378 }
379 }
380 return parsed;
381 };
382
383 function stopLoading() {
384 $input.removeClass(options.loadingClass);
385 };
386
387};
388
389$.Autocompleter.defaults = {
390 inputClass: "ac_input",
391 resultsClass: "ac_results",
392 loadingClass: "ac_loading",
393 minChars: 1,
394 delay: 400,
395 matchCase: false,
396 matchSubset: true,
397 matchContains: false,
398 cacheLength: 10,
399 max: 100,
400 mustMatch: false,
401 extraParams: {},
402 selectFirst: false,
403 formatItem: function(row) { return row[0]; },
404 formatMatch: null,
405 autoFill: false,
406 width: 0,
407 multiple: false,
408 multipleSeparator: ", ",
409 highlight: function(value, term) {
410 return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>");
411 },
412 scroll: true,
413 scrollHeight: 180
414};
415
416$.Autocompleter.Cache = function(options) {
417
418 var data = {};
419 var length = 0;
420
421 function matchSubset(s, sub) {
422 if (!options.matchCase)
423 s = s.toLowerCase();
424 var i = s.indexOf(sub);
425 if (options.matchContains == "word"){
426 i = s.toLowerCase().search("\\b" + sub.toLowerCase());
427 }
428 if (i == -1) return false;
429 return i == 0 || options.matchContains;
430 };
431
432 function add(q, value) {
433 if (length > options.cacheLength){
434 flush();
435 }
436 if (!data[q]){
437 length++;
438 }
439 data[q] = value;
440 }
441
442 function populate(){
443 if( !options.data ) return false;
444 // track the matches
445 var stMatchSets = {},
446 nullData = 0;
447
448 // no url was specified, we need to adjust the cache length to make sure it fits the local data store
449 if( !options.url ) options.cacheLength = 1;
450
451 // track all options for minChars = 0
452 stMatchSets[""] = [];
453
454 // loop through the array and create a lookup structure
455 for ( var i = 0, ol = options.data.length; i < ol; i++ ) {
456 var rawValue = options.data[i];
457 // if rawValue is a string, make an array otherwise just reference the array
458 rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue;
459
460 var value = options.formatMatch(rawValue, i+1, options.data.length);
461 if ( value === false )
462 continue;
463
464 var firstChar = value.charAt(0).toLowerCase();
465 // if no lookup array for this character exists, look it up now
466 if( !stMatchSets[firstChar] )
467 stMatchSets[firstChar] = [];
468
469 // if the match is a string
470 var row = {
471 value: value,
472 data: rawValue,
473 result: options.formatResult && options.formatResult(rawValue) || value
474 };
475
476 // push the current match into the set list
477 stMatchSets[firstChar].push(row);
478
479 // keep track of minChars zero items
480 if ( nullData++ < options.max ) {
481 stMatchSets[""].push(row);
482 }
483 };
484
485 // add the data items to the cache
486 $.each(stMatchSets, function(i, value) {
487 // increase the cache size
488 options.cacheLength++;
489 // add to the cache
490 add(i, value);
491 });
492 }
493
494 // populate any existing data
495 setTimeout(populate, 25);
496
497 function flush(){
498 data = {};
499 length = 0;
500 }
501
502 return {
503 flush: flush,
504 add: add,
505 populate: populate,
506 load: function(q) {
507 if (!options.cacheLength || !length)
508 return null;
509 /*
510 * if dealing w/local data and matchContains than we must make sure
511 * to loop through all the data collections looking for matches
512 */
513 if( !options.url && options.matchContains ){
514 // track all matches
515 var csub = [];
516 // loop through all the data grids for matches
517 for( var k in data ){
518 // don't search through the stMatchSets[""] (minChars: 0) cache
519 // this prevents duplicates
520 if( k.length > 0 ){
521 var c = data[k];
522 $.each(c, function(i, x) {
523 // if we've got a match, add it to the array
524 if (matchSubset(x.value, q)) {
525 csub.push(x);
526 }
527 });
528 }
529 }
530 return csub;
531 } else
532 // if the exact item exists, use it
533 if (data[q]){
534 return data[q];
535 } else
536 if (options.matchSubset) {
537 for (var i = q.length - 1; i >= options.minChars; i--) {
538 var c = data[q.substr(0, i)];
539 if (c) {
540 var csub = [];
541 $.each(c, function(i, x) {
542 if (matchSubset(x.value, q)) {
543 csub[csub.length] = x;
544 }
545 });
546 return csub;
547 }
548 }
549 }
550 return null;
551 }
552 };
553};
554
555$.Autocompleter.Select = function (options, input, select, config) {
556 var CLASSES = {
557 ACTIVE: "ac_over"
558 };
559
560 var listItems,
561 active = -1,
562 data,
563 term = "",
564 needsInit = true,
565 element,
566 list;
567
568 // Create results
569 function init() {
570 if (!needsInit)
571 return;
572 element = $("<div/>")
573 .hide()
574 .addClass(options.resultsClass)
575 .css("position", "absolute")
576 .appendTo(document.body);
577
578 list = $("<ul/>").appendTo(element).mouseover( function(event) {
579 if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') {
580 active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event));
581 $(target(event)).addClass(CLASSES.ACTIVE);
582 }
583 }).click(function(event) {
584 $(target(event)).addClass(CLASSES.ACTIVE);
585 select();
586 // TODO provide option to avoid setting focus again after selection? useful for cleanup-on-focus
587 input.focus();
588 return false;
589 }).mousedown(function() {
590 config.mouseDownOnSelect = true;
591 }).mouseup(function() {
592 config.mouseDownOnSelect = false;
593 });
594
595 if( options.width > 0 )
596 element.css("width", options.width);
597
598 needsInit = false;
599 }
600
601 function target(event) {
602 var element = event.target;
603 while(element && element.tagName != "LI")
604 element = element.parentNode;
605 // more fun with IE, sometimes event.target is empty, just ignore it then
606 if(!element)
607 return [];
608 return element;
609 }
610
611 function moveSelect(step) {
612 listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE);
613 movePosition(step);
614 var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE);
615 if(options.scroll) {
616 var offset = 0;
617 listItems.slice(0, active).each(function() {
618 offset += this.offsetHeight;
619 });
620 if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) {
621 list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight());
622 } else if(offset < list.scrollTop()) {
623 list.scrollTop(offset);
624 }
625 }
626 };
627
628 function movePosition(step) {
629 active += step;
630 if (active < 0) {
631 active = listItems.size() - 1;
632 } else if (active >= listItems.size()) {
633 active = 0;
634 }
635 }
636
637 function limitNumberOfItems(available) {
638 return options.max && options.max < available
639 ? options.max
640 : available;
641 }
642
643 function fillList() {
644 list.empty();
645 var max = limitNumberOfItems(data.length);
646 for (var i=0; i < max; i++) {
647 if (!data[i])
648 continue;
649 var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term);
650 if ( formatted === false )
651 continue;
652 var li = $("<li/>").html( options.highlight(formatted, term) ).addClass(i%2 == 0 ? "ac_even" : "ac_odd").appendTo(list)[0];
653 $.data(li, "ac_data", data[i]);
654 }
655 listItems = list.find("li");
656 if ( options.selectFirst ) {
657 listItems.slice(0, 1).addClass(CLASSES.ACTIVE);
658 active = 0;
659 }
660 // apply bgiframe if available
661 if ( $.fn.bgiframe )
662 list.bgiframe();
663 }
664
665 return {
666 display: function(d, q) {
667 init();
668 data = d;
669 term = q;
670 fillList();
671 },
672 next: function() {
673 moveSelect(1);
674 },
675 prev: function() {
676 moveSelect(-1);
677 },
678 pageUp: function() {
679 if (active != 0 && active - 8 < 0) {
680 moveSelect( -active );
681 } else {
682 moveSelect(-8);
683 }
684 },
685 pageDown: function() {
686 if (active != listItems.size() - 1 && active + 8 > listItems.size()) {
687 moveSelect( listItems.size() - 1 - active );
688 } else {
689 moveSelect(8);
690 }
691 },
692 hide: function() {
693 element && element.hide();
694 listItems && listItems.removeClass(CLASSES.ACTIVE);
695 active = -1;
696 },
697 visible : function() {
698 return element && element.is(":visible");
699 },
700 current: function() {
701 return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]);
702 },
703 show: function() {
704 var offset = $(input).offset();
705 element.css({
706 width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(),
707 top: offset.top + input.offsetHeight,
708 left: offset.left
709 }).show();
710 if(options.scroll) {
711 list.scrollTop(0);
712 list.css({
713 maxHeight: options.scrollHeight,
714 overflow: 'auto'
715 });
716
717 if($.browser.msie && typeof document.body.style.maxHeight === "undefined") {
718 var listHeight = 0;
719 listItems.each(function() {
720 listHeight += this.offsetHeight;
721 });
722 var scrollbarsVisible = listHeight > options.scrollHeight;
723 list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight );
724 if (!scrollbarsVisible) {
725 // IE doesn't recalculate width when scrollbar disappears
726 listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) );
727 }
728 }
729
730 }
731 },
732 selected: function() {
733 var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);
734 return selected && selected.length && $.data(selected[0], "ac_data");
735 },
736 emptyList: function (){
737 list && list.empty();
738 },
739 unbind: function() {
740 element && element.remove();
741 }
742 };
743};
744
745$.Autocompleter.Selection = function(field, start, end) {
746 if( field.createTextRange ){
747 var selRange = field.createTextRange();
748 selRange.collapse(true);
749 selRange.moveStart("character", start);
750 selRange.moveEnd("character", end);
751 selRange.select();
752 } else if( field.setSelectionRange ){
753 field.setSelectionRange(start, end);
754 } else {
755 if( field.selectionStart ){
756 field.selectionStart = start;
757 field.selectionEnd = end;
758 }
759 }
760 field.focus();
761};
762
763})(jQuery);
Note: See TracBrowser for help on using the repository browser.