Project

General

Profile

1
/*
2
Copyright 2012 Igor Vaynberg
3

    
4
Version: 3.4.5 Timestamp: Mon Nov  4 08:22:42 PST 2013
5

    
6
This software is licensed under the Apache License, Version 2.0 (the "Apache License") or the GNU
7
General Public License version 2 (the "GPL License"). You may choose either license to govern your
8
use of this software only upon the condition that you accept all of the terms of either the Apache
9
License or the GPL License.
10

    
11
You may obtain a copy of the Apache License and the GPL License at:
12

    
13
    http://www.apache.org/licenses/LICENSE-2.0
14
    http://www.gnu.org/licenses/gpl-2.0.html
15

    
16
Unless required by applicable law or agreed to in writing, software distributed under the
17
Apache License or the GPL Licesnse is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
18
CONDITIONS OF ANY KIND, either express or implied. See the Apache License and the GPL License for
19
the specific language governing permissions and limitations under the Apache License and the GPL License.
20
*/
21
(function ($) {
22
    if(typeof $.fn.each2 == "undefined") {
23
        $.extend($.fn, {
24
            /*
25
            * 4-10 times faster .each replacement
26
            * use it carefully, as it overrides jQuery context of element on each iteration
27
            */
28
            each2 : function (c) {
29
                var j = $([0]), i = -1, l = this.length;
30
                while (
31
                    ++i < l
32
                    && (j.context = j[0] = this[i])
33
                    && c.call(j[0], i, j) !== false //"this"=DOM, i=index, j=jQuery object
34
                );
35
                return this;
36
            }
37
        });
38
    }
39
})(jQuery);
40

    
41
(function ($, undefined) {
42
    "use strict";
43
    /*global document, window, jQuery, console */
44

    
45
    if (window.Select2 !== undefined) {
46
        return;
47
    }
48

    
49
    var KEY, AbstractSelect2, SingleSelect2, MultiSelect2, nextUid, sizer,
50
        lastMousePosition={x:0,y:0}, $document, scrollBarDimensions,
51

    
52
    KEY = {
53
        TAB: 9,
54
        ENTER: 13,
55
        ESC: 27,
56
        SPACE: 32,
57
        LEFT: 37,
58
        UP: 38,
59
        RIGHT: 39,
60
        DOWN: 40,
61
        SHIFT: 16,
62
        CTRL: 17,
63
        ALT: 18,
64
        PAGE_UP: 33,
65
        PAGE_DOWN: 34,
66
        HOME: 36,
67
        END: 35,
68
        BACKSPACE: 8,
69
        DELETE: 46,
70
        isArrow: function (k) {
71
            k = k.which ? k.which : k;
72
            switch (k) {
73
            case KEY.LEFT:
74
            case KEY.RIGHT:
75
            case KEY.UP:
76
            case KEY.DOWN:
77
                return true;
78
            }
79
            return false;
80
        },
81
        isControl: function (e) {
82
            var k = e.which;
83
            switch (k) {
84
            case KEY.SHIFT:
85
            case KEY.CTRL:
86
            case KEY.ALT:
87
                return true;
88
            }
89

    
90
            if (e.metaKey) return true;
91

    
92
            return false;
93
        },
94
        isFunctionKey: function (k) {
95
            k = k.which ? k.which : k;
96
            return k >= 112 && k <= 123;
97
        }
98
    },
99
    MEASURE_SCROLLBAR_TEMPLATE = "<div class='select2-measure-scrollbar'></div>",
100

    
101
    DIACRITICS = {"\u24B6":"A","\uFF21":"A","\u00C0":"A","\u00C1":"A","\u00C2":"A","\u1EA6":"A","\u1EA4":"A","\u1EAA":"A","\u1EA8":"A","\u00C3":"A","\u0100":"A","\u0102":"A","\u1EB0":"A","\u1EAE":"A","\u1EB4":"A","\u1EB2":"A","\u0226":"A","\u01E0":"A","\u00C4":"A","\u01DE":"A","\u1EA2":"A","\u00C5":"A","\u01FA":"A","\u01CD":"A","\u0200":"A","\u0202":"A","\u1EA0":"A","\u1EAC":"A","\u1EB6":"A","\u1E00":"A","\u0104":"A","\u023A":"A","\u2C6F":"A","\uA732":"AA","\u00C6":"AE","\u01FC":"AE","\u01E2":"AE","\uA734":"AO","\uA736":"AU","\uA738":"AV","\uA73A":"AV","\uA73C":"AY","\u24B7":"B","\uFF22":"B","\u1E02":"B","\u1E04":"B","\u1E06":"B","\u0243":"B","\u0182":"B","\u0181":"B","\u24B8":"C","\uFF23":"C","\u0106":"C","\u0108":"C","\u010A":"C","\u010C":"C","\u00C7":"C","\u1E08":"C","\u0187":"C","\u023B":"C","\uA73E":"C","\u24B9":"D","\uFF24":"D","\u1E0A":"D","\u010E":"D","\u1E0C":"D","\u1E10":"D","\u1E12":"D","\u1E0E":"D","\u0110":"D","\u018B":"D","\u018A":"D","\u0189":"D","\uA779":"D","\u01F1":"DZ","\u01C4":"DZ","\u01F2":"Dz","\u01C5":"Dz","\u24BA":"E","\uFF25":"E","\u00C8":"E","\u00C9":"E","\u00CA":"E","\u1EC0":"E","\u1EBE":"E","\u1EC4":"E","\u1EC2":"E","\u1EBC":"E","\u0112":"E","\u1E14":"E","\u1E16":"E","\u0114":"E","\u0116":"E","\u00CB":"E","\u1EBA":"E","\u011A":"E","\u0204":"E","\u0206":"E","\u1EB8":"E","\u1EC6":"E","\u0228":"E","\u1E1C":"E","\u0118":"E","\u1E18":"E","\u1E1A":"E","\u0190":"E","\u018E":"E","\u24BB":"F","\uFF26":"F","\u1E1E":"F","\u0191":"F","\uA77B":"F","\u24BC":"G","\uFF27":"G","\u01F4":"G","\u011C":"G","\u1E20":"G","\u011E":"G","\u0120":"G","\u01E6":"G","\u0122":"G","\u01E4":"G","\u0193":"G","\uA7A0":"G","\uA77D":"G","\uA77E":"G","\u24BD":"H","\uFF28":"H","\u0124":"H","\u1E22":"H","\u1E26":"H","\u021E":"H","\u1E24":"H","\u1E28":"H","\u1E2A":"H","\u0126":"H","\u2C67":"H","\u2C75":"H","\uA78D":"H","\u24BE":"I","\uFF29":"I","\u00CC":"I","\u00CD":"I","\u00CE":"I","\u0128":"I","\u012A":"I","\u012C":"I","\u0130":"I","\u00CF":"I","\u1E2E":"I","\u1EC8":"I","\u01CF":"I","\u0208":"I","\u020A":"I","\u1ECA":"I","\u012E":"I","\u1E2C":"I","\u0197":"I","\u24BF":"J","\uFF2A":"J","\u0134":"J","\u0248":"J","\u24C0":"K","\uFF2B":"K","\u1E30":"K","\u01E8":"K","\u1E32":"K","\u0136":"K","\u1E34":"K","\u0198":"K","\u2C69":"K","\uA740":"K","\uA742":"K","\uA744":"K","\uA7A2":"K","\u24C1":"L","\uFF2C":"L","\u013F":"L","\u0139":"L","\u013D":"L","\u1E36":"L","\u1E38":"L","\u013B":"L","\u1E3C":"L","\u1E3A":"L","\u0141":"L","\u023D":"L","\u2C62":"L","\u2C60":"L","\uA748":"L","\uA746":"L","\uA780":"L","\u01C7":"LJ","\u01C8":"Lj","\u24C2":"M","\uFF2D":"M","\u1E3E":"M","\u1E40":"M","\u1E42":"M","\u2C6E":"M","\u019C":"M","\u24C3":"N","\uFF2E":"N","\u01F8":"N","\u0143":"N","\u00D1":"N","\u1E44":"N","\u0147":"N","\u1E46":"N","\u0145":"N","\u1E4A":"N","\u1E48":"N","\u0220":"N","\u019D":"N","\uA790":"N","\uA7A4":"N","\u01CA":"NJ","\u01CB":"Nj","\u24C4":"O","\uFF2F":"O","\u00D2":"O","\u00D3":"O","\u00D4":"O","\u1ED2":"O","\u1ED0":"O","\u1ED6":"O","\u1ED4":"O","\u00D5":"O","\u1E4C":"O","\u022C":"O","\u1E4E":"O","\u014C":"O","\u1E50":"O","\u1E52":"O","\u014E":"O","\u022E":"O","\u0230":"O","\u00D6":"O","\u022A":"O","\u1ECE":"O","\u0150":"O","\u01D1":"O","\u020C":"O","\u020E":"O","\u01A0":"O","\u1EDC":"O","\u1EDA":"O","\u1EE0":"O","\u1EDE":"O","\u1EE2":"O","\u1ECC":"O","\u1ED8":"O","\u01EA":"O","\u01EC":"O","\u00D8":"O","\u01FE":"O","\u0186":"O","\u019F":"O","\uA74A":"O","\uA74C":"O","\u01A2":"OI","\uA74E":"OO","\u0222":"OU","\u24C5":"P","\uFF30":"P","\u1E54":"P","\u1E56":"P","\u01A4":"P","\u2C63":"P","\uA750":"P","\uA752":"P","\uA754":"P","\u24C6":"Q","\uFF31":"Q","\uA756":"Q","\uA758":"Q","\u024A":"Q","\u24C7":"R","\uFF32":"R","\u0154":"R","\u1E58":"R","\u0158":"R","\u0210":"R","\u0212":"R","\u1E5A":"R","\u1E5C":"R","\u0156":"R","\u1E5E":"R","\u024C":"R","\u2C64":"R","\uA75A":"R","\uA7A6":"R","\uA782":"R","\u24C8":"S","\uFF33":"S","\u1E9E":"S","\u015A":"S","\u1E64":"S","\u015C":"S","\u1E60":"S","\u0160":"S","\u1E66":"S","\u1E62":"S","\u1E68":"S","\u0218":"S","\u015E":"S","\u2C7E":"S","\uA7A8":"S","\uA784":"S","\u24C9":"T","\uFF34":"T","\u1E6A":"T","\u0164":"T","\u1E6C":"T","\u021A":"T","\u0162":"T","\u1E70":"T","\u1E6E":"T","\u0166":"T","\u01AC":"T","\u01AE":"T","\u023E":"T","\uA786":"T","\uA728":"TZ","\u24CA":"U","\uFF35":"U","\u00D9":"U","\u00DA":"U","\u00DB":"U","\u0168":"U","\u1E78":"U","\u016A":"U","\u1E7A":"U","\u016C":"U","\u00DC":"U","\u01DB":"U","\u01D7":"U","\u01D5":"U","\u01D9":"U","\u1EE6":"U","\u016E":"U","\u0170":"U","\u01D3":"U","\u0214":"U","\u0216":"U","\u01AF":"U","\u1EEA":"U","\u1EE8":"U","\u1EEE":"U","\u1EEC":"U","\u1EF0":"U","\u1EE4":"U","\u1E72":"U","\u0172":"U","\u1E76":"U","\u1E74":"U","\u0244":"U","\u24CB":"V","\uFF36":"V","\u1E7C":"V","\u1E7E":"V","\u01B2":"V","\uA75E":"V","\u0245":"V","\uA760":"VY","\u24CC":"W","\uFF37":"W","\u1E80":"W","\u1E82":"W","\u0174":"W","\u1E86":"W","\u1E84":"W","\u1E88":"W","\u2C72":"W","\u24CD":"X","\uFF38":"X","\u1E8A":"X","\u1E8C":"X","\u24CE":"Y","\uFF39":"Y","\u1EF2":"Y","\u00DD":"Y","\u0176":"Y","\u1EF8":"Y","\u0232":"Y","\u1E8E":"Y","\u0178":"Y","\u1EF6":"Y","\u1EF4":"Y","\u01B3":"Y","\u024E":"Y","\u1EFE":"Y","\u24CF":"Z","\uFF3A":"Z","\u0179":"Z","\u1E90":"Z","\u017B":"Z","\u017D":"Z","\u1E92":"Z","\u1E94":"Z","\u01B5":"Z","\u0224":"Z","\u2C7F":"Z","\u2C6B":"Z","\uA762":"Z","\u24D0":"a","\uFF41":"a","\u1E9A":"a","\u00E0":"a","\u00E1":"a","\u00E2":"a","\u1EA7":"a","\u1EA5":"a","\u1EAB":"a","\u1EA9":"a","\u00E3":"a","\u0101":"a","\u0103":"a","\u1EB1":"a","\u1EAF":"a","\u1EB5":"a","\u1EB3":"a","\u0227":"a","\u01E1":"a","\u00E4":"a","\u01DF":"a","\u1EA3":"a","\u00E5":"a","\u01FB":"a","\u01CE":"a","\u0201":"a","\u0203":"a","\u1EA1":"a","\u1EAD":"a","\u1EB7":"a","\u1E01":"a","\u0105":"a","\u2C65":"a","\u0250":"a","\uA733":"aa","\u00E6":"ae","\u01FD":"ae","\u01E3":"ae","\uA735":"ao","\uA737":"au","\uA739":"av","\uA73B":"av","\uA73D":"ay","\u24D1":"b","\uFF42":"b","\u1E03":"b","\u1E05":"b","\u1E07":"b","\u0180":"b","\u0183":"b","\u0253":"b","\u24D2":"c","\uFF43":"c","\u0107":"c","\u0109":"c","\u010B":"c","\u010D":"c","\u00E7":"c","\u1E09":"c","\u0188":"c","\u023C":"c","\uA73F":"c","\u2184":"c","\u24D3":"d","\uFF44":"d","\u1E0B":"d","\u010F":"d","\u1E0D":"d","\u1E11":"d","\u1E13":"d","\u1E0F":"d","\u0111":"d","\u018C":"d","\u0256":"d","\u0257":"d","\uA77A":"d","\u01F3":"dz","\u01C6":"dz","\u24D4":"e","\uFF45":"e","\u00E8":"e","\u00E9":"e","\u00EA":"e","\u1EC1":"e","\u1EBF":"e","\u1EC5":"e","\u1EC3":"e","\u1EBD":"e","\u0113":"e","\u1E15":"e","\u1E17":"e","\u0115":"e","\u0117":"e","\u00EB":"e","\u1EBB":"e","\u011B":"e","\u0205":"e","\u0207":"e","\u1EB9":"e","\u1EC7":"e","\u0229":"e","\u1E1D":"e","\u0119":"e","\u1E19":"e","\u1E1B":"e","\u0247":"e","\u025B":"e","\u01DD":"e","\u24D5":"f","\uFF46":"f","\u1E1F":"f","\u0192":"f","\uA77C":"f","\u24D6":"g","\uFF47":"g","\u01F5":"g","\u011D":"g","\u1E21":"g","\u011F":"g","\u0121":"g","\u01E7":"g","\u0123":"g","\u01E5":"g","\u0260":"g","\uA7A1":"g","\u1D79":"g","\uA77F":"g","\u24D7":"h","\uFF48":"h","\u0125":"h","\u1E23":"h","\u1E27":"h","\u021F":"h","\u1E25":"h","\u1E29":"h","\u1E2B":"h","\u1E96":"h","\u0127":"h","\u2C68":"h","\u2C76":"h","\u0265":"h","\u0195":"hv","\u24D8":"i","\uFF49":"i","\u00EC":"i","\u00ED":"i","\u00EE":"i","\u0129":"i","\u012B":"i","\u012D":"i","\u00EF":"i","\u1E2F":"i","\u1EC9":"i","\u01D0":"i","\u0209":"i","\u020B":"i","\u1ECB":"i","\u012F":"i","\u1E2D":"i","\u0268":"i","\u0131":"i","\u24D9":"j","\uFF4A":"j","\u0135":"j","\u01F0":"j","\u0249":"j","\u24DA":"k","\uFF4B":"k","\u1E31":"k","\u01E9":"k","\u1E33":"k","\u0137":"k","\u1E35":"k","\u0199":"k","\u2C6A":"k","\uA741":"k","\uA743":"k","\uA745":"k","\uA7A3":"k","\u24DB":"l","\uFF4C":"l","\u0140":"l","\u013A":"l","\u013E":"l","\u1E37":"l","\u1E39":"l","\u013C":"l","\u1E3D":"l","\u1E3B":"l","\u017F":"l","\u0142":"l","\u019A":"l","\u026B":"l","\u2C61":"l","\uA749":"l","\uA781":"l","\uA747":"l","\u01C9":"lj","\u24DC":"m","\uFF4D":"m","\u1E3F":"m","\u1E41":"m","\u1E43":"m","\u0271":"m","\u026F":"m","\u24DD":"n","\uFF4E":"n","\u01F9":"n","\u0144":"n","\u00F1":"n","\u1E45":"n","\u0148":"n","\u1E47":"n","\u0146":"n","\u1E4B":"n","\u1E49":"n","\u019E":"n","\u0272":"n","\u0149":"n","\uA791":"n","\uA7A5":"n","\u01CC":"nj","\u24DE":"o","\uFF4F":"o","\u00F2":"o","\u00F3":"o","\u00F4":"o","\u1ED3":"o","\u1ED1":"o","\u1ED7":"o","\u1ED5":"o","\u00F5":"o","\u1E4D":"o","\u022D":"o","\u1E4F":"o","\u014D":"o","\u1E51":"o","\u1E53":"o","\u014F":"o","\u022F":"o","\u0231":"o","\u00F6":"o","\u022B":"o","\u1ECF":"o","\u0151":"o","\u01D2":"o","\u020D":"o","\u020F":"o","\u01A1":"o","\u1EDD":"o","\u1EDB":"o","\u1EE1":"o","\u1EDF":"o","\u1EE3":"o","\u1ECD":"o","\u1ED9":"o","\u01EB":"o","\u01ED":"o","\u00F8":"o","\u01FF":"o","\u0254":"o","\uA74B":"o","\uA74D":"o","\u0275":"o","\u01A3":"oi","\u0223":"ou","\uA74F":"oo","\u24DF":"p","\uFF50":"p","\u1E55":"p","\u1E57":"p","\u01A5":"p","\u1D7D":"p","\uA751":"p","\uA753":"p","\uA755":"p","\u24E0":"q","\uFF51":"q","\u024B":"q","\uA757":"q","\uA759":"q","\u24E1":"r","\uFF52":"r","\u0155":"r","\u1E59":"r","\u0159":"r","\u0211":"r","\u0213":"r","\u1E5B":"r","\u1E5D":"r","\u0157":"r","\u1E5F":"r","\u024D":"r","\u027D":"r","\uA75B":"r","\uA7A7":"r","\uA783":"r","\u24E2":"s","\uFF53":"s","\u00DF":"s","\u015B":"s","\u1E65":"s","\u015D":"s","\u1E61":"s","\u0161":"s","\u1E67":"s","\u1E63":"s","\u1E69":"s","\u0219":"s","\u015F":"s","\u023F":"s","\uA7A9":"s","\uA785":"s","\u1E9B":"s","\u24E3":"t","\uFF54":"t","\u1E6B":"t","\u1E97":"t","\u0165":"t","\u1E6D":"t","\u021B":"t","\u0163":"t","\u1E71":"t","\u1E6F":"t","\u0167":"t","\u01AD":"t","\u0288":"t","\u2C66":"t","\uA787":"t","\uA729":"tz","\u24E4":"u","\uFF55":"u","\u00F9":"u","\u00FA":"u","\u00FB":"u","\u0169":"u","\u1E79":"u","\u016B":"u","\u1E7B":"u","\u016D":"u","\u00FC":"u","\u01DC":"u","\u01D8":"u","\u01D6":"u","\u01DA":"u","\u1EE7":"u","\u016F":"u","\u0171":"u","\u01D4":"u","\u0215":"u","\u0217":"u","\u01B0":"u","\u1EEB":"u","\u1EE9":"u","\u1EEF":"u","\u1EED":"u","\u1EF1":"u","\u1EE5":"u","\u1E73":"u","\u0173":"u","\u1E77":"u","\u1E75":"u","\u0289":"u","\u24E5":"v","\uFF56":"v","\u1E7D":"v","\u1E7F":"v","\u028B":"v","\uA75F":"v","\u028C":"v","\uA761":"vy","\u24E6":"w","\uFF57":"w","\u1E81":"w","\u1E83":"w","\u0175":"w","\u1E87":"w","\u1E85":"w","\u1E98":"w","\u1E89":"w","\u2C73":"w","\u24E7":"x","\uFF58":"x","\u1E8B":"x","\u1E8D":"x","\u24E8":"y","\uFF59":"y","\u1EF3":"y","\u00FD":"y","\u0177":"y","\u1EF9":"y","\u0233":"y","\u1E8F":"y","\u00FF":"y","\u1EF7":"y","\u1E99":"y","\u1EF5":"y","\u01B4":"y","\u024F":"y","\u1EFF":"y","\u24E9":"z","\uFF5A":"z","\u017A":"z","\u1E91":"z","\u017C":"z","\u017E":"z","\u1E93":"z","\u1E95":"z","\u01B6":"z","\u0225":"z","\u0240":"z","\u2C6C":"z","\uA763":"z"};
102

    
103
    $document = $(document);
104

    
105
    nextUid=(function() { var counter=1; return function() { return counter++; }; }());
106

    
107

    
108
    function stripDiacritics(str) {
109
        var ret, i, l, c;
110

    
111
        if (!str || str.length < 1) return str;
112

    
113
        ret = "";
114
        for (i = 0, l = str.length; i < l; i++) {
115
            c = str.charAt(i);
116
            ret += DIACRITICS[c] || c;
117
        }
118
        return ret;
119
    }
120

    
121
    function indexOf(value, array) {
122
        var i = 0, l = array.length;
123
        for (; i < l; i = i + 1) {
124
            if (equal(value, array[i])) return i;
125
        }
126
        return -1;
127
    }
128

    
129
    function measureScrollbar () {
130
        var $template = $( MEASURE_SCROLLBAR_TEMPLATE );
131
        $template.appendTo('body');
132

    
133
        var dim = {
134
            width: $template.width() - $template[0].clientWidth,
135
            height: $template.height() - $template[0].clientHeight
136
        };
137
        $template.remove();
138

    
139
        return dim;
140
    }
141

    
142
    /**
143
     * Compares equality of a and b
144
     * @param a
145
     * @param b
146
     */
147
    function equal(a, b) {
148
        if (a === b) return true;
149
        if (a === undefined || b === undefined) return false;
150
        if (a === null || b === null) return false;
151
        // Check whether 'a' or 'b' is a string (primitive or object).
152
        // The concatenation of an empty string (+'') converts its argument to a string's primitive.
153
        if (a.constructor === String) return a+'' === b+''; // a+'' - in case 'a' is a String object
154
        if (b.constructor === String) return b+'' === a+''; // b+'' - in case 'b' is a String object
155
        return false;
156
    }
157

    
158
    /**
159
     * Splits the string into an array of values, trimming each value. An empty array is returned for nulls or empty
160
     * strings
161
     * @param string
162
     * @param separator
163
     */
164
    function splitVal(string, separator) {
165
        var val, i, l;
166
        if (string === null || string.length < 1) return [];
167
        val = string.split(separator);
168
        for (i = 0, l = val.length; i < l; i = i + 1) val[i] = $.trim(val[i]);
169
        return val;
170
    }
171

    
172
    function getSideBorderPadding(element) {
173
        return element.outerWidth(false) - element.width();
174
    }
175

    
176
    function installKeyUpChangeEvent(element) {
177
        var key="keyup-change-value";
178
        element.on("keydown", function () {
179
            if ($.data(element, key) === undefined) {
180
                $.data(element, key, element.val());
181
            }
182
        });
183
        element.on("keyup", function () {
184
            var val= $.data(element, key);
185
            if (val !== undefined && element.val() !== val) {
186
                $.removeData(element, key);
187
                element.trigger("keyup-change");
188
            }
189
        });
190
    }
191

    
192
    $document.on("mousemove", function (e) {
193
        lastMousePosition.x = e.pageX;
194
        lastMousePosition.y = e.pageY;
195
    });
196

    
197
    /**
198
     * filters mouse events so an event is fired only if the mouse moved.
199
     *
200
     * filters out mouse events that occur when mouse is stationary but
201
     * the elements under the pointer are scrolled.
202
     */
203
    function installFilteredMouseMove(element) {
204
        element.on("mousemove", function (e) {
205
            var lastpos = lastMousePosition;
206
            if (lastpos === undefined || lastpos.x !== e.pageX || lastpos.y !== e.pageY) {
207
                $(e.target).trigger("mousemove-filtered", e);
208
            }
209
        });
210
    }
211

    
212
    /**
213
     * Debounces a function. Returns a function that calls the original fn function only if no invocations have been made
214
     * within the last quietMillis milliseconds.
215
     *
216
     * @param quietMillis number of milliseconds to wait before invoking fn
217
     * @param fn function to be debounced
218
     * @param ctx object to be used as this reference within fn
219
     * @return debounced version of fn
220
     */
221
    function debounce(quietMillis, fn, ctx) {
222
        ctx = ctx || undefined;
223
        var timeout;
224
        return function () {
225
            var args = arguments;
226
            window.clearTimeout(timeout);
227
            timeout = window.setTimeout(function() {
228
                fn.apply(ctx, args);
229
            }, quietMillis);
230
        };
231
    }
232

    
233
    /**
234
     * A simple implementation of a thunk
235
     * @param formula function used to lazily initialize the thunk
236
     * @return {Function}
237
     */
238
    function thunk(formula) {
239
        var evaluated = false,
240
            value;
241
        return function() {
242
            if (evaluated === false) { value = formula(); evaluated = true; }
243
            return value;
244
        };
245
    };
246

    
247
    function installDebouncedScroll(threshold, element) {
248
        var notify = debounce(threshold, function (e) { element.trigger("scroll-debounced", e);});
249
        element.on("scroll", function (e) {
250
            if (indexOf(e.target, element.get()) >= 0) notify(e);
251
        });
252
    }
253

    
254
    function focus($el) {
255
        if ($el[0] === document.activeElement) return;
256

    
257
        /* set the focus in a 0 timeout - that way the focus is set after the processing
258
            of the current event has finished - which seems like the only reliable way
259
            to set focus */
260
        window.setTimeout(function() {
261
            var el=$el[0], pos=$el.val().length, range;
262

    
263
            $el.focus();
264

    
265
            /* make sure el received focus so we do not error out when trying to manipulate the caret.
266
                sometimes modals or others listeners may steal it after its set */
267
            if ($el.is(":visible") && el === document.activeElement) {
268

    
269
                /* after the focus is set move the caret to the end, necessary when we val()
270
                    just before setting focus */
271
                if(el.setSelectionRange)
272
                {
273
                    el.setSelectionRange(pos, pos);
274
                }
275
                else if (el.createTextRange) {
276
                    range = el.createTextRange();
277
                    range.collapse(false);
278
                    range.select();
279
                }
280
            }
281
        }, 0);
282
    }
283

    
284
    function getCursorInfo(el) {
285
        el = $(el)[0];
286
        var offset = 0;
287
        var length = 0;
288
        if ('selectionStart' in el) {
289
            offset = el.selectionStart;
290
            length = el.selectionEnd - offset;
291
        } else if ('selection' in document) {
292
            el.focus();
293
            var sel = document.selection.createRange();
294
            length = document.selection.createRange().text.length;
295
            sel.moveStart('character', -el.value.length);
296
            offset = sel.text.length - length;
297
        }
298
        return { offset: offset, length: length };
299
    }
300

    
301
    function killEvent(event) {
302
        event.preventDefault();
303
        event.stopPropagation();
304
    }
305
    function killEventImmediately(event) {
306
        event.preventDefault();
307
        event.stopImmediatePropagation();
308
    }
309

    
310
    function measureTextWidth(e) {
311
        if (!sizer){
312
            var style = e[0].currentStyle || window.getComputedStyle(e[0], null);
313
            sizer = $(document.createElement("div")).css({
314
                position: "absolute",
315
                left: "-10000px",
316
                top: "-10000px",
317
                display: "none",
318
                fontSize: style.fontSize,
319
                fontFamily: style.fontFamily,
320
                fontStyle: style.fontStyle,
321
                fontWeight: style.fontWeight,
322
                letterSpacing: style.letterSpacing,
323
                textTransform: style.textTransform,
324
                whiteSpace: "nowrap"
325
            });
326
            sizer.attr("class","select2-sizer");
327
            $("body").append(sizer);
328
        }
329
        sizer.text(e.val());
330
        return sizer.width();
331
    }
332

    
333
    function syncCssClasses(dest, src, adapter) {
334
        var classes, replacements = [], adapted;
335

    
336
        classes = dest.attr("class");
337
        if (classes) {
338
            classes = '' + classes; // for IE which returns object
339
            $(classes.split(" ")).each2(function() {
340
                if (this.indexOf("select2-") === 0) {
341
                    replacements.push(this);
342
                }
343
            });
344
        }
345
        classes = src.attr("class");
346
        if (classes) {
347
            classes = '' + classes; // for IE which returns object
348
            $(classes.split(" ")).each2(function() {
349
                if (this.indexOf("select2-") !== 0) {
350
                    adapted = adapter(this);
351
                    if (adapted) {
352
                        replacements.push(adapted);
353
                    }
354
                }
355
            });
356
        }
357
        dest.attr("class", replacements.join(" "));
358
    }
359

    
360

    
361
    function markMatch(text, term, markup, escapeMarkup) {
362
        var match=stripDiacritics(text.toUpperCase()).indexOf(stripDiacritics(term.toUpperCase())),
363
            tl=term.length;
364

    
365
        if (match<0) {
366
            markup.push(escapeMarkup(text));
367
            return;
368
        }
369

    
370
        markup.push(escapeMarkup(text.substring(0, match)));
371
        markup.push("<span class='select2-match'>");
372
        markup.push(escapeMarkup(text.substring(match, match + tl)));
373
        markup.push("</span>");
374
        markup.push(escapeMarkup(text.substring(match + tl, text.length)));
375
    }
376

    
377
    function defaultEscapeMarkup(markup) {
378
        var replace_map = {
379
            '\\': '&#92;',
380
            '&': '&amp;',
381
            '<': '&lt;',
382
            '>': '&gt;',
383
            '"': '&quot;',
384
            "'": '&#39;',
385
            "/": '&#47;'
386
        };
387

    
388
        return String(markup).replace(/[&<>"'\/\\]/g, function (match) {
389
            return replace_map[match];
390
        });
391
    }
392

    
393
    /**
394
     * Produces an ajax-based query function
395
     *
396
     * @param options object containing configuration paramters
397
     * @param options.params parameter map for the transport ajax call, can contain such options as cache, jsonpCallback, etc. see $.ajax
398
     * @param options.transport function that will be used to execute the ajax request. must be compatible with parameters supported by $.ajax
399
     * @param options.url url for the data
400
     * @param options.data a function(searchTerm, pageNumber, context) that should return an object containing query string parameters for the above url.
401
     * @param options.dataType request data type: ajax, jsonp, other datatatypes supported by jQuery's $.ajax function or the transport function if specified
402
     * @param options.quietMillis (optional) milliseconds to wait before making the ajaxRequest, helps debounce the ajax function if invoked too often
403
     * @param options.results a function(remoteData, pageNumber) that converts data returned form the remote request to the format expected by Select2.
404
     *      The expected format is an object containing the following keys:
405
     *      results array of objects that will be used as choices
406
     *      more (optional) boolean indicating whether there are more results available
407
     *      Example: {results:[{id:1, text:'Red'},{id:2, text:'Blue'}], more:true}
408
     */
409
    function ajax(options) {
410
        var timeout, // current scheduled but not yet executed request
411
            handler = null,
412
            quietMillis = options.quietMillis || 100,
413
            ajaxUrl = options.url,
414
            self = this;
415

    
416
        return function (query) {
417
            window.clearTimeout(timeout);
418
            timeout = window.setTimeout(function () {
419
                var data = options.data, // ajax data function
420
                    url = ajaxUrl, // ajax url string or function
421
                    transport = options.transport || $.fn.select2.ajaxDefaults.transport,
422
                    // deprecated - to be removed in 4.0  - use params instead
423
                    deprecated = {
424
                        type: options.type || 'GET', // set type of request (GET or POST)
425
                        cache: options.cache || false,
426
                        jsonpCallback: options.jsonpCallback||undefined,
427
                        dataType: options.dataType||"json"
428
                    },
429
                    params = $.extend({}, $.fn.select2.ajaxDefaults.params, deprecated);
430

    
431
                data = data ? data.call(self, query.term, query.page, query.context) : null;
432
                url = (typeof url === 'function') ? url.call(self, query.term, query.page, query.context) : url;
433

    
434
                if (handler) { handler.abort(); }
435

    
436
                if (options.params) {
437
                    if ($.isFunction(options.params)) {
438
                        $.extend(params, options.params.call(self));
439
                    } else {
440
                        $.extend(params, options.params);
441
                    }
442
                }
443

    
444
                $.extend(params, {
445
                    url: url,
446
                    dataType: options.dataType,
447
                    data: data,
448
                    success: function (data) {
449
                        // TODO - replace query.page with query so users have access to term, page, etc.
450
                        var results = options.results(data, query.page);
451
                        query.callback(results);
452
                    }
453
                });
454
                handler = transport.call(self, params);
455
            }, quietMillis);
456
        };
457
    }
458

    
459
    /**
460
     * Produces a query function that works with a local array
461
     *
462
     * @param options object containing configuration parameters. The options parameter can either be an array or an
463
     * object.
464
     *
465
     * If the array form is used it is assumed that it contains objects with 'id' and 'text' keys.
466
     *
467
     * If the object form is used ti is assumed that it contains 'data' and 'text' keys. The 'data' key should contain
468
     * an array of objects that will be used as choices. These objects must contain at least an 'id' key. The 'text'
469
     * key can either be a String in which case it is expected that each element in the 'data' array has a key with the
470
     * value of 'text' which will be used to match choices. Alternatively, text can be a function(item) that can extract
471
     * the text.
472
     */
473
    function local(options) {
474
        var data = options, // data elements
475
            dataText,
476
            tmp,
477
            text = function (item) { return ""+item.text; }; // function used to retrieve the text portion of a data item that is matched against the search
478

    
479
         if ($.isArray(data)) {
480
            tmp = data;
481
            data = { results: tmp };
482
        }
483

    
484
         if ($.isFunction(data) === false) {
485
            tmp = data;
486
            data = function() { return tmp; };
487
        }
488

    
489
        var dataItem = data();
490
        if (dataItem.text) {
491
            text = dataItem.text;
492
            // if text is not a function we assume it to be a key name
493
            if (!$.isFunction(text)) {
494
                dataText = dataItem.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
495
                text = function (item) { return item[dataText]; };
496
            }
497
        }
498

    
499
        return function (query) {
500
            var t = query.term, filtered = { results: [] }, process;
501
            if (t === "") {
502
                query.callback(data());
503
                return;
504
            }
505

    
506
            process = function(datum, collection) {
507
                var group, attr;
508
                datum = datum[0];
509
                if (datum.children) {
510
                    group = {};
511
                    for (attr in datum) {
512
                        if (datum.hasOwnProperty(attr)) group[attr]=datum[attr];
513
                    }
514
                    group.children=[];
515
                    $(datum.children).each2(function(i, childDatum) { process(childDatum, group.children); });
516
                    if (group.children.length || query.matcher(t, text(group), datum)) {
517
                        collection.push(group);
518
                    }
519
                } else {
520
                    if (query.matcher(t, text(datum), datum)) {
521
                        collection.push(datum);
522
                    }
523
                }
524
            };
525

    
526
            $(data().results).each2(function(i, datum) { process(datum, filtered.results); });
527
            query.callback(filtered);
528
        };
529
    }
530

    
531
    // TODO javadoc
532
    function tags(data) {
533
        var isFunc = $.isFunction(data);
534
        return function (query) {
535
            var t = query.term, filtered = {results: []};
536
            $(isFunc ? data() : data).each(function () {
537
                var isObject = this.text !== undefined,
538
                    text = isObject ? this.text : this;
539
                if (t === "" || query.matcher(t, text)) {
540
                    filtered.results.push(isObject ? this : {id: this, text: this});
541
                }
542
            });
543
            query.callback(filtered);
544
        };
545
    }
546

    
547
    /**
548
     * Checks if the formatter function should be used.
549
     *
550
     * Throws an error if it is not a function. Returns true if it should be used,
551
     * false if no formatting should be performed.
552
     *
553
     * @param formatter
554
     */
555
    function checkFormatter(formatter, formatterName) {
556
        if ($.isFunction(formatter)) return true;
557
        if (!formatter) return false;
558
        throw new Error(formatterName +" must be a function or a falsy value");
559
    }
560

    
561
    function evaluate(val) {
562
        return $.isFunction(val) ? val() : val;
563
    }
564

    
565
    function countResults(results) {
566
        var count = 0;
567
        $.each(results, function(i, item) {
568
            if (item.children) {
569
                count += countResults(item.children);
570
            } else {
571
                count++;
572
            }
573
        });
574
        return count;
575
    }
576

    
577
    /**
578
     * Default tokenizer. This function uses breaks the input on substring match of any string from the
579
     * opts.tokenSeparators array and uses opts.createSearchChoice to create the choice object. Both of those
580
     * two options have to be defined in order for the tokenizer to work.
581
     *
582
     * @param input text user has typed so far or pasted into the search field
583
     * @param selection currently selected choices
584
     * @param selectCallback function(choice) callback tho add the choice to selection
585
     * @param opts select2's opts
586
     * @return undefined/null to leave the current input unchanged, or a string to change the input to the returned value
587
     */
588
    function defaultTokenizer(input, selection, selectCallback, opts) {
589
        var original = input, // store the original so we can compare and know if we need to tell the search to update its text
590
            dupe = false, // check for whether a token we extracted represents a duplicate selected choice
591
            token, // token
592
            index, // position at which the separator was found
593
            i, l, // looping variables
594
            separator; // the matched separator
595

    
596
        if (!opts.createSearchChoice || !opts.tokenSeparators || opts.tokenSeparators.length < 1) return undefined;
597

    
598
        while (true) {
599
            index = -1;
600

    
601
            for (i = 0, l = opts.tokenSeparators.length; i < l; i++) {
602
                separator = opts.tokenSeparators[i];
603
                index = input.indexOf(separator);
604
                if (index >= 0) break;
605
            }
606

    
607
            if (index < 0) break; // did not find any token separator in the input string, bail
608

    
609
            token = input.substring(0, index);
610
            input = input.substring(index + separator.length);
611

    
612
            if (token.length > 0) {
613
                token = opts.createSearchChoice.call(this, token, selection);
614
                if (token !== undefined && token !== null && opts.id(token) !== undefined && opts.id(token) !== null) {
615
                    dupe = false;
616
                    for (i = 0, l = selection.length; i < l; i++) {
617
                        if (equal(opts.id(token), opts.id(selection[i]))) {
618
                            dupe = true; break;
619
                        }
620
                    }
621

    
622
                    if (!dupe) selectCallback(token);
623
                }
624
            }
625
        }
626

    
627
        if (original!==input) return input;
628
    }
629

    
630
    /**
631
     * Creates a new class
632
     *
633
     * @param superClass
634
     * @param methods
635
     */
636
    function clazz(SuperClass, methods) {
637
        var constructor = function () {};
638
        constructor.prototype = new SuperClass;
639
        constructor.prototype.constructor = constructor;
640
        constructor.prototype.parent = SuperClass.prototype;
641
        constructor.prototype = $.extend(constructor.prototype, methods);
642
        return constructor;
643
    }
644

    
645
    AbstractSelect2 = clazz(Object, {
646

    
647
        // abstract
648
        bind: function (func) {
649
            var self = this;
650
            return function () {
651
                func.apply(self, arguments);
652
            };
653
        },
654

    
655
        // abstract
656
        init: function (opts) {
657
            var results, search, resultsSelector = ".select2-results";
658

    
659
            // prepare options
660
            this.opts = opts = this.prepareOpts(opts);
661

    
662
            this.id=opts.id;
663

    
664
            // destroy if called on an existing component
665
            if (opts.element.data("select2") !== undefined &&
666
                opts.element.data("select2") !== null) {
667
                opts.element.data("select2").destroy();
668
            }
669

    
670
            this.container = this.createContainer();
671

    
672
            this.containerId="s2id_"+(opts.element.attr("id") || "autogen"+nextUid());
673
            this.containerSelector="#"+this.containerId.replace(/([;&,\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '\\$1');
674
            this.container.attr("id", this.containerId);
675

    
676
            // cache the body so future lookups are cheap
677
            this.body = thunk(function() { return opts.element.closest("body"); });
678

    
679
            syncCssClasses(this.container, this.opts.element, this.opts.adaptContainerCssClass);
680

    
681
            this.container.attr("style", opts.element.attr("style"));
682
            this.container.css(evaluate(opts.containerCss));
683
            this.container.addClass(evaluate(opts.containerCssClass));
684

    
685
            this.elementTabIndex = this.opts.element.attr("tabindex");
686

    
687
            // swap container for the element
688
            this.opts.element
689
                .data("select2", this)
690
                .attr("tabindex", "-1")
691
                .before(this.container)
692
                .on("click.select2", killEvent); // do not leak click events
693

    
694
            this.container.data("select2", this);
695

    
696
            this.dropdown = this.container.find(".select2-drop");
697

    
698
            syncCssClasses(this.dropdown, this.opts.element, this.opts.adaptDropdownCssClass);
699

    
700
            this.dropdown.addClass(evaluate(opts.dropdownCssClass));
701
            this.dropdown.data("select2", this);
702
            this.dropdown.on("click", killEvent);
703

    
704
            this.results = results = this.container.find(resultsSelector);
705
            this.search = search = this.container.find("input.select2-input");
706

    
707
            this.queryCount = 0;
708
            this.resultsPage = 0;
709
            this.context = null;
710

    
711
            // initialize the container
712
            this.initContainer();
713

    
714
            this.container.on("click", killEvent);
715

    
716
            installFilteredMouseMove(this.results);
717
            this.dropdown.on("mousemove-filtered touchstart touchmove touchend", resultsSelector, this.bind(this.highlightUnderEvent));
718

    
719
            installDebouncedScroll(80, this.results);
720
            this.dropdown.on("scroll-debounced", resultsSelector, this.bind(this.loadMoreIfNeeded));
721

    
722
            // do not propagate change event from the search field out of the component
723
            $(this.container).on("change", ".select2-input", function(e) {e.stopPropagation();});
724
            $(this.dropdown).on("change", ".select2-input", function(e) {e.stopPropagation();});
725

    
726
            // if jquery.mousewheel plugin is installed we can prevent out-of-bounds scrolling of results via mousewheel
727
            if ($.fn.mousewheel) {
728
                results.mousewheel(function (e, delta, deltaX, deltaY) {
729
                    var top = results.scrollTop();
730
                    if (deltaY > 0 && top - deltaY <= 0) {
731
                        results.scrollTop(0);
732
                        killEvent(e);
733
                    } else if (deltaY < 0 && results.get(0).scrollHeight - results.scrollTop() + deltaY <= results.height()) {
734
                        results.scrollTop(results.get(0).scrollHeight - results.height());
735
                        killEvent(e);
736
                    }
737
                });
738
            }
739

    
740
            installKeyUpChangeEvent(search);
741
            search.on("keyup-change input paste", this.bind(this.updateResults));
742
            search.on("focus", function () { search.addClass("select2-focused"); });
743
            search.on("blur", function () { search.removeClass("select2-focused");});
744

    
745
            this.dropdown.on("mouseup", resultsSelector, this.bind(function (e) {
746
                if ($(e.target).closest(".select2-result-selectable").length > 0) {
747
                    this.highlightUnderEvent(e);
748
                    this.selectHighlighted(e);
749
                }
750
            }));
751

    
752
            // trap all mouse events from leaving the dropdown. sometimes there may be a modal that is listening
753
            // for mouse events outside of itself so it can close itself. since the dropdown is now outside the select2's
754
            // dom it will trigger the popup close, which is not what we want
755
            this.dropdown.on("click mouseup mousedown", function (e) { e.stopPropagation(); });
756

    
757
            if ($.isFunction(this.opts.initSelection)) {
758
                // initialize selection based on the current value of the source element
759
                this.initSelection();
760

    
761
                // if the user has provided a function that can set selection based on the value of the source element
762
                // we monitor the change event on the element and trigger it, allowing for two way synchronization
763
                this.monitorSource();
764
            }
765

    
766
            if (opts.maximumInputLength !== null) {
767
                this.search.attr("maxlength", opts.maximumInputLength);
768
            }
769

    
770
            var disabled = opts.element.prop("disabled");
771
            if (disabled === undefined) disabled = false;
772
            this.enable(!disabled);
773

    
774
            var readonly = opts.element.prop("readonly");
775
            if (readonly === undefined) readonly = false;
776
            this.readonly(readonly);
777

    
778
            // Calculate size of scrollbar
779
            scrollBarDimensions = scrollBarDimensions || measureScrollbar();
780

    
781
            this.autofocus = opts.element.prop("autofocus");
782
            opts.element.prop("autofocus", false);
783
            if (this.autofocus) this.focus();
784

    
785
            this.nextSearchTerm = undefined;
786
        },
787

    
788
        // abstract
789
        destroy: function () {
790
            var element=this.opts.element, select2 = element.data("select2");
791

    
792
            this.close();
793

    
794
            if (this.propertyObserver) { delete this.propertyObserver; this.propertyObserver = null; }
795

    
796
            if (select2 !== undefined) {
797
                select2.container.remove();
798
                select2.dropdown.remove();
799
                element
800
                    .removeClass("select2-offscreen")
801
                    .removeData("select2")
802
                    .off(".select2")
803
                    .prop("autofocus", this.autofocus || false);
804
                if (this.elementTabIndex) {
805
                    element.attr({tabindex: this.elementTabIndex});
806
                } else {
807
                    element.removeAttr("tabindex");
808
                }
809
                element.show();
810
            }
811
        },
812

    
813
        // abstract
814
        optionToData: function(element) {
815
            if (element.is("option")) {
816
                return {
817
                    id:element.prop("value"),
818
                    text:element.text(),
819
                    element: element.get(),
820
                    css: element.attr("class"),
821
                    disabled: element.prop("disabled"),
822
                    locked: equal(element.attr("locked"), "locked") || equal(element.data("locked"), true)
823
                };
824
            } else if (element.is("optgroup")) {
825
                return {
826
                    text:element.attr("label"),
827
                    children:[],
828
                    element: element.get(),
829
                    css: element.attr("class")
830
                };
831
            }
832
        },
833

    
834
        // abstract
835
        prepareOpts: function (opts) {
836
            var element, select, idKey, ajaxUrl, self = this;
837

    
838
            element = opts.element;
839

    
840
            if (element.get(0).tagName.toLowerCase() === "select") {
841
                this.select = select = opts.element;
842
            }
843

    
844
            if (select) {
845
                // these options are not allowed when attached to a select because they are picked up off the element itself
846
                $.each(["id", "multiple", "ajax", "query", "createSearchChoice", "initSelection", "data", "tags"], function () {
847
                    if (this in opts) {
848
                        throw new Error("Option '" + this + "' is not allowed for Select2 when attached to a <select> element.");
849
                    }
850
                });
851
            }
852

    
853
            opts = $.extend({}, {
854
                populateResults: function(container, results, query) {
855
                    var populate, id=this.opts.id;
856

    
857
                    populate=function(results, container, depth) {
858

    
859
                        var i, l, result, selectable, disabled, compound, node, label, innerContainer, formatted;
860

    
861
                        results = opts.sortResults(results, container, query);
862

    
863
                        for (i = 0, l = results.length; i < l; i = i + 1) {
864

    
865
                            result=results[i];
866

    
867
                            disabled = (result.disabled === true);
868
                            selectable = (!disabled) && (id(result) !== undefined);
869

    
870
                            compound=result.children && result.children.length > 0;
871

    
872
                            node=$("<li></li>");
873
                            node.addClass("select2-results-dept-"+depth);
874
                            node.addClass("select2-result");
875
                            node.addClass(selectable ? "select2-result-selectable" : "select2-result-unselectable");
876
                            if (disabled) { node.addClass("select2-disabled"); }
877
                            if (compound) { node.addClass("select2-result-with-children"); }
878
                            node.addClass(self.opts.formatResultCssClass(result));
879

    
880
                            label=$(document.createElement("div"));
881
                            label.addClass("select2-result-label");
882

    
883
                            formatted=opts.formatResult(result, label, query, self.opts.escapeMarkup);
884
                            if (formatted!==undefined) {
885
                                label.html(formatted);
886
                            }
887

    
888
                            node.append(label);
889

    
890
                            if (compound) {
891

    
892
                                innerContainer=$("<ul></ul>");
893
                                innerContainer.addClass("select2-result-sub");
894
                                populate(result.children, innerContainer, depth+1);
895
                                node.append(innerContainer);
896
                            }
897

    
898
                            node.data("select2-data", result);
899
                            container.append(node);
900
                        }
901
                    };
902

    
903
                    populate(results, container, 0);
904
                }
905
            }, $.fn.select2.defaults, opts);
906

    
907
            if (typeof(opts.id) !== "function") {
908
                idKey = opts.id;
909
                opts.id = function (e) { return e[idKey]; };
910
            }
911

    
912
            if ($.isArray(opts.element.data("select2Tags"))) {
913
                if ("tags" in opts) {
914
                    throw "tags specified as both an attribute 'data-select2-tags' and in options of Select2 " + opts.element.attr("id");
915
                }
916
                opts.tags=opts.element.data("select2Tags");
917
            }
918

    
919
            if (select) {
920
                opts.query = this.bind(function (query) {
921
                    var data = { results: [], more: false },
922
                        term = query.term,
923
                        children, placeholderOption, process;
924

    
925
                    process=function(element, collection) {
926
                        var group;
927
                        if (element.is("option")) {
928
                            if (query.matcher(term, element.text(), element)) {
929
                                collection.push(self.optionToData(element));
930
                            }
931
                        } else if (element.is("optgroup")) {
932
                            group=self.optionToData(element);
933
                            element.children().each2(function(i, elm) { process(elm, group.children); });
934
                            if (group.children.length>0) {
935
                                collection.push(group);
936
                            }
937
                        }
938
                    };
939

    
940
                    children=element.children();
941

    
942
                    // ignore the placeholder option if there is one
943
                    if (this.getPlaceholder() !== undefined && children.length > 0) {
944
                        placeholderOption = this.getPlaceholderOption();
945
                        if (placeholderOption) {
946
                            children=children.not(placeholderOption);
947
                        }
948
                    }
949

    
950
                    children.each2(function(i, elm) { process(elm, data.results); });
951

    
952
                    query.callback(data);
953
                });
954
                // this is needed because inside val() we construct choices from options and there id is hardcoded
955
                opts.id=function(e) { return e.id; };
956
                opts.formatResultCssClass = function(data) { return data.css; };
957
            } else {
958
                if (!("query" in opts)) {
959

    
960
                    if ("ajax" in opts) {
961
                        ajaxUrl = opts.element.data("ajax-url");
962
                        if (ajaxUrl && ajaxUrl.length > 0) {
963
                            opts.ajax.url = ajaxUrl;
964
                        }
965
                        opts.query = ajax.call(opts.element, opts.ajax);
966
                    } else if ("data" in opts) {
967
                        opts.query = local(opts.data);
968
                    } else if ("tags" in opts) {
969
                        opts.query = tags(opts.tags);
970
                        if (opts.createSearchChoice === undefined) {
971
                            opts.createSearchChoice = function (term) { return {id: $.trim(term), text: $.trim(term)}; };
972
                        }
973
                        if (opts.initSelection === undefined) {
974
                            opts.initSelection = function (element, callback) {
975
                                var data = [];
976
                                $(splitVal(element.val(), opts.separator)).each(function () {
977
                                    var obj = { id: this, text: this },
978
                                        tags = opts.tags;
979
                                    if ($.isFunction(tags)) tags=tags();
980
                                    $(tags).each(function() { if (equal(this.id, obj.id)) { obj = this; return false; } });
981
                                    data.push(obj);
982
                                });
983

    
984
                                callback(data);
985
                            };
986
                        }
987
                    }
988
                }
989
            }
990
            if (typeof(opts.query) !== "function") {
991
                throw "query function not defined for Select2 " + opts.element.attr("id");
992
            }
993

    
994
            return opts;
995
        },
996

    
997
        /**
998
         * Monitor the original element for changes and update select2 accordingly
999
         */
1000
        // abstract
1001
        monitorSource: function () {
1002
            var el = this.opts.element, sync, observer;
1003

    
1004
            el.on("change.select2", this.bind(function (e) {
1005
                if (this.opts.element.data("select2-change-triggered") !== true) {
1006
                    this.initSelection();
1007
                }
1008
            }));
1009

    
1010
            sync = this.bind(function () {
1011

    
1012
                // sync enabled state
1013
                var disabled = el.prop("disabled");
1014
                if (disabled === undefined) disabled = false;
1015
                this.enable(!disabled);
1016

    
1017
                var readonly = el.prop("readonly");
1018
                if (readonly === undefined) readonly = false;
1019
                this.readonly(readonly);
1020

    
1021
                syncCssClasses(this.container, this.opts.element, this.opts.adaptContainerCssClass);
1022
                this.container.addClass(evaluate(this.opts.containerCssClass));
1023

    
1024
                syncCssClasses(this.dropdown, this.opts.element, this.opts.adaptDropdownCssClass);
1025
                this.dropdown.addClass(evaluate(this.opts.dropdownCssClass));
1026

    
1027
            });
1028

    
1029
            // IE8-10
1030
            el.on("propertychange.select2", sync);
1031

    
1032
            // hold onto a reference of the callback to work around a chromium bug
1033
            if (this.mutationCallback === undefined) {
1034
                this.mutationCallback = function (mutations) {
1035
                    mutations.forEach(sync);
1036
                }
1037
            }
1038

    
1039
            // safari, chrome, firefox, IE11
1040
            observer = window.MutationObserver || window.WebKitMutationObserver|| window.MozMutationObserver;
1041
            if (observer !== undefined) {
1042
                if (this.propertyObserver) { delete this.propertyObserver; this.propertyObserver = null; }
1043
                this.propertyObserver = new observer(this.mutationCallback);
1044
                this.propertyObserver.observe(el.get(0), { attributes:true, subtree:false });
1045
            }
1046
        },
1047

    
1048
        // abstract
1049
        triggerSelect: function(data) {
1050
            var evt = $.Event("select2-selecting", { val: this.id(data), object: data });
1051
            this.opts.element.trigger(evt);
1052
            return !evt.isDefaultPrevented();
1053
        },
1054

    
1055
        /**
1056
         * Triggers the change event on the source element
1057
         */
1058
        // abstract
1059
        triggerChange: function (details) {
1060

    
1061
            details = details || {};
1062
            details= $.extend({}, details, { type: "change", val: this.val() });
1063
            // prevents recursive triggering
1064
            this.opts.element.data("select2-change-triggered", true);
1065
            this.opts.element.trigger(details);
1066
            this.opts.element.data("select2-change-triggered", false);
1067

    
1068
            // some validation frameworks ignore the change event and listen instead to keyup, click for selects
1069
            // so here we trigger the click event manually
1070
            this.opts.element.click();
1071

    
1072
            // ValidationEngine ignorea the change event and listens instead to blur
1073
            // so here we trigger the blur event manually if so desired
1074
            if (this.opts.blurOnChange)
1075
                this.opts.element.blur();
1076
        },
1077

    
1078
        //abstract
1079
        isInterfaceEnabled: function()
1080
        {
1081
            return this.enabledInterface === true;
1082
        },
1083

    
1084
        // abstract
1085
        enableInterface: function() {
1086
            var enabled = this._enabled && !this._readonly,
1087
                disabled = !enabled;
1088

    
1089
            if (enabled === this.enabledInterface) return false;
1090

    
1091
            this.container.toggleClass("select2-container-disabled", disabled);
1092
            this.close();
1093
            this.enabledInterface = enabled;
1094

    
1095
            return true;
1096
        },
1097

    
1098
        // abstract
1099
        enable: function(enabled) {
1100
            if (enabled === undefined) enabled = true;
1101
            if (this._enabled === enabled) return;
1102
            this._enabled = enabled;
1103

    
1104
            this.opts.element.prop("disabled", !enabled);
1105
            this.enableInterface();
1106
        },
1107

    
1108
        // abstract
1109
        disable: function() {
1110
            this.enable(false);
1111
        },
1112

    
1113
        // abstract
1114
        readonly: function(enabled) {
1115
            if (enabled === undefined) enabled = false;
1116
            if (this._readonly === enabled) return false;
1117
            this._readonly = enabled;
1118

    
1119
            this.opts.element.prop("readonly", enabled);
1120
            this.enableInterface();
1121
            return true;
1122
        },
1123

    
1124
        // abstract
1125
        opened: function () {
1126
            return this.container.hasClass("select2-dropdown-open");
1127
        },
1128

    
1129
        // abstract
1130
        positionDropdown: function() {
1131
            var $dropdown = this.dropdown,
1132
                offset = this.container.offset(),
1133
                height = this.container.outerHeight(false),
1134
                width = this.container.outerWidth(false),
1135
                dropHeight = $dropdown.outerHeight(false),
1136
                $window = $(window),
1137
                windowWidth = $window.width(),
1138
                windowHeight = $window.height(),
1139
                viewPortRight = $window.scrollLeft() + windowWidth,
1140
                viewportBottom = $window.scrollTop() + windowHeight,
1141
                dropTop = offset.top + height,
1142
                dropLeft = offset.left,
1143
                enoughRoomBelow = dropTop + dropHeight <= viewportBottom,
1144
                enoughRoomAbove = (offset.top - dropHeight) >= this.body().scrollTop(),
1145
                dropWidth = $dropdown.outerWidth(false),
1146
                enoughRoomOnRight = dropLeft + dropWidth <= viewPortRight,
1147
                aboveNow = $dropdown.hasClass("select2-drop-above"),
1148
                bodyOffset,
1149
                above,
1150
                changeDirection,
1151
                css,
1152
                resultsListNode;
1153

    
1154
            // always prefer the current above/below alignment, unless there is not enough room
1155
            if (aboveNow) {
1156
                above = true;
1157
                if (!enoughRoomAbove && enoughRoomBelow) {
1158
                    changeDirection = true;
1159
                    above = false;
1160
                }
1161
            } else {
1162
                above = false;
1163
                if (!enoughRoomBelow && enoughRoomAbove) {
1164
                    changeDirection = true;
1165
                    above = true;
1166
                }
1167
            }
1168

    
1169
            //if we are changing direction we need to get positions when dropdown is hidden;
1170
            if (changeDirection) {
1171
                $dropdown.hide();
1172
                offset = this.container.offset();
1173
                height = this.container.outerHeight(false);
1174
                width = this.container.outerWidth(false);
1175
                dropHeight = $dropdown.outerHeight(false);
1176
                viewPortRight = $window.scrollLeft() + windowWidth;
1177
                viewportBottom = $window.scrollTop() + windowHeight;
1178
                dropTop = offset.top + height;
1179
                dropLeft = offset.left;
1180
                dropWidth = $dropdown.outerWidth(false);
1181
                enoughRoomOnRight = dropLeft + dropWidth <= viewPortRight;
1182
                $dropdown.show();
1183
            }
1184

    
1185
            if (this.opts.dropdownAutoWidth) {
1186
                resultsListNode = $('.select2-results', $dropdown)[0];
1187
                $dropdown.addClass('select2-drop-auto-width');
1188
                $dropdown.css('width', '');
1189
                // Add scrollbar width to dropdown if vertical scrollbar is present
1190
                dropWidth = $dropdown.outerWidth(false) + (resultsListNode.scrollHeight === resultsListNode.clientHeight ? 0 : scrollBarDimensions.width);
1191
                dropWidth > width ? width = dropWidth : dropWidth = width;
1192
                enoughRoomOnRight = dropLeft + dropWidth <= viewPortRight;
1193
            }
1194
            else {
1195
                this.container.removeClass('select2-drop-auto-width');
1196
            }
1197

    
1198
            //console.log("below/ droptop:", dropTop, "dropHeight", dropHeight, "sum", (dropTop+dropHeight)+" viewport bottom", viewportBottom, "enough?", enoughRoomBelow);
1199
            //console.log("above/ offset.top", offset.top, "dropHeight", dropHeight, "top", (offset.top-dropHeight), "scrollTop", this.body().scrollTop(), "enough?", enoughRoomAbove);
1200

    
1201
            // fix positioning when body has an offset and is not position: static
1202
            if (this.body().css('position') !== 'static') {
1203
                bodyOffset = this.body().offset();
1204
                dropTop -= bodyOffset.top;
1205
                dropLeft -= bodyOffset.left;
1206
            }
1207

    
1208
            if (!enoughRoomOnRight) {
1209
               dropLeft = offset.left + width - dropWidth;
1210
            }
1211

    
1212
            css =  {
1213
                left: dropLeft,
1214
                width: width
1215
            };
1216

    
1217
            if (above) {
1218
                css.bottom = windowHeight - offset.top;
1219
                css.top = 'auto';
1220
                this.container.addClass("select2-drop-above");
1221
                $dropdown.addClass("select2-drop-above");
1222
            }
1223
            else {
1224
                css.top = dropTop;
1225
                css.bottom = 'auto';
1226
                this.container.removeClass("select2-drop-above");
1227
                $dropdown.removeClass("select2-drop-above");
1228
            }
1229
            css = $.extend(css, evaluate(this.opts.dropdownCss));
1230

    
1231
            $dropdown.css(css);
1232
        },
1233

    
1234
        // abstract
1235
        shouldOpen: function() {
1236
            var event;
1237

    
1238
            if (this.opened()) return false;
1239

    
1240
            if (this._enabled === false || this._readonly === true) return false;
1241

    
1242
            event = $.Event("select2-opening");
1243
            this.opts.element.trigger(event);
1244
            return !event.isDefaultPrevented();
1245
        },
1246

    
1247
        // abstract
1248
        clearDropdownAlignmentPreference: function() {
1249
            // clear the classes used to figure out the preference of where the dropdown should be opened
1250
            this.container.removeClass("select2-drop-above");
1251
            this.dropdown.removeClass("select2-drop-above");
1252
        },
1253

    
1254
        /**
1255
         * Opens the dropdown
1256
         *
1257
         * @return {Boolean} whether or not dropdown was opened. This method will return false if, for example,
1258
         * the dropdown is already open, or if the 'open' event listener on the element called preventDefault().
1259
         */
1260
        // abstract
1261
        open: function () {
1262

    
1263
            if (!this.shouldOpen()) return false;
1264

    
1265
            this.opening();
1266

    
1267
            return true;
1268
        },
1269

    
1270
        /**
1271
         * Performs the opening of the dropdown
1272
         */
1273
        // abstract
1274
        opening: function() {
1275
            var cid = this.containerId,
1276
                scroll = "scroll." + cid,
1277
                resize = "resize."+cid,
1278
                orient = "orientationchange."+cid,
1279
                mask;
1280

    
1281
            this.container.addClass("select2-dropdown-open").addClass("select2-container-active");
1282

    
1283
            this.clearDropdownAlignmentPreference();
1284

    
1285
            if(this.dropdown[0] !== this.body().children().last()[0]) {
1286
                this.dropdown.detach().appendTo(this.body());
1287
            }
1288

    
1289
            // create the dropdown mask if doesnt already exist
1290
            mask = $("#select2-drop-mask");
1291
            if (mask.length == 0) {
1292
                mask = $(document.createElement("div"));
1293
                mask.attr("id","select2-drop-mask").attr("class","select2-drop-mask");
1294
                mask.hide();
1295
                mask.appendTo(this.body());
1296
                mask.on("mousedown touchstart click", function (e) {
1297
                    var dropdown = $("#select2-drop"), self;
1298
                    if (dropdown.length > 0) {
1299
                        self=dropdown.data("select2");
1300
                        if (self.opts.selectOnBlur) {
1301
                            self.selectHighlighted({noFocus: true});
1302
                        }
1303
                        self.close({focus:true});
1304
                        e.preventDefault();
1305
                        e.stopPropagation();
1306
                    }
1307
                });
1308
            }
1309

    
1310
            // ensure the mask is always right before the dropdown
1311
            if (this.dropdown.prev()[0] !== mask[0]) {
1312
                this.dropdown.before(mask);
1313
            }
1314

    
1315
            // move the global id to the correct dropdown
1316
            $("#select2-drop").removeAttr("id");
1317
            this.dropdown.attr("id", "select2-drop");
1318

    
1319
            // show the elements
1320
            mask.show();
1321

    
1322
            this.positionDropdown();
1323
            this.dropdown.show();
1324
            this.positionDropdown();
1325

    
1326
            this.dropdown.addClass("select2-drop-active");
1327

    
1328
            // attach listeners to events that can change the position of the container and thus require
1329
            // the position of the dropdown to be updated as well so it does not come unglued from the container
1330
            var that = this;
1331
            this.container.parents().add(window).each(function () {
1332
                $(this).on(resize+" "+scroll+" "+orient, function (e) {
1333
                    that.positionDropdown();
1334
                });
1335
            });
1336

    
1337

    
1338
        },
1339

    
1340
        // abstract
1341
        close: function () {
1342
            if (!this.opened()) return;
1343

    
1344
            var cid = this.containerId,
1345
                scroll = "scroll." + cid,
1346
                resize = "resize."+cid,
1347
                orient = "orientationchange."+cid;
1348

    
1349
            // unbind event listeners
1350
            this.container.parents().add(window).each(function () { $(this).off(scroll).off(resize).off(orient); });
1351

    
1352
            this.clearDropdownAlignmentPreference();
1353

    
1354
            $("#select2-drop-mask").hide();
1355
            this.dropdown.removeAttr("id"); // only the active dropdown has the select2-drop id
1356
            this.dropdown.hide();
1357
            this.container.removeClass("select2-dropdown-open").removeClass("select2-container-active");
1358
            this.results.empty();
1359

    
1360

    
1361
            this.clearSearch();
1362
            this.search.removeClass("select2-active");
1363
            this.opts.element.trigger($.Event("select2-close"));
1364
        },
1365

    
1366
        /**
1367
         * Opens control, sets input value, and updates results.
1368
         */
1369
        // abstract
1370
        externalSearch: function (term) {
1371
            this.open();
1372
            this.search.val(term);
1373
            this.updateResults(false);
1374
        },
1375

    
1376
        // abstract
1377
        clearSearch: function () {
1378

    
1379
        },
1380

    
1381
        //abstract
1382
        getMaximumSelectionSize: function() {
1383
            return evaluate(this.opts.maximumSelectionSize);
1384
        },
1385

    
1386
        // abstract
1387
        ensureHighlightVisible: function () {
1388
            var results = this.results, children, index, child, hb, rb, y, more;
1389

    
1390
            index = this.highlight();
1391

    
1392
            if (index < 0) return;
1393

    
1394
            if (index == 0) {
1395

    
1396
                // if the first element is highlighted scroll all the way to the top,
1397
                // that way any unselectable headers above it will also be scrolled
1398
                // into view
1399

    
1400
                results.scrollTop(0);
1401
                return;
1402
            }
1403

    
1404
            children = this.findHighlightableChoices().find('.select2-result-label');
1405

    
1406
            child = $(children[index]);
1407

    
1408
            hb = child.offset().top + child.outerHeight(true);
1409

    
1410
            // if this is the last child lets also make sure select2-more-results is visible
1411
            if (index === children.length - 1) {
1412
                more = results.find("li.select2-more-results");
1413
                if (more.length > 0) {
1414
                    hb = more.offset().top + more.outerHeight(true);
1415
                }
1416
            }
1417

    
1418
            rb = results.offset().top + results.outerHeight(true);
1419
            if (hb > rb) {
1420
                results.scrollTop(results.scrollTop() + (hb - rb));
1421
            }
1422
            y = child.offset().top - results.offset().top;
1423

    
1424
            // make sure the top of the element is visible
1425
            if (y < 0 && child.css('display') != 'none' ) {
1426
                results.scrollTop(results.scrollTop() + y); // y is negative
1427
            }
1428
        },
1429

    
1430
        // abstract
1431
        findHighlightableChoices: function() {
1432
            return this.results.find(".select2-result-selectable:not(.select2-disabled, .select2-selected)");
1433
        },
1434

    
1435
        // abstract
1436
        moveHighlight: function (delta) {
1437
            var choices = this.findHighlightableChoices(),
1438
                index = this.highlight();
1439

    
1440
            while (index > -1 && index < choices.length) {
1441
                index += delta;
1442
                var choice = $(choices[index]);
1443
                if (choice.hasClass("select2-result-selectable") && !choice.hasClass("select2-disabled") && !choice.hasClass("select2-selected")) {
1444
                    this.highlight(index);
1445
                    break;
1446
                }
1447
            }
1448
        },
1449

    
1450
        // abstract
1451
        highlight: function (index) {
1452
            var choices = this.findHighlightableChoices(),
1453
                choice,
1454
                data;
1455

    
1456
            if (arguments.length === 0) {
1457
                return indexOf(choices.filter(".select2-highlighted")[0], choices.get());
1458
            }
1459

    
1460
            if (index >= choices.length) index = choices.length - 1;
1461
            if (index < 0) index = 0;
1462

    
1463
            this.removeHighlight();
1464

    
1465
            choice = $(choices[index]);
1466
            choice.addClass("select2-highlighted");
1467

    
1468
            this.ensureHighlightVisible();
1469

    
1470
            data = choice.data("select2-data");
1471
            if (data) {
1472
                this.opts.element.trigger({ type: "select2-highlight", val: this.id(data), choice: data });
1473
            }
1474
        },
1475

    
1476
        removeHighlight: function() {
1477
            this.results.find(".select2-highlighted").removeClass("select2-highlighted");
1478
        },
1479

    
1480
        // abstract
1481
        countSelectableResults: function() {
1482
            return this.findHighlightableChoices().length;
1483
        },
1484

    
1485
        // abstract
1486
        highlightUnderEvent: function (event) {
1487
            var el = $(event.target).closest(".select2-result-selectable");
1488
            if (el.length > 0 && !el.is(".select2-highlighted")) {
1489
                var choices = this.findHighlightableChoices();
1490
                this.highlight(choices.index(el));
1491
            } else if (el.length == 0) {
1492
                // if we are over an unselectable item remove all highlights
1493
                this.removeHighlight();
1494
            }
1495
        },
1496

    
1497
        // abstract
1498
        loadMoreIfNeeded: function () {
1499
            var results = this.results,
1500
                more = results.find("li.select2-more-results"),
1501
                below, // pixels the element is below the scroll fold, below==0 is when the element is starting to be visible
1502
                page = this.resultsPage + 1,
1503
                self=this,
1504
                term=this.search.val(),
1505
                context=this.context;
1506

    
1507
            if (more.length === 0) return;
1508
            below = more.offset().top - results.offset().top - results.height();
1509

    
1510
            if (below <= this.opts.loadMorePadding) {
1511
                more.addClass("select2-active");
1512
                this.opts.query({
1513
                        element: this.opts.element,
1514
                        term: term,
1515
                        page: page,
1516
                        context: context,
1517
                        matcher: this.opts.matcher,
1518
                        callback: this.bind(function (data) {
1519

    
1520
                    // ignore a response if the select2 has been closed before it was received
1521
                    if (!self.opened()) return;
1522

    
1523

    
1524
                    self.opts.populateResults.call(this, results, data.results, {term: term, page: page, context:context});
1525
                    self.postprocessResults(data, false, false);
1526

    
1527
                    if (data.more===true) {
1528
                        more.detach().appendTo(results).text(self.opts.formatLoadMore(page+1));
1529
                        window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10);
1530
                    } else {
1531
                        more.remove();
1532
                    }
1533
                    self.positionDropdown();
1534
                    self.resultsPage = page;
1535
                    self.context = data.context;
1536
                    this.opts.element.trigger({ type: "select2-loaded", items: data });
1537
                })});
1538
            }
1539
        },
1540

    
1541
        /**
1542
         * Default tokenizer function which does nothing
1543
         */
1544
        tokenize: function() {
1545

    
1546
        },
1547

    
1548
        /**
1549
         * @param initial whether or not this is the call to this method right after the dropdown has been opened
1550
         */
1551
        // abstract
1552
        updateResults: function (initial) {
1553
            var search = this.search,
1554
                results = this.results,
1555
                opts = this.opts,
1556
                data,
1557
                self = this,
1558
                input,
1559
                term = search.val(),
1560
                lastTerm = $.data(this.container, "select2-last-term"),
1561
                // sequence number used to drop out-of-order responses
1562
                queryNumber;
1563

    
1564
            // prevent duplicate queries against the same term
1565
            if (initial !== true && lastTerm && equal(term, lastTerm)) return;
1566

    
1567
            $.data(this.container, "select2-last-term", term);
1568

    
1569
            // if the search is currently hidden we do not alter the results
1570
            if (initial !== true && (this.showSearchInput === false || !this.opened())) {
1571
                return;
1572
            }
1573

    
1574
            function postRender() {
1575
                search.removeClass("select2-active");
1576
                self.positionDropdown();
1577
            }
1578

    
1579
            function render(html) {
1580
                results.html(html);
1581
                postRender();
1582
            }
1583

    
1584
            queryNumber = ++this.queryCount;
1585

    
1586
            var maxSelSize = this.getMaximumSelectionSize();
1587
            if (maxSelSize >=1) {
1588
                data = this.data();
1589
                if ($.isArray(data) && data.length >= maxSelSize && checkFormatter(opts.formatSelectionTooBig, "formatSelectionTooBig")) {
1590
                    render("<li class='select2-selection-limit'>" + opts.formatSelectionTooBig(maxSelSize) + "</li>");
1591
                    return;
1592
                }
1593
            }
1594

    
1595
            if (search.val().length < opts.minimumInputLength) {
1596
                if (checkFormatter(opts.formatInputTooShort, "formatInputTooShort")) {
1597
                    render("<li class='select2-no-results'>" + opts.formatInputTooShort(search.val(), opts.minimumInputLength) + "</li>");
1598
                } else {
1599
                    render("");
1600
                }
1601
                if (initial && this.showSearch) this.showSearch(true);
1602
                return;
1603
            }
1604

    
1605
            if (opts.maximumInputLength && search.val().length > opts.maximumInputLength) {
1606
                if (checkFormatter(opts.formatInputTooLong, "formatInputTooLong")) {
1607
                    render("<li class='select2-no-results'>" + opts.formatInputTooLong(search.val(), opts.maximumInputLength) + "</li>");
1608
                } else {
1609
                    render("");
1610
                }
1611
                return;
1612
            }
1613

    
1614
            if (opts.formatSearching && this.findHighlightableChoices().length === 0) {
1615
                render("<li class='select2-searching'>" + opts.formatSearching() + "</li>");
1616
            }
1617

    
1618
            search.addClass("select2-active");
1619

    
1620
            this.removeHighlight();
1621

    
1622
            // give the tokenizer a chance to pre-process the input
1623
            input = this.tokenize();
1624
            if (input != undefined && input != null) {
1625
                search.val(input);
1626
            }
1627

    
1628
            this.resultsPage = 1;
1629

    
1630
            opts.query({
1631
                element: opts.element,
1632
                    term: search.val(),
1633
                    page: this.resultsPage,
1634
                    context: null,
1635
                    matcher: opts.matcher,
1636
                    callback: this.bind(function (data) {
1637
                var def; // default choice
1638

    
1639
                // ignore old responses
1640
                if (queryNumber != this.queryCount) {
1641
                  return;
1642
                }
1643

    
1644
                // ignore a response if the select2 has been closed before it was received
1645
                if (!this.opened()) {
1646
                    this.search.removeClass("select2-active");
1647
                    return;
1648
                }
1649

    
1650
                // save context, if any
1651
                this.context = (data.context===undefined) ? null : data.context;
1652
                // create a default choice and prepend it to the list
1653
                if (this.opts.createSearchChoice && search.val() !== "") {
1654
                    def = this.opts.createSearchChoice.call(self, search.val(), data.results);
1655
                    if (def !== undefined && def !== null && self.id(def) !== undefined && self.id(def) !== null) {
1656
                        if ($(data.results).filter(
1657
                            function () {
1658
                                return equal(self.id(this), self.id(def));
1659
                            }).length === 0) {
1660
                            data.results.unshift(def);
1661
                        }
1662
                    }
1663
                }
1664

    
1665
                if (data.results.length === 0 && checkFormatter(opts.formatNoMatches, "formatNoMatches")) {
1666
                    render("<li class='select2-no-results'>" + opts.formatNoMatches(search.val()) + "</li>");
1667
                    return;
1668
                }
1669

    
1670
                results.empty();
1671
                self.opts.populateResults.call(this, results, data.results, {term: search.val(), page: this.resultsPage, context:null});
1672

    
1673
                if (data.more === true && checkFormatter(opts.formatLoadMore, "formatLoadMore")) {
1674
                    results.append("<li class='select2-more-results'>" + self.opts.escapeMarkup(opts.formatLoadMore(this.resultsPage)) + "</li>");
1675
                    window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10);
1676
                }
1677

    
1678
                this.postprocessResults(data, initial);
1679

    
1680
                postRender();
1681

    
1682
                this.opts.element.trigger({ type: "select2-loaded", items: data });
1683
            })});
1684
        },
1685

    
1686
        // abstract
1687
        cancel: function () {
1688
            this.close();
1689
        },
1690

    
1691
        // abstract
1692
        blur: function () {
1693
            // if selectOnBlur == true, select the currently highlighted option
1694
            if (this.opts.selectOnBlur)
1695
                this.selectHighlighted({noFocus: true});
1696

    
1697
            this.close();
1698
            this.container.removeClass("select2-container-active");
1699
            // synonymous to .is(':focus'), which is available in jquery >= 1.6
1700
            if (this.search[0] === document.activeElement) { this.search.blur(); }
1701
            this.clearSearch();
1702
            this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
1703
        },
1704

    
1705
        // abstract
1706
        focusSearch: function () {
1707
            focus(this.search);
1708
        },
1709

    
1710
        // abstract
1711
        selectHighlighted: function (options) {
1712
            var index=this.highlight(),
1713
                highlighted=this.results.find(".select2-highlighted"),
1714
                data = highlighted.closest('.select2-result').data("select2-data");
1715

    
1716
            if (data) {
1717
                this.highlight(index);
1718
                this.onSelect(data, options);
1719
            } else if (options && options.noFocus) {
1720
                this.close();
1721
            }
1722
        },
1723

    
1724
        // abstract
1725
        getPlaceholder: function () {
1726
            var placeholderOption;
1727
            return this.opts.element.attr("placeholder") ||
1728
                this.opts.element.attr("data-placeholder") || // jquery 1.4 compat
1729
                this.opts.element.data("placeholder") ||
1730
                this.opts.placeholder ||
1731
                ((placeholderOption = this.getPlaceholderOption()) !== undefined ? placeholderOption.text() : undefined);
1732
        },
1733

    
1734
        // abstract
1735
        getPlaceholderOption: function() {
1736
            if (this.select) {
1737
                var firstOption = this.select.children('option').first();
1738
                if (this.opts.placeholderOption !== undefined ) {
1739
                    //Determine the placeholder option based on the specified placeholderOption setting
1740
                    return (this.opts.placeholderOption === "first" && firstOption) ||
1741
                           (typeof this.opts.placeholderOption === "function" && this.opts.placeholderOption(this.select));
1742
                } else if (firstOption.text() === "" && firstOption.val() === "") {
1743
                    //No explicit placeholder option specified, use the first if it's blank
1744
                    return firstOption;
1745
                }
1746
            }
1747
        },
1748

    
1749
        /**
1750
         * Get the desired width for the container element.  This is
1751
         * derived first from option `width` passed to select2, then
1752
         * the inline 'style' on the original element, and finally
1753
         * falls back to the jQuery calculated element width.
1754
         */
1755
        // abstract
1756
        initContainerWidth: function () {
1757
            function resolveContainerWidth() {
1758
                var style, attrs, matches, i, l, attr;
1759

    
1760
                if (this.opts.width === "off") {
1761
                    return null;
1762
                } else if (this.opts.width === "element"){
1763
                    return this.opts.element.outerWidth(false) === 0 ? 'auto' : this.opts.element.outerWidth(false) + 'px';
1764
                } else if (this.opts.width === "copy" || this.opts.width === "resolve") {
1765
                    // check if there is inline style on the element that contains width
1766
                    style = this.opts.element.attr('style');
1767
                    if (style !== undefined) {
1768
                        attrs = style.split(';');
1769
                        for (i = 0, l = attrs.length; i < l; i = i + 1) {
1770
                            attr = attrs[i].replace(/\s/g, '');
1771
                            matches = attr.match(/^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i);
1772
                            if (matches !== null && matches.length >= 1)
1773
                                return matches[1];
1774
                        }
1775
                    }
1776

    
1777
                    if (this.opts.width === "resolve") {
1778
                        // next check if css('width') can resolve a width that is percent based, this is sometimes possible
1779
                        // when attached to input type=hidden or elements hidden via css
1780
                        style = this.opts.element.css('width');
1781
                        if (style.indexOf("%") > 0) return style;
1782

    
1783
                        // finally, fallback on the calculated width of the element
1784
                        return (this.opts.element.outerWidth(false) === 0 ? 'auto' : this.opts.element.outerWidth(false) + 'px');
1785
                    }
1786

    
1787
                    return null;
1788
                } else if ($.isFunction(this.opts.width)) {
1789
                    return this.opts.width();
1790
                } else {
1791
                    return this.opts.width;
1792
               }
1793
            };
1794

    
1795
            var width = resolveContainerWidth.call(this);
1796
            if (width !== null) {
1797
                this.container.css("width", width);
1798
            }
1799
        }
1800
    });
1801

    
1802
    SingleSelect2 = clazz(AbstractSelect2, {
1803

    
1804
        // single
1805

    
1806
        createContainer: function () {
1807
            var container = $(document.createElement("div")).attr({
1808
                "class": "select2-container"
1809
            }).html([
1810
                "<a href='javascript:void(0)' onclick='return false;' class='select2-choice' tabindex='-1'>",
1811
                "   <span class='select2-chosen'>&nbsp;</span><abbr class='select2-search-choice-close'></abbr>",
1812
                "   <span class='select2-arrow'><b></b></span>",
1813
                "</a>",
1814
                "<input class='select2-focusser select2-offscreen' type='text'/>",
1815
                "<div class='select2-drop select2-display-none'>",
1816
                "   <div class='select2-search'>",
1817
                "       <input type='text' autocomplete='off' autocorrect='off' autocapitalize='off' spellcheck='false' class='select2-input'/>",
1818
                "   </div>",
1819
                "   <ul class='select2-results'>",
1820
                "   </ul>",
1821
                "</div>"].join(""));
1822
            return container;
1823
        },
1824

    
1825
        // single
1826
        enableInterface: function() {
1827
            if (this.parent.enableInterface.apply(this, arguments)) {
1828
                this.focusser.prop("disabled", !this.isInterfaceEnabled());
1829
            }
1830
        },
1831

    
1832
        // single
1833
        opening: function () {
1834
            var el, range, len;
1835

    
1836
            if (this.opts.minimumResultsForSearch >= 0) {
1837
                this.showSearch(true);
1838
            }
1839

    
1840
            this.parent.opening.apply(this, arguments);
1841

    
1842
            if (this.showSearchInput !== false) {
1843
                // IE appends focusser.val() at the end of field :/ so we manually insert it at the beginning using a range
1844
                // all other browsers handle this just fine
1845

    
1846
                this.search.val(this.focusser.val());
1847
            }
1848
            this.search.focus();
1849
            // move the cursor to the end after focussing, otherwise it will be at the beginning and
1850
            // new text will appear *before* focusser.val()
1851
            el = this.search.get(0);
1852
            if (el.createTextRange) {
1853
                range = el.createTextRange();
1854
                range.collapse(false);
1855
                range.select();
1856
            } else if (el.setSelectionRange) {
1857
                len = this.search.val().length;
1858
                el.setSelectionRange(len, len);
1859
            }
1860

    
1861
            // initializes search's value with nextSearchTerm (if defined by user)
1862
            // ignore nextSearchTerm if the dropdown is opened by the user pressing a letter
1863
            if(this.search.val() === "") {
1864
                if(this.nextSearchTerm != undefined){
1865
                    this.search.val(this.nextSearchTerm);
1866
                    this.search.select();
1867
                }
1868
            }
1869

    
1870
            this.focusser.prop("disabled", true).val("");
1871
            this.updateResults(true);
1872
            this.opts.element.trigger($.Event("select2-open"));
1873
        },
1874

    
1875
        // single
1876
        close: function (params) {
1877
            if (!this.opened()) return;
1878
            this.parent.close.apply(this, arguments);
1879

    
1880
            params = params || {focus: true};
1881
            this.focusser.removeAttr("disabled");
1882

    
1883
            if (params.focus) {
1884
                this.focusser.focus();
1885
            }
1886
        },
1887

    
1888
        // single
1889
        focus: function () {
1890
            if (this.opened()) {
1891
                this.close();
1892
            } else {
1893
                this.focusser.removeAttr("disabled");
1894
                this.focusser.focus();
1895
            }
1896
        },
1897

    
1898
        // single
1899
        isFocused: function () {
1900
            return this.container.hasClass("select2-container-active");
1901
        },
1902

    
1903
        // single
1904
        cancel: function () {
1905
            this.parent.cancel.apply(this, arguments);
1906
            this.focusser.removeAttr("disabled");
1907
            this.focusser.focus();
1908
        },
1909

    
1910
        // single
1911
        destroy: function() {
1912
            $("label[for='" + this.focusser.attr('id') + "']")
1913
                .attr('for', this.opts.element.attr("id"));
1914
            this.parent.destroy.apply(this, arguments);
1915
        },
1916

    
1917
        // single
1918
        initContainer: function () {
1919

    
1920
            var selection,
1921
                container = this.container,
1922
                dropdown = this.dropdown;
1923

    
1924
            if (this.opts.minimumResultsForSearch < 0) {
1925
                this.showSearch(false);
1926
            } else {
1927
                this.showSearch(true);
1928
            }
1929

    
1930
            this.selection = selection = container.find(".select2-choice");
1931

    
1932
            this.focusser = container.find(".select2-focusser");
1933

    
1934
            // rewrite labels from original element to focusser
1935
            this.focusser.attr("id", "s2id_autogen"+nextUid());
1936

    
1937
            $("label[for='" + this.opts.element.attr("id") + "']")
1938
                .attr('for', this.focusser.attr('id'));
1939

    
1940
            this.focusser.attr("tabindex", this.elementTabIndex);
1941

    
1942
            this.search.on("keydown", this.bind(function (e) {
1943
                if (!this.isInterfaceEnabled()) return;
1944

    
1945
                if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) {
1946
                    // prevent the page from scrolling
1947
                    killEvent(e);
1948
                    return;
1949
                }
1950

    
1951
                switch (e.which) {
1952
                    case KEY.UP:
1953
                    case KEY.DOWN:
1954
                        this.moveHighlight((e.which === KEY.UP) ? -1 : 1);
1955
                        killEvent(e);
1956
                        return;
1957
                    case KEY.ENTER:
1958
                        this.selectHighlighted();
1959
                        killEvent(e);
1960
                        return;
1961
                    case KEY.TAB:
1962
                        this.selectHighlighted({noFocus: true});
1963
                        return;
1964
                    case KEY.ESC:
1965
                        this.cancel(e);
1966
                        killEvent(e);
1967
                        return;
1968
                }
1969
            }));
1970

    
1971
            this.search.on("blur", this.bind(function(e) {
1972
                // a workaround for chrome to keep the search field focussed when the scroll bar is used to scroll the dropdown.
1973
                // without this the search field loses focus which is annoying
1974
                if (document.activeElement === this.body().get(0)) {
1975
                    window.setTimeout(this.bind(function() {
1976
                        this.search.focus();
1977
                    }), 0);
1978
                }
1979
            }));
1980

    
1981
            this.focusser.on("keydown", this.bind(function (e) {
1982
                if (!this.isInterfaceEnabled()) return;
1983

    
1984
                if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) {
1985
                    return;
1986
                }
1987

    
1988
                if (this.opts.openOnEnter === false && e.which === KEY.ENTER) {
1989
                    killEvent(e);
1990
                    return;
1991
                }
1992

    
1993
                if (e.which == KEY.DOWN || e.which == KEY.UP
1994
                    || (e.which == KEY.ENTER && this.opts.openOnEnter)) {
1995

    
1996
                    if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) return;
1997

    
1998
                    this.open();
1999
                    killEvent(e);
2000
                    return;
2001
                }
2002

    
2003
                if (e.which == KEY.DELETE || e.which == KEY.BACKSPACE) {
2004
                    if (this.opts.allowClear) {
2005
                        this.clear();
2006
                    }
2007
                    killEvent(e);
2008
                    return;
2009
                }
2010
            }));
2011

    
2012

    
2013
            installKeyUpChangeEvent(this.focusser);
2014
            this.focusser.on("keyup-change input", this.bind(function(e) {
2015
                if (this.opts.minimumResultsForSearch >= 0) {
2016
                    e.stopPropagation();
2017
                    if (this.opened()) return;
2018
                    this.open();
2019
                }
2020
            }));
2021

    
2022
            selection.on("mousedown", "abbr", this.bind(function (e) {
2023
                if (!this.isInterfaceEnabled()) return;
2024
                this.clear();
2025
                killEventImmediately(e);
2026
                this.close();
2027
                this.selection.focus();
2028
            }));
2029

    
2030
            selection.on("mousedown", this.bind(function (e) {
2031

    
2032
                if (!this.container.hasClass("select2-container-active")) {
2033
                    this.opts.element.trigger($.Event("select2-focus"));
2034
                }
2035

    
2036
                if (this.opened()) {
2037
                    this.close();
2038
                } else if (this.isInterfaceEnabled()) {
2039
                    this.open();
2040
                }
2041

    
2042
                killEvent(e);
2043
            }));
2044

    
2045
            dropdown.on("mousedown", this.bind(function() { this.search.focus(); }));
2046

    
2047
            selection.on("focus", this.bind(function(e) {
2048
                killEvent(e);
2049
            }));
2050

    
2051
            this.focusser.on("focus", this.bind(function(){
2052
                if (!this.container.hasClass("select2-container-active")) {
2053
                    this.opts.element.trigger($.Event("select2-focus"));
2054
                }
2055
                this.container.addClass("select2-container-active");
2056
            })).on("blur", this.bind(function() {
2057
                if (!this.opened()) {
2058
                    this.container.removeClass("select2-container-active");
2059
                    this.opts.element.trigger($.Event("select2-blur"));
2060
                }
2061
            }));
2062
            this.search.on("focus", this.bind(function(){
2063
                if (!this.container.hasClass("select2-container-active")) {
2064
                    this.opts.element.trigger($.Event("select2-focus"));
2065
                }
2066
                this.container.addClass("select2-container-active");
2067
            }));
2068

    
2069
            this.initContainerWidth();
2070
            this.opts.element.addClass("select2-offscreen");
2071
            this.setPlaceholder();
2072

    
2073
        },
2074

    
2075
        // single
2076
        clear: function(triggerChange) {
2077
            var data=this.selection.data("select2-data");
2078
            if (data) { // guard against queued quick consecutive clicks
2079
                var evt = $.Event("select2-clearing");
2080
                this.opts.element.trigger(evt);
2081
                if (evt.isDefaultPrevented()) {
2082
                    return;
2083
                }
2084
                var placeholderOption = this.getPlaceholderOption();
2085
                this.opts.element.val(placeholderOption ? placeholderOption.val() : "");
2086
                this.selection.find(".select2-chosen").empty();
2087
                this.selection.removeData("select2-data");
2088
                this.setPlaceholder();
2089

    
2090
                if (triggerChange !== false){
2091
                    this.opts.element.trigger({ type: "select2-removed", val: this.id(data), choice: data });
2092
                    this.triggerChange({removed:data});
2093
                }
2094
            }
2095
        },
2096

    
2097
        /**
2098
         * Sets selection based on source element's value
2099
         */
2100
        // single
2101
        initSelection: function () {
2102
            var selected;
2103
            if (this.isPlaceholderOptionSelected()) {
2104
                this.updateSelection(null);
2105
                this.close();
2106
                this.setPlaceholder();
2107
            } else {
2108
                var self = this;
2109
                this.opts.initSelection.call(null, this.opts.element, function(selected){
2110
                    if (selected !== undefined && selected !== null) {
2111
                        self.updateSelection(selected);
2112
                        self.close();
2113
                        self.setPlaceholder();
2114
                    }
2115
                });
2116
            }
2117
        },
2118

    
2119
        isPlaceholderOptionSelected: function() {
2120
            var placeholderOption;
2121
            if (!this.getPlaceholder()) return false; // no placeholder specified so no option should be considered
2122
            return ((placeholderOption = this.getPlaceholderOption()) !== undefined && placeholderOption.prop("selected"))
2123
                || (this.opts.element.val() === "")
2124
                || (this.opts.element.val() === undefined)
2125
                || (this.opts.element.val() === null);
2126
        },
2127

    
2128
        // single
2129
        prepareOpts: function () {
2130
            var opts = this.parent.prepareOpts.apply(this, arguments),
2131
                self=this;
2132

    
2133
            if (opts.element.get(0).tagName.toLowerCase() === "select") {
2134
                // install the selection initializer
2135
                opts.initSelection = function (element, callback) {
2136
                    var selected = element.find("option").filter(function() { return this.selected });
2137
                    // a single select box always has a value, no need to null check 'selected'
2138
                    callback(self.optionToData(selected));
2139
                };
2140
            } else if ("data" in opts) {
2141
                // install default initSelection when applied to hidden input and data is local
2142
                opts.initSelection = opts.initSelection || function (element, callback) {
2143
                    var id = element.val();
2144
                    //search in data by id, storing the actual matching item
2145
                    var match = null;
2146
                    opts.query({
2147
                        matcher: function(term, text, el){
2148
                            var is_match = equal(id, opts.id(el));
2149
                            if (is_match) {
2150
                                match = el;
2151
                            }
2152
                            return is_match;
2153
                        },
2154
                        callback: !$.isFunction(callback) ? $.noop : function() {
2155
                            callback(match);
2156
                        }
2157
                    });
2158
                };
2159
            }
2160

    
2161
            return opts;
2162
        },
2163

    
2164
        // single
2165
        getPlaceholder: function() {
2166
            // if a placeholder is specified on a single select without a valid placeholder option ignore it
2167
            if (this.select) {
2168
                if (this.getPlaceholderOption() === undefined) {
2169
                    return undefined;
2170
                }
2171
            }
2172

    
2173
            return this.parent.getPlaceholder.apply(this, arguments);
2174
        },
2175

    
2176
        // single
2177
        setPlaceholder: function () {
2178
            var placeholder = this.getPlaceholder();
2179

    
2180
            if (this.isPlaceholderOptionSelected() && placeholder !== undefined) {
2181

    
2182
                // check for a placeholder option if attached to a select
2183
                if (this.select && this.getPlaceholderOption() === undefined) return;
2184

    
2185
                this.selection.find(".select2-chosen").html(this.opts.escapeMarkup(placeholder));
2186

    
2187
                this.selection.addClass("select2-default");
2188

    
2189
                this.container.removeClass("select2-allowclear");
2190
            }
2191
        },
2192

    
2193
        // single
2194
        postprocessResults: function (data, initial, noHighlightUpdate) {
2195
            var selected = 0, self = this, showSearchInput = true;
2196

    
2197
            // find the selected element in the result list
2198

    
2199
            this.findHighlightableChoices().each2(function (i, elm) {
2200
                if (equal(self.id(elm.data("select2-data")), self.opts.element.val())) {
2201
                    selected = i;
2202
                    return false;
2203
                }
2204
            });
2205

    
2206
            // and highlight it
2207
            if (noHighlightUpdate !== false) {
2208
                if (initial === true && selected >= 0) {
2209
                    this.highlight(selected);
2210
                } else {
2211
                    this.highlight(0);
2212
                }
2213
            }
2214

    
2215
            // hide the search box if this is the first we got the results and there are enough of them for search
2216

    
2217
            if (initial === true) {
2218
                var min = this.opts.minimumResultsForSearch;
2219
                if (min >= 0) {
2220
                    this.showSearch(countResults(data.results) >= min);
2221
                }
2222
            }
2223
        },
2224

    
2225
        // single
2226
        showSearch: function(showSearchInput) {
2227
            if (this.showSearchInput === showSearchInput) return;
2228

    
2229
            this.showSearchInput = showSearchInput;
2230

    
2231
            this.dropdown.find(".select2-search").toggleClass("select2-search-hidden", !showSearchInput);
2232
            this.dropdown.find(".select2-search").toggleClass("select2-offscreen", !showSearchInput);
2233
            //add "select2-with-searchbox" to the container if search box is shown
2234
            $(this.dropdown, this.container).toggleClass("select2-with-searchbox", showSearchInput);
2235
        },
2236

    
2237
        // single
2238
        onSelect: function (data, options) {
2239

    
2240
            if (!this.triggerSelect(data)) { return; }
2241

    
2242
            var old = this.opts.element.val(),
2243
                oldData = this.data();
2244

    
2245
            this.opts.element.val(this.id(data));
2246
            this.updateSelection(data);
2247

    
2248
            this.opts.element.trigger({ type: "select2-selected", val: this.id(data), choice: data });
2249

    
2250
            this.nextSearchTerm = this.opts.nextSearchTerm(data, this.search.val());
2251
            this.close();
2252

    
2253
            if (!options || !options.noFocus)
2254
                this.focusser.focus();
2255

    
2256
            if (!equal(old, this.id(data))) { this.triggerChange({added:data,removed:oldData}); }
2257
        },
2258

    
2259
        // single
2260
        updateSelection: function (data) {
2261

    
2262
            var container=this.selection.find(".select2-chosen"), formatted, cssClass;
2263

    
2264
            this.selection.data("select2-data", data);
2265

    
2266
            container.empty();
2267
            if (data !== null) {
2268
                formatted=this.opts.formatSelection(data, container, this.opts.escapeMarkup);
2269
            }
2270
            if (formatted !== undefined) {
2271
                container.append(formatted);
2272
            }
2273
            cssClass=this.opts.formatSelectionCssClass(data, container);
2274
            if (cssClass !== undefined) {
2275
                container.addClass(cssClass);
2276
            }
2277

    
2278
            this.selection.removeClass("select2-default");
2279

    
2280
            if (this.opts.allowClear && this.getPlaceholder() !== undefined) {
2281
                this.container.addClass("select2-allowclear");
2282
            }
2283
        },
2284

    
2285
        // single
2286
        val: function () {
2287
            var val,
2288
                triggerChange = false,
2289
                data = null,
2290
                self = this,
2291
                oldData = this.data();
2292

    
2293
            if (arguments.length === 0) {
2294
                return this.opts.element.val();
2295
            }
2296

    
2297
            val = arguments[0];
2298

    
2299
            if (arguments.length > 1) {
2300
                triggerChange = arguments[1];
2301
            }
2302

    
2303
            if (this.select) {
2304
                this.select
2305
                    .val(val)
2306
                    .find("option").filter(function() { return this.selected }).each2(function (i, elm) {
2307
                        data = self.optionToData(elm);
2308
                        return false;
2309
                    });
2310
                this.updateSelection(data);
2311
                this.setPlaceholder();
2312
                if (triggerChange) {
2313
                    this.triggerChange({added: data, removed:oldData});
2314
                }
2315
            } else {
2316
                // val is an id. !val is true for [undefined,null,'',0] - 0 is legal
2317
                if (!val && val !== 0) {
2318
                    this.clear(triggerChange);
2319
                    return;
2320
                }
2321
                if (this.opts.initSelection === undefined) {
2322
                    throw new Error("cannot call val() if initSelection() is not defined");
2323
                }
2324
                this.opts.element.val(val);
2325
                this.opts.initSelection(this.opts.element, function(data){
2326
                    self.opts.element.val(!data ? "" : self.id(data));
2327
                    self.updateSelection(data);
2328
                    self.setPlaceholder();
2329
                    if (triggerChange) {
2330
                        self.triggerChange({added: data, removed:oldData});
2331
                    }
2332
                });
2333
            }
2334
        },
2335

    
2336
        // single
2337
        clearSearch: function () {
2338
            this.search.val("");
2339
            this.focusser.val("");
2340
        },
2341

    
2342
        // single
2343
        data: function(value) {
2344
            var data,
2345
                triggerChange = false;
2346

    
2347
            if (arguments.length === 0) {
2348
                data = this.selection.data("select2-data");
2349
                if (data == undefined) data = null;
2350
                return data;
2351
            } else {
2352
                if (arguments.length > 1) {
2353
                    triggerChange = arguments[1];
2354
                }
2355
                if (!value) {
2356
                    this.clear(triggerChange);
2357
                } else {
2358
                    data = this.data();
2359
                    this.opts.element.val(!value ? "" : this.id(value));
2360
                    this.updateSelection(value);
2361
                    if (triggerChange) {
2362
                        this.triggerChange({added: value, removed:data});
2363
                    }
2364
                }
2365
            }
2366
        }
2367
    });
