[IMP] website editor: change link editor modal layout
[odoo/odoo.git] / addons / web / static / lib / select2 / select2.js
1 /*
2  Copyright 2012 Igor Vaynberg
3
4  Version: @@ver@@ Timestamp: @@timestamp@@
5
6  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in
7  compliance with the License. You may obtain a copy of the License in the LICENSE file, or at:
8
9  http://www.apache.org/licenses/LICENSE-2.0
10
11  Unless required by applicable law or agreed to in writing, software distributed under the License is
12  distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  See the License for the specific language governing permissions and limitations under the License.
14  */
15  (function ($) {
16         if(typeof $.fn.each2 == "undefined"){
17                 $.fn.extend({
18                         /*
19                         * 4-10 times faster .each replacement
20                         * use it carefully, as it overrides jQuery context of element on each iteration
21                         */
22                         each2 : function (c) {
23                                 var j = $([0]), i = -1, l = this.length;
24                                 while (
25                                         ++i < l
26                                         && (j.context = j[0] = this[i])
27                                         && c.call(j[0], i, j) !== false //"this"=DOM, i=index, j=jQuery object
28                                 );
29                                 return this;
30                         }
31                 });
32         }
33 })(jQuery);
34
35 (function ($, undefined) {
36     "use strict";
37     /*global document, window, jQuery, console */
38
39     if (window.Select2 !== undefined) {
40         return;
41     }
42
43     var KEY, AbstractSelect2, SingleSelect2, MultiSelect2, nextUid, sizer;
44
45     KEY = {
46         TAB: 9,
47         ENTER: 13,
48         ESC: 27,
49         SPACE: 32,
50         LEFT: 37,
51         UP: 38,
52         RIGHT: 39,
53         DOWN: 40,
54         SHIFT: 16,
55         CTRL: 17,
56         ALT: 18,
57         PAGE_UP: 33,
58         PAGE_DOWN: 34,
59         HOME: 36,
60         END: 35,
61         BACKSPACE: 8,
62         DELETE: 46,
63         isArrow: function (k) {
64             k = k.which ? k.which : k;
65             switch (k) {
66             case KEY.LEFT:
67             case KEY.RIGHT:
68             case KEY.UP:
69             case KEY.DOWN:
70                 return true;
71             }
72             return false;
73         },
74         isControl: function (e) {
75             var k = e.which;
76             switch (k) {
77             case KEY.SHIFT:
78             case KEY.CTRL:
79             case KEY.ALT:
80                 return true;
81             }
82
83             if (e.metaKey) return true;
84
85             return false;
86         },
87         isFunctionKey: function (k) {
88             k = k.which ? k.which : k;
89             return k >= 112 && k <= 123;
90         }
91     };
92
93     nextUid=(function() { var counter=1; return function() { return counter++; }; }());
94
95     function indexOf(value, array) {
96         var i = 0, l = array.length, v;
97
98         if (typeof value === "undefined") {
99           return -1;
100         }
101
102         if (value.constructor === String) {
103             for (; i < l; i = i + 1) if (value.localeCompare(array[i]) === 0) return i;
104         } else {
105             for (; i < l; i = i + 1) {
106                 v = array[i];
107                 if (v.constructor === String) {
108                     if (v.localeCompare(value) === 0) return i;
109                 } else {
110                     if (v === value) return i;
111                 }
112             }
113         }
114         return -1;
115     }
116
117     /**
118      * Compares equality of a and b taking into account that a and b may be strings, in which case localeCompare is used
119      * @param a
120      * @param b
121      */
122     function equal(a, b) {
123         if (a === b) return true;
124         if (a === undefined || b === undefined) return false;
125         if (a === null || b === null) return false;
126         if (a.constructor === String) return a.localeCompare(b) === 0;
127         if (b.constructor === String) return b.localeCompare(a) === 0;
128         return false;
129     }
130
131     /**
132      * Splits the string into an array of values, trimming each value. An empty array is returned for nulls or empty
133      * strings
134      * @param string
135      * @param separator
136      */
137     function splitVal(string, separator) {
138         var val, i, l;
139         if (string === null || string.length < 1) return [];
140         val = string.split(separator);
141         for (i = 0, l = val.length; i < l; i = i + 1) val[i] = $.trim(val[i]);
142         return val;
143     }
144
145     function getSideBorderPadding(element) {
146         return element.outerWidth() - element.width();
147     }
148
149     function installKeyUpChangeEvent(element) {
150         var key="keyup-change-value";
151         element.bind("keydown", function () {
152             if ($.data(element, key) === undefined) {
153                 $.data(element, key, element.val());
154             }
155         });
156         element.bind("keyup", function () {
157             var val= $.data(element, key);
158             if (val !== undefined && element.val() !== val) {
159                 $.removeData(element, key);
160                 element.trigger("keyup-change");
161             }
162         });
163     }
164
165     $(document).delegate("*", "mousemove", function (e) {
166         $.data(document, "select2-lastpos", {x: e.pageX, y: e.pageY});
167     });
168
169     /**
170      * filters mouse events so an event is fired only if the mouse moved.
171      *
172      * filters out mouse events that occur when mouse is stationary but
173      * the elements under the pointer are scrolled.
174      */
175     function installFilteredMouseMove(element) {
176             element.bind("mousemove", function (e) {
177             var lastpos = $.data(document, "select2-lastpos");
178             if (lastpos === undefined || lastpos.x !== e.pageX || lastpos.y !== e.pageY) {
179                 $(e.target).trigger("mousemove-filtered", e);
180             }
181         });
182     }
183
184     /**
185      * Debounces a function. Returns a function that calls the original fn function only if no invocations have been made
186      * within the last quietMillis milliseconds.
187      *
188      * @param quietMillis number of milliseconds to wait before invoking fn
189      * @param fn function to be debounced
190      * @param ctx object to be used as this reference within fn
191      * @return debounced version of fn
192      */
193     function debounce(quietMillis, fn, ctx) {
194         ctx = ctx || undefined;
195         var timeout;
196         return function () {
197             var args = arguments;
198             window.clearTimeout(timeout);
199             timeout = window.setTimeout(function() {
200                 fn.apply(ctx, args);
201             }, quietMillis);
202         };
203     }
204
205     /**
206      * A simple implementation of a thunk
207      * @param formula function used to lazily initialize the thunk
208      * @return {Function}
209      */
210     function thunk(formula) {
211         var evaluated = false,
212             value;
213         return function() {
214             if (evaluated === false) { value = formula(); evaluated = true; }
215             return value;
216         };
217     };
218
219     function installDebouncedScroll(threshold, element) {
220         var notify = debounce(threshold, function (e) { element.trigger("scroll-debounced", e);});
221         element.bind("scroll", function (e) {
222             if (indexOf(e.target, element.get()) >= 0) notify(e);
223         });
224     }
225
226     function killEvent(event) {
227         event.preventDefault();
228         event.stopPropagation();
229     }
230
231     function measureTextWidth(e) {
232         if (!sizer){
233                 var style = e[0].currentStyle || window.getComputedStyle(e[0], null);
234                 sizer = $("<div></div>").css({
235                     position: "absolute",
236                     left: "-10000px",
237                     top: "-10000px",
238                     display: "none",
239                     fontSize: style.fontSize,
240                     fontFamily: style.fontFamily,
241                     fontStyle: style.fontStyle,
242                     fontWeight: style.fontWeight,
243                     letterSpacing: style.letterSpacing,
244                     textTransform: style.textTransform,
245                     whiteSpace: "nowrap"
246                 });
247                 $("body").append(sizer);
248         }
249         sizer.text(e.val());
250         return sizer.width();
251     }
252
253     function markMatch(text, term, markup) {
254         var match=text.toUpperCase().indexOf(term.toUpperCase()),
255             tl=term.length;
256
257         if (match<0) {
258             markup.push(text);
259             return;
260         }
261
262         markup.push(text.substring(0, match));
263         markup.push("<span class='select2-match'>");
264         markup.push(text.substring(match, match + tl));
265         markup.push("</span>");
266         markup.push(text.substring(match + tl, text.length));
267     }
268
269     /**
270      * Produces an ajax-based query function
271      *
272      * @param options object containing configuration paramters
273      * @param options.transport function that will be used to execute the ajax request. must be compatible with parameters supported by $.ajax
274      * @param options.url url for the data
275      * @param options.data a function(searchTerm, pageNumber, context) that should return an object containing query string parameters for the above url.
276      * @param options.dataType request data type: ajax, jsonp, other datatatypes supported by jQuery's $.ajax function or the transport function if specified
277      * @param options.traditional a boolean flag that should be true if you wish to use the traditional style of param serialization for the ajax request
278      * @param options.quietMillis (optional) milliseconds to wait before making the ajaxRequest, helps debounce the ajax function if invoked too often
279      * @param options.results a function(remoteData, pageNumber) that converts data returned form the remote request to the format expected by Select2.
280      *      The expected format is an object containing the following keys:
281      *      results array of objects that will be used as choices
282      *      more (optional) boolean indicating whether there are more results available
283      *      Example: {results:[{id:1, text:'Red'},{id:2, text:'Blue'}], more:true}
284      */
285     function ajax(options) {
286         var timeout, // current scheduled but not yet executed request
287             requestSequence = 0, // sequence used to drop out-of-order responses
288             handler = null,
289             quietMillis = options.quietMillis || 100;
290
291         return function (query) {
292             window.clearTimeout(timeout);
293             timeout = window.setTimeout(function () {
294                 requestSequence += 1; // increment the sequence
295                 var requestNumber = requestSequence, // this request's sequence number
296                     data = options.data, // ajax data function
297                     transport = options.transport || $.ajax,
298                     traditional = options.traditional || false,
299                     type = options.type || 'GET'; // set type of request (GET or POST)
300
301                 data = data.call(this, query.term, query.page, query.context);
302
303                 if( null !== handler) { handler.abort(); }
304
305                 handler = transport.call(null, {
306                     url: options.url,
307                     dataType: options.dataType,
308                     data: data,
309                     type: type,
310                     traditional: traditional,
311                     success: function (data) {
312                         if (requestNumber < requestSequence) {
313                             return;
314                         }
315                         // TODO 3.0 - replace query.page with query so users have access to term, page, etc.
316                         var results = options.results(data, query.page);
317                         query.callback(results);
318                     }
319                 });
320             }, quietMillis);
321         };
322     }
323
324     /**
325      * Produces a query function that works with a local array
326      *
327      * @param options object containing configuration parameters. The options parameter can either be an array or an
328      * object.
329      *
330      * If the array form is used it is assumed that it contains objects with 'id' and 'text' keys.
331      *
332      * If the object form is used ti is assumed that it contains 'data' and 'text' keys. The 'data' key should contain
333      * an array of objects that will be used as choices. These objects must contain at least an 'id' key. The 'text'
334      * key can either be a String in which case it is expected that each element in the 'data' array has a key with the
335      * value of 'text' which will be used to match choices. Alternatively, text can be a function(item) that can extract
336      * the text.
337      */
338     function local(options) {
339         var data = options, // data elements
340             dataText,
341             text = function (item) { return ""+item.text; }; // function used to retrieve the text portion of a data item that is matched against the search
342
343         if (!$.isArray(data)) {
344             text = data.text;
345             // if text is not a function we assume it to be a key name
346             if (!$.isFunction(text)) {
347               dataText = data.text; // we need to store this in a separate variable because in the next step data gets reset and data.text is no longer available
348               text = function (item) { return item[dataText]; };
349             }
350             data = data.results;
351         }
352
353         return function (query) {
354             var t = query.term, filtered = { results: [] }, process;
355             if (t === "") {
356                 query.callback({results: data});
357                 return;
358             }
359
360             process = function(datum, collection) {
361                 var group, attr;
362                 datum = datum[0];
363                 if (datum.children) {
364                     group = {};
365                     for (attr in datum) {
366                         if (datum.hasOwnProperty(attr)) group[attr]=datum[attr];
367                     }
368                     group.children=[];
369                     $(datum.children).each2(function(i, childDatum) { process(childDatum, group.children); });
370                     if (group.children.length) {
371                         collection.push(group);
372                     }
373                 } else {
374                     if (query.matcher(t, text(datum))) {
375                         collection.push(datum);
376                     }
377                 }
378             };
379
380             $(data).each2(function(i, datum) { process(datum, filtered.results); });
381             query.callback(filtered);
382         };
383     }
384
385     // TODO javadoc
386     function tags(data) {
387         // TODO even for a function we should probably return a wrapper that does the same object/string check as
388         // the function for arrays. otherwise only functions that return objects are supported.
389         if ($.isFunction(data)) {
390             return data;
391         }
392
393         // if not a function we assume it to be an array
394
395         return function (query) {
396             var t = query.term, filtered = {results: []};
397             $(data).each(function () {
398                 var isObject = this.text !== undefined,
399                     text = isObject ? this.text : this;
400                 if (t === "" || query.matcher(t, text)) {
401                     filtered.results.push(isObject ? this : {id: this, text: this});
402                 }
403             });
404             query.callback(filtered);
405         };
406     }
407
408     /**
409      * Checks if the formatter function should be used.
410      *
411      * Throws an error if it is not a function. Returns true if it should be used,
412      * false if no formatting should be performed.
413      *
414      * @param formatter
415      */
416     function checkFormatter(formatter, formatterName) {
417         if ($.isFunction(formatter)) return true;
418         if (!formatter) return false;
419         throw new Error("formatterName must be a function or a falsy value");
420     }
421
422     function evaluate(val) {
423         return $.isFunction(val) ? val() : val;
424     }
425
426     function countResults(results) {
427         var count = 0;
428         $.each(results, function(i, item) {
429             if (item.children) {
430                 count += countResults(item.children);
431             } else {
432                 count++;
433             }
434         });
435         return count;
436     }
437
438     /**
439      * Default tokenizer. This function uses breaks the input on substring match of any string from the
440      * opts.tokenSeparators array and uses opts.createSearchChoice to create the choice object. Both of those
441      * two options have to be defined in order for the tokenizer to work.
442      *
443      * @param input text user has typed so far or pasted into the search field
444      * @param selection currently selected choices
445      * @param selectCallback function(choice) callback tho add the choice to selection
446      * @param opts select2's opts
447      * @return undefined/null to leave the current input unchanged, or a string to change the input to the returned value
448      */
449     function defaultTokenizer(input, selection, selectCallback, opts) {
450         var original = input, // store the original so we can compare and know if we need to tell the search to update its text
451             dupe = false, // check for whether a token we extracted represents a duplicate selected choice
452             token, // token
453             index, // position at which the separator was found
454             i, l, // looping variables
455             separator; // the matched separator
456
457         if (!opts.createSearchChoice || !opts.tokenSeparators || opts.tokenSeparators.length < 1) return undefined;
458
459         while (true) {
460             index = -1;
461
462             for (i = 0, l = opts.tokenSeparators.length; i < l; i++) {
463                 separator = opts.tokenSeparators[i];
464                 index = input.indexOf(separator);
465                 if (index >= 0) break;
466             }
467
468             if (index < 0) break; // did not find any token separator in the input string, bail
469
470             token = input.substring(0, index);
471             input = input.substring(index + separator.length);
472
473             if (token.length > 0) {
474                 token = opts.createSearchChoice(token, selection);
475                 if (token !== undefined && token !== null && opts.id(token) !== undefined && opts.id(token) !== null) {
476                     dupe = false;
477                     for (i = 0, l = selection.length; i < l; i++) {
478                         if (equal(opts.id(token), opts.id(selection[i]))) {
479                             dupe = true; break;
480                         }
481                     }
482
483                     if (!dupe) selectCallback(token);
484                 }
485             }
486         }
487
488         if (original.localeCompare(input) != 0) return input;
489     }
490
491     /**
492      * blurs any Select2 container that has focus when an element outside them was clicked or received focus
493      *
494      * also takes care of clicks on label tags that point to the source element
495      */
496     $(document).ready(function () {
497         $(document).delegate("*", "mousedown touchend", function (e) {
498             var target = $(e.target).closest("div.select2-container").get(0), attr;
499             if (target) {
500                 $(document).find("div.select2-container-active").each(function () {
501                     if (this !== target) $(this).data("select2").blur();
502                 });
503             } else {
504                 target = $(e.target).closest("div.select2-drop").get(0);
505                 $(document).find("div.select2-drop-active").each(function () {
506                     if (this !== target) $(this).data("select2").blur();
507                 });
508             }
509
510             target=$(e.target);
511             attr = target.attr("for");
512             if ("LABEL" === e.target.tagName && attr && attr.length > 0) {
513                 target = $("#"+attr);
514                 target = target.data("select2");
515                 if (target !== undefined) { target.focus(); e.preventDefault();}
516             }
517         });
518     });
519
520     /**
521      * Creates a new class
522      *
523      * @param superClass
524      * @param methods
525      */
526     function clazz(SuperClass, methods) {
527         var constructor = function () {};
528         constructor.prototype = new SuperClass;
529         constructor.prototype.constructor = constructor;
530         constructor.prototype.parent = SuperClass.prototype;
531         constructor.prototype = $.extend(constructor.prototype, methods);
532         return constructor;
533     }
534
535     AbstractSelect2 = clazz(Object, {
536
537         // abstract
538         bind: function (func) {
539             var self = this;
540             return function () {
541                 func.apply(self, arguments);
542             };
543         },
544
545         // abstract
546         init: function (opts) {
547             var results, search, resultsSelector = ".select2-results";
548
549             // prepare options
550             this.opts = opts = this.prepareOpts(opts);
551
552             this.id=opts.id;
553
554             // destroy if called on an existing component
555             if (opts.element.data("select2") !== undefined &&
556                 opts.element.data("select2") !== null) {
557                 this.destroy();
558             }
559
560             this.enabled=true;
561             this.container = this.createContainer();
562
563             this.containerId="s2id_"+(opts.element.attr("id") || "autogen"+nextUid());
564             this.containerSelector="#"+this.containerId.replace(/([;&,\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '\\$1');
565             this.container.attr("id", this.containerId);
566
567             // cache the body so future lookups are cheap
568             this.body = thunk(function() { return opts.element.closest("body"); });
569
570             if (opts.element.attr("class") !== undefined) {
571                 this.container.addClass(opts.element.attr("class").replace(/validate\[[\S ]+] ?/, ''));
572             }
573
574             this.container.css(evaluate(opts.containerCss));
575             this.container.addClass(evaluate(opts.containerCssClass));
576
577             // swap container for the element
578             this.opts.element
579                 .data("select2", this)
580                 .hide()
581                 .before(this.container);
582             this.container.data("select2", this);
583
584             this.dropdown = this.container.find(".select2-drop");
585             this.dropdown.addClass(evaluate(opts.dropdownCssClass));
586             this.dropdown.data("select2", this);
587
588             this.results = results = this.container.find(resultsSelector);
589             this.search = search = this.container.find("input.select2-input");
590
591             search.attr("tabIndex", this.opts.element.attr("tabIndex"));
592
593             this.resultsPage = 0;
594             this.context = null;
595
596             // initialize the container
597             this.initContainer();
598             this.initContainerWidth();
599
600             installFilteredMouseMove(this.results);
601             this.dropdown.delegate(resultsSelector, "mousemove-filtered", this.bind(this.highlightUnderEvent));
602
603             installDebouncedScroll(80, this.results);
604             this.dropdown.delegate(resultsSelector, "scroll-debounced", this.bind(this.loadMoreIfNeeded));
605
606             // if jquery.mousewheel plugin is installed we can prevent out-of-bounds scrolling of results via mousewheel
607             if ($.fn.mousewheel) {
608                 results.mousewheel(function (e, delta, deltaX, deltaY) {
609                     var top = results.scrollTop(), height;
610                     if (deltaY > 0 && top - deltaY <= 0) {
611                         results.scrollTop(0);
612                         killEvent(e);
613                     } else if (deltaY < 0 && results.get(0).scrollHeight - results.scrollTop() + deltaY <= results.height()) {
614                         results.scrollTop(results.get(0).scrollHeight - results.height());
615                         killEvent(e);
616                     }
617                 });
618             }
619
620             installKeyUpChangeEvent(search);
621             search.bind("keyup-change", this.bind(this.updateResults));
622             search.bind("focus", function () { search.addClass("select2-focused"); if (search.val() === " ") search.val(""); });
623             search.bind("blur", function () { search.removeClass("select2-focused");});
624
625             this.dropdown.delegate(resultsSelector, "mouseup", this.bind(function (e) {
626                 if ($(e.target).closest(".select2-result-selectable:not(.select2-disabled)").length > 0) {
627                     this.highlightUnderEvent(e);
628                     this.selectHighlighted(e);
629                 } else {
630                     this.focusSearch();
631                 }
632                 killEvent(e);
633             }));
634
635             // trap all mouse events from leaving the dropdown. sometimes there may be a modal that is listening
636             // for mouse events outside of itself so it can close itself. since the dropdown is now outside the select2's
637             // dom it will trigger the popup close, which is not what we want
638             this.dropdown.bind("click mouseup mousedown", function (e) { e.stopPropagation(); });
639
640             if ($.isFunction(this.opts.initSelection)) {
641                 // initialize selection based on the current value of the source element
642                 this.initSelection();
643
644                 // if the user has provided a function that can set selection based on the value of the source element
645                 // we monitor the change event on the element and trigger it, allowing for two way synchronization
646                 this.monitorSource();
647             }
648
649             if (opts.element.is(":disabled") || opts.element.is("[readonly='readonly']")) this.disable();
650         },
651
652         // abstract
653         destroy: function () {
654             var select2 = this.opts.element.data("select2");
655             if (select2 !== undefined) {
656                 select2.container.remove();
657                 select2.dropdown.remove();
658                 select2.opts.element
659                     .removeData("select2")
660                     .unbind(".select2")
661                     .show();
662             }
663         },
664
665         // abstract
666         prepareOpts: function (opts) {
667             var element, select, idKey, ajaxUrl;
668
669             element = opts.element;
670
671             if (element.get(0).tagName.toLowerCase() === "select") {
672                 this.select = select = opts.element;
673             }
674
675             if (select) {
676                 // these options are not allowed when attached to a select because they are picked up off the element itself
677                 $.each(["id", "multiple", "ajax", "query", "createSearchChoice", "initSelection", "data", "tags"], function () {
678                     if (this in opts) {
679                         throw new Error("Option '" + this + "' is not allowed for Select2 when attached to a <select> element.");
680                     }
681                 });
682             }
683
684             opts = $.extend({}, {
685                 populateResults: function(container, results, query) {
686                     var populate,  data, result, children, id=this.opts.id, self=this;
687
688                     populate=function(results, container, depth) {
689
690                         var i, l, result, selectable, compound, node, label, innerContainer, formatted;
691                         for (i = 0, l = results.length; i < l; i = i + 1) {
692
693                             result=results[i];
694                             selectable=id(result) !== undefined;
695                             compound=("children" in result) && result.children.length > 0;
696
697                             node=$("<li></li>");
698                             node.addClass("select2-results-dept-"+depth);
699                             node.addClass("select2-result");
700                             node.addClass(selectable ? "select2-result-selectable" : "select2-result-unselectable");
701                             if (compound) { node.addClass("select2-result-with-children"); }
702                             node.addClass(self.opts.formatResultCssClass(result));
703
704                             label=$("<div></div>");
705                             label.addClass("select2-result-label");
706
707                             formatted=opts.formatResult(result, label, query);
708                             if (formatted!==undefined) {
709                                 label.html(self.opts.escapeMarkup(formatted));
710                             }
711
712                             node.append(label);
713
714                             if (compound) {
715
716                                 innerContainer=$("<ul></ul>");
717                                 innerContainer.addClass("select2-result-sub");
718                                 populate(result.children, innerContainer, depth+1);
719                                 node.append(innerContainer);
720                             }
721
722                             node.data("select2-data", result);
723                             container.append(node);
724                         }
725                     };
726
727                     populate(results, container, 0);
728                 }
729             }, $.fn.select2.defaults, opts);
730
731             if (typeof(opts.id) !== "function") {
732                 idKey = opts.id;
733                 opts.id = function (e) { return e[idKey]; };
734             }
735
736             if (select) {
737                 opts.query = this.bind(function (query) {
738                     var data = { results: [], more: false },
739                         term = query.term,
740                         children, firstChild, process;
741
742                     process=function(element, collection) {
743                         var group;
744                         if (element.is("option")) {
745                             if (query.matcher(term, element.text(), element)) {
746                                 collection.push({id:element.attr("value"), text:element.text(), element: element.get(), css: element.attr("class")});
747                             }
748                         } else if (element.is("optgroup")) {
749                             group={text:element.attr("label"), children:[], element: element.get(), css: element.attr("class")};
750                             element.children().each2(function(i, elm) { process(elm, group.children); });
751                             if (group.children.length>0) {
752                                 collection.push(group);
753                             }
754                         }
755                     };
756
757                     children=element.children();
758
759                     // ignore the placeholder option if there is one
760                     if (this.getPlaceholder() !== undefined && children.length > 0) {
761                         firstChild = children[0];
762                         if ($(firstChild).text() === "") {
763                             children=children.not(firstChild);
764                         }
765                     }
766
767                     children.each2(function(i, elm) { process(elm, data.results); });
768
769                     query.callback(data);
770                 });
771                 // this is needed because inside val() we construct choices from options and there id is hardcoded
772                 opts.id=function(e) { return e.id; };
773                 opts.formatResultCssClass = function(data) { return data.css; }
774             } else {
775                 if (!("query" in opts)) {
776                     if ("ajax" in opts) {
777                         ajaxUrl = opts.element.data("ajax-url");
778                         if (ajaxUrl && ajaxUrl.length > 0) {
779                             opts.ajax.url = ajaxUrl;
780                         }
781                         opts.query = ajax(opts.ajax);
782                     } else if ("data" in opts) {
783                         opts.query = local(opts.data);
784                     } else if ("tags" in opts) {
785                         opts.query = tags(opts.tags);
786                         opts.createSearchChoice = function (term) { return {id: term, text: term}; };
787                         opts.initSelection = function (element, callback) {
788                             var data = [];
789                             $(splitVal(element.val(), opts.separator)).each(function () {
790                                 var id = this, text = this, tags=opts.tags;
791                                 if ($.isFunction(tags)) tags=tags();
792                                 $(tags).each(function() { if (equal(this.id, id)) { text = this.text; return false; } });
793                                 data.push({id: id, text: text});
794                             });
795
796                             callback(data);
797                         };
798                     }
799                 }
800             }
801             if (typeof(opts.query) !== "function") {
802                 throw "query function not defined for Select2 " + opts.element.attr("id");
803             }
804
805             return opts;
806         },
807
808         /**
809          * Monitor the original element for changes and update select2 accordingly
810          */
811         // abstract
812         monitorSource: function () {
813             this.opts.element.bind("change.select2", this.bind(function (e) {
814                 if (this.opts.element.data("select2-change-triggered") !== true) {
815                     this.initSelection();
816                 }
817             }));
818         },
819
820         /**
821          * Triggers the change event on the source element
822          */
823         // abstract
824         triggerChange: function (details) {
825
826             details = details || {};
827             details= $.extend({}, details, { type: "change", val: this.val() });
828             // prevents recursive triggering
829             this.opts.element.data("select2-change-triggered", true);
830             this.opts.element.trigger(details);
831             this.opts.element.data("select2-change-triggered", false);
832
833             // some validation frameworks ignore the change event and listen instead to keyup, click for selects
834             // so here we trigger the click event manually
835             this.opts.element.click();
836
837             // ValidationEngine ignorea the change event and listens instead to blur
838             // so here we trigger the blur event manually if so desired
839             if (this.opts.blurOnChange)
840                 this.opts.element.blur();
841         },
842
843
844         // abstract
845         enable: function() {
846             if (this.enabled) return;
847
848             this.enabled=true;
849             this.container.removeClass("select2-container-disabled");
850         },
851
852         // abstract
853         disable: function() {
854             if (!this.enabled) return;
855
856             this.close();
857
858             this.enabled=false;
859             this.container.addClass("select2-container-disabled");
860         },
861
862         // abstract
863         opened: function () {
864             return this.container.hasClass("select2-dropdown-open");
865         },
866
867         // abstract
868         positionDropdown: function() {
869             var offset = this.container.offset(),
870                 height = this.container.outerHeight(),
871                 width = this.container.outerWidth(),
872                 dropHeight = this.dropdown.outerHeight(),
873                 viewportBottom = $(window).scrollTop() + document.documentElement.clientHeight,
874                 dropTop = offset.top + height,
875                 dropLeft = offset.left,
876                 enoughRoomBelow = dropTop + dropHeight <= viewportBottom,
877                 enoughRoomAbove = (offset.top - dropHeight) >= this.body().scrollTop(),
878                 aboveNow = this.dropdown.hasClass("select2-drop-above"),
879                 bodyOffset,
880                 above,
881                 css;
882
883             // console.log("below/ droptop:", dropTop, "dropHeight", dropHeight, "sum", (dropTop+dropHeight)+" viewport bottom", viewportBottom, "enough?", enoughRoomBelow);
884             // console.log("above/ offset.top", offset.top, "dropHeight", dropHeight, "top", (offset.top-dropHeight), "scrollTop", this.body().scrollTop(), "enough?", enoughRoomAbove);
885
886             // fix positioning when body has an offset and is not position: static
887
888             if (this.body().css('position') !== 'static') {
889                 bodyOffset = this.body().offset();
890                 dropTop -= bodyOffset.top;
891                 dropLeft -= bodyOffset.left;
892             }
893
894             // always prefer the current above/below alignment, unless there is not enough room
895
896             if (aboveNow) {
897                 above = true;
898                 if (!enoughRoomAbove && enoughRoomBelow) above = false;
899             } else {
900                 above = false;
901                 if (!enoughRoomBelow && enoughRoomAbove) above = true;
902             }
903
904             if (above) {
905                 dropTop = offset.top - dropHeight;
906                 this.container.addClass("select2-drop-above");
907                 this.dropdown.addClass("select2-drop-above");
908             }
909             else {
910                 this.container.removeClass("select2-drop-above");
911                 this.dropdown.removeClass("select2-drop-above");
912             }
913
914             css = $.extend({
915                 top: dropTop,
916                 left: dropLeft,
917                 width: width
918             }, evaluate(this.opts.dropdownCss));
919
920             this.dropdown.css(css);
921         },
922
923         // abstract
924         shouldOpen: function() {
925             var event;
926
927             if (this.opened()) return false;
928
929             event = jQuery.Event("open");
930             this.opts.element.trigger(event);
931             return !event.isDefaultPrevented();
932         },
933
934         // abstract
935         clearDropdownAlignmentPreference: function() {
936             // clear the classes used to figure out the preference of where the dropdown should be opened
937             this.container.removeClass("select2-drop-above");
938             this.dropdown.removeClass("select2-drop-above");
939         },
940
941         /**
942          * Opens the dropdown
943          *
944          * @return {Boolean} whether or not dropdown was opened. This method will return false if, for example,
945          * the dropdown is already open, or if the 'open' event listener on the element called preventDefault().
946          */
947         // abstract
948         open: function () {
949
950             if (!this.shouldOpen()) return false;
951
952             window.setTimeout(this.bind(this.opening), 1);
953
954             return true;
955         },
956
957         /**
958          * Performs the opening of the dropdown
959          */
960         // abstract
961         opening: function() {
962             var cid = this.containerId, selector = this.containerSelector,
963                 scroll = "scroll." + cid, resize = "resize." + cid;
964
965             this.container.parents().each(function() {
966                 $(this).bind(scroll, function() {
967                     var s2 = $(selector);
968                     if (s2.length == 0) {
969                         $(this).unbind(scroll);
970                     }
971                     s2.select2("close");
972                 });
973             });
974
975             $(window).bind(resize, function() {
976                 var s2 = $(selector);
977                 if (s2.length == 0) {
978                     $(window).unbind(resize);
979                 }
980                 s2.select2("close");
981             });
982
983             this.clearDropdownAlignmentPreference();
984
985             if (this.search.val() === " ") { this.search.val(""); }
986
987             this.container.addClass("select2-dropdown-open").addClass("select2-container-active");
988
989             this.updateResults(true);
990
991             if(this.dropdown[0] !== this.body().children().last()[0]) {
992                 this.dropdown.detach().appendTo(this.body());
993             }
994
995             this.dropdown.show();
996
997             this.positionDropdown();
998             this.dropdown.addClass("select2-drop-active");
999
1000             this.ensureHighlightVisible();
1001
1002             this.focusSearch();
1003         },
1004
1005         // abstract
1006         close: function () {
1007             if (!this.opened()) return;
1008
1009             var self = this;
1010
1011             this.container.parents().each(function() {
1012                 $(this).unbind("scroll." + self.containerId);
1013             });
1014             $(window).unbind("resize." + this.containerId);
1015
1016             this.clearDropdownAlignmentPreference();
1017
1018             this.dropdown.hide();
1019             this.container.removeClass("select2-dropdown-open").removeClass("select2-container-active");
1020             this.results.empty();
1021             this.clearSearch();
1022
1023             this.opts.element.trigger(jQuery.Event("close"));
1024         },
1025
1026         // abstract
1027         clearSearch: function () {
1028
1029         },
1030
1031         // abstract
1032         ensureHighlightVisible: function () {
1033             var results = this.results, children, index, child, hb, rb, y, more;
1034
1035             index = this.highlight();
1036
1037             if (index < 0) return;
1038
1039             if (index == 0) {
1040
1041                 // if the first element is highlighted scroll all the way to the top,
1042                 // that way any unselectable headers above it will also be scrolled
1043                 // into view
1044
1045                 results.scrollTop(0);
1046                 return;
1047             }
1048
1049             children = results.find(".select2-result-selectable");
1050
1051             child = $(children[index]);
1052
1053             hb = child.offset().top + child.outerHeight();
1054
1055             // if this is the last child lets also make sure select2-more-results is visible
1056             if (index === children.length - 1) {
1057                 more = results.find("li.select2-more-results");
1058                 if (more.length > 0) {
1059                     hb = more.offset().top + more.outerHeight();
1060                 }
1061             }
1062
1063             rb = results.offset().top + results.outerHeight();
1064             if (hb > rb) {
1065                 results.scrollTop(results.scrollTop() + (hb - rb));
1066             }
1067             y = child.offset().top - results.offset().top;
1068
1069             // make sure the top of the element is visible
1070             if (y < 0) {
1071                 results.scrollTop(results.scrollTop() + y); // y is negative
1072             }
1073         },
1074
1075         // abstract
1076         moveHighlight: function (delta) {
1077             var choices = this.results.find(".select2-result-selectable"),
1078                 index = this.highlight();
1079
1080             while (index > -1 && index < choices.length) {
1081                 index += delta;
1082                 var choice = $(choices[index]);
1083                 if (choice.hasClass("select2-result-selectable") && !choice.hasClass("select2-disabled")) {
1084                     this.highlight(index);
1085                     break;
1086                 }
1087             }
1088         },
1089
1090         // abstract
1091         highlight: function (index) {
1092             var choices = this.results.find(".select2-result-selectable").not(".select2-disabled");
1093
1094             if (arguments.length === 0) {
1095                 return indexOf(choices.filter(".select2-highlighted")[0], choices.get());
1096             }
1097
1098             if (index >= choices.length) index = choices.length - 1;
1099             if (index < 0) index = 0;
1100
1101             choices.removeClass("select2-highlighted");
1102
1103             $(choices[index]).addClass("select2-highlighted");
1104             this.ensureHighlightVisible();
1105
1106         },
1107
1108         // abstract
1109         countSelectableResults: function() {
1110             return this.results.find(".select2-result-selectable").not(".select2-disabled").length;
1111         },
1112
1113         // abstract
1114         highlightUnderEvent: function (event) {
1115             var el = $(event.target).closest(".select2-result-selectable");
1116             if (el.length > 0 && !el.is(".select2-highlighted")) {
1117                         var choices = this.results.find('.select2-result-selectable');
1118                 this.highlight(choices.index(el));
1119             } else if (el.length == 0) {
1120                 // if we are over an unselectable item remove al highlights
1121                 this.results.find(".select2-highlighted").removeClass("select2-highlighted");
1122             }
1123         },
1124
1125         // abstract
1126         loadMoreIfNeeded: function () {
1127             var results = this.results,
1128                 more = results.find("li.select2-more-results"),
1129                 below, // pixels the element is below the scroll fold, below==0 is when the element is starting to be visible
1130                 offset = -1, // index of first element without data
1131                 page = this.resultsPage + 1,
1132                 self=this,
1133                 term=this.search.val(),
1134                 context=this.context;
1135
1136             if (more.length === 0) return;
1137             below = more.offset().top - results.offset().top - results.height();
1138
1139             if (below <= 0) {
1140                 more.addClass("select2-active");
1141                 this.opts.query({
1142                         term: term,
1143                         page: page,
1144                         context: context,
1145                         matcher: this.opts.matcher,
1146                         callback: this.bind(function (data) {
1147
1148                     // ignore a response if the select2 has been closed before it was received
1149                     if (!self.opened()) return;
1150
1151
1152                     self.opts.populateResults.call(this, results, data.results, {term: term, page: page, context:context});
1153
1154                     if (data.more===true) {
1155                         more.detach().appendTo(results).text(self.opts.formatLoadMore(page+1));
1156                         window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10);
1157                     } else {
1158                         more.remove();
1159                     }
1160                     self.positionDropdown();
1161                     self.resultsPage = page;
1162                 })});
1163             }
1164         },
1165
1166         /**
1167          * Default tokenizer function which does nothing
1168          */
1169         tokenize: function() {
1170
1171         },
1172
1173         /**
1174          * @param initial whether or not this is the call to this method right after the dropdown has been opened
1175          */
1176         // abstract
1177         updateResults: function (initial) {
1178             var search = this.search, results = this.results, opts = this.opts, data, self=this, input;
1179
1180             // if the search is currently hidden we do not alter the results
1181             if (initial !== true && (this.showSearchInput === false || !this.opened())) {
1182                 return;
1183             }
1184
1185             search.addClass("select2-active");
1186
1187             function postRender() {
1188                 results.scrollTop(0);
1189                 search.removeClass("select2-active");
1190                 self.positionDropdown();
1191             }
1192
1193             function render(html) {
1194                 results.html(self.opts.escapeMarkup(html));
1195                 postRender();
1196             }
1197
1198             if (opts.maximumSelectionSize >=1) {
1199                 data = this.data();
1200                 if ($.isArray(data) && data.length >= opts.maximumSelectionSize && checkFormatter(opts.formatSelectionTooBig, "formatSelectionTooBig")) {
1201                     render("<li class='select2-selection-limit'>" + opts.formatSelectionTooBig(opts.maximumSelectionSize) + "</li>");
1202                     return;
1203                 }
1204             }
1205
1206             if (search.val().length < opts.minimumInputLength && checkFormatter(opts.formatInputTooShort, "formatInputTooShort")) {
1207                 render("<li class='select2-no-results'>" + opts.formatInputTooShort(search.val(), opts.minimumInputLength) + "</li>");
1208                 return;
1209             }
1210             else {
1211                 render("<li class='select2-searching'>" + opts.formatSearching() + "</li>");
1212             }
1213
1214             // give the tokenizer a chance to pre-process the input
1215             input = this.tokenize();
1216             if (input != undefined && input != null) {
1217                 search.val(input);
1218             }
1219
1220             this.resultsPage = 1;
1221             opts.query({
1222                     term: search.val(),
1223                     page: this.resultsPage,
1224                     context: null,
1225                     matcher: opts.matcher,
1226                     callback: this.bind(function (data) {
1227                 var def; // default choice
1228
1229                 // ignore a response if the select2 has been closed before it was received
1230                 if (!this.opened()) return;
1231
1232                 // save context, if any
1233                 this.context = (data.context===undefined) ? null : data.context;
1234
1235                 // create a default choice and prepend it to the list
1236                 if (this.opts.createSearchChoice && search.val() !== "") {
1237                     def = this.opts.createSearchChoice.call(null, search.val(), data.results);
1238                     if (def !== undefined && def !== null && self.id(def) !== undefined && self.id(def) !== null) {
1239                         if ($(data.results).filter(
1240                             function () {
1241                                 return equal(self.id(this), self.id(def));
1242                             }).length === 0) {
1243                             data.results.unshift(def);
1244                         }
1245                     }
1246                 }
1247
1248                 if (data.results.length === 0 && checkFormatter(opts.formatNoMatches, "formatNoMatches")) {
1249                     render("<li class='select2-no-results'>" + opts.formatNoMatches(search.val()) + "</li>");
1250                     return;
1251                 }
1252
1253                 results.empty();
1254                 self.opts.populateResults.call(this, results, data.results, {term: search.val(), page: this.resultsPage, context:null});
1255
1256                 if (data.more === true && checkFormatter(opts.formatLoadMore, "formatLoadMore")) {
1257                     results.append("<li class='select2-more-results'>" + self.opts.escapeMarkup(opts.formatLoadMore(this.resultsPage)) + "</li>");
1258                     window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10);
1259                 }
1260
1261                 this.postprocessResults(data, initial);
1262
1263                 postRender();
1264             })});
1265         },
1266
1267         // abstract
1268         cancel: function () {
1269             this.close();
1270         },
1271
1272         // abstract
1273         blur: function () {
1274             this.close();
1275             this.container.removeClass("select2-container-active");
1276             this.dropdown.removeClass("select2-drop-active");
1277             // synonymous to .is(':focus'), which is available in jquery >= 1.6
1278             if (this.search[0] === document.activeElement) { this.search.blur(); }
1279             this.clearSearch();
1280             this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
1281         },
1282
1283         // abstract
1284         focusSearch: function () {
1285             // need to do it here as well as in timeout so it works in IE
1286             this.search.show();
1287             this.search.focus();
1288
1289             /* we do this in a timeout so that current event processing can complete before this code is executed.
1290              this makes sure the search field is focussed even if the current event would blur it */
1291             window.setTimeout(this.bind(function () {
1292                 // reset the value so IE places the cursor at the end of the input box
1293                 this.search.show();
1294                 this.search.focus();
1295                 this.search.val(this.search.val());
1296             }), 10);
1297         },
1298
1299         // abstract
1300         selectHighlighted: function () {
1301             var index=this.highlight(),
1302                 highlighted=this.results.find(".select2-highlighted").not(".select2-disabled"),
1303                 data = highlighted.closest('.select2-result-selectable').data("select2-data");
1304             if (data) {
1305                 highlighted.addClass("select2-disabled");
1306                 this.highlight(index);
1307                 this.onSelect(data);
1308             }
1309         },
1310
1311         // abstract
1312         getPlaceholder: function () {
1313             return this.opts.element.attr("placeholder") ||
1314                 this.opts.element.attr("data-placeholder") || // jquery 1.4 compat
1315                 this.opts.element.data("placeholder") ||
1316                 this.opts.placeholder;
1317         },
1318
1319         /**
1320          * Get the desired width for the container element.  This is
1321          * derived first from option `width` passed to select2, then
1322          * the inline 'style' on the original element, and finally
1323          * falls back to the jQuery calculated element width.
1324          */
1325         // abstract
1326         initContainerWidth: function () {
1327             function resolveContainerWidth() {
1328                 var style, attrs, matches, i, l;
1329
1330                 if (this.opts.width === "off") {
1331                     return null;
1332                 } else if (this.opts.width === "element"){
1333                     return this.opts.element.outerWidth() === 0 ? 'auto' : this.opts.element.outerWidth() + 'px';
1334                 } else if (this.opts.width === "copy" || this.opts.width === "resolve") {
1335                     // check if there is inline style on the element that contains width
1336                     style = this.opts.element.attr('style');
1337                     if (style !== undefined) {
1338                         attrs = style.split(';');
1339                         for (i = 0, l = attrs.length; i < l; i = i + 1) {
1340                             matches = attrs[i].replace(/\s/g, '')
1341                                 .match(/width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/);
1342                             if (matches !== null && matches.length >= 1)
1343                                 return matches[1];
1344                         }
1345                     }
1346
1347                     if (this.opts.width === "resolve") {
1348                         // next check if css('width') can resolve a width that is percent based, this is sometimes possible
1349                         // when attached to input type=hidden or elements hidden via css
1350                         style = this.opts.element.css('width');
1351                         if (style.indexOf("%") > 0) return style;
1352
1353                         // finally, fallback on the calculated width of the element
1354                         return (this.opts.element.outerWidth() === 0 ? 'auto' : this.opts.element.outerWidth() + 'px');
1355                     }
1356
1357                     return null;
1358                 } else if ($.isFunction(this.opts.width)) {
1359                     return this.opts.width();
1360                 } else {
1361                     return this.opts.width;
1362                }
1363             };
1364
1365             var width = resolveContainerWidth.call(this);
1366             if (width !== null) {
1367                 this.container.attr("style", "width: "+width);
1368             }
1369         }
1370     });
1371
1372     SingleSelect2 = clazz(AbstractSelect2, {
1373
1374         // single
1375
1376                 createContainer: function () {
1377             var container = $("<div></div>", {
1378                 "class": "select2-container"
1379             }).html([
1380                 "    <a href='#' onclick='return false;' class='select2-choice'>",
1381                 "   <span></span><abbr class='select2-search-choice-close' style='display:none;'></abbr>",
1382                 "   <div><b></b></div>" ,
1383                 "</a>",
1384                 "    <div class='select2-drop select2-offscreen'>" ,
1385                 "   <div class='select2-search'>" ,
1386                 "       <input type='text' autocomplete='off' class='select2-input'/>" ,
1387                 "   </div>" ,
1388                 "   <ul class='select2-results'>" ,
1389                 "   </ul>" ,
1390                 "</div>"].join(""));
1391             return container;
1392         },
1393
1394         // single
1395         opening: function () {
1396             this.search.show();
1397             this.parent.opening.apply(this, arguments);
1398             this.dropdown.removeClass("select2-offscreen");
1399         },
1400
1401         // single
1402         close: function () {
1403             if (!this.opened()) return;
1404             this.parent.close.apply(this, arguments);
1405             this.dropdown.removeAttr("style").addClass("select2-offscreen").insertAfter(this.selection).show();
1406         },
1407
1408         // single
1409         focus: function () {
1410             this.close();
1411             this.selection.focus();
1412         },
1413
1414         // single
1415         isFocused: function () {
1416             return this.selection[0] === document.activeElement;
1417         },
1418
1419         // single
1420         cancel: function () {
1421             this.parent.cancel.apply(this, arguments);
1422             this.selection.focus();
1423         },
1424
1425         // single
1426         initContainer: function () {
1427
1428             var selection,
1429                 container = this.container,
1430                 dropdown = this.dropdown,
1431                 clickingInside = false;
1432
1433             this.selection = selection = container.find(".select2-choice");
1434
1435             this.search.bind("keydown", this.bind(function (e) {
1436                 if (!this.enabled) return;
1437
1438                 if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) {
1439                     // prevent the page from scrolling
1440                     killEvent(e);
1441                     return;
1442                 }
1443
1444                 if (this.opened()) {
1445                     switch (e.which) {
1446                         case KEY.UP:
1447                         case KEY.DOWN:
1448                             this.moveHighlight((e.which === KEY.UP) ? -1 : 1);
1449                             killEvent(e);
1450                             return;
1451                         case KEY.TAB:
1452                         case KEY.ENTER:
1453                             this.selectHighlighted();
1454                             killEvent(e);
1455                             return;
1456                         case KEY.ESC:
1457                             this.cancel(e);
1458                             killEvent(e);
1459                             return;
1460                     }
1461                 } else {
1462
1463                     if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) {
1464                         return;
1465                     }
1466
1467                     if (this.opts.openOnEnter === false && e.which === KEY.ENTER) {
1468                         return;
1469                     }
1470
1471                     this.open();
1472
1473                     if (e.which === KEY.ENTER) {
1474                         // do not propagate the event otherwise we open, and propagate enter which closes
1475                         return;
1476                     }
1477                 }
1478             }));
1479
1480             this.search.bind("focus", this.bind(function() {
1481                 this.selection.attr("tabIndex", "-1");
1482             }));
1483             this.search.bind("blur", this.bind(function() {
1484                 if (!this.opened()) this.container.removeClass("select2-container-active");
1485                 window.setTimeout(this.bind(function() { this.selection.attr("tabIndex", this.opts.element.attr("tabIndex")); }), 10);
1486             }));
1487
1488             selection.bind("mousedown", this.bind(function (e) {
1489                 clickingInside = true;
1490
1491                 if (this.opened()) {
1492                     this.close();
1493                     this.selection.focus();
1494                 } else if (this.enabled) {
1495                     this.open();
1496                 }
1497
1498                 clickingInside = false;
1499             }));
1500
1501             dropdown.bind("mousedown", this.bind(function() { this.search.focus(); }));
1502
1503             selection.bind("focus", this.bind(function() {
1504                 this.container.addClass("select2-container-active");
1505                 // hide the search so the tab key does not focus on it
1506                 this.search.attr("tabIndex", "-1");
1507             }));
1508
1509             selection.bind("blur", this.bind(function() {
1510                 if (!this.opened()) {
1511                     this.container.removeClass("select2-container-active");
1512                 }
1513                 window.setTimeout(this.bind(function() { this.search.attr("tabIndex", this.opts.element.attr("tabIndex")); }), 10);
1514             }));
1515
1516             selection.bind("keydown", this.bind(function(e) {
1517                 if (!this.enabled) return;
1518
1519                 if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) {
1520                     // prevent the page from scrolling
1521                     killEvent(e);
1522                     return;
1523                 }
1524
1525                 if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e)
1526                  || e.which === KEY.ESC) {
1527                     return;
1528                 }
1529
1530                 if (this.opts.openOnEnter === false && e.which === KEY.ENTER) {
1531                     return;
1532                 }
1533
1534                 if (e.which == KEY.DELETE) {
1535                     if (this.opts.allowClear) {
1536                         this.clear();
1537                     }
1538                     return;
1539                 }
1540
1541                 this.open();
1542
1543                 if (e.which === KEY.ENTER) {
1544                     // do not propagate the event otherwise we open, and propagate enter which closes
1545                     killEvent(e);
1546                     return;
1547                 }
1548
1549                 // do not set the search input value for non-alpha-numeric keys
1550                 // otherwise pressing down results in a '(' being set in the search field
1551                 if (e.which < 48 ) { // '0' == 48
1552                     killEvent(e);
1553                     return;
1554                 }
1555
1556                 var keyWritten = String.fromCharCode(e.which).toLowerCase();
1557
1558                 if (e.shiftKey) {
1559                     keyWritten = keyWritten.toUpperCase();
1560                 }
1561
1562                 // focus the field before calling val so the cursor ends up after the value instead of before
1563                 this.search.focus();
1564                 this.search.val(keyWritten);
1565
1566                 // prevent event propagation so it doesnt replay on the now focussed search field and result in double key entry
1567                 killEvent(e);
1568             }));
1569
1570             selection.delegate("abbr", "mousedown", this.bind(function (e) {
1571                 if (!this.enabled) return;
1572                 this.clear();
1573                 killEvent(e);
1574                 this.close();
1575                 this.triggerChange();
1576                 this.selection.focus();
1577             }));
1578
1579             this.setPlaceholder();
1580
1581             this.search.bind("focus", this.bind(function() {
1582                 this.container.addClass("select2-container-active");
1583             }));
1584         },
1585
1586         // single
1587         clear: function() {
1588             this.opts.element.val("");
1589             this.selection.find("span").empty();
1590             this.selection.removeData("select2-data");
1591             this.setPlaceholder();
1592         },
1593
1594         /**
1595          * Sets selection based on source element's value
1596          */
1597         // single
1598         initSelection: function () {
1599             var selected;
1600             if (this.opts.element.val() === "") {
1601                 this.close();
1602                 this.setPlaceholder();
1603             } else {
1604                 var self = this;
1605                 this.opts.initSelection.call(null, this.opts.element, function(selected){
1606                     if (selected !== undefined && selected !== null) {
1607                         self.updateSelection(selected);
1608                         self.close();
1609                         self.setPlaceholder();
1610                     }
1611                 });
1612             }
1613         },
1614
1615         // single
1616         prepareOpts: function () {
1617             var opts = this.parent.prepareOpts.apply(this, arguments);
1618
1619             if (opts.element.get(0).tagName.toLowerCase() === "select") {
1620                 // install the selection initializer
1621                 opts.initSelection = function (element, callback) {
1622                     var selected = element.find(":selected");
1623                     // a single select box always has a value, no need to null check 'selected'
1624                     if ($.isFunction(callback))
1625                         callback({id: selected.attr("value"), text: selected.text()});
1626                 };
1627             }
1628
1629             return opts;
1630         },
1631
1632         // single
1633         setPlaceholder: function () {
1634             var placeholder = this.getPlaceholder();
1635
1636             if (this.opts.element.val() === "" && placeholder !== undefined) {
1637
1638                 // check for a first blank option if attached to a select
1639                 if (this.select && this.select.find("option:first").text() !== "") return;
1640
1641                 this.selection.find("span").html(this.opts.escapeMarkup(placeholder));
1642
1643                 this.selection.addClass("select2-default");
1644
1645                 this.selection.find("abbr").hide();
1646             }
1647         },
1648
1649         // single
1650         postprocessResults: function (data, initial) {
1651             var selected = 0, self = this, showSearchInput = true;
1652
1653             // find the selected element in the result list
1654
1655             this.results.find(".select2-result-selectable").each2(function (i, elm) {
1656                 if (equal(self.id(elm.data("select2-data")), self.opts.element.val())) {
1657                     selected = i;
1658                     return false;
1659                 }
1660             });
1661
1662             // and highlight it
1663
1664             this.highlight(selected);
1665
1666             // hide the search box if this is the first we got the results and there are a few of them
1667
1668             if (initial === true) {
1669                 showSearchInput = this.showSearchInput = countResults(data.results) >= this.opts.minimumResultsForSearch;
1670                 this.dropdown.find(".select2-search")[showSearchInput ? "removeClass" : "addClass"]("select2-search-hidden");
1671
1672                 //add "select2-with-searchbox" to the container if search box is shown
1673                 $(this.dropdown, this.container)[showSearchInput ? "addClass" : "removeClass"]("select2-with-searchbox");
1674             }
1675
1676         },
1677
1678         // single
1679         onSelect: function (data) {
1680             var old = this.opts.element.val();
1681
1682             this.opts.element.val(this.id(data));
1683             this.updateSelection(data);
1684             this.close();
1685             this.selection.focus();
1686
1687             if (!equal(old, this.id(data))) { this.triggerChange(); }
1688         },
1689
1690         // single
1691         updateSelection: function (data) {
1692
1693             var container=this.selection.find("span"), formatted;
1694
1695             this.selection.data("select2-data", data);
1696
1697             container.empty();
1698             formatted=this.opts.formatSelection(data, container);
1699             if (formatted !== undefined) {
1700                 container.append(this.opts.escapeMarkup(formatted));
1701             }
1702
1703             this.selection.removeClass("select2-default");
1704
1705             if (this.opts.allowClear && this.getPlaceholder() !== undefined) {
1706                 this.selection.find("abbr").show();
1707             }
1708         },
1709
1710         // single
1711         val: function () {
1712             var val, data = null, self = this;
1713
1714             if (arguments.length === 0) {
1715                 return this.opts.element.val();
1716             }
1717
1718             val = arguments[0];
1719
1720             if (this.select) {
1721                 this.select
1722                     .val(val)
1723                     .find(":selected").each2(function (i, elm) {
1724                         data = {id: elm.attr("value"), text: elm.text()};
1725                         return false;
1726                     });
1727                 this.updateSelection(data);
1728                 this.setPlaceholder();
1729             } else {
1730                 if (this.opts.initSelection === undefined) {
1731                     throw new Error("cannot call val() if initSelection() is not defined");
1732                 }
1733                 // val is an id. !val is true for [undefined,null,'']
1734                 if (!val) {
1735                     this.clear();
1736                     return;
1737                 }
1738                 this.opts.element.val(val);
1739                 this.opts.initSelection(this.opts.element, function(data){
1740                     self.opts.element.val(!data ? "" : self.id(data));
1741                     self.updateSelection(data);
1742                     self.setPlaceholder();
1743                 });
1744             }
1745         },
1746
1747         // single
1748         clearSearch: function () {
1749             this.search.val("");
1750         },
1751
1752         // single
1753         data: function(value) {
1754             var data;
1755
1756             if (arguments.length === 0) {
1757                 data = this.selection.data("select2-data");
1758                 if (data == undefined) data = null;
1759                 return data;
1760             } else {
1761                 if (!value || value === "") {
1762                     this.clear();
1763                 } else {
1764                     this.opts.element.val(!value ? "" : this.id(value));
1765                     this.updateSelection(value);
1766                 }
1767             }
1768         }
1769     });
1770
1771     MultiSelect2 = clazz(AbstractSelect2, {
1772
1773         // multi
1774         createContainer: function () {
1775             var container = $("<div></div>", {
1776                 "class": "select2-container select2-container-multi"
1777             }).html([
1778                 "    <ul class='select2-choices'>",
1779                 //"<li class='select2-search-choice'><span>California</span><a href="javascript:void(0)" class="select2-search-choice-close"></a></li>" ,
1780                 "  <li class='select2-search-field'>" ,
1781                 "    <input type='text' autocomplete='off' class='select2-input'>" ,
1782                 "  </li>" ,
1783                 "</ul>" ,
1784                 "<div class='select2-drop select2-drop-multi' style='display:none;'>" ,
1785                 "   <ul class='select2-results'>" ,
1786                 "   </ul>" ,
1787                 "</div>"].join(""));
1788                         return container;
1789         },
1790
1791         // multi
1792         prepareOpts: function () {
1793             var opts = this.parent.prepareOpts.apply(this, arguments);
1794
1795             // TODO validate placeholder is a string if specified
1796
1797             if (opts.element.get(0).tagName.toLowerCase() === "select") {
1798                 // install sthe selection initializer
1799                 opts.initSelection = function (element,callback) {
1800
1801                     var data = [];
1802                     element.find(":selected").each2(function (i, elm) {
1803                         data.push({id: elm.attr("value"), text: elm.text()});
1804                     });
1805
1806                     if ($.isFunction(callback))
1807                         callback(data);
1808                 };
1809             }
1810
1811             return opts;
1812         },
1813
1814         // multi
1815         initContainer: function () {
1816
1817             var selector = ".select2-choices", selection;
1818
1819             this.searchContainer = this.container.find(".select2-search-field");
1820             this.selection = selection = this.container.find(selector);
1821
1822             this.search.bind("keydown", this.bind(function (e) {
1823                 if (!this.enabled) return;
1824
1825                 if (e.which === KEY.BACKSPACE && this.search.val() === "") {
1826                     this.close();
1827
1828                     var choices,
1829                         selected = selection.find(".select2-search-choice-focus");
1830                     if (selected.length > 0) {
1831                         this.unselect(selected.first());
1832                         this.search.width(10);
1833                         killEvent(e);
1834                         return;
1835                     }
1836
1837                     choices = selection.find(".select2-search-choice");
1838                     if (choices.length > 0) {
1839                         choices.last().addClass("select2-search-choice-focus");
1840                     }
1841                 } else {
1842                     selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
1843                 }
1844
1845                 if (this.opened()) {
1846                     switch (e.which) {
1847                     case KEY.UP:
1848                     case KEY.DOWN:
1849                         this.moveHighlight((e.which === KEY.UP) ? -1 : 1);
1850                         killEvent(e);
1851                         return;
1852                     case KEY.ENTER:
1853                     case KEY.TAB:
1854                         this.selectHighlighted();
1855                         killEvent(e);
1856                         return;
1857                     case KEY.ESC:
1858                         this.cancel(e);
1859                         killEvent(e);
1860                         return;
1861                     }
1862                 }
1863
1864                 if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e)
1865                  || e.which === KEY.BACKSPACE || e.which === KEY.ESC) {
1866                     return;
1867                 }
1868
1869                 if (this.opts.openOnEnter === false && e.which === KEY.ENTER) {
1870                     return;
1871                 }
1872
1873                 this.open();
1874
1875                 if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) {
1876                     // prevent the page from scrolling
1877                     killEvent(e);
1878                 }
1879             }));
1880
1881             this.search.bind("keyup", this.bind(this.resizeSearch));
1882
1883             this.search.bind("blur", this.bind(function(e) {
1884                 this.container.removeClass("select2-container-active");
1885                 this.search.removeClass("select2-focused");
1886                 this.clearSearch();
1887                 e.stopImmediatePropagation();
1888             }));
1889
1890             this.container.delegate(selector, "mousedown", this.bind(function (e) {
1891                 if (!this.enabled) return;
1892                 if ($(e.target).closest(".select2-search-choice").length > 0) {
1893                     // clicked inside a select2 search choice, do not open
1894                     return;
1895                 }
1896                 this.clearPlaceholder();
1897                 this.open();
1898                 this.focusSearch();
1899                 e.preventDefault();
1900             }));
1901
1902             this.container.delegate(selector, "focus", this.bind(function () {
1903                 if (!this.enabled) return;
1904                 this.container.addClass("select2-container-active");
1905                 this.dropdown.addClass("select2-drop-active");
1906                 this.clearPlaceholder();
1907             }));
1908
1909             // set the placeholder if necessary
1910             this.clearSearch();
1911         },
1912
1913         // multi
1914         enable: function() {
1915             if (this.enabled) return;
1916
1917             this.parent.enable.apply(this, arguments);
1918
1919             this.search.removeAttr("disabled");
1920         },
1921
1922         // multi
1923         disable: function() {
1924             if (!this.enabled) return;
1925
1926             this.parent.disable.apply(this, arguments);
1927
1928             this.search.attr("disabled", true);
1929         },
1930
1931         // multi
1932         initSelection: function () {
1933             var data;
1934             if (this.opts.element.val() === "") {
1935                 this.updateSelection([]);
1936                 this.close();
1937                 // set the placeholder if necessary
1938                 this.clearSearch();
1939             }
1940             if (this.select || this.opts.element.val() !== "") {
1941                 var self = this;
1942                 this.opts.initSelection.call(null, this.opts.element, function(data){
1943                     if (data !== undefined && data !== null) {
1944                         self.updateSelection(data);
1945                         self.close();
1946                         // set the placeholder if necessary
1947                         self.clearSearch();
1948                     }
1949                 });
1950             }
1951         },
1952
1953         // multi
1954         clearSearch: function () {
1955             var placeholder = this.getPlaceholder();
1956
1957             if (placeholder !== undefined  && this.getVal().length === 0 && this.search.hasClass("select2-focused") === false) {
1958                 this.search.val(placeholder).addClass("select2-default");
1959                 // stretch the search box to full width of the container so as much of the placeholder is visible as possible
1960                 this.resizeSearch();
1961             } else {
1962                 // we set this to " " instead of "" and later clear it on focus() because there is a firefox bug
1963                 // that does not properly render the caret when the field starts out blank
1964                 this.search.val(" ").width(10);
1965             }
1966         },
1967
1968         // multi
1969         clearPlaceholder: function () {
1970             if (this.search.hasClass("select2-default")) {
1971                 this.search.val("").removeClass("select2-default");
1972             } else {
1973                 // work around for the space character we set to avoid firefox caret bug
1974                 if (this.search.val() === " ") this.search.val("");
1975             }
1976         },
1977
1978         // multi
1979         opening: function () {
1980             this.parent.opening.apply(this, arguments);
1981
1982             this.clearPlaceholder();
1983                         this.resizeSearch();
1984             this.focusSearch();
1985         },
1986
1987         // multi
1988         close: function () {
1989             if (!this.opened()) return;
1990             this.parent.close.apply(this, arguments);
1991         },
1992
1993         // multi
1994         focus: function () {
1995             this.close();
1996             this.search.focus();
1997         },
1998
1999         // multi
2000         isFocused: function () {
2001             return this.search.hasClass("select2-focused");
2002         },
2003
2004         // multi
2005         updateSelection: function (data) {
2006             var ids = [], filtered = [], self = this;
2007
2008             // filter out duplicates
2009             $(data).each(function () {
2010                 if (indexOf(self.id(this), ids) < 0) {
2011                     ids.push(self.id(this));
2012                     filtered.push(this);
2013                 }
2014             });
2015             data = filtered;
2016
2017             this.selection.find(".select2-search-choice").remove();
2018             $(data).each(function () {
2019                 self.addSelectedChoice(this);
2020             });
2021             self.postprocessResults();
2022         },
2023
2024         tokenize: function() {
2025             var input = this.search.val();
2026             input = this.opts.tokenizer(input, this.data(), this.bind(this.onSelect), this.opts);
2027             if (input != null && input != undefined) {
2028                 this.search.val(input);
2029                 if (input.length > 0) {
2030                     this.open();
2031                 }
2032             }
2033
2034         },
2035
2036         // multi
2037         onSelect: function (data) {
2038             this.addSelectedChoice(data);
2039             if (this.select) { this.postprocessResults(); }
2040
2041             if (this.opts.closeOnSelect) {
2042                 this.close();
2043                 this.search.width(10);
2044             } else {
2045                 if (this.countSelectableResults()>0) {
2046                     this.search.width(10);
2047                     this.resizeSearch();
2048                     this.positionDropdown();
2049                 } else {
2050                     // if nothing left to select close
2051                     this.close();
2052                 }
2053             }
2054
2055             // since its not possible to select an element that has already been
2056             // added we do not need to check if this is a new element before firing change
2057             this.triggerChange({ added: data });
2058
2059             this.focusSearch();
2060         },
2061
2062         // multi
2063         cancel: function () {
2064             this.close();
2065             this.focusSearch();
2066         },
2067
2068         // multi
2069         addSelectedChoice: function (data) {
2070             var choice=$(
2071                     "<li class='select2-search-choice'>" +
2072                     "    <div></div>" +
2073                     "    <a href='#' onclick='return false;' class='select2-search-choice-close' tabindex='-1'></a>" +
2074                     "</li>"),
2075                 id = this.id(data),
2076                 val = this.getVal(),
2077                 formatted;
2078
2079             formatted=this.opts.formatSelection(data, choice);
2080             choice.find("div").replaceWith("<div>"+this.opts.escapeMarkup(formatted)+"</div>");
2081             choice.find(".select2-search-choice-close")
2082                 .bind("mousedown", killEvent)
2083                 .bind("click dblclick", this.bind(function (e) {
2084                 if (!this.enabled) return;
2085
2086                 $(e.target).closest(".select2-search-choice").fadeOut('fast', this.bind(function(){
2087                     this.unselect($(e.target));
2088                     this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
2089                     this.close();
2090                     this.focusSearch();
2091                 })).dequeue();
2092                 killEvent(e);
2093             })).bind("focus", this.bind(function () {
2094                 if (!this.enabled) return;
2095                 this.container.addClass("select2-container-active");
2096                 this.dropdown.addClass("select2-drop-active");
2097             }));
2098
2099             choice.data("select2-data", data);
2100             choice.insertBefore(this.searchContainer);
2101
2102             val.push(id);
2103             this.setVal(val);
2104         },
2105
2106         // multi
2107         unselect: function (selected) {
2108             var val = this.getVal(),
2109                 data,
2110                 index;
2111
2112             selected = selected.closest(".select2-search-choice");
2113
2114             if (selected.length === 0) {
2115                 throw "Invalid argument: " + selected + ". Must be .select2-search-choice";
2116             }
2117
2118             data = selected.data("select2-data");
2119
2120             index = indexOf(this.id(data), val);
2121
2122             if (index >= 0) {
2123                 val.splice(index, 1);
2124                 this.setVal(val);
2125                 if (this.select) this.postprocessResults();
2126             }
2127             selected.remove();
2128             this.triggerChange({ removed: data });
2129         },
2130
2131         // multi
2132         postprocessResults: function () {
2133             var val = this.getVal(),
2134                 choices = this.results.find(".select2-result-selectable"),
2135                 compound = this.results.find(".select2-result-with-children"),
2136                 self = this;
2137
2138             choices.each2(function (i, choice) {
2139                 var id = self.id(choice.data("select2-data"));
2140                 if (indexOf(id, val) >= 0) {
2141                     choice.addClass("select2-disabled").removeClass("select2-result-selectable");
2142                 } else {
2143                     choice.removeClass("select2-disabled").addClass("select2-result-selectable");
2144                 }
2145             });
2146
2147             compound.each2(function(i, e) {
2148                 if (e.find(".select2-result-selectable").length==0) {
2149                     e.addClass("select2-disabled");
2150                 } else {
2151                     e.removeClass("select2-disabled");
2152                 }
2153             });
2154
2155             choices.each2(function (i, choice) {
2156                 if (!choice.hasClass("select2-disabled") && choice.hasClass("select2-result-selectable")) {
2157                     self.highlight(0);
2158                     return false;
2159                 }
2160             });
2161
2162         },
2163
2164         // multi
2165         resizeSearch: function () {
2166
2167             var minimumWidth, left, maxWidth, containerLeft, searchWidth,
2168                 sideBorderPadding = getSideBorderPadding(this.search);
2169
2170             minimumWidth = measureTextWidth(this.search) + 10;
2171
2172             left = this.search.offset().left;
2173
2174             maxWidth = this.selection.width();
2175             containerLeft = this.selection.offset().left;
2176
2177             searchWidth = maxWidth - (left - containerLeft) - sideBorderPadding;
2178             if (searchWidth < minimumWidth) {
2179                 searchWidth = maxWidth - sideBorderPadding;
2180             }
2181
2182             if (searchWidth < 40) {
2183                 searchWidth = maxWidth - sideBorderPadding;
2184             }
2185             this.search.width(searchWidth);
2186         },
2187
2188         // multi
2189         getVal: function () {
2190             var val;
2191             if (this.select) {
2192                 val = this.select.val();
2193                 return val === null ? [] : val;
2194             } else {
2195                 val = this.opts.element.val();
2196                 return splitVal(val, this.opts.separator);
2197             }
2198         },
2199
2200         // multi
2201         setVal: function (val) {
2202             var unique;
2203             if (this.select) {
2204                 this.select.val(val);
2205             } else {
2206                 unique = [];
2207                 // filter out duplicates
2208                 $(val).each(function () {
2209                     if (indexOf(this, unique) < 0) unique.push(this);
2210                 });
2211                 this.opts.element.val(unique.length === 0 ? "" : unique.join(this.opts.separator));
2212             }
2213         },
2214
2215         // multi
2216         val: function () {
2217             var val, data = [], self=this;
2218
2219             if (arguments.length === 0) {
2220                 return this.getVal();
2221             }
2222
2223             val = arguments[0];
2224
2225             if (!val) {
2226                 this.opts.element.val("");
2227                 this.updateSelection([]);
2228                 this.clearSearch();
2229                 return;
2230             }
2231
2232             // val is a list of ids
2233             this.setVal(val);
2234
2235             if (this.select) {
2236                 this.select.find(":selected").each(function () {
2237                     data.push({id: $(this).attr("value"), text: $(this).text()});
2238                 });
2239                 this.updateSelection(data);
2240             } else {
2241                 if (this.opts.initSelection === undefined) {
2242                     throw new Error("val() cannot be called if initSelection() is not defined")
2243                 }
2244
2245                 this.opts.initSelection(this.opts.element, function(data){
2246                     var ids=$(data).map(self.id);
2247                     self.setVal(ids);
2248                     self.updateSelection(data);
2249                     self.clearSearch();
2250                 });
2251             }
2252             this.clearSearch();
2253         },
2254
2255         // multi
2256         onSortStart: function() {
2257             if (this.select) {
2258                 throw new Error("Sorting of elements is not supported when attached to <select>. Attach to <input type='hidden'/> instead.");
2259             }
2260
2261             // collapse search field into 0 width so its container can be collapsed as well
2262             this.search.width(0);
2263             // hide the container
2264             this.searchContainer.hide();
2265         },
2266
2267         // multi
2268         onSortEnd:function() {
2269
2270             var val=[], self=this;
2271
2272             // show search and move it to the end of the list
2273             this.searchContainer.show();
2274             // make sure the search container is the last item in the list
2275             this.searchContainer.appendTo(this.searchContainer.parent());
2276             // since we collapsed the width in dragStarted, we resize it here
2277             this.resizeSearch();
2278
2279             // update selection
2280
2281             this.selection.find(".select2-search-choice").each(function() {
2282                 val.push(self.opts.id($(this).data("select2-data")));
2283             });
2284             this.setVal(val);
2285             this.triggerChange();
2286         },
2287
2288         // multi
2289         data: function(values) {
2290             var self=this, ids;
2291             if (arguments.length === 0) {
2292                  return this.selection
2293                      .find(".select2-search-choice")
2294                      .map(function() { return $(this).data("select2-data"); })
2295                      .get();
2296             } else {
2297                 if (!values) { values = []; }
2298                 ids = $.map(values, function(e) { return self.opts.id(e)});
2299                 this.setVal(ids);
2300                 this.updateSelection(values);
2301                 this.clearSearch();
2302             }
2303         }
2304     });
2305
2306     $.fn.select2 = function () {
2307
2308         var args = Array.prototype.slice.call(arguments, 0),
2309             opts,
2310             select2,
2311             value, multiple, allowedMethods = ["val", "destroy", "opened", "open", "close", "focus", "isFocused", "container", "onSortStart", "onSortEnd", "enable", "disable", "positionDropdown", "data"];
2312
2313         this.each(function () {
2314             if (args.length === 0 || typeof(args[0]) === "object") {
2315                 opts = args.length === 0 ? {} : $.extend({}, args[0]);
2316                 opts.element = $(this);
2317
2318                 if (opts.element.get(0).tagName.toLowerCase() === "select") {
2319                     multiple = opts.element.attr("multiple");
2320                 } else {
2321                     multiple = opts.multiple || false;
2322                     if ("tags" in opts) {opts.multiple = multiple = true;}
2323                 }
2324
2325                 select2 = multiple ? new MultiSelect2() : new SingleSelect2();
2326                 select2.init(opts);
2327             } else if (typeof(args[0]) === "string") {
2328
2329                 if (indexOf(args[0], allowedMethods) < 0) {
2330                     throw "Unknown method: " + args[0];
2331                 }
2332
2333                 value = undefined;
2334                 select2 = $(this).data("select2");
2335                 if (select2 === undefined) return;
2336                 if (args[0] === "container") {
2337                     value=select2.container;
2338                 } else {
2339                     value = select2[args[0]].apply(select2, args.slice(1));
2340                 }
2341                 if (value !== undefined) {return false;}
2342             } else {
2343                 throw "Invalid arguments to select2 plugin: " + args;
2344             }
2345         });
2346         return (value === undefined) ? this : value;
2347     };
2348
2349     // plugin defaults, accessible to users
2350     $.fn.select2.defaults = {
2351         width: "copy",
2352         closeOnSelect: true,
2353         openOnEnter: true,
2354         containerCss: {},
2355         dropdownCss: {},
2356         containerCssClass: "",
2357         dropdownCssClass: "",
2358         formatResult: function(result, container, query) {
2359             var markup=[];
2360             markMatch(result.text, query.term, markup);
2361             return markup.join("");
2362         },
2363         formatSelection: function (data, container) {
2364             return data.text;
2365         },
2366         formatResultCssClass: function(data) {return undefined;},
2367         formatNoMatches: function () { return "No matches found"; },
2368         formatInputTooShort: function (input, min) { return "Please enter " + (min - input.length) + " more characters"; },
2369         formatSelectionTooBig: function (limit) { return "You can only select " + limit + " item" + (limit == 1 ? "" : "s"); },
2370         formatLoadMore: function (pageNumber) { return "Loading more results..."; },
2371         formatSearching: function () { return "Searching..."; },
2372         minimumResultsForSearch: 0,
2373         minimumInputLength: 0,
2374         maximumSelectionSize: 0,
2375         id: function (e) { return e.id; },
2376         matcher: function(term, text) {
2377             return text.toUpperCase().indexOf(term.toUpperCase()) >= 0;
2378         },
2379         separator: ",",
2380         tokenSeparators: [],
2381         tokenizer: defaultTokenizer,
2382         escapeMarkup: function (markup) {
2383             if (markup && typeof(markup) === "string") {
2384                 return markup.replace(/&/g, "&amp;");
2385             }
2386             return markup;
2387         },
2388         blurOnChange: false
2389     };
2390
2391     // exports
2392     window.Select2 = {
2393         query: {
2394             ajax: ajax,
2395             local: local,
2396             tags: tags
2397         }, util: {
2398             debounce: debounce,
2399             markMatch: markMatch
2400         }, "class": {
2401             "abstract": AbstractSelect2,
2402             "single": SingleSelect2,
2403             "multi": MultiSelect2
2404         }
2405     };
2406
2407 }(jQuery));