2 Copyright 2012 Igor Vaynberg
4 Version: @@ver@@ Timestamp: @@timestamp@@
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.
11 You may obtain a copy of the Apache License and the GPL License at:
13 http://www.apache.org/licenses/LICENSE-2.0
14 http://www.gnu.org/licenses/gpl-2.0.html
16 Unless required by applicable law or agreed to in writing, software distributed under the
17 Apache License or the GPL License 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.
22 if(typeof $.fn
.each2
== "undefined") {
25 * 4-10 times faster .each replacement
26 * use it carefully, as it overrides jQuery context of element on each iteration
28 each2 : function (c
) {
29 var j
= $([0]), i
= -1, l
= this.length
;
32 && (j
.context
= j
[0] = this[i
])
33 && c
.call(j
[0], i
, j
) !== false //"this"=DOM, i=index, j=jQuery object
41 (function ($, undefined) {
43 /*global document, window, console */
45 var AbstractSelect2
, SingleSelect2
, MultiSelect2
, nextUid
, sizer
,
46 lastMousePosition
={x
:0,y
:0}, $document
, scrollBarDimensions
,
66 isArrow: function (k
) {
67 k
= k
.which
? k
.which
: k
;
77 isControl: function (e
) {
86 if (e
.metaKey
) return true;
90 isFunctionKey: function (k
) {
91 k
= k
.which
? k
.which
: k
;
92 return k
>= 112 && k
<= 123;
95 MEASURE_SCROLLBAR_TEMPLATE
= "<div class='select2-measure-scrollbar'></div>",
97 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","\u0386":"\u0391","\u0388":"\u0395","\u0389":"\u0397","\u038A":"\u0399","\u03AA":"\u0399","\u038C":"\u039F","\u038E":"\u03A5","\u03AB":"\u03A5","\u038F":"\u03A9","\u03AC":"\u03B1","\u03AD":"\u03B5","\u03AE":"\u03B7","\u03AF":"\u03B9","\u03CA":"\u03B9","\u0390":"\u03B9","\u03CC":"\u03BF","\u03CD":"\u03C5","\u03CB":"\u03C5","\u03B0":"\u03C5","\u03C9":"\u03C9","\u03C2":"\u03C3"};
99 $document
= $(document
);
101 nextUid
=(function() { var counter
=1; return function() { return counter
++; }; }());
104 function reinsertElement(element
) {
105 var placeholder
= $(document
.createTextNode(''));
107 element
.before(placeholder
);
108 placeholder
.before(element
);
109 placeholder
.remove();
112 function stripDiacritics(str
) {
113 // Used 'uni range + named function' from http://jsperf.com/diacritics/18
115 return DIACRITICS
[a
] || a
;
118 return str
.replace(/[^\u0000-\u007E]/g, match
);
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
;
129 function measureScrollbar () {
130 var $template
= $( MEASURE_SCROLLBAR_TEMPLATE
);
131 $template
.appendTo(document
.body
);
134 width
: $template
.width() - $template
[0].clientWidth
,
135 height
: $template
.height() - $template
[0].clientHeight
143 * Compares equality of a and b
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
159 * Splits the string into an array of values, transforming each value. An empty array is returned for nulls or empty
164 function splitVal(string
, separator
, transform
) {
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
] = transform(val
[i
]);
172 function getSideBorderPadding(element
) {
173 return element
.outerWidth(false) - element
.width();
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());
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");
194 * filters mouse events so an event is fired only if the mouse moved.
196 * filters out mouse events that occur when mouse is stationary but
197 * the elements under the pointer are scrolled.
199 function installFilteredMouseMove(element
) {
200 element
.on("mousemove", function (e
) {
201 var lastpos
= lastMousePosition
;
202 if (lastpos
=== undefined || lastpos
.x
!== e
.pageX
|| lastpos
.y
!== e
.pageY
) {
203 $(e
.target
).trigger("mousemove-filtered", e
);
209 * Debounces a function. Returns a function that calls the original fn function only if no invocations have been made
210 * within the last quietMillis milliseconds.
212 * @param quietMillis number of milliseconds to wait before invoking fn
213 * @param fn function to be debounced
214 * @param ctx object to be used as this reference within fn
215 * @return debounced version of fn
217 function debounce(quietMillis
, fn
, ctx
) {
218 ctx
= ctx
|| undefined;
221 var args
= arguments
;
222 window
.clearTimeout(timeout
);
223 timeout
= window
.setTimeout(function() {
229 function installDebouncedScroll(threshold
, element
) {
230 var notify
= debounce(threshold
, function (e
) { element
.trigger("scroll-debounced", e
);});
231 element
.on("scroll", function (e
) {
232 if (indexOf(e
.target
, element
.get()) >= 0) notify(e
);
236 function focus($el
) {
237 if ($el
[0] === document
.activeElement
) return;
239 /* set the focus in a 0 timeout - that way the focus is set after the processing
240 of the current event has finished - which seems like the only reliable way
242 window
.setTimeout(function() {
243 var el
=$el
[0], pos
=$el
.val().length
, range
;
247 /* make sure el received focus so we do not error out when trying to manipulate the caret.
248 sometimes modals or others listeners may steal it after its set */
249 var isVisible
= (el
.offsetWidth
> 0 || el
.offsetHeight
> 0);
250 if (isVisible
&& el
=== document
.activeElement
) {
252 /* after the focus is set move the caret to the end, necessary when we val()
253 just before setting focus */
254 if(el
.setSelectionRange
)
256 el
.setSelectionRange(pos
, pos
);
258 else if (el
.createTextRange
) {
259 range
= el
.createTextRange();
260 range
.collapse(false);
267 function getCursorInfo(el
) {
271 if ('selectionStart' in el
) {
272 offset
= el
.selectionStart
;
273 length
= el
.selectionEnd
- offset
;
274 } else if ('selection' in document
) {
276 var sel
= document
.selection
.createRange();
277 length
= document
.selection
.createRange().text
.length
;
278 sel
.moveStart('character', -el
.value
.length
);
279 offset
= sel
.text
.length
- length
;
281 return { offset
: offset
, length
: length
};
284 function killEvent(event
) {
285 event
.preventDefault();
286 event
.stopPropagation();
288 function killEventImmediately(event
) {
289 event
.preventDefault();
290 event
.stopImmediatePropagation();
293 function measureTextWidth(e
) {
295 var style
= e
[0].currentStyle
|| window
.getComputedStyle(e
[0], null);
296 sizer
= $(document
.createElement("div")).css({
297 position
: "absolute",
301 fontSize
: style
.fontSize
,
302 fontFamily
: style
.fontFamily
,
303 fontStyle
: style
.fontStyle
,
304 fontWeight
: style
.fontWeight
,
305 letterSpacing
: style
.letterSpacing
,
306 textTransform
: style
.textTransform
,
309 sizer
.attr("class","select2-sizer");
310 $(document
.body
).append(sizer
);
313 return sizer
.width();
316 function syncCssClasses(dest
, src
, adapter
) {
317 var classes
, replacements
= [], adapted
;
319 classes
= $.trim(dest
.attr("class"));
322 classes
= '' + classes
; // for IE which returns object
324 $(classes
.split(/\s+/)).each2(function() {
325 if (this.indexOf("select2-") === 0) {
326 replacements
.push(this);
331 classes
= $.trim(src
.attr("class"));
334 classes
= '' + classes
; // for IE which returns object
336 $(classes
.split(/\s+/)).each2(function() {
337 if (this.indexOf("select2-") !== 0) {
338 adapted
= adapter(this);
341 replacements
.push(adapted
);
347 dest
.attr("class", replacements
.join(" "));
351 function markMatch(text
, term
, markup
, escapeMarkup
) {
352 var match
=stripDiacritics(text
.toUpperCase()).indexOf(stripDiacritics(term
.toUpperCase())),
356 markup
.push(escapeMarkup(text
));
360 markup
.push(escapeMarkup(text
.substring(0, match
)));
361 markup
.push("<span class='select2-match'>");
362 markup
.push(escapeMarkup(text
.substring(match
, match
+ tl
)));
363 markup
.push("</span>");
364 markup
.push(escapeMarkup(text
.substring(match
+ tl
, text
.length
)));
367 function defaultEscapeMarkup(markup
) {
378 return String(markup
).replace(/[&<>"'\/\\]/g, function (match
) {
379 return replace_map
[match
];
384 * Produces an ajax-based query function
386 * @param options object containing configuration parameters
387 * @param options.params parameter map for the transport ajax call, can contain such options as cache, jsonpCallback, etc. see $.ajax
388 * @param options.transport function that will be used to execute the ajax request. must be compatible with parameters supported by $.ajax
389 * @param options.url url for the data
390 * @param options.data a function(searchTerm, pageNumber, context) that should return an object containing query string parameters for the above url.
391 * @param options.dataType request data type: ajax, jsonp, other datatypes supported by jQuery's $.ajax function or the transport function if specified
392 * @param options.quietMillis (optional) milliseconds to wait before making the ajaxRequest, helps debounce the ajax function if invoked too often
393 * @param options.results a function(remoteData, pageNumber, query) that converts data returned form the remote request to the format expected by Select2.
394 * The expected format is an object containing the following keys:
395 * results array of objects that will be used as choices
396 * more (optional) boolean indicating whether there are more results available
397 * Example: {results:[{id:1, text:'Red'},{id:2, text:'Blue'}], more:true}
399 function ajax(options
) {
400 var timeout
, // current scheduled but not yet executed request
402 quietMillis
= options
.quietMillis
|| 100,
403 ajaxUrl
= options
.url
,
406 return function (query
) {
407 window
.clearTimeout(timeout
);
408 timeout
= window
.setTimeout(function () {
409 var data
= options
.data
, // ajax data function
410 url
= ajaxUrl
, // ajax url string or function
411 transport
= options
.transport
|| $.fn
.select2
.ajaxDefaults
.transport
,
412 // deprecated - to be removed in 4.0 - use params instead
414 type
: options
.type
|| 'GET', // set type of request (GET or POST)
415 cache
: options
.cache
|| false,
416 jsonpCallback
: options
.jsonpCallback
||undefined,
417 dataType
: options
.dataType
||"json"
419 params
= $.extend({}, $.fn
.select2
.ajaxDefaults
.params
, deprecated
);
421 data
= data
? data
.call(self
, query
.term
, query
.page
, query
.context
) : null;
422 url
= (typeof url
=== 'function') ? url
.call(self
, query
.term
, query
.page
, query
.context
) : url
;
424 if (handler
&& typeof handler
.abort
=== "function") { handler
.abort(); }
426 if (options
.params
) {
427 if ($.isFunction(options
.params
)) {
428 $.extend(params
, options
.params
.call(self
));
430 $.extend(params
, options
.params
);
436 dataType
: options
.dataType
,
438 success: function (data
) {
439 // TODO - replace query.page with query so users have access to term, page, etc.
440 // added query as third paramter to keep backwards compatibility
441 var results
= options
.results(data
, query
.page
, query
);
442 query
.callback(results
);
444 error: function(jqXHR
, textStatus
, errorThrown
){
448 textStatus
: textStatus
,
449 errorThrown
: errorThrown
452 query
.callback(results
);
455 handler
= transport
.call(self
, params
);
461 * Produces a query function that works with a local array
463 * @param options object containing configuration parameters. The options parameter can either be an array or an
466 * If the array form is used it is assumed that it contains objects with 'id' and 'text' keys.
468 * If the object form is used it is assumed that it contains 'data' and 'text' keys. The 'data' key should contain
469 * an array of objects that will be used as choices. These objects must contain at least an 'id' key. The 'text'
470 * key can either be a String in which case it is expected that each element in the 'data' array has a key with the
471 * value of 'text' which will be used to match choices. Alternatively, text can be a function(item) that can extract
474 function local(options
) {
475 var data
= options
, // data elements
478 text = function (item
) { return ""+item
.text
; }; // function used to retrieve the text portion of a data item that is matched against the search
480 if ($.isArray(data
)) {
482 data
= { results
: tmp
};
485 if ($.isFunction(data
) === false) {
487 data = function() { return tmp
; };
490 var dataItem
= data();
492 text
= dataItem
.text
;
493 // if text is not a function we assume it to be a key name
494 if (!$.isFunction(text
)) {
495 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
496 text = function (item
) { return item
[dataText
]; };
500 return function (query
) {
501 var t
= query
.term
, filtered
= { results
: [] }, process
;
503 query
.callback(data());
507 process = function(datum
, collection
) {
510 if (datum
.children
) {
512 for (attr
in datum
) {
513 if (datum
.hasOwnProperty(attr
)) group
[attr
]=datum
[attr
];
516 $(datum
.children
).each2(function(i
, childDatum
) { process(childDatum
, group
.children
); });
517 if (group
.children
.length
|| query
.matcher(t
, text(group
), datum
)) {
518 collection
.push(group
);
521 if (query
.matcher(t
, text(datum
), datum
)) {
522 collection
.push(datum
);
527 $(data().results
).each2(function(i
, datum
) { process(datum
, filtered
.results
); });
528 query
.callback(filtered
);
533 function tags(data
) {
534 var isFunc
= $.isFunction(data
);
535 return function (query
) {
536 var t
= query
.term
, filtered
= {results
: []};
537 var result
= isFunc
? data(query
) : data
;
538 if ($.isArray(result
)) {
539 $(result
).each(function () {
540 var isObject
= this.text
!== undefined,
541 text
= isObject
? this.text
: this;
542 if (t
=== "" || query
.matcher(t
, text
)) {
543 filtered
.results
.push(isObject
? this : {id
: this, text
: this});
546 query
.callback(filtered
);
552 * Checks if the formatter function should be used.
554 * Throws an error if it is not a function. Returns true if it should be used,
555 * false if no formatting should be performed.
559 function checkFormatter(formatter
, formatterName
) {
560 if ($.isFunction(formatter
)) return true;
561 if (!formatter
) return false;
562 if (typeof(formatter
) === 'string') return true;
563 throw new Error(formatterName
+" must be a string, function, or falsy value");
567 * Returns a given value
568 * If given a function, returns its output
570 * @param val string|function
571 * @param context value of "this" to be passed to function
574 function evaluate(val
, context
) {
575 if ($.isFunction(val
)) {
576 var args
= Array
.prototype.slice
.call(arguments
, 2);
577 return val
.apply(context
, args
);
582 function countResults(results
) {
584 $.each(results
, function(i
, item
) {
586 count
+= countResults(item
.children
);
595 * Default tokenizer. This function uses breaks the input on substring match of any string from the
596 * opts.tokenSeparators array and uses opts.createSearchChoice to create the choice object. Both of those
597 * two options have to be defined in order for the tokenizer to work.
599 * @param input text user has typed so far or pasted into the search field
600 * @param selection currently selected choices
601 * @param selectCallback function(choice) callback tho add the choice to selection
602 * @param opts select2's opts
603 * @return undefined/null to leave the current input unchanged, or a string to change the input to the returned value
605 function defaultTokenizer(input
, selection
, selectCallback
, opts
) {
606 var original
= input
, // store the original so we can compare and know if we need to tell the search to update its text
607 dupe
= false, // check for whether a token we extracted represents a duplicate selected choice
609 index
, // position at which the separator was found
610 i
, l
, // looping variables
611 separator
; // the matched separator
613 if (!opts
.createSearchChoice
|| !opts
.tokenSeparators
|| opts
.tokenSeparators
.length
< 1) return undefined;
618 for (i
= 0, l
= opts
.tokenSeparators
.length
; i
< l
; i
++) {
619 separator
= opts
.tokenSeparators
[i
];
620 index
= input
.indexOf(separator
);
621 if (index
>= 0) break;
624 if (index
< 0) break; // did not find any token separator in the input string, bail
626 token
= input
.substring(0, index
);
627 input
= input
.substring(index
+ separator
.length
);
629 if (token
.length
> 0) {
630 token
= opts
.createSearchChoice
.call(this, token
, selection
);
631 if (token
!== undefined && token
!== null && opts
.id(token
) !== undefined && opts
.id(token
) !== null) {
633 for (i
= 0, l
= selection
.length
; i
< l
; i
++) {
634 if (equal(opts
.id(token
), opts
.id(selection
[i
]))) {
639 if (!dupe
) selectCallback(token
);
644 if (original
!==input
) return input
;
647 function cleanupJQueryElements() {
650 $.each(arguments
, function (i
, element
) {
651 self
[element
].remove();
652 self
[element
] = null;
657 * Creates a new class
662 function clazz(SuperClass
, methods
) {
663 var constructor = function () {};
664 constructor.prototype = new SuperClass
;
665 constructor.prototype.constructor = constructor;
666 constructor.prototype.parent
= SuperClass
.prototype;
667 constructor.prototype = $.extend(constructor.prototype, methods
);
671 AbstractSelect2
= clazz(Object
, {
674 bind: function (func
) {
677 func
.apply(self
, arguments
);
682 init: function (opts
) {
683 var results
, search
, resultsSelector
= ".select2-results";
686 this.opts
= opts
= this.prepareOpts(opts
);
690 // destroy if called on an existing component
691 if (opts
.element
.data("select2") !== undefined &&
692 opts
.element
.data("select2") !== null) {
693 opts
.element
.data("select2").destroy();
696 this.container
= this.createContainer();
698 this.liveRegion
= $('.select2-hidden-accessible');
699 if (this.liveRegion
.length
== 0) {
700 this.liveRegion
= $("<span>", {
702 "aria-live": "polite"
704 .addClass("select2-hidden-accessible")
705 .appendTo(document
.body
);
708 this.containerId
="s2id_"+(opts
.element
.attr("id") || "autogen"+nextUid());
709 this.containerEventName
= this.containerId
710 .replace(/([.])/g, '_')
711 .replace(/([;&,\-\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '\\$1');
712 this.container
.attr("id", this.containerId
);
714 this.container
.attr("title", opts
.element
.attr("title"));
716 this.body
= $(document
.body
);
718 syncCssClasses(this.container
, this.opts
.element
, this.opts
.adaptContainerCssClass
);
720 this.container
.attr("style", opts
.element
.attr("style"));
721 this.container
.css(evaluate(opts
.containerCss
, this.opts
.element
));
722 this.container
.addClass(evaluate(opts
.containerCssClass
, this.opts
.element
));
724 this.elementTabIndex
= this.opts
.element
.attr("tabindex");
726 // swap container for the element
728 .data("select2", this)
729 .attr("tabindex", "-1")
730 .before(this.container
)
731 .on("click.select2", killEvent
); // do not leak click events
733 this.container
.data("select2", this);
735 this.dropdown
= this.container
.find(".select2-drop");
737 syncCssClasses(this.dropdown
, this.opts
.element
, this.opts
.adaptDropdownCssClass
);
739 this.dropdown
.addClass(evaluate(opts
.dropdownCssClass
, this.opts
.element
));
740 this.dropdown
.data("select2", this);
741 this.dropdown
.on("click", killEvent
);
743 this.results
= results
= this.container
.find(resultsSelector
);
744 this.search
= search
= this.container
.find("input.select2-input");
747 this.resultsPage
= 0;
750 // initialize the container
751 this.initContainer();
753 this.container
.on("click", killEvent
);
755 installFilteredMouseMove(this.results
);
757 this.dropdown
.on("mousemove-filtered", resultsSelector
, this.bind(this.highlightUnderEvent
));
758 this.dropdown
.on("touchstart touchmove touchend", resultsSelector
, this.bind(function (event
) {
759 this._touchEvent
= true;
760 this.highlightUnderEvent(event
);
762 this.dropdown
.on("touchmove", resultsSelector
, this.bind(this.touchMoved
));
763 this.dropdown
.on("touchstart touchend", resultsSelector
, this.bind(this.clearTouchMoved
));
765 // Waiting for a click event on touch devices to select option and hide dropdown
766 // otherwise click will be triggered on an underlying element
767 this.dropdown
.on('click', this.bind(function (event
) {
768 if (this._touchEvent
) {
769 this._touchEvent
= false;
770 this.selectHighlighted();
774 installDebouncedScroll(80, this.results
);
775 this.dropdown
.on("scroll-debounced", resultsSelector
, this.bind(this.loadMoreIfNeeded
));
777 // do not propagate change event from the search field out of the component
778 $(this.container
).on("change", ".select2-input", function(e
) {e
.stopPropagation();});
779 $(this.dropdown
).on("change", ".select2-input", function(e
) {e
.stopPropagation();});
781 // if jquery.mousewheel plugin is installed we can prevent out-of-bounds scrolling of results via mousewheel
782 if ($.fn
.mousewheel
) {
783 results
.mousewheel(function (e
, delta
, deltaX
, deltaY
) {
784 var top
= results
.scrollTop();
785 if (deltaY
> 0 && top
- deltaY
<= 0) {
786 results
.scrollTop(0);
788 } else if (deltaY
< 0 && results
.get(0).scrollHeight
- results
.scrollTop() + deltaY
<= results
.height()) {
789 results
.scrollTop(results
.get(0).scrollHeight
- results
.height());
795 installKeyUpChangeEvent(search
);
796 search
.on("keyup-change input paste", this.bind(this.updateResults
));
797 search
.on("focus", function () { search
.addClass("select2-focused"); });
798 search
.on("blur", function () { search
.removeClass("select2-focused");});
800 this.dropdown
.on("mouseup", resultsSelector
, this.bind(function (e
) {
801 if ($(e
.target
).closest(".select2-result-selectable").length
> 0) {
802 this.highlightUnderEvent(e
);
803 this.selectHighlighted(e
);
807 // trap all mouse events from leaving the dropdown. sometimes there may be a modal that is listening
808 // for mouse events outside of itself so it can close itself. since the dropdown is now outside the select2's
809 // dom it will trigger the popup close, which is not what we want
810 // focusin can cause focus wars between modals and select2 since the dropdown is outside the modal.
811 this.dropdown
.on("click mouseup mousedown touchstart touchend focusin", function (e
) { e
.stopPropagation(); });
813 this.lastSearchTerm
= undefined;
815 if ($.isFunction(this.opts
.initSelection
)) {
816 // initialize selection based on the current value of the source element
817 this.initSelection();
819 // if the user has provided a function that can set selection based on the value of the source element
820 // we monitor the change event on the element and trigger it, allowing for two way synchronization
821 this.monitorSource();
824 if (opts
.maximumInputLength
!== null) {
825 this.search
.attr("maxlength", opts
.maximumInputLength
);
828 var disabled
= opts
.element
.prop("disabled");
829 if (disabled
=== undefined) disabled
= false;
830 this.enable(!disabled
);
832 var readonly
= opts
.element
.prop("readonly");
833 if (readonly
=== undefined) readonly
= false;
834 this.readonly(readonly
);
836 // Calculate size of scrollbar
837 scrollBarDimensions
= scrollBarDimensions
|| measureScrollbar();
839 this.autofocus
= opts
.element
.prop("autofocus");
840 opts
.element
.prop("autofocus", false);
841 if (this.autofocus
) this.focus();
843 this.search
.attr("placeholder", opts
.searchInputPlaceholder
);
847 destroy: function () {
848 var element
=this.opts
.element
, select2
= element
.data("select2"), self
= this;
852 if (element
.length
&& element
[0].detachEvent
&& self
._sync
) {
853 element
.each(function () {
855 this.detachEvent("onpropertychange", self
._sync
);
859 if (this.propertyObserver
) {
860 this.propertyObserver
.disconnect();
861 this.propertyObserver
= null;
865 if (select2
!== undefined) {
866 select2
.container
.remove();
867 select2
.liveRegion
.remove();
868 select2
.dropdown
.remove();
869 element
.removeData("select2")
871 if (!element
.is("input[type='hidden']")) {
874 .prop("autofocus", this.autofocus
|| false);
875 if (this.elementTabIndex
) {
876 element
.attr({tabindex
: this.elementTabIndex
});
878 element
.removeAttr("tabindex");
882 element
.css("display", "");
886 cleanupJQueryElements
.call(this,
896 optionToData: function(element
) {
897 if (element
.is("option")) {
899 id
:element
.prop("value"),
901 element
: element
.get(),
902 css
: element
.attr("class"),
903 disabled
: element
.prop("disabled"),
904 locked
: equal(element
.attr("locked"), "locked") || equal(element
.data("locked"), true)
906 } else if (element
.is("optgroup")) {
908 text
:element
.attr("label"),
910 element
: element
.get(),
911 css
: element
.attr("class")
917 prepareOpts: function (opts
) {
918 var element
, select
, idKey
, ajaxUrl
, self
= this;
920 element
= opts
.element
;
922 if (element
.get(0).tagName
.toLowerCase() === "select") {
923 this.select
= select
= opts
.element
;
927 // these options are not allowed when attached to a select because they are picked up off the element itself
928 $.each(["id", "multiple", "ajax", "query", "createSearchChoice", "initSelection", "data", "tags"], function () {
930 throw new Error("Option '" + this + "' is not allowed for Select2 when attached to a <select> element.");
935 opts
= $.extend({}, {
936 populateResults: function(container
, results
, query
) {
937 var populate
, id
=this.opts
.id
, liveRegion
=this.liveRegion
;
939 populate=function(results
, container
, depth
) {
941 var i
, l
, result
, selectable
, disabled
, compound
, node
, label
, innerContainer
, formatted
;
943 results
= opts
.sortResults(results
, container
, query
);
945 // collect the created nodes for bulk append
947 for (i
= 0, l
= results
.length
; i
< l
; i
= i
+ 1) {
951 disabled
= (result
.disabled
=== true);
952 selectable
= (!disabled
) && (id(result
) !== undefined);
954 compound
=result
.children
&& result
.children
.length
> 0;
957 node
.addClass("select2-results-dept-"+depth
);
958 node
.addClass("select2-result");
959 node
.addClass(selectable
? "select2-result-selectable" : "select2-result-unselectable");
960 if (disabled
) { node
.addClass("select2-disabled"); }
961 if (compound
) { node
.addClass("select2-result-with-children"); }
962 node
.addClass(self
.opts
.formatResultCssClass(result
));
963 node
.attr("role", "presentation");
965 label
=$(document
.createElement("div"));
966 label
.addClass("select2-result-label");
967 label
.attr("id", "select2-result-label-" + nextUid());
968 label
.attr("role", "option");
970 formatted
=opts
.formatResult(result
, label
, query
, self
.opts
.escapeMarkup
);
971 if (formatted
!==undefined) {
972 label
.html(formatted
);
979 innerContainer
=$("<ul></ul>");
980 innerContainer
.addClass("select2-result-sub");
981 populate(result
.children
, innerContainer
, depth
+1);
982 node
.append(innerContainer
);
985 node
.data("select2-data", result
);
989 // bulk append the created nodes
990 container
.append(nodes
);
991 liveRegion
.text(opts
.formatMatches(results
.length
));
994 populate(results
, container
, 0);
996 }, $.fn
.select2
.defaults
, opts
);
998 if (typeof(opts
.id
) !== "function") {
1000 opts
.id = function (e
) { return e
[idKey
]; };
1003 if ($.isArray(opts
.element
.data("select2Tags"))) {
1004 if ("tags" in opts
) {
1005 throw "tags specified as both an attribute 'data-select2-tags' and in options of Select2 " + opts
.element
.attr("id");
1007 opts
.tags
=opts
.element
.data("select2Tags");
1011 opts
.query
= this.bind(function (query
) {
1012 var data
= { results
: [], more
: false },
1014 children
, placeholderOption
, process
;
1016 process=function(element
, collection
) {
1018 if (element
.is("option")) {
1019 if (query
.matcher(term
, element
.text(), element
)) {
1020 collection
.push(self
.optionToData(element
));
1022 } else if (element
.is("optgroup")) {
1023 group
=self
.optionToData(element
);
1024 element
.children().each2(function(i
, elm
) { process(elm
, group
.children
); });
1025 if (group
.children
.length
>0) {
1026 collection
.push(group
);
1031 children
=element
.children();
1033 // ignore the placeholder option if there is one
1034 if (this.getPlaceholder() !== undefined && children
.length
> 0) {
1035 placeholderOption
= this.getPlaceholderOption();
1036 if (placeholderOption
) {
1037 children
=children
.not(placeholderOption
);
1041 children
.each2(function(i
, elm
) { process(elm
, data
.results
); });
1043 query
.callback(data
);
1045 // this is needed because inside val() we construct choices from options and their id is hardcoded
1046 opts
.id=function(e
) { return e
.id
; };
1048 if (!("query" in opts
)) {
1050 if ("ajax" in opts
) {
1051 ajaxUrl
= opts
.element
.data("ajax-url");
1052 if (ajaxUrl
&& ajaxUrl
.length
> 0) {
1053 opts
.ajax
.url
= ajaxUrl
;
1055 opts
.query
= ajax
.call(opts
.element
, opts
.ajax
);
1056 } else if ("data" in opts
) {
1057 opts
.query
= local(opts
.data
);
1058 } else if ("tags" in opts
) {
1059 opts
.query
= tags(opts
.tags
);
1060 if (opts
.createSearchChoice
=== undefined) {
1061 opts
.createSearchChoice = function (term
) { return {id
: $.trim(term
), text
: $.trim(term
)}; };
1063 if (opts
.initSelection
=== undefined) {
1064 opts
.initSelection = function (element
, callback
) {
1066 $(splitVal(element
.val(), opts
.separator
, opts
.transformVal
)).each(function () {
1067 var obj
= { id
: this, text
: this },
1069 if ($.isFunction(tags
)) tags
=tags();
1070 $(tags
).each(function() { if (equal(this.id
, obj
.id
)) { obj
= this; return false; } });
1080 if (typeof(opts
.query
) !== "function") {
1081 throw "query function not defined for Select2 " + opts
.element
.attr("id");
1084 if (opts
.createSearchChoicePosition
=== 'top') {
1085 opts
.createSearchChoicePosition = function(list
, item
) { list
.unshift(item
); };
1087 else if (opts
.createSearchChoicePosition
=== 'bottom') {
1088 opts
.createSearchChoicePosition = function(list
, item
) { list
.push(item
); };
1090 else if (typeof(opts
.createSearchChoicePosition
) !== "function") {
1091 throw "invalid createSearchChoicePosition option must be 'top', 'bottom' or a custom function";
1098 * Monitor the original element for changes and update select2 accordingly
1101 monitorSource: function () {
1102 var el
= this.opts
.element
, observer
, self
= this;
1104 el
.on("change.select2", this.bind(function (e
) {
1105 if (this.opts
.element
.data("select2-change-triggered") !== true) {
1106 this.initSelection();
1110 this._sync
= this.bind(function () {
1112 // sync enabled state
1113 var disabled
= el
.prop("disabled");
1114 if (disabled
=== undefined) disabled
= false;
1115 this.enable(!disabled
);
1117 var readonly
= el
.prop("readonly");
1118 if (readonly
=== undefined) readonly
= false;
1119 this.readonly(readonly
);
1121 if (this.container
) {
1122 syncCssClasses(this.container
, this.opts
.element
, this.opts
.adaptContainerCssClass
);
1123 this.container
.addClass(evaluate(this.opts
.containerCssClass
, this.opts
.element
));
1126 if (this.dropdown
) {
1127 syncCssClasses(this.dropdown
, this.opts
.element
, this.opts
.adaptDropdownCssClass
);
1128 this.dropdown
.addClass(evaluate(this.opts
.dropdownCssClass
, this.opts
.element
));
1133 // IE8-10 (IE9/10 won't fire propertyChange via attachEventListener)
1134 if (el
.length
&& el
[0].attachEvent
) {
1135 el
.each(function() {
1136 this.attachEvent("onpropertychange", self
._sync
);
1140 // safari, chrome, firefox, IE11
1141 observer
= window
.MutationObserver
|| window
.WebKitMutationObserver
|| window
.MozMutationObserver
;
1142 if (observer
!== undefined) {
1143 if (this.propertyObserver
) { delete this.propertyObserver
; this.propertyObserver
= null; }
1144 this.propertyObserver
= new observer(function (mutations
) {
1145 $.each(mutations
, self
._sync
);
1147 this.propertyObserver
.observe(el
.get(0), { attributes
:true, subtree
:false });
1152 triggerSelect: function(data
) {
1153 var evt
= $.Event("select2-selecting", { val
: this.id(data
), object
: data
, choice
: data
});
1154 this.opts
.element
.trigger(evt
);
1155 return !evt
.isDefaultPrevented();
1159 * Triggers the change event on the source element
1162 triggerChange: function (details
) {
1164 details
= details
|| {};
1165 details
= $.extend({}, details
, { type
: "change", val
: this.val() });
1166 // prevents recursive triggering
1167 this.opts
.element
.data("select2-change-triggered", true);
1168 this.opts
.element
.trigger(details
);
1169 this.opts
.element
.data("select2-change-triggered", false);
1171 // some validation frameworks ignore the change event and listen instead to keyup, click for selects
1172 // so here we trigger the click event manually
1173 this.opts
.element
.click();
1175 // ValidationEngine ignores the change event and listens instead to blur
1176 // so here we trigger the blur event manually if so desired
1177 if (this.opts
.blurOnChange
)
1178 this.opts
.element
.blur();
1182 isInterfaceEnabled: function()
1184 return this.enabledInterface
=== true;
1188 enableInterface: function() {
1189 var enabled
= this._enabled
&& !this._readonly
,
1190 disabled
= !enabled
;
1192 if (enabled
=== this.enabledInterface
) return false;
1194 this.container
.toggleClass("select2-container-disabled", disabled
);
1196 this.enabledInterface
= enabled
;
1202 enable: function(enabled
) {
1203 if (enabled
=== undefined) enabled
= true;
1204 if (this._enabled
=== enabled
) return;
1205 this._enabled
= enabled
;
1207 this.opts
.element
.prop("disabled", !enabled
);
1208 this.enableInterface();
1212 disable: function() {
1217 readonly: function(enabled
) {
1218 if (enabled
=== undefined) enabled
= false;
1219 if (this._readonly
=== enabled
) return;
1220 this._readonly
= enabled
;
1222 this.opts
.element
.prop("readonly", enabled
);
1223 this.enableInterface();
1227 opened: function () {
1228 return (this.container
) ? this.container
.hasClass("select2-dropdown-open") : false;
1232 positionDropdown: function() {
1233 var $dropdown
= this.dropdown
,
1234 container
= this.container
,
1235 offset
= container
.offset(),
1236 height
= container
.outerHeight(false),
1237 width
= container
.outerWidth(false),
1238 dropHeight
= $dropdown
.outerHeight(false),
1239 $window
= $(window
),
1240 windowWidth
= $window
.width(),
1241 windowHeight
= $window
.height(),
1242 viewPortRight
= $window
.scrollLeft() + windowWidth
,
1243 viewportBottom
= $window
.scrollTop() + windowHeight
,
1244 dropTop
= offset
.top
+ height
,
1245 dropLeft
= offset
.left
,
1246 enoughRoomBelow
= dropTop
+ dropHeight
<= viewportBottom
,
1247 enoughRoomAbove
= (offset
.top
- dropHeight
) >= $window
.scrollTop(),
1248 dropWidth
= $dropdown
.outerWidth(false),
1249 enoughRoomOnRight = function() {
1250 return dropLeft
+ dropWidth
<= viewPortRight
;
1252 enoughRoomOnLeft = function() {
1253 return offset
.left
+ viewPortRight
+ container
.outerWidth(false) > dropWidth
;
1255 aboveNow
= $dropdown
.hasClass("select2-drop-above"),
1262 // always prefer the current above/below alignment, unless there is not enough room
1265 if (!enoughRoomAbove
&& enoughRoomBelow
) {
1266 changeDirection
= true;
1271 if (!enoughRoomBelow
&& enoughRoomAbove
) {
1272 changeDirection
= true;
1277 //if we are changing direction we need to get positions when dropdown is hidden;
1278 if (changeDirection
) {
1280 offset
= this.container
.offset();
1281 height
= this.container
.outerHeight(false);
1282 width
= this.container
.outerWidth(false);
1283 dropHeight
= $dropdown
.outerHeight(false);
1284 viewPortRight
= $window
.scrollLeft() + windowWidth
;
1285 viewportBottom
= $window
.scrollTop() + windowHeight
;
1286 dropTop
= offset
.top
+ height
;
1287 dropLeft
= offset
.left
;
1288 dropWidth
= $dropdown
.outerWidth(false);
1291 // fix so the cursor does not move to the left within the search-textbox in IE
1295 if (this.opts
.dropdownAutoWidth
) {
1296 resultsListNode
= $('.select2-results', $dropdown
)[0];
1297 $dropdown
.addClass('select2-drop-auto-width');
1298 $dropdown
.css('width', '');
1299 // Add scrollbar width to dropdown if vertical scrollbar is present
1300 dropWidth
= $dropdown
.outerWidth(false) + (resultsListNode
.scrollHeight
=== resultsListNode
.clientHeight
? 0 : scrollBarDimensions
.width
);
1301 dropWidth
> width
? width
= dropWidth
: dropWidth
= width
;
1302 dropHeight
= $dropdown
.outerHeight(false);
1305 this.container
.removeClass('select2-drop-auto-width');
1308 //console.log("below/ droptop:", dropTop, "dropHeight", dropHeight, "sum", (dropTop+dropHeight)+" viewport bottom", viewportBottom, "enough?", enoughRoomBelow);
1309 //console.log("above/ offset.top", offset.top, "dropHeight", dropHeight, "top", (offset.top-dropHeight), "scrollTop", this.body.scrollTop(), "enough?", enoughRoomAbove);
1311 // fix positioning when body has an offset and is not position: static
1312 if (this.body
.css('position') !== 'static') {
1313 bodyOffset
= this.body
.offset();
1314 dropTop
-= bodyOffset
.top
;
1315 dropLeft
-= bodyOffset
.left
;
1318 if (!enoughRoomOnRight() && enoughRoomOnLeft()) {
1319 dropLeft
= offset
.left
+ this.container
.outerWidth(false) - dropWidth
;
1328 this.container
.addClass("select2-drop-above");
1329 $dropdown
.addClass("select2-drop-above");
1330 dropHeight
= $dropdown
.outerHeight(false);
1331 css
.top
= offset
.top
- dropHeight
;
1332 css
.bottom
= 'auto';
1336 css
.bottom
= 'auto';
1337 this.container
.removeClass("select2-drop-above");
1338 $dropdown
.removeClass("select2-drop-above");
1340 css
= $.extend(css
, evaluate(this.opts
.dropdownCss
, this.opts
.element
));
1346 shouldOpen: function() {
1349 if (this.opened()) return false;
1351 if (this._enabled
=== false || this._readonly
=== true) return false;
1353 event
= $.Event("select2-opening");
1354 this.opts
.element
.trigger(event
);
1355 return !event
.isDefaultPrevented();
1359 clearDropdownAlignmentPreference: function() {
1360 // clear the classes used to figure out the preference of where the dropdown should be opened
1361 this.container
.removeClass("select2-drop-above");
1362 this.dropdown
.removeClass("select2-drop-above");
1366 * Opens the dropdown
1368 * @return {Boolean} whether or not dropdown was opened. This method will return false if, for example,
1369 * the dropdown is already open, or if the 'open' event listener on the element called preventDefault().
1374 if (!this.shouldOpen()) return false;
1378 // Only bind the document mousemove when the dropdown is visible
1379 $document
.on("mousemove.select2Event", function (e
) {
1380 lastMousePosition
.x
= e
.pageX
;
1381 lastMousePosition
.y
= e
.pageY
;
1388 * Performs the opening of the dropdown
1391 opening: function() {
1392 var cid
= this.containerEventName
,
1393 scroll
= "scroll." + cid
,
1394 resize
= "resize."+cid
,
1395 orient
= "orientationchange."+cid
,
1398 this.container
.addClass("select2-dropdown-open").addClass("select2-container-active");
1400 this.clearDropdownAlignmentPreference();
1402 if(this.dropdown
[0] !== this.body
.children().last()[0]) {
1403 this.dropdown
.detach().appendTo(this.body
);
1406 // create the dropdown mask if doesn't already exist
1407 mask
= $("#select2-drop-mask");
1408 if (mask
.length
=== 0) {
1409 mask
= $(document
.createElement("div"));
1410 mask
.attr("id","select2-drop-mask").attr("class","select2-drop-mask");
1412 mask
.appendTo(this.body
);
1413 mask
.on("mousedown touchstart click", function (e
) {
1414 // Prevent IE from generating a click event on the body
1415 reinsertElement(mask
);
1417 var dropdown
= $("#select2-drop"), self
;
1418 if (dropdown
.length
> 0) {
1419 self
=dropdown
.data("select2");
1420 if (self
.opts
.selectOnBlur
) {
1421 self
.selectHighlighted({noFocus
: true});
1425 e
.stopPropagation();
1430 // ensure the mask is always right before the dropdown
1431 if (this.dropdown
.prev()[0] !== mask
[0]) {
1432 this.dropdown
.before(mask
);
1435 // move the global id to the correct dropdown
1436 $("#select2-drop").removeAttr("id");
1437 this.dropdown
.attr("id", "select2-drop");
1439 // show the elements
1442 this.positionDropdown();
1443 this.dropdown
.show();
1444 this.positionDropdown();
1446 this.dropdown
.addClass("select2-drop-active");
1448 // attach listeners to events that can change the position of the container and thus require
1449 // the position of the dropdown to be updated as well so it does not come unglued from the container
1451 this.container
.parents().add(window
).each(function () {
1452 $(this).on(resize
+" "+scroll
+" "+orient
, function (e
) {
1453 if (that
.opened()) that
.positionDropdown();
1461 close: function () {
1462 if (!this.opened()) return;
1464 var cid
= this.containerEventName
,
1465 scroll
= "scroll." + cid
,
1466 resize
= "resize."+cid
,
1467 orient
= "orientationchange."+cid
;
1469 // unbind event listeners
1470 this.container
.parents().add(window
).each(function () { $(this).off(scroll
).off(resize
).off(orient
); });
1472 this.clearDropdownAlignmentPreference();
1474 $("#select2-drop-mask").hide();
1475 this.dropdown
.removeAttr("id"); // only the active dropdown has the select2-drop id
1476 this.dropdown
.hide();
1477 this.container
.removeClass("select2-dropdown-open").removeClass("select2-container-active");
1478 this.results
.empty();
1480 // Now that the dropdown is closed, unbind the global document mousemove event
1481 $document
.off("mousemove.select2Event");
1484 this.search
.removeClass("select2-active");
1486 // Remove the aria active descendant for highlighted element
1487 this.search
.removeAttr("aria-activedescendant");
1488 this.opts
.element
.trigger($.Event("select2-close"));
1492 * Opens control, sets input value, and updates results.
1495 externalSearch: function (term
) {
1497 this.search
.val(term
);
1498 this.updateResults(false);
1502 clearSearch: function () {
1507 * @return {Boolean} Whether or not search value was changed.
1510 prefillNextSearchTerm: function () {
1511 // initializes search's value with nextSearchTerm (if defined by user)
1512 // ignore nextSearchTerm if the dropdown is opened by the user pressing a letter
1513 if(this.search
.val() !== "") {
1517 var nextSearchTerm
= this.opts
.nextSearchTerm(this.data(), this.lastSearchTerm
);
1518 if(nextSearchTerm
!== undefined){
1519 this.search
.val(nextSearchTerm
);
1520 this.search
.select();
1528 getMaximumSelectionSize: function() {
1529 return evaluate(this.opts
.maximumSelectionSize
, this.opts
.element
);
1533 ensureHighlightVisible: function () {
1534 var results
= this.results
, children
, index
, child
, hb
, rb
, y
, more
, topOffset
;
1536 index
= this.highlight();
1538 if (index
< 0) return;
1542 // if the first element is highlighted scroll all the way to the top,
1543 // that way any unselectable headers above it will also be scrolled
1546 results
.scrollTop(0);
1550 children
= this.findHighlightableChoices().find('.select2-result-label');
1552 child
= $(children
[index
]);
1554 topOffset
= (child
.offset() || {}).top
|| 0;
1556 hb
= topOffset
+ child
.outerHeight(true);
1558 // if this is the last child lets also make sure select2-more-results is visible
1559 if (index
=== children
.length
- 1) {
1560 more
= results
.find("li.select2-more-results");
1561 if (more
.length
> 0) {
1562 hb
= more
.offset().top
+ more
.outerHeight(true);
1566 rb
= results
.offset().top
+ results
.outerHeight(false);
1568 results
.scrollTop(results
.scrollTop() + (hb
- rb
));
1570 y
= topOffset
- results
.offset().top
;
1572 // make sure the top of the element is visible
1573 if (y
< 0 && child
.css('display') != 'none' ) {
1574 results
.scrollTop(results
.scrollTop() + y
); // y is negative
1579 findHighlightableChoices: function() {
1580 return this.results
.find(".select2-result-selectable:not(.select2-disabled):not(.select2-selected)");
1584 moveHighlight: function (delta
) {
1585 var choices
= this.findHighlightableChoices(),
1586 index
= this.highlight();
1588 while (index
> -1 && index
< choices
.length
) {
1590 var choice
= $(choices
[index
]);
1591 if (choice
.hasClass("select2-result-selectable") && !choice
.hasClass("select2-disabled") && !choice
.hasClass("select2-selected")) {
1592 this.highlight(index
);
1599 highlight: function (index
) {
1600 var choices
= this.findHighlightableChoices(),
1604 if (arguments
.length
=== 0) {
1605 return indexOf(choices
.filter(".select2-highlighted")[0], choices
.get());
1608 if (index
>= choices
.length
) index
= choices
.length
- 1;
1609 if (index
< 0) index
= 0;
1611 this.removeHighlight();
1613 choice
= $(choices
[index
]);
1614 choice
.addClass("select2-highlighted");
1616 // ensure assistive technology can determine the active choice
1617 this.search
.attr("aria-activedescendant", choice
.find(".select2-result-label").attr("id"));
1619 this.ensureHighlightVisible();
1621 this.liveRegion
.text(choice
.text());
1623 data
= choice
.data("select2-data");
1625 this.opts
.element
.trigger({ type
: "select2-highlight", val
: this.id(data
), choice
: data
});
1629 removeHighlight: function() {
1630 this.results
.find(".select2-highlighted").removeClass("select2-highlighted");
1633 touchMoved: function() {
1634 this._touchMoved
= true;
1637 clearTouchMoved: function() {
1638 this._touchMoved
= false;
1642 countSelectableResults: function() {
1643 return this.findHighlightableChoices().length
;
1647 highlightUnderEvent: function (event
) {
1648 var el
= $(event
.target
).closest(".select2-result-selectable");
1649 if (el
.length
> 0 && !el
.is(".select2-highlighted")) {
1650 var choices
= this.findHighlightableChoices();
1651 this.highlight(choices
.index(el
));
1652 } else if (el
.length
== 0) {
1653 // if we are over an unselectable item remove all highlights
1654 this.removeHighlight();
1659 loadMoreIfNeeded: function () {
1660 var results
= this.results
,
1661 more
= results
.find("li.select2-more-results"),
1662 below
, // pixels the element is below the scroll fold, below==0 is when the element is starting to be visible
1663 page
= this.resultsPage
+ 1,
1665 term
=this.search
.val(),
1666 context
=this.context
;
1668 if (more
.length
=== 0) return;
1669 below
= more
.offset().top
- results
.offset().top
- results
.height();
1671 if (below
<= this.opts
.loadMorePadding
) {
1672 more
.addClass("select2-active");
1674 element
: this.opts
.element
,
1678 matcher
: this.opts
.matcher
,
1679 callback
: this.bind(function (data
) {
1681 // ignore a response if the select2 has been closed before it was received
1682 if (!self
.opened()) return;
1685 self
.opts
.populateResults
.call(this, results
, data
.results
, {term
: term
, page
: page
, context
:context
});
1686 self
.postprocessResults(data
, false, false);
1688 if (data
.more
===true) {
1689 more
.detach().appendTo(results
).html(self
.opts
.escapeMarkup(evaluate(self
.opts
.formatLoadMore
, self
.opts
.element
, page
+1)));
1690 window
.setTimeout(function() { self
.loadMoreIfNeeded(); }, 10);
1694 self
.positionDropdown();
1695 self
.resultsPage
= page
;
1696 self
.context
= data
.context
;
1697 this.opts
.element
.trigger({ type
: "select2-loaded", items
: data
});
1703 * Default tokenizer function which does nothing
1705 tokenize: function() {
1710 * @param initial whether or not this is the call to this method right after the dropdown has been opened
1713 updateResults: function (initial
) {
1714 var search
= this.search
,
1715 results
= this.results
,
1720 term
= search
.val(),
1721 lastTerm
= $.data(this.container
, "select2-last-term"),
1722 // sequence number used to drop out-of-order responses
1725 // prevent duplicate queries against the same term
1726 if (initial
!== true && lastTerm
&& equal(term
, lastTerm
)) return;
1728 $.data(this.container
, "select2-last-term", term
);
1730 // if the search is currently hidden we do not alter the results
1731 if (initial
!== true && (this.showSearchInput
=== false || !this.opened())) {
1735 function postRender() {
1736 search
.removeClass("select2-active");
1737 self
.positionDropdown();
1738 if (results
.find('.select2-no-results,.select2-selection-limit,.select2-searching').length
) {
1739 self
.liveRegion
.text(results
.text());
1742 self
.liveRegion
.text(self
.opts
.formatMatches(results
.find('.select2-result-selectable:not(".select2-selected")').length
));
1746 function render(html
) {
1751 queryNumber
= ++this.queryCount
;
1753 var maxSelSize
= this.getMaximumSelectionSize();
1754 if (maxSelSize
>=1) {
1756 if ($.isArray(data
) && data
.length
>= maxSelSize
&& checkFormatter(opts
.formatSelectionTooBig
, "formatSelectionTooBig")) {
1757 render("<li class='select2-selection-limit'>" + evaluate(opts
.formatSelectionTooBig
, opts
.element
, maxSelSize
) + "</li>");
1762 if (search
.val().length
< opts
.minimumInputLength
) {
1763 if (checkFormatter(opts
.formatInputTooShort
, "formatInputTooShort")) {
1764 render("<li class='select2-no-results'>" + evaluate(opts
.formatInputTooShort
, opts
.element
, search
.val(), opts
.minimumInputLength
) + "</li>");
1768 if (initial
&& this.showSearch
) this.showSearch(true);
1772 if (opts
.maximumInputLength
&& search
.val().length
> opts
.maximumInputLength
) {
1773 if (checkFormatter(opts
.formatInputTooLong
, "formatInputTooLong")) {
1774 render("<li class='select2-no-results'>" + evaluate(opts
.formatInputTooLong
, opts
.element
, search
.val(), opts
.maximumInputLength
) + "</li>");
1781 if (opts
.formatSearching
&& this.findHighlightableChoices().length
=== 0) {
1782 render("<li class='select2-searching'>" + evaluate(opts
.formatSearching
, opts
.element
) + "</li>");
1785 search
.addClass("select2-active");
1787 this.removeHighlight();
1789 // give the tokenizer a chance to pre-process the input
1790 input
= this.tokenize();
1791 if (input
!= undefined && input
!= null) {
1795 this.resultsPage
= 1;
1798 element
: opts
.element
,
1800 page
: this.resultsPage
,
1802 matcher
: opts
.matcher
,
1803 callback
: this.bind(function (data
) {
1804 var def
; // default choice
1806 // ignore old responses
1807 if (queryNumber
!= this.queryCount
) {
1811 // ignore a response if the select2 has been closed before it was received
1812 if (!this.opened()) {
1813 this.search
.removeClass("select2-active");
1817 // handle ajax error
1818 if(data
.hasError
!== undefined && checkFormatter(opts
.formatAjaxError
, "formatAjaxError")) {
1819 render("<li class='select2-ajax-error'>" + evaluate(opts
.formatAjaxError
, opts
.element
, data
.jqXHR
, data
.textStatus
, data
.errorThrown
) + "</li>");
1823 // save context, if any
1824 this.context
= (data
.context
===undefined) ? null : data
.context
;
1825 // create a default choice and prepend it to the list
1826 if (this.opts
.createSearchChoice
&& search
.val() !== "") {
1827 def
= this.opts
.createSearchChoice
.call(self
, search
.val(), data
.results
);
1828 if (def
!== undefined && def
!== null && self
.id(def
) !== undefined && self
.id(def
) !== null) {
1829 if ($(data
.results
).filter(
1831 return equal(self
.id(this), self
.id(def
));
1833 this.opts
.createSearchChoicePosition(data
.results
, def
);
1838 if (data
.results
.length
=== 0 && checkFormatter(opts
.formatNoMatches
, "formatNoMatches")) {
1839 render("<li class='select2-no-results'>" + evaluate(opts
.formatNoMatches
, opts
.element
, search
.val()) + "</li>");
1840 this.showSearch
&& this.showSearch(search
.val());
1845 self
.opts
.populateResults
.call(this, results
, data
.results
, {term
: search
.val(), page
: this.resultsPage
, context
:null});
1847 if (data
.more
=== true && checkFormatter(opts
.formatLoadMore
, "formatLoadMore")) {
1848 results
.append("<li class='select2-more-results'>" + opts
.escapeMarkup(evaluate(opts
.formatLoadMore
, opts
.element
, this.resultsPage
)) + "</li>");
1849 window
.setTimeout(function() { self
.loadMoreIfNeeded(); }, 10);
1852 this.postprocessResults(data
, initial
);
1856 this.opts
.element
.trigger({ type
: "select2-loaded", items
: data
});
1861 cancel: function () {
1867 // if selectOnBlur == true, select the currently highlighted option
1868 if (this.opts
.selectOnBlur
)
1869 this.selectHighlighted({noFocus
: true});
1872 this.container
.removeClass("select2-container-active");
1873 // synonymous to .is(':focus'), which is available in jquery >= 1.6
1874 if (this.search
[0] === document
.activeElement
) { this.search
.blur(); }
1876 this.selection
.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
1880 focusSearch: function () {
1885 selectHighlighted: function (options
) {
1886 if (this._touchMoved
) {
1887 this.clearTouchMoved();
1890 var index
=this.highlight(),
1891 highlighted
=this.results
.find(".select2-highlighted"),
1892 data
= highlighted
.closest('.select2-result').data("select2-data");
1895 this.highlight(index
);
1896 this.onSelect(data
, options
);
1897 } else if (options
&& options
.noFocus
) {
1903 getPlaceholder: function () {
1904 var placeholderOption
;
1905 return this.opts
.element
.attr("placeholder") ||
1906 this.opts
.element
.attr("data-placeholder") || // jquery 1.4 compat
1907 this.opts
.element
.data("placeholder") ||
1908 this.opts
.placeholder
||
1909 ((placeholderOption
= this.getPlaceholderOption()) !== undefined ? placeholderOption
.text() : undefined);
1913 getPlaceholderOption: function() {
1915 var firstOption
= this.select
.children('option').first();
1916 if (this.opts
.placeholderOption
!== undefined ) {
1917 //Determine the placeholder option based on the specified placeholderOption setting
1918 return (this.opts
.placeholderOption
=== "first" && firstOption
) ||
1919 (typeof this.opts
.placeholderOption
=== "function" && this.opts
.placeholderOption(this.select
));
1920 } else if ($.trim(firstOption
.text()) === "" && firstOption
.val() === "") {
1921 //No explicit placeholder option specified, use the first if it's blank
1928 * Get the desired width for the container element. This is
1929 * derived first from option `width` passed to select2, then
1930 * the inline 'style' on the original element, and finally
1931 * falls back to the jQuery calculated element width.
1934 initContainerWidth: function () {
1935 function resolveContainerWidth() {
1936 var style
, attrs
, matches
, i
, l
, attr
;
1938 if (this.opts
.width
=== "off") {
1940 } else if (this.opts
.width
=== "element"){
1941 return this.opts
.element
.outerWidth(false) === 0 ? 'auto' : this.opts
.element
.outerWidth(false) + 'px';
1942 } else if (this.opts
.width
=== "copy" || this.opts
.width
=== "resolve") {
1943 // check if there is inline style on the element that contains width
1944 style
= this.opts
.element
.attr('style');
1945 if (typeof(style
) === "string") {
1946 attrs
= style
.split(';');
1947 for (i
= 0, l
= attrs
.length
; i
< l
; i
= i
+ 1) {
1948 attr
= attrs
[i
].replace(/\s/g, '');
1949 matches
= attr
.match(/^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i);
1950 if (matches
!== null && matches
.length
>= 1)
1955 if (this.opts
.width
=== "resolve") {
1956 // next check if css('width') can resolve a width that is percent based, this is sometimes possible
1957 // when attached to input type=hidden or elements hidden via css
1958 style
= this.opts
.element
.css('width');
1959 if (style
.indexOf("%") > 0) return style
;
1961 // finally, fallback on the calculated width of the element
1962 return (this.opts
.element
.outerWidth(false) === 0 ? 'auto' : this.opts
.element
.outerWidth(false) + 'px');
1966 } else if ($.isFunction(this.opts
.width
)) {
1967 return this.opts
.width();
1969 return this.opts
.width
;
1973 var width
= resolveContainerWidth
.call(this);
1974 if (width
!== null) {
1975 this.container
.css("width", width
);
1980 SingleSelect2
= clazz(AbstractSelect2
, {
1984 createContainer: function () {
1985 var container
= $(document
.createElement("div")).attr({
1986 "class": "select2-container"
1988 "<a href='javascript:void(0)' class='select2-choice' tabindex='-1'>",
1989 " <span class='select2-chosen'> </span><abbr class='select2-search-choice-close'></abbr>",
1990 " <span class='select2-arrow' role='presentation'><b role='presentation'></b></span>",
1992 "<label for='' class='select2-offscreen'></label>",
1993 "<input class='select2-focusser select2-offscreen' type='text' aria-haspopup='true' role='button' />",
1994 "<div class='select2-drop select2-display-none'>",
1995 " <div class='select2-search'>",
1996 " <label for='' class='select2-offscreen'></label>",
1997 " <input type='text' autocomplete='off' autocorrect='off' autocapitalize='off' spellcheck='false' class='select2-input' role='combobox' aria-expanded='true'",
1998 " aria-autocomplete='list' />",
2000 " <ul class='select2-results' role='listbox'>",
2002 "</div>"].join(""));
2007 enableInterface: function() {
2008 if (this.parent
.enableInterface
.apply(this, arguments
)) {
2009 this.focusser
.prop("disabled", !this.isInterfaceEnabled());
2014 opening: function () {
2017 if (this.opts
.minimumResultsForSearch
>= 0) {
2018 this.showSearch(true);
2021 this.parent
.opening
.apply(this, arguments
);
2023 if (this.showSearchInput
!== false) {
2024 // IE appends focusser.val() at the end of field :/ so we manually insert it at the beginning using a range
2025 // all other browsers handle this just fine
2027 this.search
.val(this.focusser
.val());
2029 if (this.opts
.shouldFocusInput(this)) {
2030 this.search
.focus();
2031 // move the cursor to the end after focussing, otherwise it will be at the beginning and
2032 // new text will appear *before* focusser.val()
2033 el
= this.search
.get(0);
2034 if (el
.createTextRange
) {
2035 range
= el
.createTextRange();
2036 range
.collapse(false);
2038 } else if (el
.setSelectionRange
) {
2039 len
= this.search
.val().length
;
2040 el
.setSelectionRange(len
, len
);
2044 this.prefillNextSearchTerm();
2046 this.focusser
.prop("disabled", true).val("");
2047 this.updateResults(true);
2048 this.opts
.element
.trigger($.Event("select2-open"));
2052 close: function () {
2053 if (!this.opened()) return;
2054 this.parent
.close
.apply(this, arguments
);
2056 this.focusser
.prop("disabled", false);
2058 if (this.opts
.shouldFocusInput(this)) {
2059 this.focusser
.focus();
2064 focus: function () {
2065 if (this.opened()) {
2068 this.focusser
.prop("disabled", false);
2069 if (this.opts
.shouldFocusInput(this)) {
2070 this.focusser
.focus();
2076 isFocused: function () {
2077 return this.container
.hasClass("select2-container-active");
2081 cancel: function () {
2082 this.parent
.cancel
.apply(this, arguments
);
2083 this.focusser
.prop("disabled", false);
2085 if (this.opts
.shouldFocusInput(this)) {
2086 this.focusser
.focus();
2091 destroy: function() {
2092 $("label[for='" + this.focusser
.attr('id') + "']")
2093 .attr('for', this.opts
.element
.attr("id"));
2094 this.parent
.destroy
.apply(this, arguments
);
2096 cleanupJQueryElements
.call(this,
2103 initContainer: function () {
2106 container
= this.container
,
2107 dropdown
= this.dropdown
,
2108 idSuffix
= nextUid(),
2111 if (this.opts
.minimumResultsForSearch
< 0) {
2112 this.showSearch(false);
2114 this.showSearch(true);
2117 this.selection
= selection
= container
.find(".select2-choice");
2119 this.focusser
= container
.find(".select2-focusser");
2121 // add aria associations
2122 selection
.find(".select2-chosen").attr("id", "select2-chosen-"+idSuffix
);
2123 this.focusser
.attr("aria-labelledby", "select2-chosen-"+idSuffix
);
2124 this.results
.attr("id", "select2-results-"+idSuffix
);
2125 this.search
.attr("aria-owns", "select2-results-"+idSuffix
);
2127 // rewrite labels from original element to focusser
2128 this.focusser
.attr("id", "s2id_autogen"+idSuffix
);
2130 elementLabel
= $("label[for='" + this.opts
.element
.attr("id") + "']");
2131 this.opts
.element
.on('focus.select2', this.bind(function () { this.focus(); }));
2133 this.focusser
.prev()
2134 .text(elementLabel
.text())
2135 .attr('for', this.focusser
.attr('id'));
2137 // Ensure the original element retains an accessible name
2138 var originalTitle
= this.opts
.element
.attr("title");
2139 this.opts
.element
.attr("title", (originalTitle
|| elementLabel
.text()));
2141 this.focusser
.attr("tabindex", this.elementTabIndex
);
2143 // write label for search field using the label from the focusser element
2144 this.search
.attr("id", this.focusser
.attr('id') + '_search');
2147 .text($("label[for='" + this.focusser
.attr('id') + "']").text())
2148 .attr('for', this.search
.attr('id'));
2150 this.search
.on("keydown", this.bind(function (e
) {
2151 if (!this.isInterfaceEnabled()) return;
2153 // filter 229 keyCodes (input method editor is processing key input)
2154 if (229 == e
.keyCode
) return;
2156 if (e
.which
=== KEY
.PAGE_UP
|| e
.which
=== KEY
.PAGE_DOWN
) {
2157 // prevent the page from scrolling
2165 this.moveHighlight((e
.which
=== KEY
.UP
) ? -1 : 1);
2169 this.selectHighlighted();
2173 this.selectHighlighted({noFocus
: true});
2182 this.search
.on("blur", this.bind(function(e
) {
2183 // a workaround for chrome to keep the search field focussed when the scroll bar is used to scroll the dropdown.
2184 // without this the search field loses focus which is annoying
2185 if (document
.activeElement
=== this.body
.get(0)) {
2186 window
.setTimeout(this.bind(function() {
2187 if (this.opened() && this.results
&& this.results
.length
> 1) {
2188 this.search
.focus();
2194 this.focusser
.on("keydown", this.bind(function (e
) {
2195 if (!this.isInterfaceEnabled()) return;
2197 if (e
.which
=== KEY
.TAB
|| KEY
.isControl(e
) || KEY
.isFunctionKey(e
) || e
.which
=== KEY
.ESC
) {
2201 if (this.opts
.openOnEnter
=== false && e
.which
=== KEY
.ENTER
) {
2206 if (e
.which
== KEY
.DOWN
|| e
.which
== KEY
.UP
2207 || (e
.which
== KEY
.ENTER
&& this.opts
.openOnEnter
)) {
2209 if (e
.altKey
|| e
.ctrlKey
|| e
.shiftKey
|| e
.metaKey
) return;
2216 if (e
.which
== KEY
.DELETE
|| e
.which
== KEY
.BACKSPACE
) {
2217 if (this.opts
.allowClear
) {
2226 installKeyUpChangeEvent(this.focusser
);
2227 this.focusser
.on("keyup-change input", this.bind(function(e
) {
2228 if (this.opts
.minimumResultsForSearch
>= 0) {
2229 e
.stopPropagation();
2230 if (this.opened()) return;
2235 selection
.on("mousedown touchstart", "abbr", this.bind(function (e
) {
2236 if (!this.isInterfaceEnabled()) {
2241 killEventImmediately(e
);
2244 if (this.selection
) {
2245 this.selection
.focus();
2249 selection
.on("mousedown touchstart", this.bind(function (e
) {
2250 // Prevent IE from generating a click event on the body
2251 reinsertElement(selection
);
2253 if (!this.container
.hasClass("select2-container-active")) {
2254 this.opts
.element
.trigger($.Event("select2-focus"));
2257 if (this.opened()) {
2259 } else if (this.isInterfaceEnabled()) {
2266 dropdown
.on("mousedown touchstart", this.bind(function() {
2267 if (this.opts
.shouldFocusInput(this)) {
2268 this.search
.focus();
2272 selection
.on("focus", this.bind(function(e
) {
2276 this.focusser
.on("focus", this.bind(function(){
2277 if (!this.container
.hasClass("select2-container-active")) {
2278 this.opts
.element
.trigger($.Event("select2-focus"));
2280 this.container
.addClass("select2-container-active");
2281 })).on("blur", this.bind(function() {
2282 if (!this.opened()) {
2283 this.container
.removeClass("select2-container-active");
2284 this.opts
.element
.trigger($.Event("select2-blur"));
2287 this.search
.on("focus", this.bind(function(){
2288 if (!this.container
.hasClass("select2-container-active")) {
2289 this.opts
.element
.trigger($.Event("select2-focus"));
2291 this.container
.addClass("select2-container-active");
2294 this.initContainerWidth();
2295 this.opts
.element
.hide();
2296 this.setPlaceholder();
2301 clear: function(triggerChange
) {
2302 var data
=this.selection
.data("select2-data");
2303 if (data
) { // guard against queued quick consecutive clicks
2304 var evt
= $.Event("select2-clearing");
2305 this.opts
.element
.trigger(evt
);
2306 if (evt
.isDefaultPrevented()) {
2309 var placeholderOption
= this.getPlaceholderOption();
2310 this.opts
.element
.val(placeholderOption
? placeholderOption
.val() : "");
2311 this.selection
.find(".select2-chosen").empty();
2312 this.selection
.removeData("select2-data");
2313 this.setPlaceholder();
2315 if (triggerChange
!== false){
2316 this.opts
.element
.trigger({ type
: "select2-removed", val
: this.id(data
), choice
: data
});
2317 this.triggerChange({removed
:data
});
2323 * Sets selection based on source element's value
2326 initSelection: function () {
2328 if (this.isPlaceholderOptionSelected()) {
2329 this.updateSelection(null);
2331 this.setPlaceholder();
2334 this.opts
.initSelection
.call(null, this.opts
.element
, function(selected
){
2335 if (selected
!== undefined && selected
!== null) {
2336 self
.updateSelection(selected
);
2338 self
.setPlaceholder();
2339 self
.lastSearchTerm
= self
.search
.val();
2345 isPlaceholderOptionSelected: function() {
2346 var placeholderOption
;
2347 if (this.getPlaceholder() === undefined) return false; // no placeholder specified so no option should be considered
2348 return ((placeholderOption
= this.getPlaceholderOption()) !== undefined && placeholderOption
.prop("selected"))
2349 || (this.opts
.element
.val() === "")
2350 || (this.opts
.element
.val() === undefined)
2351 || (this.opts
.element
.val() === null);
2355 prepareOpts: function () {
2356 var opts
= this.parent
.prepareOpts
.apply(this, arguments
),
2359 if (opts
.element
.get(0).tagName
.toLowerCase() === "select") {
2360 // install the selection initializer
2361 opts
.initSelection = function (element
, callback
) {
2362 var selected
= element
.find("option").filter(function() { return this.selected
&& !this.disabled
});
2363 // a single select box always has a value, no need to null check 'selected'
2364 callback(self
.optionToData(selected
));
2366 } else if ("data" in opts
) {
2367 // install default initSelection when applied to hidden input and data is local
2368 opts
.initSelection
= opts
.initSelection
|| function (element
, callback
) {
2369 var id
= element
.val();
2370 //search in data by id, storing the actual matching item
2373 matcher: function(term
, text
, el
){
2374 var is_match
= equal(id
, opts
.id(el
));
2380 callback
: !$.isFunction(callback
) ? $.noop : function() {
2391 getPlaceholder: function() {
2392 // if a placeholder is specified on a single select without a valid placeholder option ignore it
2394 if (this.getPlaceholderOption() === undefined) {
2399 return this.parent
.getPlaceholder
.apply(this, arguments
);
2403 setPlaceholder: function () {
2404 var placeholder
= this.getPlaceholder();
2406 if (this.isPlaceholderOptionSelected() && placeholder
!== undefined) {
2408 // check for a placeholder option if attached to a select
2409 if (this.select
&& this.getPlaceholderOption() === undefined) return;
2411 this.selection
.find(".select2-chosen").html(this.opts
.escapeMarkup(placeholder
));
2413 this.selection
.addClass("select2-default");
2415 this.container
.removeClass("select2-allowclear");
2420 postprocessResults: function (data
, initial
, noHighlightUpdate
) {
2421 var selected
= 0, self
= this, showSearchInput
= true;
2423 // find the selected element in the result list
2425 this.findHighlightableChoices().each2(function (i
, elm
) {
2426 if (equal(self
.id(elm
.data("select2-data")), self
.opts
.element
.val())) {
2433 if (noHighlightUpdate
!== false) {
2434 if (initial
=== true && selected
>= 0) {
2435 this.highlight(selected
);
2441 // hide the search box if this is the first we got the results and there are enough of them for search
2443 if (initial
=== true) {
2444 var min
= this.opts
.minimumResultsForSearch
;
2446 this.showSearch(countResults(data
.results
) >= min
);
2452 showSearch: function(showSearchInput
) {
2453 if (this.showSearchInput
=== showSearchInput
) return;
2455 this.showSearchInput
= showSearchInput
;
2457 this.dropdown
.find(".select2-search").toggleClass("select2-search-hidden", !showSearchInput
);
2458 this.dropdown
.find(".select2-search").toggleClass("select2-offscreen", !showSearchInput
);
2459 //add "select2-with-searchbox" to the container if search box is shown
2460 $(this.dropdown
, this.container
).toggleClass("select2-with-searchbox", showSearchInput
);
2464 onSelect: function (data
, options
) {
2466 if (!this.triggerSelect(data
)) { return; }
2468 var old
= this.opts
.element
.val(),
2469 oldData
= this.data();
2471 this.opts
.element
.val(this.id(data
));
2472 this.updateSelection(data
);
2474 this.opts
.element
.trigger({ type
: "select2-selected", val
: this.id(data
), choice
: data
});
2476 this.lastSearchTerm
= this.search
.val();
2479 if ((!options
|| !options
.noFocus
) && this.opts
.shouldFocusInput(this)) {
2480 this.focusser
.focus();
2483 if (!equal(old
, this.id(data
))) {
2484 this.triggerChange({ added
: data
, removed
: oldData
});
2489 updateSelection: function (data
) {
2491 var container
=this.selection
.find(".select2-chosen"), formatted
, cssClass
;
2493 this.selection
.data("select2-data", data
);
2496 if (data
!== null) {
2497 formatted
=this.opts
.formatSelection(data
, container
, this.opts
.escapeMarkup
);
2499 if (formatted
!== undefined) {
2500 container
.append(formatted
);
2502 cssClass
=this.opts
.formatSelectionCssClass(data
, container
);
2503 if (cssClass
!== undefined) {
2504 container
.addClass(cssClass
);
2507 this.selection
.removeClass("select2-default");
2509 if (this.opts
.allowClear
&& this.getPlaceholder() !== undefined) {
2510 this.container
.addClass("select2-allowclear");
2517 triggerChange
= false,
2520 oldData
= this.data();
2522 if (arguments
.length
=== 0) {
2523 return this.opts
.element
.val();
2528 if (arguments
.length
> 1) {
2529 triggerChange
= arguments
[1];
2535 .find("option").filter(function() { return this.selected
}).each2(function (i
, elm
) {
2536 data
= self
.optionToData(elm
);
2539 this.updateSelection(data
);
2540 this.setPlaceholder();
2541 if (triggerChange
) {
2542 this.triggerChange({added
: data
, removed
:oldData
});
2545 // val is an id. !val is true for [undefined,null,'',0] - 0 is legal
2546 if (!val
&& val
!== 0) {
2547 this.clear(triggerChange
);
2550 if (this.opts
.initSelection
=== undefined) {
2551 throw new Error("cannot call val() if initSelection() is not defined");
2553 this.opts
.element
.val(val
);
2554 this.opts
.initSelection(this.opts
.element
, function(data
){
2555 self
.opts
.element
.val(!data
? "" : self
.id(data
));
2556 self
.updateSelection(data
);
2557 self
.setPlaceholder();
2558 if (triggerChange
) {
2559 self
.triggerChange({added
: data
, removed
:oldData
});
2566 clearSearch: function () {
2567 this.search
.val("");
2568 this.focusser
.val("");
2572 data: function(value
) {
2574 triggerChange
= false;
2576 if (arguments
.length
=== 0) {
2577 data
= this.selection
.data("select2-data");
2578 if (data
== undefined) data
= null;
2581 if (arguments
.length
> 1) {
2582 triggerChange
= arguments
[1];
2585 this.clear(triggerChange
);
2588 this.opts
.element
.val(!value
? "" : this.id(value
));
2589 this.updateSelection(value
);
2590 if (triggerChange
) {
2591 this.triggerChange({added
: value
, removed
:data
});
2598 MultiSelect2
= clazz(AbstractSelect2
, {
2601 createContainer: function () {
2602 var container
= $(document
.createElement("div")).attr({
2603 "class": "select2-container select2-container-multi"
2605 "<ul class='select2-choices'>",
2606 " <li class='select2-search-field'>",
2607 " <label for='' class='select2-offscreen'></label>",
2608 " <input type='text' autocomplete='off' autocorrect='off' autocapitalize='off' spellcheck='false' class='select2-input'>",
2611 "<div class='select2-drop select2-drop-multi select2-display-none'>",
2612 " <ul class='select2-results'>",
2614 "</div>"].join(""));
2619 prepareOpts: function () {
2620 var opts
= this.parent
.prepareOpts
.apply(this, arguments
),
2623 // TODO validate placeholder is a string if specified
2624 if (opts
.element
.get(0).tagName
.toLowerCase() === "select") {
2625 // install the selection initializer
2626 opts
.initSelection = function (element
, callback
) {
2630 element
.find("option").filter(function() { return this.selected
&& !this.disabled
}).each2(function (i
, elm
) {
2631 data
.push(self
.optionToData(elm
));
2635 } else if ("data" in opts
) {
2636 // install default initSelection when applied to hidden input and data is local
2637 opts
.initSelection
= opts
.initSelection
|| function (element
, callback
) {
2638 var ids
= splitVal(element
.val(), opts
.separator
, opts
.transformVal
);
2639 //search in data by array of ids, storing matching items in a list
2642 matcher: function(term
, text
, el
){
2643 var is_match
= $.grep(ids
, function(id
) {
2644 return equal(id
, opts
.id(el
));
2651 callback
: !$.isFunction(callback
) ? $.noop : function() {
2652 // reorder matches based on the order they appear in the ids array because right now
2653 // they are in the order in which they appear in data array
2655 for (var i
= 0; i
< ids
.length
; i
++) {
2657 for (var j
= 0; j
< matches
.length
; j
++) {
2658 var match
= matches
[j
];
2659 if (equal(id
, opts
.id(match
))) {
2660 ordered
.push(match
);
2661 matches
.splice(j
, 1);
2676 selectChoice: function (choice
) {
2678 var selected
= this.container
.find(".select2-search-choice-focus");
2679 if (selected
.length
&& choice
&& choice
[0] == selected
[0]) {
2682 if (selected
.length
) {
2683 this.opts
.element
.trigger("choice-deselected", selected
);
2685 selected
.removeClass("select2-search-choice-focus");
2686 if (choice
&& choice
.length
) {
2688 choice
.addClass("select2-search-choice-focus");
2689 this.opts
.element
.trigger("choice-selected", choice
);
2695 destroy: function() {
2696 $("label[for='" + this.search
.attr('id') + "']")
2697 .attr('for', this.opts
.element
.attr("id"));
2698 this.parent
.destroy
.apply(this, arguments
);
2700 cleanupJQueryElements
.call(this,
2707 initContainer: function () {
2709 var selector
= ".select2-choices", selection
;
2711 this.searchContainer
= this.container
.find(".select2-search-field");
2712 this.selection
= selection
= this.container
.find(selector
);
2715 this.selection
.on("click", ".select2-container:not(.select2-container-disabled) .select2-search-choice:not(.select2-locked)", function (e
) {
2716 _this
.search
[0].focus();
2717 _this
.selectChoice($(this));
2720 // rewrite labels from original element to focusser
2721 this.search
.attr("id", "s2id_autogen"+nextUid());
2724 .text($("label[for='" + this.opts
.element
.attr("id") + "']").text())
2725 .attr('for', this.search
.attr('id'));
2726 this.opts
.element
.on('focus.select2', this.bind(function () { this.focus(); }));
2728 this.search
.on("input paste", this.bind(function() {
2729 if (this.search
.attr('placeholder') && this.search
.val().length
== 0) return;
2730 if (!this.isInterfaceEnabled()) return;
2731 if (!this.opened()) {
2736 this.search
.attr("tabindex", this.elementTabIndex
);
2739 this.search
.on("keydown", this.bind(function (e
) {
2740 if (!this.isInterfaceEnabled()) return;
2743 var selected
= selection
.find(".select2-search-choice-focus");
2744 var prev
= selected
.prev(".select2-search-choice:not(.select2-locked)");
2745 var next
= selected
.next(".select2-search-choice:not(.select2-locked)");
2746 var pos
= getCursorInfo(this.search
);
2748 if (selected
.length
&&
2749 (e
.which
== KEY
.LEFT
|| e
.which
== KEY
.RIGHT
|| e
.which
== KEY
.BACKSPACE
|| e
.which
== KEY
.DELETE
|| e
.which
== KEY
.ENTER
)) {
2750 var selectedChoice
= selected
;
2751 if (e
.which
== KEY
.LEFT
&& prev
.length
) {
2752 selectedChoice
= prev
;
2754 else if (e
.which
== KEY
.RIGHT
) {
2755 selectedChoice
= next
.length
? next
: null;
2757 else if (e
.which
=== KEY
.BACKSPACE
) {
2758 if (this.unselect(selected
.first())) {
2759 this.search
.width(10);
2760 selectedChoice
= prev
.length
? prev
: next
;
2762 } else if (e
.which
== KEY
.DELETE
) {
2763 if (this.unselect(selected
.first())) {
2764 this.search
.width(10);
2765 selectedChoice
= next
.length
? next
: null;
2767 } else if (e
.which
== KEY
.ENTER
) {
2768 selectedChoice
= null;
2771 this.selectChoice(selectedChoice
);
2773 if (!selectedChoice
|| !selectedChoice
.length
) {
2777 } else if (((e
.which
=== KEY
.BACKSPACE
&& this.keydowns
== 1)
2778 || e
.which
== KEY
.LEFT
) && (pos
.offset
== 0 && !pos
.length
)) {
2780 this.selectChoice(selection
.find(".select2-search-choice:not(.select2-locked)").last());
2784 this.selectChoice(null);
2787 if (this.opened()) {
2791 this.moveHighlight((e
.which
=== KEY
.UP
) ? -1 : 1);
2795 this.selectHighlighted();
2799 this.selectHighlighted({noFocus
:true});
2809 if (e
.which
=== KEY
.TAB
|| KEY
.isControl(e
) || KEY
.isFunctionKey(e
)
2810 || e
.which
=== KEY
.BACKSPACE
|| e
.which
=== KEY
.ESC
) {
2814 if (e
.which
=== KEY
.ENTER
) {
2815 if (this.opts
.openOnEnter
=== false) {
2817 } else if (e
.altKey
|| e
.ctrlKey
|| e
.shiftKey
|| e
.metaKey
) {
2824 if (e
.which
=== KEY
.PAGE_UP
|| e
.which
=== KEY
.PAGE_DOWN
) {
2825 // prevent the page from scrolling
2829 if (e
.which
=== KEY
.ENTER
) {
2830 // prevent form from being submitted
2836 this.search
.on("keyup", this.bind(function (e
) {
2838 this.resizeSearch();
2842 this.search
.on("blur", this.bind(function(e
) {
2843 this.container
.removeClass("select2-container-active");
2844 this.search
.removeClass("select2-focused");
2845 this.selectChoice(null);
2846 if (!this.opened()) this.clearSearch();
2847 e
.stopImmediatePropagation();
2848 this.opts
.element
.trigger($.Event("select2-blur"));
2851 this.container
.on("click", selector
, this.bind(function (e
) {
2852 if (!this.isInterfaceEnabled()) return;
2853 if ($(e
.target
).closest(".select2-search-choice").length
> 0) {
2854 // clicked inside a select2 search choice, do not open
2857 this.selectChoice(null);
2858 this.clearPlaceholder();
2859 if (!this.container
.hasClass("select2-container-active")) {
2860 this.opts
.element
.trigger($.Event("select2-focus"));
2867 this.container
.on("focus", selector
, this.bind(function () {
2868 if (!this.isInterfaceEnabled()) return;
2869 if (!this.container
.hasClass("select2-container-active")) {
2870 this.opts
.element
.trigger($.Event("select2-focus"));
2872 this.container
.addClass("select2-container-active");
2873 this.dropdown
.addClass("select2-drop-active");
2874 this.clearPlaceholder();
2877 this.initContainerWidth();
2878 this.opts
.element
.hide();
2880 // set the placeholder if necessary
2885 enableInterface: function() {
2886 if (this.parent
.enableInterface
.apply(this, arguments
)) {
2887 this.search
.prop("disabled", !this.isInterfaceEnabled());
2892 initSelection: function () {
2894 if (this.opts
.element
.val() === "" && this.opts
.element
.text() === "") {
2895 this.updateSelection([]);
2897 // set the placeholder if necessary
2900 if (this.select
|| this.opts
.element
.val() !== "") {
2902 this.opts
.initSelection
.call(null, this.opts
.element
, function(data
){
2903 if (data
!== undefined && data
!== null) {
2904 self
.updateSelection(data
);
2906 // set the placeholder if necessary
2914 clearSearch: function () {
2915 var placeholder
= this.getPlaceholder(),
2916 maxWidth
= this.getMaxSearchWidth();
2918 if (placeholder
!== undefined && this.getVal().length
=== 0 && this.search
.hasClass("select2-focused") === false) {
2919 this.search
.val(placeholder
).addClass("select2-default");
2920 // stretch the search box to full width of the container so as much of the placeholder is visible as possible
2921 // 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
2922 this.search
.width(maxWidth
> 0 ? maxWidth
: this.container
.css("width"));
2924 this.search
.val("").width(10);
2929 clearPlaceholder: function () {
2930 if (this.search
.hasClass("select2-default")) {
2931 this.search
.val("").removeClass("select2-default");
2936 opening: function () {
2937 this.clearPlaceholder(); // should be done before super so placeholder is not used to search
2938 this.resizeSearch();
2940 this.parent
.opening
.apply(this, arguments
);
2944 this.prefillNextSearchTerm();
2945 this.updateResults(true);
2947 if (this.opts
.shouldFocusInput(this)) {
2948 this.search
.focus();
2950 this.opts
.element
.trigger($.Event("select2-open"));
2954 close: function () {
2955 if (!this.opened()) return;
2956 this.parent
.close
.apply(this, arguments
);
2960 focus: function () {
2962 this.search
.focus();
2966 isFocused: function () {
2967 return this.search
.hasClass("select2-focused");
2971 updateSelection: function (data
) {
2972 var ids
= {}, filtered
= [], self
= this;
2974 // filter out duplicates
2975 $(data
).each(function () {
2976 if (!(self
.id(this) in ids
)) {
2977 ids
[self
.id(this)] = 0;
2978 filtered
.push(this);
2982 this.selection
.find(".select2-search-choice").remove();
2983 this.addSelectedChoice(filtered
);
2984 self
.postprocessResults();
2988 tokenize: function() {
2989 var input
= this.search
.val();
2990 input
= this.opts
.tokenizer
.call(this, input
, this.data(), this.bind(this.onSelect
), this.opts
);
2991 if (input
!= null && input
!= undefined) {
2992 this.search
.val(input
);
2993 if (input
.length
> 0) {
3001 onSelect: function (data
, options
) {
3003 if (!this.triggerSelect(data
) || data
.text
=== "") { return; }
3005 this.addSelectedChoice(data
);
3007 this.opts
.element
.trigger({ type
: "selected", val
: this.id(data
), choice
: data
});
3009 // keep track of the search's value before it gets cleared
3010 this.lastSearchTerm
= this.search
.val();
3013 this.updateResults();
3015 if (this.select
|| !this.opts
.closeOnSelect
) this.postprocessResults(data
, false, this.opts
.closeOnSelect
===true);
3017 if (this.opts
.closeOnSelect
) {
3019 this.search
.width(10);
3021 if (this.countSelectableResults()>0) {
3022 this.search
.width(10);
3023 this.resizeSearch();
3024 if (this.getMaximumSelectionSize() > 0 && this.val().length
>= this.getMaximumSelectionSize()) {
3025 // if we reached max selection size repaint the results so choices
3026 // are replaced with the max selection reached message
3027 this.updateResults(true);
3029 // initializes search's value with nextSearchTerm and update search result
3030 if (this.prefillNextSearchTerm()) {
3031 this.updateResults();
3034 this.positionDropdown();
3036 // if nothing left to select close
3038 this.search
.width(10);
3042 // since its not possible to select an element that has already been
3043 // added we do not need to check if this is a new element before firing change
3044 this.triggerChange({ added
: data
});
3046 if (!options
|| !options
.noFocus
)
3051 cancel: function () {
3056 addSelectedChoice: function (data
) {
3057 var val
= this.getVal(), self
= this;
3058 $(data
).each(function () {
3059 val
.push(self
.createChoice(this));
3064 createChoice: function (data
) {
3065 var enableChoice
= !data
.locked
,
3067 "<li class='select2-search-choice'>" +
3069 " <a href='#' class='select2-search-choice-close' tabindex='-1'></a>" +
3072 "<li class='select2-search-choice select2-locked'>" +
3075 var choice
= enableChoice
? enabledItem
: disabledItem
,
3080 formatted
=this.opts
.formatSelection(data
, choice
.find("div"), this.opts
.escapeMarkup
);
3081 if (formatted
!= undefined) {
3082 choice
.find("div").replaceWith($("<div></div>").html(formatted
));
3084 cssClass
=this.opts
.formatSelectionCssClass(data
, choice
.find("div"));
3085 if (cssClass
!= undefined) {
3086 choice
.addClass(cssClass
);
3090 choice
.find(".select2-search-choice-close")
3091 .on("mousedown", killEvent
)
3092 .on("click dblclick", this.bind(function (e
) {
3093 if (!this.isInterfaceEnabled()) return;
3095 this.unselect($(e
.target
));
3096 this.selection
.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
3100 })).on("focus", this.bind(function () {
3101 if (!this.isInterfaceEnabled()) return;
3102 this.container
.addClass("select2-container-active");
3103 this.dropdown
.addClass("select2-drop-active");
3107 choice
.data("select2-data", data
);
3108 choice
.insertBefore(this.searchContainer
);
3114 unselect: function (selected
) {
3115 var val
= this.getVal(),
3118 selected
= selected
.closest(".select2-search-choice");
3120 if (selected
.length
=== 0) {
3121 throw "Invalid argument: " + selected
+ ". Must be .select2-search-choice";
3124 data
= selected
.data("select2-data");
3127 // prevent a race condition when the 'x' is clicked really fast repeatedly the event can be queued
3128 // and invoked on an element already removed
3132 var evt
= $.Event("select2-removing");
3133 evt
.val
= this.id(data
);
3135 this.opts
.element
.trigger(evt
);
3137 if (evt
.isDefaultPrevented()) {
3141 while((index
= indexOf(this.id(data
), val
)) >= 0) {
3142 val
.splice(index
, 1);
3144 if (this.select
) this.postprocessResults();
3149 this.opts
.element
.trigger({ type
: "select2-removed", val
: this.id(data
), choice
: data
});
3150 this.triggerChange({ removed
: data
});
3156 postprocessResults: function (data
, initial
, noHighlightUpdate
) {
3157 var val
= this.getVal(),
3158 choices
= this.results
.find(".select2-result"),
3159 compound
= this.results
.find(".select2-result-with-children"),
3162 choices
.each2(function (i
, choice
) {
3163 var id
= self
.id(choice
.data("select2-data"));
3164 if (indexOf(id
, val
) >= 0) {
3165 choice
.addClass("select2-selected");
3166 // mark all children of the selected parent as selected
3167 choice
.find(".select2-result-selectable").addClass("select2-selected");
3171 compound
.each2(function(i
, choice
) {
3172 // hide an optgroup if it doesn't have any selectable children
3173 if (!choice
.is('.select2-result-selectable')
3174 && choice
.find(".select2-result-selectable:not(.select2-selected)").length
=== 0) {
3175 choice
.addClass("select2-selected");
3179 if (this.highlight() == -1 && noHighlightUpdate
!== false && this.opts
.closeOnSelect
=== true){
3183 //If all results are chosen render formatNoMatches
3184 if(!this.opts
.createSearchChoice
&& !choices
.filter('.select2-result:not(.select2-selected)').length
> 0){
3185 if(!data
|| data
&& !data
.more
&& this.results
.find(".select2-no-results").length
=== 0) {
3186 if (checkFormatter(self
.opts
.formatNoMatches
, "formatNoMatches")) {
3187 this.results
.append("<li class='select2-no-results'>" + evaluate(self
.opts
.formatNoMatches
, self
.opts
.element
, self
.search
.val()) + "</li>");
3195 getMaxSearchWidth: function() {
3196 return this.selection
.width() - getSideBorderPadding(this.search
);
3200 resizeSearch: function () {
3201 var minimumWidth
, left
, maxWidth
, containerLeft
, searchWidth
,
3202 sideBorderPadding
= getSideBorderPadding(this.search
);
3204 minimumWidth
= measureTextWidth(this.search
) + 10;
3206 left
= this.search
.offset().left
;
3208 maxWidth
= this.selection
.width();
3209 containerLeft
= this.selection
.offset().left
;
3211 searchWidth
= maxWidth
- (left
- containerLeft
) - sideBorderPadding
;
3213 if (searchWidth
< minimumWidth
) {
3214 searchWidth
= maxWidth
- sideBorderPadding
;
3217 if (searchWidth
< 40) {
3218 searchWidth
= maxWidth
- sideBorderPadding
;
3221 if (searchWidth
<= 0) {
3222 searchWidth
= minimumWidth
;
3225 this.search
.width(Math
.floor(searchWidth
));
3229 getVal: function () {
3232 val
= this.select
.val();
3233 return val
=== null ? [] : val
;
3235 val
= this.opts
.element
.val();
3236 return splitVal(val
, this.opts
.separator
, this.opts
.transformVal
);
3241 setVal: function (val
) {
3243 this.select
.val(val
);
3245 var unique
= [], valMap
= {};
3246 // filter out duplicates
3247 $(val
).each(function () {
3248 if (!(this in valMap
)) {
3253 this.opts
.element
.val(unique
.length
=== 0 ? "" : unique
.join(this.opts
.separator
));
3258 buildChangeDetails: function (old
, current
) {
3259 var current
= current
.slice(0),
3262 // remove intersection from each array
3263 for (var i
= 0; i
< current
.length
; i
++) {
3264 for (var j
= 0; j
< old
.length
; j
++) {
3265 if (equal(this.opts
.id(current
[i
]), this.opts
.id(old
[j
]))) {
3266 current
.splice(i
, 1);
3274 return {added
: current
, removed
: old
};
3279 val: function (val
, triggerChange
) {
3280 var oldData
, self
=this;
3282 if (arguments
.length
=== 0) {
3283 return this.getVal();
3286 oldData
=this.data();
3287 if (!oldData
.length
) oldData
=[];
3289 // val is an id. !val is true for [undefined,null,'',0] - 0 is legal
3290 if (!val
&& val
!== 0) {
3291 this.opts
.element
.val("");
3292 this.updateSelection([]);
3294 if (triggerChange
) {
3295 this.triggerChange({added
: this.data(), removed
: oldData
});
3300 // val is a list of ids
3304 this.opts
.initSelection(this.select
, this.bind(this.updateSelection
));
3305 if (triggerChange
) {
3306 this.triggerChange(this.buildChangeDetails(oldData
, this.data()));
3309 if (this.opts
.initSelection
=== undefined) {
3310 throw new Error("val() cannot be called if initSelection() is not defined");
3313 this.opts
.initSelection(this.opts
.element
, function(data
){
3314 var ids
=$.map(data
, self
.id
);
3316 self
.updateSelection(data
);
3318 if (triggerChange
) {
3319 self
.triggerChange(self
.buildChangeDetails(oldData
, self
.data()));
3327 onSortStart: function() {
3329 throw new Error("Sorting of elements is not supported when attached to <select>. Attach to <input type='hidden'/> instead.");
3332 // collapse search field into 0 width so its container can be collapsed as well
3333 this.search
.width(0);
3334 // hide the container
3335 this.searchContainer
.hide();
3339 onSortEnd:function() {
3341 var val
=[], self
=this;
3343 // show search and move it to the end of the list
3344 this.searchContainer
.show();
3345 // make sure the search container is the last item in the list
3346 this.searchContainer
.appendTo(this.searchContainer
.parent());
3347 // since we collapsed the width in dragStarted, we resize it here
3348 this.resizeSearch();
3351 this.selection
.find(".select2-search-choice").each(function() {
3352 val
.push(self
.opts
.id($(this).data("select2-data")));
3355 this.triggerChange();
3359 data: function(values
, triggerChange
) {
3360 var self
=this, ids
, old
;
3361 if (arguments
.length
=== 0) {
3362 return this.selection
3363 .children(".select2-search-choice")
3364 .map(function() { return $(this).data("select2-data"); })
3368 if (!values
) { values
= []; }
3369 ids
= $.map(values
, function(e
) { return self
.opts
.id(e
); });
3371 this.updateSelection(values
);
3373 if (triggerChange
) {
3374 this.triggerChange(this.buildChangeDetails(old
, this.data()));
3380 $.fn
.select2 = function () {
3382 var args
= Array
.prototype.slice
.call(arguments
, 0),
3385 method
, value
, multiple
,
3386 allowedMethods
= ["val", "destroy", "opened", "open", "close", "focus", "isFocused", "container", "dropdown", "onSortStart", "onSortEnd", "enable", "disable", "readonly", "positionDropdown", "data", "search"],
3387 valueMethods
= ["opened", "isFocused", "container", "dropdown"],
3388 propertyMethods
= ["val", "data"],
3389 methodsMap
= { search
: "externalSearch" };
3391 this.each(function () {
3392 if (args
.length
=== 0 || typeof(args
[0]) === "object") {
3393 opts
= args
.length
=== 0 ? {} : $.extend({}, args
[0]);
3394 opts
.element
= $(this);
3396 if (opts
.element
.get(0).tagName
.toLowerCase() === "select") {
3397 multiple
= opts
.element
.prop("multiple");
3399 multiple
= opts
.multiple
|| false;
3400 if ("tags" in opts
) {opts
.multiple
= multiple
= true;}
3403 select2
= multiple
? new MultiSelect2() : new SingleSelect2();
3405 } else if (typeof(args
[0]) === "string") {
3407 if (indexOf(args
[0], allowedMethods
) < 0) {
3408 throw "Unknown method: " + args
[0];
3412 select2
= $(this).data("select2");
3413 if (select2
=== undefined) return;
3417 if (method
=== "container") {
3418 value
= select2
.container
;
3419 } else if (method
=== "dropdown") {
3420 value
= select2
.dropdown
;
3422 if (methodsMap
[method
]) method
= methodsMap
[method
];
3424 value
= select2
[method
].apply(select2
, args
.slice(1));
3426 if (indexOf(args
[0], valueMethods
) >= 0
3427 || (indexOf(args
[0], propertyMethods
) >= 0 && args
.length
== 1)) {
3428 return false; // abort the iteration, ready to return first matched value
3431 throw "Invalid arguments to select2 plugin: " + args
;
3434 return (value
=== undefined) ? this : value
;
3437 // plugin defaults, accessible to users
3438 $.fn
.select2
.defaults
= {
3441 closeOnSelect
: true,
3445 containerCssClass
: "",
3446 dropdownCssClass
: "",
3447 formatResult: function(result
, container
, query
, escapeMarkup
) {
3449 markMatch(this.text(result
), query
.term
, markup
, escapeMarkup
);
3450 return markup
.join("");
3452 transformVal: function(val
) {
3455 formatSelection: function (data
, container
, escapeMarkup
) {
3456 return data
? escapeMarkup(this.text(data
)) : undefined;
3458 sortResults: function (results
, container
, query
) {
3461 formatResultCssClass: function(data
) {return data
.css
;},
3462 formatSelectionCssClass: function(data
, container
) {return undefined;},
3463 minimumResultsForSearch
: 0,
3464 minimumInputLength
: 0,
3465 maximumInputLength
: null,
3466 maximumSelectionSize
: 0,
3467 id: function (e
) { return e
== undefined ? null : e
.id
; },
3468 text: function (e
) {
3469 if (e
&& this.data
&& this.data
.text
) {
3470 if ($.isFunction(this.data
.text
)) {
3471 return this.data
.text(e
);
3473 return e
[this.data
.text
];
3479 matcher: function(term
, text
) {
3480 return stripDiacritics(''+text
).toUpperCase().indexOf(stripDiacritics(''+term
).toUpperCase()) >= 0;
3483 tokenSeparators
: [],
3484 tokenizer
: defaultTokenizer
,
3485 escapeMarkup
: defaultEscapeMarkup
,
3486 blurOnChange
: false,
3487 selectOnBlur
: false,
3488 adaptContainerCssClass: function(c
) { return c
; },
3489 adaptDropdownCssClass: function(c
) { return null; },
3490 nextSearchTerm: function(selectedObject
, currentSearchTerm
) { return undefined; },
3491 searchInputPlaceholder
: '',
3492 createSearchChoicePosition
: 'top',
3493 shouldFocusInput: function (instance
) {
3494 // Attempt to detect touch devices
3495 var supportsTouchEvents
= (('ontouchstart' in window
) ||
3496 (navigator
.msMaxTouchPoints
> 0));
3498 // Only devices which support touch events should be special cased
3499 if (!supportsTouchEvents
) {
3503 // Never focus the input if search is disabled
3504 if (instance
.opts
.minimumResultsForSearch
< 0) {
3512 $.fn
.select2
.locales
= [];
3514 $.fn
.select2
.locales
['en'] = {
3515 formatMatches: function (matches
) { if (matches
=== 1) { return "One result is available, press enter to select it."; } return matches
+ " results are available, use up and down arrow keys to navigate."; },
3516 formatNoMatches: function () { return "No matches found"; },
3517 formatAjaxError: function (jqXHR
, textStatus
, errorThrown
) { return "Loading failed"; },
3518 formatInputTooShort: function (input
, min
) { var n
= min
- input
.length
; return "Please enter " + n
+ " or more character" + (n
== 1 ? "" : "s"); },
3519 formatInputTooLong: function (input
, max
) { var n
= input
.length
- max
; return "Please delete " + n
+ " character" + (n
== 1 ? "" : "s"); },
3520 formatSelectionTooBig: function (limit
) { return "You can only select " + limit
+ " item" + (limit
== 1 ? "" : "s"); },
3521 formatLoadMore: function (pageNumber
) { return "Loading more results…"; },
3522 formatSearching: function () { return "Searching…"; }
3525 $.extend($.fn
.select2
.defaults
, $.fn
.select2
.locales
['en']);
3527 $.fn
.select2
.ajaxDefaults
= {