2368

    
2369
    MultiSelect2 = clazz(AbstractSelect2, {
2370

    
2371
        // multi
2372
        createContainer: function () {
2373
            var container = $(document.createElement("div")).attr({
2374
                "class": "select2-container select2-container-multi"
2375
            }).html([
2376
                "<ul class='select2-choices'>",
2377
                "  <li class='select2-search-field'>",
2378
                "    <input type='text' autocomplete='off' autocorrect='off' autocapitalize='off' spellcheck='false' class='select2-input'>",
2379
                "  </li>",
2380
                "</ul>",
2381
                "<div class='select2-drop select2-drop-multi select2-display-none'>",
2382
                "   <ul class='select2-results'>",
2383
                "   </ul>",
2384
                "</div>"].join(""));
2385
            return container;
2386
        },
2387

    
2388
        // multi
2389
        prepareOpts: function () {
2390
            var opts = this.parent.prepareOpts.apply(this, arguments),
2391
                self=this;
2392

    
2393
            // TODO validate placeholder is a string if specified
2394

    
2395
            if (opts.element.get(0).tagName.toLowerCase() === "select") {
2396
                // install sthe selection initializer
2397
                opts.initSelection = function (element, callback) {
2398

    
2399
                    var data = [];
2400

    
2401
                    element.find("option").filter(function() { return this.selected }).each2(function (i, elm) {
2402
                        data.push(self.optionToData(elm));
2403
                    });
2404
                    callback(data);
2405
                };
2406
            } else if ("data" in opts) {
2407
                // install default initSelection when applied to hidden input and data is local
2408
                opts.initSelection = opts.initSelection || function (element, callback) {
2409
                    var ids = splitVal(element.val(), opts.separator);
2410
                    //search in data by array of ids, storing matching items in a list
2411
                    var matches = [];
2412
                    opts.query({
2413
                        matcher: function(term, text, el){
2414
                            var is_match = $.grep(ids, function(id) {
2415
                                return equal(id, opts.id(el));
2416
                            }).length;
2417
                            if (is_match) {
2418
                                matches.push(el);
2419
                            }
2420
                            return is_match;
2421
                        },
2422
                        callback: !$.isFunction(callback) ? $.noop : function() {
2423
                            // reorder matches based on the order they appear in the ids array because right now
2424
                            // they are in the order in which they appear in data array
2425
                            var ordered = [];
2426
                            for (var i = 0; i < ids.length; i++) {
2427
                                var id = ids[i];
2428
                                for (var j = 0; j < matches.length; j++) {
2429
                                    var match = matches[j];
2430
                                    if (equal(id, opts.id(match))) {
2431
                                        ordered.push(match);
2432
                                        matches.splice(j, 1);
2433
                                        break;
2434
                                    }
2435
                                }
2436
                            }
2437
                            callback(ordered);
2438
                        }
2439
                    });
2440
                };
2441
            }
2442

    
2443
            return opts;
2444
        },
2445

    
2446
        // multi
2447
        selectChoice: function (choice) {
2448

    
2449
            var selected = this.container.find(".select2-search-choice-focus");
2450
            if (selected.length && choice && choice[0] == selected[0]) {
2451

    
2452
            } else {
2453
                if (selected.length) {
2454
                    this.opts.element.trigger("choice-deselected", selected);
2455
                }
2456
                selected.removeClass("select2-search-choice-focus");
2457
                if (choice && choice.length) {
2458
                    this.close();
2459
                    choice.addClass("select2-search-choice-focus");
2460
                    this.opts.element.trigger("choice-selected", choice);
2461
                }
2462
            }
2463
        },
2464

    
2465
        // multi
2466
        destroy: function() {
2467
            $("label[for='" + this.search.attr('id') + "']")
2468
                .attr('for', this.opts.element.attr("id"));
2469
            this.parent.destroy.apply(this, arguments);
2470
        },
2471

    
2472
        // multi
2473
        initContainer: function () {
2474

    
2475
            var selector = ".select2-choices", selection;
2476

    
2477
            this.searchContainer = this.container.find(".select2-search-field");
2478
            this.selection = selection = this.container.find(selector);
2479

    
2480
            var _this = this;
2481
            this.selection.on("click", ".select2-search-choice:not(.select2-locked)", function (e) {
2482
                //killEvent(e);
2483
                _this.search[0].focus();
2484
                _this.selectChoice($(this));
2485
            });
2486

    
2487
            // rewrite labels from original element to focusser
2488
            this.search.attr("id", "s2id_autogen"+nextUid());
2489
            $("label[for='" + this.opts.element.attr("id") + "']")
2490
                .attr('for', this.search.attr('id'));
2491

    
2492
            this.search.on("input paste", this.bind(function() {
2493
                if (!this.isInterfaceEnabled()) return;
2494
                if (!this.opened()) {
2495
                    this.open();
2496
                }
2497
            }));
2498

    
2499
            this.search.attr("tabindex", this.elementTabIndex);
2500

    
2501
            this.keydowns = 0;
2502
            this.search.on("keydown", this.bind(function (e) {
2503
                if (!this.isInterfaceEnabled()) return;
2504

    
2505
                ++this.keydowns;
2506
                var selected = selection.find(".select2-search-choice-focus");
2507
                var prev = selected.prev(".select2-search-choice:not(.select2-locked)");
2508
                var next = selected.next(".select2-search-choice:not(.select2-locked)");
2509
                var pos = getCursorInfo(this.search);
2510

    
2511
                if (selected.length &&
2512
                    (e.which == KEY.LEFT || e.which == KEY.RIGHT || e.which == KEY.BACKSPACE || e.which == KEY.DELETE || e.which == KEY.ENTER)) {
2513
                    var selectedChoice = selected;
2514
                    if (e.which == KEY.LEFT && prev.length) {
2515
                        selectedChoice = prev;
2516
                    }
2517
                    else if (e.which == KEY.RIGHT) {
2518
                        selectedChoice = next.length ? next : null;
2519
                    }
2520
                    else if (e.which === KEY.BACKSPACE) {
2521
                        this.unselect(selected.first());
2522
                        this.search.width(10);
2523
                        selectedChoice = prev.length ? prev : next;
2524
                    } else if (e.which == KEY.DELETE) {
2525
                        this.unselect(selected.first());
2526
                        this.search.width(10);
2527
                        selectedChoice = next.length ? next : null;
2528
                    } else if (e.which == KEY.ENTER) {
2529
                        selectedChoice = null;
2530
                    }
2531

    
2532
                    this.selectChoice(selectedChoice);
2533
                    killEvent(e);
2534
                    if (!selectedChoice || !selectedChoice.length) {
2535
                        this.open();
2536
                    }
2537
                    return;
2538
                } else if (((e.which === KEY.BACKSPACE && this.keydowns == 1)
2539
                    || e.which == KEY.LEFT) && (pos.offset == 0 && !pos.length)) {
2540

    
2541
                    this.selectChoice(selection.find(".select2-search-choice:not(.select2-locked)").last());
2542
                    killEvent(e);
2543
                    return;
2544
                } else {
2545
                    this.selectChoice(null);
2546
                }
2547

    
2548
                if (this.opened()) {
2549
                    switch (e.which) {
2550
                    case KEY.UP:
2551
                    case KEY.DOWN:
2552
                        this.moveHighlight((e.which === KEY.UP) ? -1 : 1);
2553
                        killEvent(e);
2554
                        return;
2555
                    case KEY.ENTER:
2556
                        this.selectHighlighted();
2557
                        killEvent(e);
2558
                        return;
2559
                    case KEY.TAB:
2560
                        this.selectHighlighted({noFocus:true});
2561
                        this.close();
2562
                        return;
2563
                    case KEY.ESC:
2564
                        this.cancel(e);
2565
                        killEvent(e);
2566
                        return;
2567
                    }
2568
                }
2569

    
2570
                if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e)
2571
                 || e.which === KEY.BACKSPACE || e.which === KEY.ESC) {
2572
                    return;
2573
                }
2574

    
2575
                if (e.which === KEY.ENTER) {
2576
                    if (this.opts.openOnEnter === false) {
2577
                        return;
2578
                    } else if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) {
2579
                        return;
2580
                    }
2581
                }
2582

    
2583
                this.open();
2584

    
2585
                if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) {
2586
                    // prevent the page from scrolling
2587
                    killEvent(e);
2588
                }
2589

    
2590
                if (e.which === KEY.ENTER) {
2591
                    // prevent form from being submitted
2592
                    killEvent(e);
2593
                }
2594

    
2595
            }));
