Dotclear

source: admin/js/jquery/jquery.autocomplete.js @ 1035:e5c9bc267f69

Revision 1035:e5c9bc267f69, 20.8 KB checked in by franck <carnet.franck.paul@…>, 13 years ago (diff)

Add user id completion in posts actions. Fixes #1022. The user's list is limited to the 100 biggest contributors (in nb of posts), but this should be enhanced with some Ajax if possible.

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

Sites map