root/galaxy-central/static/scripts/jquery.autocomplete.js @ 2

リビジョン 2, 27.1 KB (コミッタ: hatakeyama, 14 年 前)

import galaxy-central

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