2596

    
2597
            this.search.on("keyup", this.bind(function (e) {
2598
                this.keydowns = 0;
2599
                this.resizeSearch();
2600
            })
2601
            );
2602

    
2603
            this.search.on("blur", this.bind(function(e) {
2604
                this.container.removeClass("select2-container-active");
2605
                this.search.removeClass("select2-focused");
2606
                this.selectChoice(null);
2607
                if (!this.opened()) this.clearSearch();
2608
                e.stopImmediatePropagation();
2609
                this.opts.element.trigger($.Event("select2-blur"));
2610
            }));
2611

    
2612
            this.container.on("click", selector, this.bind(function (e) {
2613
                if (!this.isInterfaceEnabled()) return;
2614
                if ($(e.target).closest(".select2-search-choice").length > 0) {
2615
                    // clicked inside a select2 search choice, do not open
2616
                    return;
2617
                }
2618
                this.selectChoice(null);
2619
                this.clearPlaceholder();
2620
                if (!this.container.hasClass("select2-container-active")) {
2621
                    this.opts.element.trigger($.Event("select2-focus"));
2622
                }
2623
                this.open();
2624
                this.focusSearch();
2625
                e.preventDefault();
2626
            }));
2627

    
2628
            this.container.on("focus", selector, this.bind(function () {
2629
                if (!this.isInterfaceEnabled()) return;
2630
                if (!this.container.hasClass("select2-container-active")) {
2631
                    this.opts.element.trigger($.Event("select2-focus"));
2632
                }
2633
                this.container.addClass("select2-container-active");
2634
                this.dropdown.addClass("select2-drop-active");
2635
                this.clearPlaceholder();
2636
            }));
2637

    
2638
            this.initContainerWidth();
2639
            this.opts.element.addClass("select2-offscreen");
2640

    
2641
            // set the placeholder if necessary
2642
            this.clearSearch();
2643
        },
2644

    
2645
        // multi
2646
        enableInterface: function() {
2647
            if (this.parent.enableInterface.apply(this, arguments)) {
2648
                this.search.prop("disabled", !this.isInterfaceEnabled());
2649
            }
2650
        },
2651

    
2652
        // multi
2653
        initSelection: function () {
2654
            var data;
2655
            if (this.opts.element.val() === "" && this.opts.element.text() === "") {
2656
                this.updateSelection([]);
2657
                this.close();
2658
                // set the placeholder if necessary
2659
                this.clearSearch();
2660
            }
2661
            if (this.select || this.opts.element.val() !== "") {
2662
                var self = this;
2663
                this.opts.initSelection.call(null, this.opts.element, function(data){
2664
                    if (data !== undefined && data !== null) {
2665
                        self.updateSelection(data);
2666
                        self.close();
2667
                        // set the placeholder if necessary
2668
                        self.clearSearch();
2669
                    }
2670
                });
2671
            }
2672
        },
2673

    
2674
        // multi
2675
        clearSearch: function () {
2676
            var placeholder = this.getPlaceholder(),
2677
                maxWidth = this.getMaxSearchWidth();
2678

    
2679
            if (placeholder !== undefined  && this.getVal().length === 0 && this.search.hasClass("select2-focused") === false) {
2680
                this.search.val(placeholder).addClass("select2-default");
2681
                // stretch the search box to full width of the container so as much of the placeholder is visible as possible
2682
                // we could call this.resizeSearch(), but we do not because that requires a sizer and we do not want to create one so early because of a firefox bug, see #944
2683
                this.search.width(maxWidth > 0 ? maxWidth : this.container.css("width"));
2684
            } else {
2685
                this.search.val("").width(10);
2686
            }
2687
        },
2688

    
2689
        // multi
2690
        clearPlaceholder: function () {
2691
            if (this.search.hasClass("select2-default")) {
2692
                this.search.val("").removeClass("select2-default");
2693
            }
2694
        },
2695

    
2696
        // multi
2697
        opening: function () {
2698
            this.clearPlaceholder(); // should be done before super so placeholder is not used to search
2699
            this.resizeSearch();
2700

    
2701
            this.parent.opening.apply(this, arguments);
2702

    
2703
            this.focusSearch();
2704

    
2705
            this.updateResults(true);
2706
            this.search.focus();
2707
            this.opts.element.trigger($.Event("select2-open"));
2708
        },
2709

    
2710
        // multi
2711
        close: function () {
2712
            if (!this.opened()) return;
2713
            this.parent.close.apply(this, arguments);
2714
        },
2715

    
2716
        // multi
2717
        focus: function () {
2718
            this.close();
2719
            this.search.focus();
2720
        },
2721

    
2722
        // multi
2723
        isFocused: function () {
2724
            return this.search.hasClass("select2-focused");
2725
        },
2726

    
2727
        // multi
2728
        updateSelection: function (data) {
2729
            var ids = [], filtered = [], self = this;
2730

    
2731
            // filter out duplicates
2732
            $(data).each(function () {
2733
                if (indexOf(self.id(this), ids) < 0) {
2734
                    ids.push(self.id(this));
2735
                    filtered.push(this);
2736
                }
2737
            });
2738
            data = filtered;
2739

    
2740
            this.selection.find(".select2-search-choice").remove();
2741
            $(data).each(function () {
2742
                self.addSelectedChoice(this);
2743
            });
2744
            self.postprocessResults();
2745
        },
2746

    
2747
        // multi
2748
        tokenize: function() {
2749
            var input = this.search.val();
2750
            input = this.opts.tokenizer.call(this, input, this.data(), this.bind(this.onSelect), this.opts);
2751
            if (input != null && input != undefined) {
2752
                this.search.val(input);
2753
                if (input.length > 0) {
2754
                    this.open();
2755
                }
2756
            }
2757

    
2758
        },
2759

    
2760
        // multi
2761
        onSelect: function (data, options) {
2762

    
2763
            if (!this.triggerSelect(data)) { return; }
2764

    
2765
            this.addSelectedChoice(data);
2766

    
2767
            this.opts.element.trigger({ type: "selected", val: this.id(data), choice: data });
2768

    
2769
            if (this.select || !this.opts.closeOnSelect) this.postprocessResults(data, false, this.opts.closeOnSelect===true);
2770

    
2771
            if (this.opts.closeOnSelect) {
2772
                this.close();
2773
                this.search.width(10);
2774
            } else {
2775
                if (this.countSelectableResults()>0) {
2776
                    this.search.width(10);
2777
                    this.resizeSearch();
2778
                    if (this.getMaximumSelectionSize() > 0 && this.val().length >= this.getMaximumSelectionSize()) {
2779
                        // if we reached max selection size repaint the results so choices
2780
                        // are replaced with the max selection reached message
2781
                        this.updateResults(true);
2782
                    }
2783
                    this.positionDropdown();
2784
                } else {
2785
                    // if nothing left to select close
2786
                    this.close();
2787
                    this.search.width(10);
2788
                }
2789
            }
2790

    
2791
            // since its not possible to select an element that has already been
2792
            // added we do not need to check if this is a new element before firing change
2793
            this.triggerChange({ added: data });
2794

    
2795
            if (!options || !options.noFocus)
2796
                this.focusSearch();
2797
        },
2798

    
2799
        // multi
2800
        cancel: function () {
2801
            this.close();
2802
            this.focusSearch();
2803
        },
2804

    
2805
        addSelectedChoice: function (data) {
2806
            var enableChoice = !data.locked,
2807
                enabledItem = $(
2808
                    "<li class='select2-search-choice'>" +
2809
                    "    <div></div>" +
2810
                    "    <a href='#' onclick='return false;' class='select2-search-choice-close' tabindex='-1'></a>" +
2811
                    "</li>"),
2812
                disabledItem = $(
2813
                    "<li class='select2-search-choice select2-locked'>" +
2814
                    "<div></div>" +
2815
                    "</li>");
2816
            var choice = enableChoice ? enabledItem : disabledItem,
2817
                id = this.id(data),
2818
                val = this.getVal(),
2819
                formatted,
2820
                cssClass;
2821

    
2822
            formatted=this.opts.formatSelection(data, choice.find("div"), this.opts.escapeMarkup);
2823
            if (formatted != undefined) {
2824
                choice.find("div").replaceWith("<div>"+formatted+"</div>");
2825
            }
2826
            cssClass=this.opts.formatSelectionCssClass(data, choice.find("div"));
2827
            if (cssClass != undefined) {
2828
                choice.addClass(cssClass);
2829
            }
2830

    
2831
            if(enableChoice){
2832
              choice.find(".select2-search-choice-close")
2833
                  .on("mousedown", killEvent)
2834
                  .on("click dblclick", this.bind(function (e) {
2835
                  if (!this.isInterfaceEnabled()) return;
2836

    
2837
                  $(e.target).closest(".select2-search-choice").fadeOut('fast', this.bind(function(){
2838
                      this.unselect($(e.target));
2839
                      this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
2840
                      this.close();
2841
                      this.focusSearch();
2842
                  })).dequeue();
2843
                  killEvent(e);
2844
              })).on("focus", this.bind(function () {
2845
                  if (!this.isInterfaceEnabled()) return;
2846
                  this.container.addClass("select2-container-active");
2847
                  this.dropdown.addClass("select2-drop-active");
2848
              }));
2849
            }
2850

    
2851
            choice.data("select2-data", data);
2852
            choice.insertBefore(this.searchContainer);
2853

    
2854
            val.push(id);
2855
            this.setVal(val);
2856
        },
2857

    
2858
        // multi
2859
        unselect: function (selected) {
2860
            var val = this.getVal(),
2861
                data,
2862
                index;
2863
            selected = selected.closest(".select2-search-choice");
2864

    
2865
            if (selected.length === 0) {
2866
                throw "Invalid argument: " + selected + ". Must be .select2-search-choice";
2867
            }
2868

    
2869
            data = selected.data("select2-data");
2870

    
2871
            if (!data) {
2872
                // prevent a race condition when the 'x' is clicked really fast repeatedly the event can be queued
2873
                // and invoked on an element already removed
2874
                return;
2875
            }
2876

    
2877
            while((index = indexOf(this.id(data), val)) >= 0) {
2878
                val.splice(index, 1);
2879
                this.setVal(val);
2880
                if (this.select) this.postprocessResults();
2881
            }
2882

    
2883
            var evt = $.Event("select2-removing");
2884
            evt.val = this.id(data);
2885
            evt.choice = data;
2886
            this.opts.element.trigger(evt);
2887

    
2888
            if (evt.isDefaultPrevented()) {
2889
                return;
2890
            }
2891

    
2892
            selected.remove();
2893

    
2894
            this.opts.element.trigger({ type: "select2-removed", val: this.id(data), choice: data });
2895
            this.triggerChange({ removed: data });
2896
        },
2897

    
2898
        // multi
2899
        postprocessResults: function (data, initial, noHighlightUpdate) {
2900
            var val = this.getVal(),
2901
                choices = this.results.find(".select2-result"),
2902
                compound = this.results.find(".select2-result-with-children"),
2903
                self = this;
2904

    
2905
            choices.each2(function (i, choice) {
2906
                var id = self.id(choice.data("select2-data"));
2907
                if (indexOf(id, val) >= 0) {
2908
                    choice.addClass("select2-selected");
2909
                    // mark all children of the selected parent as selected
2910
                    choice.find(".select2-result-selectable").addClass("select2-selected");
2911
                }
2912
            });
2913

    
2914
            compound.each2(function(i, choice) {
2915
                // hide an optgroup if it doesnt have any selectable children
2916
                if (!choice.is('.select2-result-selectable')
2917
                    && choice.find(".select2-result-selectable:not(.select2-selected)").length === 0) {
2918
                    choice.addClass("select2-selected");
2919
                }
2920
            });
2921

    
2922
            if (this.highlight() == -1 && noHighlightUpdate !== false){
2923
                self.highlight(0);
2924
            }
2925

    
2926
            //If all results are chosen render formatNoMAtches
2927
            if(!this.opts.createSearchChoice && !choices.filter('.select2-result:not(.select2-selected)').length > 0){
2928
                if(!data || data && !data.more && this.results.find(".select2-no-results").length === 0) {
2929
                    if (checkFormatter(self.opts.formatNoMatches, "formatNoMatches")) {
2930
                        this.results.append("<li class='select2-no-results'>" + self.opts.formatNoMatches(self.search.val()) + "</li>");
2931
                    }
2932
                }
2933
            }
2934

    
2935
        },
2936

    
2937
        // multi
2938
        getMaxSearchWidth: function() {
2939
            return this.selection.width() - getSideBorderPadding(this.search);
2940
        },
2941

    
2942
        // multi
2943
        resizeSearch: function () {
2944
            var minimumWidth, left, maxWidth, containerLeft, searchWidth,
2945
                sideBorderPadding = getSideBorderPadding(this.search);
2946

    
2947
            minimumWidth = measureTextWidth(this.search) + 10;
2948

    
2949
            left = this.search.offset().left;
2950

    
2951
            maxWidth = this.selection.width();
2952
            containerLeft = this.selection.offset().left;
2953

    
2954
            searchWidth = maxWidth - (left - containerLeft) - sideBorderPadding;
2955

    
2956
            if (searchWidth < minimumWidth) {
2957
                searchWidth = maxWidth - sideBorderPadding;
2958
            }
2959

    
2960
            if (searchWidth < 40) {
2961
                searchWidth = maxWidth - sideBorderPadding;
2962
            }
2963

    
2964
            if (searchWidth <= 0) {
2965
              searchWidth = minimumWidth;
2966
            }
2967

    
2968
            this.search.width(Math.floor(searchWidth));
2969
        },
2970

    
2971
        // multi
2972
        getVal: function () {
2973
            var val;
2974
            if (this.select) {
2975
                val = this.select.val();
2976
                return val === null ? [] : val;
2977
            } else {
2978
                val = this.opts.element.val();
2979
                return splitVal(val, this.opts.separator);
2980
            }
2981
        },
2982

    
2983
        // multi
2984
        setVal: function (val) {
2985
            var unique;
2986
            if (this.select) {
2987
                this.select.val(val);
2988
            } else {
2989
                unique = [];
2990
                // filter out duplicates
2991
                $(val).each(function () {
2992
                    if (indexOf(this, unique) < 0) unique.push(this);
2993
                });
2994
                this.opts.element.val(unique.length === 0 ? "" : unique.join(this.opts.separator));
2995
            }
2996
        },
2997

    
2998
        // multi
2999
        buildChangeDetails: function (old, current) {
3000
            var current = current.slice(0),
3001
                old = old.slice(0);
3002

    
3003
            // remove intersection from each array
3004
            for (var i = 0; i < current.length; i++) {
3005
                for (var j = 0; j < old.length; j++) {
3006
                    if (equal(this.opts.id(current[i]), this.opts.id(old[j]))) {
3007
                        current.splice(i, 1);
3008
                        if(i>0){
3009
                        	i--;
3010
                        }
3011
                        old.splice(j, 1);
3012
                        j--;
3013
                    }
3014
                }
3015
            }
3016

    
3017
            return {added: current, removed: old};
3018
        },
3019

    
3020

    
3021
        // multi
3022
        val: function (val, triggerChange) {
3023
            var oldData, self=this;
3024

    
3025
            if (arguments.length === 0) {
3026
                return this.getVal();
3027
            }
3028

    
3029
            oldData=this.data();
3030
            if (!oldData.length) oldData=[];
3031

    
3032
            // val is an id. !val is true for [undefined,null,'',0] - 0 is legal
3033
            if (!val && val !== 0) {
3034
                this.opts.element.val("");
3035
                this.updateSelection([]);
3036
                this.clearSearch();
3037
                if (triggerChange) {
3038
                    this.triggerChange({added: this.data(), removed: oldData});
3039
                }
3040
                return;
3041
            }
3042

    
3043
            // val is a list of ids
3044
            this.setVal(val);
3045

    
3046
            if (this.select) {
3047
                this.opts.initSelection(this.select, this.bind(this.updateSelection));
3048
                if (triggerChange) {
3049
                    this.triggerChange(this.buildChangeDetails(oldData, this.data()));
3050
                }
3051
            } else {
3052
                if (this.opts.initSelection === undefined) {
3053
                    throw new Error("val() cannot be called if initSelection() is not defined");
3054
                }
3055

    
3056
                this.opts.initSelection(this.opts.element, function(data){
3057
                    var ids=$.map(data, self.id);
3058
                    self.setVal(ids);
3059
                    self.updateSelection(data);
3060
                    self.clearSearch();
3061
                    if (triggerChange) {
3062
                        self.triggerChange(self.buildChangeDetails(oldData, self.data()));
3063
                    }
3064
                });
3065
            }
3066
            this.clearSearch();
3067
        },
3068

    
3069
        // multi
3070
        onSortStart: function() {
3071
            if (this.select) {
3072
                throw new Error("Sorting of elements is not supported when attached to <select>. Attach to <input type='hidden'/> instead.");
3073
            }
3074

    
3075
            // collapse search field into 0 width so its container can be collapsed as well
3076
            this.search.width(0);
3077
            // hide the container
3078
            this.searchContainer.hide();
3079
        },
3080

    
3081
        // multi
3082
        onSortEnd:function() {
3083

    
3084
            var val=[], self=this;
3085

    
3086
            // show search and move it to the end of the list
3087
            this.searchContainer.show();
3088
            // make sure the search container is the last item in the list
3089
            this.searchContainer.appendTo(this.searchContainer.parent());
3090
            // since we collapsed the width in dragStarted, we resize it here
3091
            this.resizeSearch();
3092

    
3093
            // update selection
3094
            this.selection.find(".select2-search-choice").each(function() {
3095
                val.push(self.opts.id($(this).data("select2-data")));
3096
            });
3097
            this.setVal(val);
3098
            this.triggerChange();
3099
        },
3100

    
3101
        // multi
3102
        data: function(values, triggerChange) {
3103
            var self=this, ids, old;
3104
            if (arguments.length === 0) {
3105
                 return this.selection
3106
                     .find(".select2-search-choice")
3107
                     .map(function() { return $(this).data("select2-data"); })
3108
                     .get();
3109
            } else {
3110
                old = this.data();
3111
                if (!values) { values = []; }
3112
                ids = $.map(values, function(e) { return self.opts.id(e); });
3113
                this.setVal(ids);
3114
                this.updateSelection(values);
3115
                this.clearSearch();
3116
                if (triggerChange) {
3117
                    this.triggerChange(this.buildChangeDetails(old, this.data()));
3118
                }
3119
            }
3120
        }
3121
    });
3122

    
3123
    $.fn.select2 = function () {
3124

    
3125
        var args = Array.prototype.slice.call(arguments, 0),
3126
            opts,
3127
            select2,
3128
            method, value, multiple,
3129
            allowedMethods = ["val", "destroy", "opened", "open", "close", "focus", "isFocused", "container", "dropdown", "onSortStart", "onSortEnd", "enable", "disable", "readonly", "positionDropdown", "data", "search", "updateResults"],
3130
            valueMethods = ["opened", "isFocused", "container", "dropdown"],
3131
            propertyMethods = ["val", "data"],
3132
            methodsMap = { search: "externalSearch" };
3133

    
3134
        this.each(function () {
3135
            if (args.length === 0 || typeof(args[0]) === "object") {
3136
                opts = args.length === 0 ? {} : $.extend({}, args[0]);
3137
                opts.element = $(this);
3138

    
3139
                if (opts.element.get(0).tagName.toLowerCase() === "select") {
3140
                    multiple = opts.element.prop("multiple");
3141
                } else {
3142
                    multiple = opts.multiple || false;
3143
                    if ("tags" in opts) {opts.multiple = multiple = true;}
3144
                }
3145

    
3146
                select2 = multiple ? new MultiSelect2() : new SingleSelect2();
3147
                select2.init(opts);
3148
            } else if (typeof(args[0]) === "string") {
3149

    
3150
                if (indexOf(args[0], allowedMethods) < 0) {
3151
                    throw "Unknown method: " + args[0];
3152
                }
3153

    
3154
                value = undefined;
3155
                select2 = $(this).data("select2");
3156
                if (select2 === undefined) return;
3157

    
3158
                method=args[0];
3159

    
3160
                if (method === "container") {
3161
                    value = select2.container;
3162
                } else if (method === "dropdown") {
3163
                    value = select2.dropdown;
3164
                } else {
3165
                    if (methodsMap[method]) method = methodsMap[method];
3166

    
3167
                    value = select2[method].apply(select2, args.slice(1));
3168
                }
3169
                if (indexOf(args[0], valueMethods) >= 0
3170
                    || (indexOf(args[0], propertyMethods) && args.length == 1)) {
3171
                    return false; // abort the iteration, ready to return first matched value
3172
                }
3173
            } else {
3174
                throw "Invalid arguments to select2 plugin: " + args;
3175
            }
3176
        });
3177
        return (value === undefined) ? this : value;
3178
    };
3179

    
3180
    // plugin defaults, accessible to users
3181
    $.fn.select2.defaults = {
3182
        width: "copy",
3183
        loadMorePadding: 0,
3184
        closeOnSelect: true,
3185
        openOnEnter: true,
3186
        containerCss: {},
3187
        dropdownCss: {},
3188
        containerCssClass: "",
3189
        dropdownCssClass: "",
3190
        formatResult: function(result, container, query, escapeMarkup) {
3191
            var markup=[];
3192
            markMatch(result.text, query.term, markup, escapeMarkup);
3193
            return markup.join("");
3194
        },
3195
        formatSelection: function (data, container, escapeMarkup) {
3196
            return data ? escapeMarkup(data.text) : undefined;
3197
        },
3198
        sortResults: function (results, container, query) {
3199
            return results;
3200
        },
3201
        formatResultCssClass: function(data) {return undefined;},
3202
        formatSelectionCssClass: function(data, container) {return undefined;},
3203
        formatNoMatches: function () { return "No matches found"; },
3204
        formatInputTooShort: function (input, min) { var n = min - input.length; return "Please enter " + n + " more character" + (n == 1? "" : "s"); },
3205
        formatInputTooLong: function (input, max) { var n = input.length - max; return "Please delete " + n + " character" + (n == 1? "" : "s"); },
3206
        formatSelectionTooBig: function (limit) { return "You can only select " + limit + " item" + (limit == 1 ? "" : "s"); },
3207
        formatLoadMore: function (pageNumber) { return "Loading more results..."; },
3208
        formatSearching: function () { return "Searching..."; },
3209
        minimumResultsForSearch: 0,
3210
        minimumInputLength: 0,
3211
        maximumInputLength: null,
3212
        maximumSelectionSize: 0,
3213
        id: function (e) { return e.id; },
3214
        matcher: function(term, text) {
3215
            return stripDiacritics(''+text).toUpperCase().indexOf(stripDiacritics(''+term).toUpperCase()) >= 0;
3216
        },
3217
        separator: ",",
3218
        tokenSeparators: [],
3219
        tokenizer: defaultTokenizer,
3220
        escapeMarkup: defaultEscapeMarkup,
3221
        blurOnChange: false,
3222
        selectOnBlur: false,
3223
        adaptContainerCssClass: function(c) { return c; },
3224
        adaptDropdownCssClass: function(c) { return null; },
3225
        nextSearchTerm: function(selectedObject, currentSearchTerm) { return undefined; }
3226
    };
3227

    
3228
    $.fn.select2.ajaxDefaults = {
3229
        transport: $.ajax,
3230
        params: {
3231
            type: "GET",
3232
            cache: false,
3233
            dataType: "json"
3234
        }
3235
    };
3236

    
3237
    // exports
3238
    window.Select2 = {
3239
        query: {
3240
            ajax: ajax,
3241
            local: local,
3242
            tags: tags
3243
        }, util: {
3244
            debounce: debounce,
3245
            markMatch: markMatch,
3246
            escapeMarkup: defaultEscapeMarkup,
3247
            stripDiacritics: stripDiacritics
3248
        }, "class": {
3249
            "abstract": AbstractSelect2,
3250
            "single": SingleSelect2,
3251
            "multi": MultiSelect2
3252
        }
3253
    };
3254

    
3255
}(jQuery));
(9-9/54)