2 Copyright 2012 Igor Vaynberg
4 Version: 3.4.6 Timestamp: Sat Mar 22 22:30:15 EDT 2014
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, jQuery, console */
45 if (window.Select2 !== undefined) {
49 var KEY, AbstractSelect2, SingleSelect2, MultiSelect2, nextUid, sizer,
50 lastMousePosition={x:0,y:0}, $document, scrollBarDimensions,
70 isArrow: function (k) {
71 k = k.which ? k.which : k;
81 isControl: function (e) {
90 if (e.metaKey) return true;
94 isFunctionKey: function (k) {
95 k = k.which ? k.which : k;
96 return k >= 112 && k <= 123;
99 MEASURE_SCROLLBAR_TEMPLATE = "<div class='select2-measure-scrollbar'></div>",
101 DIACRITICS = {"\u24B6":"A","\uFF21":"A","\u00C0":"A","\u00C1":"A","\u00C2":"A","\u1EA6":"A","\u1EA4":"A","\u1EAA":"A","\u1EA8":"A","\u00C3":"A","\u0100":"A","\u0102":"A","\u1EB0":"A","\u1EAE":"A","\u1EB4":"A","\u1EB2":"A","\u0226":"A","\u01E0":"A","\u00C4":"A","\u01DE":"A","\u1EA2":"A","\u00C5":"A","\u01FA":"A","\u01CD":"A","\u0200":"A","\u0202":"A","\u1EA0":"A","\u1EAC":"A","\u1EB6":"A","\u1E00":"A","\u0104":"A","\u023A":"A","\u2C6F":"A","\uA732":"AA","\u00C6":"AE","\u01FC":"AE","\u01E2":"AE","\uA734":"AO","\uA736":"AU","\uA738":"AV","\uA73A":"AV","\uA73C":"AY","\u24B7":"B","\uFF22":"B","\u1E02":"B","\u1E04":"B","\u1E06":"B","\u0243":"B","\u0182":"B","\u0181":"B","\u24B8":"C","\uFF23":"C","\u0106":"C","\u0108":"C","\u010A":"C","\u010C":"C","\u00C7":"C","\u1E08":"C","\u0187":"C","\u023B":"C","\uA73E":"C","\u24B9":"D","\uFF24":"D","\u1E0A":"D","\u010E":"D","\u1E0C":"D","\u1E10":"D","\u1E12":"D","\u1E0E":"D","\u0110":"D","\u018B":"D","\u018A":"D","\u0189":"D","\uA779":"D","\u01F1":"DZ","\u01C4":"DZ","\u01F2":"Dz","\u01C5":"Dz","\u24BA":"E","\uFF25":"E","\u00C8":"E","\u00C9":"E","\u00CA":"E","\u1EC0":"E","\u1EBE":"E","\u1EC4":"E","\u1EC2":"E","\u1EBC":"E","\u0112":"E","\u1E14":"E","\u1E16":"E","\u0114":"E","\u0116":"E","\u00CB":"E","\u1EBA":"E","\u011A":"E","\u0204":"E","\u0206":"E","\u1EB8":"E","\u1EC6":"E","\u0228":"E","\u1E1C":"E","\u0118":"E","\u1E18":"E","\u1E1A":"E","\u0190":"E","\u018E":"E","\u24BB":"F","\uFF26":"F","\u1E1E":"F","\u0191":"F","\uA77B":"F","\u24BC":"G","\uFF27":"G","\u01F4":"G","\u011C":"G","\u1E20":"G","\u011E":"G","\u0120":"G","\u01E6":"G","\u0122":"G","\u01E4":"G","\u0193":"G","\uA7A0":"G","\uA77D":"G","\uA77E":"G","\u24BD":"H","\uFF28":"H","\u0124":"H","\u1E22":"H","\u1E26":"H","\u021E":"H","\u1E24":"H","\u1E28":"H","\u1E2A":"H","\u0126":"H","\u2C67":"H","\u2C75":"H","\uA78D":"H","\u24BE":"I","\uFF29":"I","\u00CC":"I","\u00CD":"I","\u00CE":"I","\u0128":"I","\u012A":"I","\u012C":"I","\u0130":"I","\u00CF":"I","\u1E2E":"I","\u1EC8":"I","\u01CF":"I","\u0208":"I","\u020A":"I","\u1ECA":"I","\u012E":"I","\u1E2C":"I","\u0197":"I","\u24BF":"J","\uFF2A":"J","\u0134":"J","\u0248":"J","\u24C0":"K","\uFF2B":"K","\u1E30":"K","\u01E8":"K","\u1E32":"K","\u0136":"K","\u1E34":"K","\u0198":"K","\u2C69":"K","\uA740":"K","\uA742":"K","\uA744":"K","\uA7A2":"K","\u24C1":"L","\uFF2C":"L","\u013F":"L","\u0139":"L","\u013D":"L","\u1E36":"L","\u1E38":"L","\u013B":"L","\u1E3C":"L","\u1E3A":"L","\u0141":"L","\u023D":"L","\u2C62":"L","\u2C60":"L","\uA748":"L","\uA746":"L","\uA780":"L","\u01C7":"LJ","\u01C8":"Lj","\u24C2":"M","\uFF2D":"M","\u1E3E":"M","\u1E40":"M","\u1E42":"M","\u2C6E":"M","\u019C":"M","\u24C3":"N","\uFF2E":"N","\u01F8":"N","\u0143":"N","\u00D1":"N","\u1E44":"N","\u0147":"N","\u1E46":"N","\u0145":"N","\u1E4A":"N","\u1E48":"N","\u0220":"N","\u019D":"N","\uA790":"N","\uA7A4":"N","\u01CA":"NJ","\u01CB":"Nj","\u24C4":"O","\uFF2F":"O","\u00D2":"O","\u00D3":"O","\u00D4":"O","\u1ED2":"O","\u1ED0":"O","\u1ED6":"O","\u1ED4":"O","\u00D5":"O","\u1E4C":"O","\u022C":"O","\u1E4E":"O","\u014C":"O","\u1E50":"O","\u1E52":"O","\u014E":"O","\u022E":"O","\u0230":"O","\u00D6":"O","\u022A":"O","\u1ECE":"O","\u0150":"O","\u01D1":"O","\u020C":"O","\u020E":"O","\u01A0":"O","\u1EDC":"O","\u1EDA":"O","\u1EE0":"O","\u1EDE":"O","\u1EE2":"O","\u1ECC":"O","\u1ED8":"O","\u01EA":"O","\u01EC":"O","\u00D8":"O","\u01FE":"O","\u0186":"O","\u019F":"O","\uA74A":"O","\uA74C":"O","\u01A2":"OI","\uA74E":"OO","\u0222":"OU","\u24C5":"P","\uFF30":"P","\u1E54":"P","\u1E56":"P","\u01A4":"P","\u2C63":"P","\uA750":"P","\uA752":"P","\uA754":"P","\u24C6":"Q","\uFF31":"Q","\uA756":"Q","\uA758":"Q","\u024A":"Q","\u24C7":"R","\uFF32":"R","\u0154":"R","\u1E58":"R","\u0158":"R","\u0210":"R","\u0212":"R","\u1E5A":"R","\u1E5C":"R","\u0156":"R","\u1E5E":"R","\u024C":"R","\u2C64":"R","\uA75A":"R","\uA7A6":"R","\uA782":"R","\u24C8":"S","\uFF33":"S","\u1E9E":"S","\u015A":"S","\u1E64":"S","\u015C":"S","\u1E60":"S","\u0160":"S","\u1E66":"S","\u1E62":"S","\u1E68":"S","\u0218":"S","\u015E":"S","\u2C7E":"S","\uA7A8":"S","\uA784":"S","\u24C9":"T","\uFF34":"T","\u1E6A":"T","\u0164":"T","\u1E6C":"T","\u021A":"T","\u0162":"T","\u1E70":"T","\u1E6E":"T","\u0166":"T","\u01AC":"T","\u01AE":"T","\u023E":"T","\uA786":"T","\uA728":"TZ","\u24CA":"U","\uFF35":"U","\u00D9":"U","\u00DA":"U","\u00DB":"U","\u0168":"U","\u1E78":"U","\u016A":"U","\u1E7A":"U","\u016C":"U","\u00DC":"U","\u01DB":"U","\u01D7":"U","\u01D5":"U","\u01D9":"U","\u1EE6":"U","\u016E":"U","\u0170":"U","\u01D3":"U","\u0214":"U","\u0216":"U","\u01AF":"U","\u1EEA":"U","\u1EE8":"U","\u1EEE":"U","\u1EEC":"U","\u1EF0":"U","\u1EE4":"U","\u1E72":"U","\u0172":"U","\u1E76":"U","\u1E74":"U","\u0244":"U","\u24CB":"V","\uFF36":"V","\u1E7C":"V","\u1E7E":"V","\u01B2":"V","\uA75E":"V","\u0245":"V","\uA760":"VY","\u24CC":"W","\uFF37":"W","\u1E80":"W","\u1E82":"W","\u0174":"W","\u1E86":"W","\u1E84":"W","\u1E88":"W","\u2C72":"W","\u24CD":"X","\uFF38":"X","\u1E8A":"X","\u1E8C":"X","\u24CE":"Y","\uFF39":"Y","\u1EF2":"Y","\u00DD":"Y","\u0176":"Y","\u1EF8":"Y","\u0232":"Y","\u1E8E":"Y","\u0178":"Y","\u1EF6":"Y","\u1EF4":"Y","\u01B3":"Y","\u024E":"Y","\u1EFE":"Y","\u24CF":"Z","\uFF3A":"Z","\u0179":"Z","\u1E90":"Z","\u017B":"Z","\u017D":"Z","\u1E92":"Z","\u1E94":"Z","\u01B5":"Z","\u0224":"Z","\u2C7F":"Z","\u2C6B":"Z","\uA762":"Z","\u24D0":"a","\uFF41":"a","\u1E9A":"a","\u00E0":"a","\u00E1":"a","\u00E2":"a","\u1EA7":"a","\u1EA5":"a","\u1EAB":"a","\u1EA9":"a","\u00E3":"a","\u0101":"a","\u0103":"a","\u1EB1":"a","\u1EAF":"a","\u1EB5":"a","\u1EB3":"a","\u0227":"a","\u01E1":"a","\u00E4":"a","\u01DF":"a","\u1EA3":"a","\u00E5":"a","\u01FB":"a","\u01CE":"a","\u0201":"a","\u0203":"a","\u1EA1":"a","\u1EAD":"a","\u1EB7":"a","\u1E01":"a","\u0105":"a","\u2C65":"a","\u0250":"a","\uA733":"aa","\u00E6":"ae","\u01FD":"ae","\u01E3":"ae","\uA735":"ao","\uA737":"au","\uA739":"av","\uA73B":"av","\uA73D":"ay","\u24D1":"b","\uFF42":"b","\u1E03":"b","\u1E05":"b","\u1E07":"b","\u0180":"b","\u0183":"b","\u0253":"b","\u24D2":"c","\uFF43":"c","\u0107":"c","\u0109":"c","\u010B":"c","\u010D":"c","\u00E7":"c","\u1E09":"c","\u0188":"c","\u023C":"c","\uA73F":"c","\u2184":"c","\u24D3":"d","\uFF44":"d","\u1E0B":"d","\u010F":"d","\u1E0D":"d","\u1E11":"d","\u1E13":"d","\u1E0F":"d","\u0111":"d","\u018C":"d","\u0256":"d","\u0257":"d","\uA77A":"d","\u01F3":"dz","\u01C6":"dz","\u24D4":"e","\uFF45":"e","\u00E8":"e","\u00E9":"e","\u00EA":"e","\u1EC1":"e","\u1EBF":"e","\u1EC5":"e","\u1EC3":"e","\u1EBD":"e","\u0113":"e","\u1E15":"e","\u1E17":"e","\u0115":"e","\u0117":"e","\u00EB":"e","\u1EBB":"e","\u011B":"e","\u0205":"e","\u0207":"e","\u1EB9":"e","\u1EC7":"e","\u0229":"e","\u1E1D":"e","\u0119":"e","\u1E19":"e","\u1E1B":"e","\u0247":"e","\u025B":"e","\u01DD":"e","\u24D5":"f","\uFF46":"f","\u1E1F":"f","\u0192":"f","\uA77C":"f","\u24D6":"g","\uFF47":"g","\u01F5":"g","\u011D":"g","\u1E21":"g","\u011F":"g","\u0121":"g","\u01E7":"g","\u0123":"g","\u01E5":"g","\u0260":"g","\uA7A1":"g","\u1D79":"g","\uA77F":"g","\u24D7":"h","\uFF48":"h","\u0125":"h","\u1E23":"h","\u1E27":"h","\u021F":"h","\u1E25":"h","\u1E29":"h","\u1E2B":"h","\u1E96":"h","\u0127":"h","\u2C68":"h","\u2C76":"h","\u0265":"h","\u0195":"hv","\u24D8":"i","\uFF49":"i","\u00EC":"i","\u00ED":"i","\u00EE":"i","\u0129":"i","\u012B":"i","\u012D":"i","\u00EF":"i","\u1E2F":"i","\u1EC9":"i","\u01D0":"i","\u0209":"i","\u020B":"i","\u1ECB":"i","\u012F":"i","\u1E2D":"i","\u0268":"i","\u0131":"i","\u24D9":"j","\uFF4A":"j","\u0135":"j","\u01F0":"j","\u0249":"j","\u24DA":"k","\uFF4B":"k","\u1E31":"k","\u01E9":"k","\u1E33":"k","\u0137":"k","\u1E35":"k","\u0199":"k","\u2C6A":"k","\uA741":"k","\uA743":"k","\uA745":"k","\uA7A3":"k","\u24DB":"l","\uFF4C":"l","\u0140":"l","\u013A":"l","\u013E":"l","\u1E37":"l","\u1E39":"l","\u013C":"l","\u1E3D":"l","\u1E3B":"l","\u017F":"l","\u0142":"l","\u019A":"l","\u026B":"l","\u2C61":"l","\uA749":"l","\uA781":"l","\uA747":"l","\u01C9":"lj","\u24DC":"m","\uFF4D":"m","\u1E3F":"m","\u1E41":"m","\u1E43":"m","\u0271":"m","\u026F":"m","\u24DD":"n","\uFF4E":"n","\u01F9":"n","\u0144":"n","\u00F1":"n","\u1E45":"n","\u0148":"n","\u1E47":"n","\u0146":"n","\u1E4B":"n","\u1E49":"n","\u019E":"n","\u0272":"n","\u0149":"n","\uA791":"n","\uA7A5":"n","\u01CC":"nj","\u24DE":"o","\uFF4F":"o","\u00F2":"o","\u00F3":"o","\u00F4":"o","\u1ED3":"o","\u1ED1":"o","\u1ED7":"o","\u1ED5":"o","\u00F5":"o","\u1E4D":"o","\u022D":"o","\u1E4F":"o","\u014D":"o","\u1E51":"o","\u1E53":"o","\u014F":"o","\u022F":"o","\u0231":"o","\u00F6":"o","\u022B":"o","\u1ECF":"o","\u0151":"o","\u01D2":"o","\u020D":"o","\u020F":"o","\u01A1":"o","\u1EDD":"o","\u1EDB":"o","\u1EE1":"o","\u1EDF":"o","\u1EE3":"o","\u1ECD":"o","\u1ED9":"o","\u01EB":"o","\u01ED":"o","\u00F8":"o","\u01FF":"o","\u0254":"o","\uA74B":"o","\uA74D":"o","\u0275":"o","\u01A3":"oi","\u0223":"ou","\uA74F":"oo","\u24DF":"p","\uFF50":"p","\u1E55":"p","\u1E57":"p","\u01A5":"p","\u1D7D":"p","\uA751":"p","\uA753":"p","\uA755":"p","\u24E0":"q","\uFF51":"q","\u024B":"q","\uA757":"q","\uA759":"q","\u24E1":"r","\uFF52":"r","\u0155":"r","\u1E59":"r","\u0159":"r","\u0211":"r","\u0213":"r","\u1E5B":"r","\u1E5D":"r","\u0157":"r","\u1E5F":"r","\u024D":"r","\u027D":"r","\uA75B":"r","\uA7A7":"r","\uA783":"r","\u24E2":"s","\uFF53":"s","\u00DF":"s","\u015B":"s","\u1E65":"s","\u015D":"s","\u1E61":"s","\u0161":"s","\u1E67":"s","\u1E63":"s","\u1E69":"s","\u0219":"s","\u015F":"s","\u023F":"s","\uA7A9":"s","\uA785":"s","\u1E9B":"s","\u24E3":"t","\uFF54":"t","\u1E6B":"t","\u1E97":"t","\u0165":"t","\u1E6D":"t","\u021B":"t","\u0163":"t","\u1E71":"t","\u1E6F":"t","\u0167":"t","\u01AD":"t","\u0288":"t","\u2C66":"t","\uA787":"t","\uA729":"tz","\u24E4":"u","\uFF55":"u","\u00F9":"u","\u00FA":"u","\u00FB":"u","\u0169":"u","\u1E79":"u","\u016B":"u","\u1E7B":"u","\u016D":"u","\u00FC":"u","\u01DC":"u","\u01D8":"u","\u01D6":"u","\u01DA":"u","\u1EE7":"u","\u016F":"u","\u0171":"u","\u01D4":"u","\u0215":"u","\u0217":"u","\u01B0":"u","\u1EEB":"u","\u1EE9":"u","\u1EEF":"u","\u1EED":"u","\u1EF1":"u","\u1EE5":"u","\u1E73":"u","\u0173":"u","\u1E77":"u","\u1E75":"u","\u0289":"u","\u24E5":"v","\uFF56":"v","\u1E7D":"v","\u1E7F":"v","\u028B":"v","\uA75F":"v","\u028C":"v","\uA761":"vy","\u24E6":"w","\uFF57":"w","\u1E81":"w","\u1E83":"w","\u0175":"w","\u1E87":"w","\u1E85":"w","\u1E98":"w","\u1E89":"w","\u2C73":"w","\u24E7":"x","\uFF58":"x","\u1E8B":"x","\u1E8D":"x","\u24E8":"y","\uFF59":"y","\u1EF3":"y","\u00FD":"y","\u0177":"y","\u1EF9":"y","\u0233":"y","\u1E8F":"y","\u00FF":"y","\u1EF7":"y","\u1E99":"y","\u1EF5":"y","\u01B4":"y","\u024F":"y","\u1EFF":"y","\u24E9":"z","\uFF5A":"z","\u017A":"z","\u1E91":"z","\u017C":"z","\u017E":"z","\u1E93":"z","\u1E95":"z","\u01B6":"z","\u0225":"z","\u0240":"z","\u2C6C":"z","\uA763":"z"};
103 $document = $(document);
105 nextUid=(function() { var counter=1; return function() { return counter++; }; }());
108 function reinsertElement(element) {
109 var placeholder = $(document.createTextNode(''));
111 element.before(placeholder);
112 placeholder.before(element);
113 placeholder.remove();
116 function stripDiacritics(str) {
119 if (!str || str.length < 1) return str;
122 for (i = 0, l = str.length; i < l; i++) {
124 ret += DIACRITICS[c] || c;
129 function indexOf(value, array) {
130 var i = 0, l = array.length;
131 for (; i < l; i = i + 1) {
132 if (equal(value, array[i])) return i;
137 function measureScrollbar () {
138 var $template = $( MEASURE_SCROLLBAR_TEMPLATE );
139 $template.appendTo('body');
142 width: $template.width() - $template[0].clientWidth,
143 height: $template.height() - $template[0].clientHeight
151 * Compares equality of a and b
155 function equal(a, b) {
156 if (a === b) return true;
157 if (a === undefined || b === undefined) return false;
158 if (a === null || b === null) return false;
159 // Check whether 'a' or 'b' is a string (primitive or object).
160 // The concatenation of an empty string (+'') converts its argument to a string's primitive.
161 if (a.constructor === String) return a+'' === b+''; // a+'' - in case 'a' is a String object
162 if (b.constructor === String) return b+'' === a+''; // b+'' - in case 'b' is a String object
167 * Splits the string into an array of values, trimming each value. An empty array is returned for nulls or empty
172 function splitVal(string, separator) {
174 if (string === null || string.length < 1) return [];
175 val = string.split(separator);
176 for (i = 0, l = val.length; i < l; i = i + 1) val[i] = $.trim(val[i]);
180 function getSideBorderPadding(element) {
181 return element.outerWidth(false) - element.width();
184 function installKeyUpChangeEvent(element) {
185 var key="keyup-change-value";
186 element.on("keydown", function () {
187 if ($.data(element, key) === undefined) {
188 $.data(element, key, element.val());
191 element.on("keyup", function () {
192 var val= $.data(element, key);
193 if (val !== undefined && element.val() !== val) {
194 $.removeData(element, key);
195 element.trigger("keyup-change");
200 $document.on("mousemove", function (e) {
201 lastMousePosition.x = e.pageX;
202 lastMousePosition.y = e.pageY;
206 * filters mouse events so an event is fired only if the mouse moved.
208 * filters out mouse events that occur when mouse is stationary but
209 * the elements under the pointer are scrolled.
211 function installFilteredMouseMove(element) {
212 element.on("mousemove", function (e) {
213 var lastpos = lastMousePosition;
214 if (lastpos === undefined || lastpos.x !== e.pageX || lastpos.y !== e.pageY) {
215 $(e.target).trigger("mousemove-filtered", e);
221 * Debounces a function. Returns a function that calls the original fn function only if no invocations have been made
222 * within the last quietMillis milliseconds.
224 * @param quietMillis number of milliseconds to wait before invoking fn
225 * @param fn function to be debounced
226 * @param ctx object to be used as this reference within fn
227 * @return debounced version of fn
229 function debounce(quietMillis, fn, ctx) {
230 ctx = ctx || undefined;
233 var args = arguments;
234 window.clearTimeout(timeout);
235 timeout = window.setTimeout(function() {
242 * A simple implementation of a thunk
243 * @param formula function used to lazily initialize the thunk
246 function thunk(formula) {
247 var evaluated = false,
250 if (evaluated === false) { value = formula(); evaluated = true; }
255 function installDebouncedScroll(threshold, element) {
256 var notify = debounce(threshold, function (e) { element.trigger("scroll-debounced", e);});
257 element.on("scroll", function (e) {
258 if (indexOf(e.target, element.get()) >= 0) notify(e);
262 function focus($el) {
263 if ($el[0] === document.activeElement) return;
265 /* set the focus in a 0 timeout - that way the focus is set after the processing
266 of the current event has finished - which seems like the only reliable way
268 window.setTimeout(function() {
269 var el=$el[0], pos=$el.val().length, range;
273 /* make sure el received focus so we do not error out when trying to manipulate the caret.
274 sometimes modals or others listeners may steal it after its set */
275 var isVisible = (el.offsetWidth > 0 || el.offsetHeight > 0);
276 if (isVisible && el === document.activeElement) {
278 /* after the focus is set move the caret to the end, necessary when we val()
279 just before setting focus */
280 if(el.setSelectionRange)
282 el.setSelectionRange(pos, pos);
284 else if (el.createTextRange) {
285 range = el.createTextRange();
286 range.collapse(false);
293 function getCursorInfo(el) {
297 if ('selectionStart' in el) {
298 offset = el.selectionStart;
299 length = el.selectionEnd - offset;
300 } else if ('selection' in document) {
302 var sel = document.selection.createRange();
303 length = document.selection.createRange().text.length;
304 sel.moveStart('character', -el.value.length);
305 offset = sel.text.length - length;
307 return { offset: offset, length: length };
310 function killEvent(event) {
311 event.preventDefault();
312 event.stopPropagation();
314 function killEventImmediately(event) {
315 event.preventDefault();
316 event.stopImmediatePropagation();
319 function measureTextWidth(e) {
321 var style = e[0].currentStyle || window.getComputedStyle(e[0], null);
322 sizer = $(document.createElement("div")).css({
323 position: "absolute",
327 fontSize: style.fontSize,
328 fontFamily: style.fontFamily,
329 fontStyle: style.fontStyle,
330 fontWeight: style.fontWeight,
331 letterSpacing: style.letterSpacing,
332 textTransform: style.textTransform,
335 sizer.attr("class","select2-sizer");
336 $("body").append(sizer);
339 return sizer.width();
342 function syncCssClasses(dest, src, adapter) {
343 var classes, replacements = [], adapted;
345 classes = dest.attr("class");
347 classes = '' + classes; // for IE which returns object
348 $(classes.split(" ")).each2(function() {
349 if (this.indexOf("select2-") === 0) {
350 replacements.push(this);
354 classes = src.attr("class");
356 classes = '' + classes; // for IE which returns object
357 $(classes.split(" ")).each2(function() {
358 if (this.indexOf("select2-") !== 0) {
359 adapted = adapter(this);
361 replacements.push(adapted);
366 dest.attr("class", replacements.join(" "));
370 function markMatch(text, term, markup, escapeMarkup) {
371 var match=stripDiacritics(text.toUpperCase()).indexOf(stripDiacritics(term.toUpperCase())),
375 markup.push(escapeMarkup(text));
379 markup.push(escapeMarkup(text.substring(0, match)));
380 markup.push("<span class='select2-match'>");
381 markup.push(escapeMarkup(text.substring(match, match + tl)));
382 markup.push("</span>");
383 markup.push(escapeMarkup(text.substring(match + tl, text.length)));
386 function defaultEscapeMarkup(markup) {
397 return String(markup).replace(/[&<>"'\/\\]/g, function (match) {
398 return replace_map[match];
403 * Produces an ajax-based query function
405 * @param options object containing configuration parameters
406 * @param options.params parameter map for the transport ajax call, can contain such options as cache, jsonpCallback, etc. see $.ajax
407 * @param options.transport function that will be used to execute the ajax request. must be compatible with parameters supported by $.ajax
408 * @param options.url url for the data
409 * @param options.data a function(searchTerm, pageNumber, context) that should return an object containing query string parameters for the above url.
410 * @param options.dataType request data type: ajax, jsonp, other datatypes supported by jQuery's $.ajax function or the transport function if specified
411 * @param options.quietMillis (optional) milliseconds to wait before making the ajaxRequest, helps debounce the ajax function if invoked too often
412 * @param options.results a function(remoteData, pageNumber) that converts data returned form the remote request to the format expected by Select2.
413 * The expected format is an object containing the following keys:
414 * results array of objects that will be used as choices
415 * more (optional) boolean indicating whether there are more results available
416 * Example: {results:[{id:1, text:'Red'},{id:2, text:'Blue'}], more:true}
418 function ajax(options) {
419 var timeout, // current scheduled but not yet executed request
421 quietMillis = options.quietMillis || 100,
422 ajaxUrl = options.url,
425 return function (query) {
426 window.clearTimeout(timeout);
427 timeout = window.setTimeout(function () {
428 var data = options.data, // ajax data function
429 url = ajaxUrl, // ajax url string or function
430 transport = options.transport || $.fn.select2.ajaxDefaults.transport,
431 // deprecated - to be removed in 4.0 - use params instead
433 type: options.type || 'GET', // set type of request (GET or POST)
434 cache: options.cache || false,
435 jsonpCallback: options.jsonpCallback||undefined,
436 dataType: options.dataType||"json"
438 params = $.extend({}, $.fn.select2.ajaxDefaults.params, deprecated);
440 data = data ? data.call(self, query.term, query.page, query.context) : null;
441 url = (typeof url === 'function') ? url.call(self, query.term, query.page, query.context) : url;
443 if (handler && typeof handler.abort === "function") { handler.abort(); }
445 if (options.params) {
446 if ($.isFunction(options.params)) {
447 $.extend(params, options.params.call(self));
449 $.extend(params, options.params);
455 dataType: options.dataType,
457 success: function (data) {
458 // TODO - replace query.page with query so users have access to term, page, etc.
459 var results = options.results(data, query.page);
460 query.callback(results);
463 handler = transport.call(self, params);
469 * Produces a query function that works with a local array
471 * @param options object containing configuration parameters. The options parameter can either be an array or an
474 * If the array form is used it is assumed that it contains objects with 'id' and 'text' keys.
476 * If the object form is used ti is assumed that it contains 'data' and 'text' keys. The 'data' key should contain
477 * an array of objects that will be used as choices. These objects must contain at least an 'id' key. The 'text'
478 * key can either be a String in which case it is expected that each element in the 'data' array has a key with the
479 * value of 'text' which will be used to match choices. Alternatively, text can be a function(item) that can extract
482 function local(options) {
483 var data = options, // data elements
486 text = function (item) { return ""+item.text; }; // function used to retrieve the text portion of a data item that is matched against the search
488 if ($.isArray(data)) {
490 data = { results: tmp };
493 if ($.isFunction(data) === false) {
495 data = function() { return tmp; };
498 var dataItem = data();
500 text = dataItem.text;
501 // if text is not a function we assume it to be a key name
502 if (!$.isFunction(text)) {
503 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
504 text = function (item) { return item[dataText]; };
508 return function (query) {
509 var t = query.term, filtered = { results: [] }, process;
511 query.callback(data());
515 process = function(datum, collection) {
518 if (datum.children) {
520 for (attr in datum) {
521 if (datum.hasOwnProperty(attr)) group[attr]=datum[attr];
524 $(datum.children).each2(function(i, childDatum) { process(childDatum, group.children); });
525 if (group.children.length || query.matcher(t, text(group), datum)) {
526 collection.push(group);
529 if (query.matcher(t, text(datum), datum)) {
530 collection.push(datum);
535 $(data().results).each2(function(i, datum) { process(datum, filtered.results); });
536 query.callback(filtered);
541 function tags(data) {
542 var isFunc = $.isFunction(data);
543 return function (query) {
544 var t = query.term, filtered = {results: []};
545 $(isFunc ? data() : data).each(function () {
546 var isObject = this.text !== undefined,
547 text = isObject ? this.text : this;
548 if (t === "" || query.matcher(t, text)) {
549 filtered.results.push(isObject ? this : {id: this, text: this});
552 query.callback(filtered);
557 * Checks if the formatter function should be used.
559 * Throws an error if it is not a function. Returns true if it should be used,
560 * false if no formatting should be performed.
564 function checkFormatter(formatter, formatterName) {
565 if ($.isFunction(formatter)) return true;
566 if (!formatter) return false;
567 if (typeof(formatter) === 'string') return true;
568 throw new Error(formatterName +" must be a string, function, or falsy value");
571 function evaluate(val) {
572 if ($.isFunction(val)) {
573 var args = Array.prototype.slice.call(arguments, 1);
574 return val.apply(null, args);
579 function countResults(results) {
581 $.each(results, function(i, item) {
583 count += countResults(item.children);
592 * Default tokenizer. This function uses breaks the input on substring match of any string from the
593 * opts.tokenSeparators array and uses opts.createSearchChoice to create the choice object. Both of those
594 * two options have to be defined in order for the tokenizer to work.
596 * @param input text user has typed so far or pasted into the search field
597 * @param selection currently selected choices
598 * @param selectCallback function(choice) callback tho add the choice to selection
599 * @param opts select2's opts
600 * @return undefined/null to leave the current input unchanged, or a string to change the input to the returned value
602 function defaultTokenizer(input, selection, selectCallback, opts) {
603 var original = input, // store the original so we can compare and know if we need to tell the search to update its text
604 dupe = false, // check for whether a token we extracted represents a duplicate selected choice
606 index, // position at which the separator was found
607 i, l, // looping variables
608 separator; // the matched separator
610 if (!opts.createSearchChoice || !opts.tokenSeparators || opts.tokenSeparators.length < 1) return undefined;
615 for (i = 0, l = opts.tokenSeparators.length; i < l; i++) {
616 separator = opts.tokenSeparators[i];
617 index = input.indexOf(separator);
618 if (index >= 0) break;
621 if (index < 0) break; // did not find any token separator in the input string, bail
623 token = input.substring(0, index);
624 input = input.substring(index + separator.length);
626 if (token.length > 0) {
627 token = opts.createSearchChoice.call(this, token, selection);
628 if (token !== undefined && token !== null && opts.id(token) !== undefined && opts.id(token) !== null) {
630 for (i = 0, l = selection.length; i < l; i++) {
631 if (equal(opts.id(token), opts.id(selection[i]))) {
636 if (!dupe) selectCallback(token);
641 if (original!==input) return input;
645 * Creates a new class
650 function clazz(SuperClass, methods) {
651 var constructor = function () {};
652 constructor.prototype = new SuperClass;
653 constructor.prototype.constructor = constructor;
654 constructor.prototype.parent = SuperClass.prototype;
655 constructor.prototype = $.extend(constructor.prototype, methods);
659 AbstractSelect2 = clazz(Object, {
662 bind: function (func) {
665 func.apply(self, arguments);
670 init: function (opts) {
671 var results, search, resultsSelector = ".select2-results";
674 this.opts = opts = this.prepareOpts(opts);
678 // destroy if called on an existing component
679 if (opts.element.data("select2") !== undefined &&
680 opts.element.data("select2") !== null) {
681 opts.element.data("select2").destroy();
684 this.container = this.createContainer();
686 this.liveRegion = $("<span>", {
688 "aria-live": "polite"
690 .addClass("select2-hidden-accessible")
691 .appendTo(document.body);
693 this.containerId="s2id_"+(opts.element.attr("id") || "autogen"+nextUid()).replace(/([;&,\-\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '\\$1');
694 this.containerSelector="#"+this.containerId;
695 this.container.attr("id", this.containerId);
697 // cache the body so future lookups are cheap
698 this.body = thunk(function() { return opts.element.closest("body"); });
700 syncCssClasses(this.container, this.opts.element, this.opts.adaptContainerCssClass);
702 this.container.attr("style", opts.element.attr("style"));
703 this.container.css(evaluate(opts.containerCss));
704 this.container.addClass(evaluate(opts.containerCssClass));
706 this.elementTabIndex = this.opts.element.attr("tabindex");
708 // swap container for the element
710 .data("select2", this)
711 .attr("tabindex", "-1")
712 .before(this.container)
713 .on("click.select2", killEvent); // do not leak click events
715 this.container.data("select2", this);
717 this.dropdown = this.container.find(".select2-drop");
719 syncCssClasses(this.dropdown, this.opts.element, this.opts.adaptDropdownCssClass);
721 this.dropdown.addClass(evaluate(opts.dropdownCssClass));
722 this.dropdown.data("select2", this);
723 this.dropdown.on("click", killEvent);
725 this.results = results = this.container.find(resultsSelector);
726 this.search = search = this.container.find("input.select2-input");
729 this.resultsPage = 0;
732 // initialize the container
733 this.initContainer();
735 this.container.on("click", killEvent);
737 installFilteredMouseMove(this.results);
738 this.dropdown.on("mousemove-filtered touchstart touchmove touchend", resultsSelector, this.bind(this.highlightUnderEvent));
739 this.dropdown.on("touchend", resultsSelector, this.bind(this.selectHighlighted));
740 this.dropdown.on("touchmove", resultsSelector, this.bind(this.touchMoved));
741 this.dropdown.on("touchstart touchend", resultsSelector, this.bind(this.clearTouchMoved));
743 installDebouncedScroll(80, this.results);
744 this.dropdown.on("scroll-debounced", resultsSelector, this.bind(this.loadMoreIfNeeded));
746 // do not propagate change event from the search field out of the component
747 $(this.container).on("change", ".select2-input", function(e) {e.stopPropagation();});
748 $(this.dropdown).on("change", ".select2-input", function(e) {e.stopPropagation();});
750 // if jquery.mousewheel plugin is installed we can prevent out-of-bounds scrolling of results via mousewheel
751 if ($.fn.mousewheel) {
752 results.mousewheel(function (e, delta, deltaX, deltaY) {
753 var top = results.scrollTop();
754 if (deltaY > 0 && top - deltaY <= 0) {
755 results.scrollTop(0);
757 } else if (deltaY < 0 && results.get(0).scrollHeight - results.scrollTop() + deltaY <= results.height()) {
758 results.scrollTop(results.get(0).scrollHeight - results.height());
764 installKeyUpChangeEvent(search);
765 search.on("keyup-change input paste", this.bind(this.updateResults));
766 search.on("focus", function () { search.addClass("select2-focused"); });
767 search.on("blur", function () { search.removeClass("select2-focused");});
769 this.dropdown.on("mouseup", resultsSelector, this.bind(function (e) {
770 if ($(e.target).closest(".select2-result-selectable").length > 0) {
771 this.highlightUnderEvent(e);
772 this.selectHighlighted(e);
776 // trap all mouse events from leaving the dropdown. sometimes there may be a modal that is listening
777 // for mouse events outside of itself so it can close itself. since the dropdown is now outside the select2's
778 // dom it will trigger the popup close, which is not what we want
779 // focusin can cause focus wars between modals and select2 since the dropdown is outside the modal.
780 this.dropdown.on("click mouseup mousedown focusin", function (e) { e.stopPropagation(); });
782 this.nextSearchTerm = undefined;
784 if ($.isFunction(this.opts.initSelection)) {
785 // initialize selection based on the current value of the source element
786 this.initSelection();
788 // if the user has provided a function that can set selection based on the value of the source element
789 // we monitor the change event on the element and trigger it, allowing for two way synchronization
790 this.monitorSource();
793 if (opts.maximumInputLength !== null) {
794 this.search.attr("maxlength", opts.maximumInputLength);
797 var disabled = opts.element.prop("disabled");
798 if (disabled === undefined) disabled = false;
799 this.enable(!disabled);
801 var readonly = opts.element.prop("readonly");
802 if (readonly === undefined) readonly = false;
803 this.readonly(readonly);
805 // Calculate size of scrollbar
806 scrollBarDimensions = scrollBarDimensions || measureScrollbar();
808 this.autofocus = opts.element.prop("autofocus");
809 opts.element.prop("autofocus", false);
810 if (this.autofocus) this.focus();
812 this.search.attr("placeholder", opts.searchInputPlaceholder);
816 destroy: function () {
817 var element=this.opts.element, select2 = element.data("select2");
821 if (this.propertyObserver) { delete this.propertyObserver; this.propertyObserver = null; }
823 if (select2 !== undefined) {
824 select2.container.remove();
825 select2.liveRegion.remove();
826 select2.dropdown.remove();
828 .removeClass("select2-offscreen")
829 .removeData("select2")
831 .prop("autofocus", this.autofocus || false);
832 if (this.elementTabIndex) {
833 element.attr({tabindex: this.elementTabIndex});
835 element.removeAttr("tabindex");
842 optionToData: function(element) {
843 if (element.is("option")) {
845 id:element.prop("value"),
847 element: element.get(),
848 css: element.attr("class"),
849 disabled: element.prop("disabled"),
850 locked: equal(element.attr("locked"), "locked") || equal(element.data("locked"), true)
852 } else if (element.is("optgroup")) {
854 text:element.attr("label"),
856 element: element.get(),
857 css: element.attr("class")
863 prepareOpts: function (opts) {
864 var element, select, idKey, ajaxUrl, self = this;
866 element = opts.element;
868 if (element.get(0).tagName.toLowerCase() === "select") {
869 this.select = select = opts.element;
873 // these options are not allowed when attached to a select because they are picked up off the element itself
874 $.each(["id", "multiple", "ajax", "query", "createSearchChoice", "initSelection", "data", "tags"], function () {
876 throw new Error("Option '" + this + "' is not allowed for Select2 when attached to a <select> element.");
881 opts = $.extend({}, {
882 populateResults: function(container, results, query) {
883 var populate, id=this.opts.id, liveRegion=this.liveRegion;
885 populate=function(results, container, depth) {
887 var i, l, result, selectable, disabled, compound, node, label, innerContainer, formatted;
889 results = opts.sortResults(results, container, query);
891 for (i = 0, l = results.length; i < l; i = i + 1) {
895 disabled = (result.disabled === true);
896 selectable = (!disabled) && (id(result) !== undefined);
898 compound=result.children && result.children.length > 0;
901 node.addClass("select2-results-dept-"+depth);
902 node.addClass("select2-result");
903 node.addClass(selectable ? "select2-result-selectable" : "select2-result-unselectable");
904 if (disabled) { node.addClass("select2-disabled"); }
905 if (compound) { node.addClass("select2-result-with-children"); }
906 node.addClass(self.opts.formatResultCssClass(result));
907 node.attr("role", "presentation");
909 label=$(document.createElement("div"));
910 label.addClass("select2-result-label");
911 label.attr("id", "select2-result-label-" + nextUid());
912 label.attr("role", "option");
914 formatted=opts.formatResult(result, label, query, self.opts.escapeMarkup);
915 if (formatted!==undefined) {
916 label.html(formatted);
923 innerContainer=$("<ul></ul>");
924 innerContainer.addClass("select2-result-sub");
925 populate(result.children, innerContainer, depth+1);
926 node.append(innerContainer);
929 node.data("select2-data", result);
930 container.append(node);
933 liveRegion.text(opts.formatMatches(results.length));
936 populate(results, container, 0);
938 }, $.fn.select2.defaults, opts);
940 if (typeof(opts.id) !== "function") {
942 opts.id = function (e) { return e[idKey]; };
945 if ($.isArray(opts.element.data("select2Tags"))) {
946 if ("tags" in opts) {
947 throw "tags specified as both an attribute 'data-select2-tags' and in options of Select2 " + opts.element.attr("id");
949 opts.tags=opts.element.data("select2Tags");
953 opts.query = this.bind(function (query) {
954 var data = { results: [], more: false },
956 children, placeholderOption, process;
958 process=function(element, collection) {
960 if (element.is("option")) {
961 if (query.matcher(term, element.text(), element)) {
962 collection.push(self.optionToData(element));
964 } else if (element.is("optgroup")) {
965 group=self.optionToData(element);
966 element.children().each2(function(i, elm) { process(elm, group.children); });
967 if (group.children.length>0) {
968 collection.push(group);
973 children=element.children();
975 // ignore the placeholder option if there is one
976 if (this.getPlaceholder() !== undefined && children.length > 0) {
977 placeholderOption = this.getPlaceholderOption();
978 if (placeholderOption) {
979 children=children.not(placeholderOption);
983 children.each2(function(i, elm) { process(elm, data.results); });
985 query.callback(data);
987 // this is needed because inside val() we construct choices from options and there id is hardcoded
988 opts.id=function(e) { return e.id; };
990 if (!("query" in opts)) {
992 if ("ajax" in opts) {
993 ajaxUrl = opts.element.data("ajax-url");
994 if (ajaxUrl && ajaxUrl.length > 0) {
995 opts.ajax.url = ajaxUrl;
997 opts.query = ajax.call(opts.element, opts.ajax);
998 } else if ("data" in opts) {
999 opts.query = local(opts.data);
1000 } else if ("tags" in opts) {
1001 opts.query = tags(opts.tags);
1002 if (opts.createSearchChoice === undefined) {
1003 opts.createSearchChoice = function (term) { return {id: $.trim(term), text: $.trim(term)}; };
1005 if (opts.initSelection === undefined) {
1006 opts.initSelection = function (element, callback) {
1008 $(splitVal(element.val(), opts.separator)).each(function () {
1009 var obj = { id: this, text: this },
1011 if ($.isFunction(tags)) tags=tags();
1012 $(tags).each(function() { if (equal(this.id, obj.id)) { obj = this; return false; } });
1022 if (typeof(opts.query) !== "function") {
1023 console.log(typeof(opts.query));
1024 console.log(opts.query);
1025 throw "query function not defined for Select2 " + opts.element.attr("id");
1028 if (opts.createSearchChoicePosition === 'top') {
1029 opts.createSearchChoicePosition = function(list, item) { list.unshift(item); };
1031 else if (opts.createSearchChoicePosition === 'bottom') {
1032 opts.createSearchChoicePosition = function(list, item) { list.push(item); };
1034 else if (typeof(opts.createSearchChoicePosition) !== "function") {
1035 throw "invalid createSearchChoicePosition option must be 'top', 'bottom' or a custom function";
1042 * Monitor the original element for changes and update select2 accordingly
1045 monitorSource: function () {
1046 var el = this.opts.element, sync, observer;
1048 el.on("change.select2", this.bind(function (e) {
1049 if (this.opts.element.data("select2-change-triggered") !== true) {
1050 this.initSelection();
1054 sync = this.bind(function () {
1056 // sync enabled state
1057 var disabled = el.prop("disabled");
1058 if (disabled === undefined) disabled = false;
1059 this.enable(!disabled);
1061 var readonly = el.prop("readonly");
1062 if (readonly === undefined) readonly = false;
1063 this.readonly(readonly);
1065 syncCssClasses(this.container, this.opts.element, this.opts.adaptContainerCssClass);
1066 this.container.addClass(evaluate(this.opts.containerCssClass));
1068 syncCssClasses(this.dropdown, this.opts.element, this.opts.adaptDropdownCssClass);
1069 this.dropdown.addClass(evaluate(this.opts.dropdownCssClass));
1074 el.on("propertychange.select2", sync);
1076 // hold onto a reference of the callback to work around a chromium bug
1077 if (this.mutationCallback === undefined) {
1078 this.mutationCallback = function (mutations) {
1079 mutations.forEach(sync);
1083 // safari, chrome, firefox, IE11
1084 observer = window.MutationObserver || window.WebKitMutationObserver|| window.MozMutationObserver;
1085 if (observer !== undefined) {
1086 if (this.propertyObserver) { delete this.propertyObserver; this.propertyObserver = null; }
1087 this.propertyObserver = new observer(this.mutationCallback);
1088 this.propertyObserver.observe(el.get(0), { attributes:true, subtree:false });
1093 triggerSelect: function(data) {
1094 var evt = $.Event("select2-selecting", { val: this.id(data), object: data });
1095 this.opts.element.trigger(evt);
1096 return !evt.isDefaultPrevented();
1100 * Triggers the change event on the source element
1103 triggerChange: function (details) {
1105 details = details || {};
1106 details= $.extend({}, details, { type: "change", val: this.val() });
1107 // prevents recursive triggering
1108 this.opts.element.data("select2-change-triggered", true);
1109 this.opts.element.trigger(details);
1110 this.opts.element.data("select2-change-triggered", false);
1112 // some validation frameworks ignore the change event and listen instead to keyup, click for selects
1113 // so here we trigger the click event manually
1114 this.opts.element.click();
1116 // ValidationEngine ignores the change event and listens instead to blur
1117 // so here we trigger the blur event manually if so desired
1118 if (this.opts.blurOnChange)
1119 this.opts.element.blur();
1123 isInterfaceEnabled: function()
1125 return this.enabledInterface === true;
1129 enableInterface: function() {
1130 var enabled = this._enabled && !this._readonly,
1131 disabled = !enabled;
1133 if (enabled === this.enabledInterface) return false;
1135 this.container.toggleClass("select2-container-disabled", disabled);
1137 this.enabledInterface = enabled;
1143 enable: function(enabled) {
1144 if (enabled === undefined) enabled = true;
1145 if (this._enabled === enabled) return;
1146 this._enabled = enabled;
1148 this.opts.element.prop("disabled", !enabled);
1149 this.enableInterface();
1153 disable: function() {
1158 readonly: function(enabled) {
1159 if (enabled === undefined) enabled = false;
1160 if (this._readonly === enabled) return;
1161 this._readonly = enabled;
1163 this.opts.element.prop("readonly", enabled);
1164 this.enableInterface();
1168 opened: function () {
1169 return this.container.hasClass("select2-dropdown-open");
1173 positionDropdown: function(init) {
1174 var drop = $(this.dropdown.context).find('.select2-drop');
1177 drop.addClass('force-ghost');
1179 $('#select2-drop').removeClass('force-ghost');
1180 var offset = this.container.offset();
1181 this.container.css({left: offset.left % 1 != 0 ? 0.5 : 1.0});
1182 var offset = this.container.offset();
1183 var $dropdown = this.dropdown,
1184 height = this.container.outerHeight(false),
1185 width = this.container[0].getBoundingClientRect().width, //this.container.outerWidth(false),
1186 dropHeight = $dropdown.outerHeight(false),
1187 $window = $(window),
1188 windowWidth = $window.width(),
1189 windowHeight = $window.height(),
1190 viewPortRight = $window.scrollLeft() + windowWidth,
1191 viewportBottom = $window.scrollTop() + windowHeight,
1192 dropTop = offset.top + height,
1193 dropLeft = offset.left,
1194 enoughRoomBelow = dropTop + dropHeight <= viewportBottom,
1195 enoughRoomAbove = (offset.top - dropHeight) >= $window.scrollTop(),
1196 dropWidth = $dropdown.outerWidth(false),
1197 enoughRoomOnRight = dropLeft + dropWidth <= viewPortRight,
1198 aboveNow = $dropdown.hasClass("select2-drop-above"),
1205 // always prefer the current above/below alignment, unless there is not enough room
1208 if (!enoughRoomAbove && enoughRoomBelow) {
1209 changeDirection = true;
1214 if (!enoughRoomBelow && enoughRoomAbove) {
1215 changeDirection = true;
1220 //if we are changing direction we need to get positions when dropdown is hidden;
1221 if (changeDirection) {
1223 offset = this.container[0].getBoundingClientRect(); //this.container.offset();
1224 height = this.container.outerHeight(false);
1225 width = this.container[0].getBoundingClientRect().width; //this.container.outerWidth(false);
1226 dropHeight = $dropdown.outerHeight(false);
1227 viewPortRight = $window.scrollLeft() + windowWidth;
1228 viewportBottom = $window.scrollTop() + windowHeight;
1229 dropTop = offset.top + height;
1230 dropLeft = offset.left;
1231 dropWidth = $dropdown.outerWidth(false);
1232 enoughRoomOnRight = dropLeft + dropWidth <= viewPortRight;
1236 if (this.opts.dropdownAutoWidth) {
1237 resultsListNode = $('.select2-results', $dropdown)[0];
1238 $dropdown.addClass('select2-drop-auto-width');
1239 $dropdown.css('width', '');
1240 // Add scrollbar width to dropdown if vertical scrollbar is present
1241 dropWidth = $dropdown.outerWidth(false) + (resultsListNode.scrollHeight === resultsListNode.clientHeight ? 0 : scrollBarDimensions.width);
1242 dropWidth > width ? width = dropWidth : dropWidth = width;
1243 enoughRoomOnRight = dropLeft + dropWidth <= viewPortRight;
1246 this.container.removeClass('select2-drop-auto-width');
1248 // fix positioning when body has an offset and is not position: static
1249 if (this.body().css('position') !== 'static') {
1250 bodyOffset = this.body().offset();
1251 dropTop -= bodyOffset.top;
1252 dropLeft -= bodyOffset.left;
1255 if (!enoughRoomOnRight) {
1256 dropLeft = offset.left + this.container.outerWidth(false) - dropWidth;
1264 css.top = offset.top - dropHeight;
1265 css.bottom = 'auto';
1266 this.container.addClass("select2-drop-above");
1267 $dropdown.addClass("select2-drop-above");
1271 css.bottom = 'auto';
1272 this.container.removeClass("select2-drop-above");
1273 $dropdown.removeClass("select2-drop-above");
1275 css = $.extend(css, evaluate(this.opts.dropdownCss));
1281 shouldOpen: function() {
1284 if (this.opened()) return false;
1286 if (this._enabled === false || this._readonly === true) return false;
1288 event = $.Event("select2-opening");
1289 this.opts.element.trigger(event);
1290 return !event.isDefaultPrevented();
1294 clearDropdownAlignmentPreference: function() {
1295 // clear the classes used to figure out the preference of where the dropdown should be opened
1296 this.container.removeClass("select2-drop-above");
1297 this.dropdown.removeClass("select2-drop-above");
1301 * Opens the dropdown
1303 * @return {Boolean} whether or not dropdown was opened. This method will return false if, for example,
1304 * the dropdown is already open, or if the 'open' event listener on the element called preventDefault().
1309 if (!this.shouldOpen()) return false;
1317 * Performs the opening of the dropdown
1320 opening: function() {
1321 var cid = this.containerId,
1322 scroll = "scroll." + cid,
1323 resize = "resize."+cid,
1324 orient = "orientationchange."+cid,
1327 this.container.addClass("select2-dropdown-open").addClass("select2-container-active");
1329 this.clearDropdownAlignmentPreference();
1331 if(this.dropdown[0] !== this.body().children().last()[0]) {
1332 this.dropdown.detach().appendTo(this.body());
1335 // create the dropdown mask if doesn't already exist
1336 mask = $("#select2-drop-mask");
1337 if (mask.length == 0) {
1338 mask = $(document.createElement("div"));
1339 mask.attr("id","select2-drop-mask").attr("class","select2-drop-mask");
1341 mask.appendTo(this.body());
1342 mask.on("mousedown touchstart click", function (e) {
1343 // Prevent IE from generating a click event on the body
1344 reinsertElement(mask);
1346 var dropdown = $("#select2-drop"), self;
1347 if (dropdown.length > 0) {
1348 self=dropdown.data("select2");
1349 if (self.opts.selectOnBlur) {
1350 self.selectHighlighted({noFocus: true});
1354 e.stopPropagation();
1359 // ensure the mask is always right before the dropdown
1360 if (this.dropdown.prev()[0] !== mask[0]) {
1361 this.dropdown.before(mask);
1364 // move the global id to the correct dropdown
1365 $("#select2-drop").removeAttr("id");
1366 this.dropdown.attr("id", "select2-drop");
1368 // show the elements
1371 this.positionDropdown();
1372 this.dropdown.show();
1373 this.positionDropdown();
1375 this.dropdown.addClass("select2-drop-active");
1377 // attach listeners to events that can change the position of the container and thus require
1378 // the position of the dropdown to be updated as well so it does not come unglued from the container
1380 this.container.parents().add(window).each(function () {
1381 $(this).on(resize+" "+scroll+" "+orient, function (e) {
1382 that.positionDropdown();
1390 close: function () {
1391 if (!this.opened()) return;
1393 var cid = this.containerId,
1394 scroll = "scroll." + cid,
1395 resize = "resize."+cid,
1396 orient = "orientationchange."+cid;
1398 // unbind event listeners
1399 this.container.parents().add(window).each(function () { $(this).off(scroll).off(resize).off(orient); });
1401 this.clearDropdownAlignmentPreference();
1403 $("#select2-drop-mask").hide();
1404 this.dropdown.removeAttr("id"); // only the active dropdown has the select2-drop id
1405 this.dropdown.hide();
1406 this.container.removeClass("select2-dropdown-open").removeClass("select2-container-active");
1407 this.results.empty();
1411 this.search.removeClass("select2-active");
1412 this.opts.element.trigger($.Event("select2-close"));
1416 * Opens control, sets input value, and updates results.
1419 externalSearch: function (term) {
1421 this.search.val(term);
1422 this.updateResults(false);
1426 clearSearch: function () {
1431 getMaximumSelectionSize: function() {
1432 return evaluate(this.opts.maximumSelectionSize);
1436 ensureHighlightVisible: function () {
1437 var results = this.results, children, index, child, hb, rb, y, more;
1439 index = this.highlight();
1441 if (index < 0) return;
1445 // if the first element is highlighted scroll all the way to the top,
1446 // that way any unselectable headers above it will also be scrolled
1449 results.scrollTop(0);
1453 children = this.findHighlightableChoices().find('.select2-result-label');
1455 child = $(children[index]);
1457 hb = child.offset().top + child.outerHeight(true);
1459 // if this is the last child lets also make sure select2-more-results is visible
1460 if (index === children.length - 1) {
1461 more = results.find("li.select2-more-results");
1462 if (more.length > 0) {
1463 hb = more.offset().top + more.outerHeight(true);
1467 rb = results.offset().top + results.outerHeight(true);
1469 results.scrollTop(results.scrollTop() + (hb - rb));
1471 y = child.offset().top - results.offset().top;
1473 // make sure the top of the element is visible
1474 if (y < 0 && child.css('display') != 'none' ) {
1475 results.scrollTop(results.scrollTop() + y); // y is negative
1480 findHighlightableChoices: function() {
1481 return this.results.find(".select2-result-selectable:not(.select2-disabled):not(.select2-selected)");
1485 moveHighlight: function (delta) {
1486 var choices = this.findHighlightableChoices(),
1487 index = this.highlight();
1489 while (index > -1 && index < choices.length) {
1491 var choice = $(choices[index]);
1492 if (choice.hasClass("select2-result-selectable") && !choice.hasClass("select2-disabled") && !choice.hasClass("select2-selected")) {
1493 this.highlight(index);
1500 highlight: function (index) {
1501 var choices = this.findHighlightableChoices(),
1505 if (arguments.length === 0) {
1506 return indexOf(choices.filter(".select2-highlighted")[0], choices.get());
1509 if (index >= choices.length) index = choices.length - 1;
1510 if (index < 0) index = 0;
1512 this.removeHighlight();
1514 choice = $(choices[index]);
1515 choice.addClass("select2-highlighted");
1517 // ensure assistive technology can determine the active choice
1518 this.search.attr("aria-activedescendant", choice.find(".select2-result-label").attr("id"));
1520 this.ensureHighlightVisible();
1522 this.liveRegion.text(choice.text());
1524 data = choice.data("select2-data");
1526 this.opts.element.trigger({ type: "select2-highlight", val: this.id(data), choice: data });
1530 removeHighlight: function() {
1531 this.results.find(".select2-highlighted").removeClass("select2-highlighted");
1534 touchMoved: function() {
1535 this._touchMoved = true;
1538 clearTouchMoved: function() {
1539 this._touchMoved = false;
1543 countSelectableResults: function() {
1544 return this.findHighlightableChoices().length;
1548 highlightUnderEvent: function (event) {
1549 var el = $(event.target).closest(".select2-result-selectable");
1550 if (el.length > 0 && !el.is(".select2-highlighted")) {
1551 var choices = this.findHighlightableChoices();
1552 this.highlight(choices.index(el));
1553 } else if (el.length == 0) {
1554 // if we are over an unselectable item remove all highlights
1555 this.removeHighlight();
1560 loadMoreIfNeeded: function () {
1561 var results = this.results,
1562 more = results.find("li.select2-more-results"),
1563 below, // pixels the element is below the scroll fold, below==0 is when the element is starting to be visible
1564 page = this.resultsPage + 1,
1566 term=this.search.val(),
1567 context=this.context;
1569 if (more.length === 0) return;
1570 below = more.offset().top - results.offset().top - results.height();
1572 if (below <= this.opts.loadMorePadding) {
1573 more.addClass("select2-active");
1575 element: this.opts.element,
1579 matcher: this.opts.matcher,
1580 callback: this.bind(function (data) {
1582 // ignore a response if the select2 has been closed before it was received
1583 if (!self.opened()) return;
1586 self.opts.populateResults.call(this, results, data.results, {term: term, page: page, context:context});
1587 self.postprocessResults(data, false, false);
1589 if (data.more===true) {
1590 more.detach().appendTo(results).text(evaluate(self.opts.formatLoadMore, page+1));
1591 window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10);
1595 self.positionDropdown();
1596 self.resultsPage = page;
1597 self.context = data.context;
1598 this.opts.element.trigger({ type: "select2-loaded", items: data });
1604 * Default tokenizer function which does nothing
1606 tokenize: function() {
1611 * @param initial whether or not this is the call to this method right after the dropdown has been opened
1614 updateResults: function (initial) {
1615 var search = this.search,
1616 results = this.results,
1621 term = search.val(),
1622 lastTerm = $.data(this.container, "select2-last-term"),
1623 // sequence number used to drop out-of-order responses
1626 // prevent duplicate queries against the same term
1627 if (initial !== true && lastTerm && equal(term, lastTerm)) return;
1629 $.data(this.container, "select2-last-term", term);
1631 // if the search is currently hidden we do not alter the results
1632 if (initial !== true && (this.showSearchInput === false || !this.opened())) {
1636 function postRender() {
1637 search.removeClass("select2-active");
1638 self.positionDropdown('init');
1639 if (results.find('.select2-no-results,.select2-selection-limit,.select2-searching').length) {
1640 self.liveRegion.text(results.text());
1643 self.liveRegion.text(self.opts.formatMatches(results.find('.select2-result-selectable').length));
1647 function render(html) {
1652 queryNumber = ++this.queryCount;
1654 var maxSelSize = this.getMaximumSelectionSize();
1655 if (maxSelSize >=1) {
1657 if ($.isArray(data) && data.length >= maxSelSize && checkFormatter(opts.formatSelectionTooBig, "formatSelectionTooBig")) {
1658 render("<li class='select2-selection-limit'>" + evaluate(opts.formatSelectionTooBig, maxSelSize) + "</li>");
1663 if (search.val().length < opts.minimumInputLength) {
1664 if (checkFormatter(opts.formatInputTooShort, "formatInputTooShort")) {
1665 render("<li class='select2-no-results'>" + evaluate(opts.formatInputTooShort, search.val(), opts.minimumInputLength) + "</li>");
1669 if (initial && this.showSearch) this.showSearch(true);
1673 if (opts.maximumInputLength && search.val().length > opts.maximumInputLength) {
1674 if (checkFormatter(opts.formatInputTooLong, "formatInputTooLong")) {
1675 render("<li class='select2-no-results'>" + evaluate(opts.formatInputTooLong, search.val(), opts.maximumInputLength) + "</li>");
1682 if (opts.formatSearching && this.findHighlightableChoices().length === 0) {
1683 render("<li class='select2-searching'>" + evaluate(opts.formatSearching) + "</li>");
1686 search.addClass("select2-active");
1688 this.removeHighlight();
1690 // give the tokenizer a chance to pre-process the input
1691 input = this.tokenize();
1692 if (input != undefined && input != null) {
1696 this.resultsPage = 1;
1699 element: opts.element,
1701 page: this.resultsPage,
1703 matcher: opts.matcher,
1704 callback: this.bind(function (data) {
1705 var def; // default choice
1707 // ignore old responses
1708 if (queryNumber != this.queryCount) {
1712 // ignore a response if the select2 has been closed before it was received
1713 if (!this.opened()) {
1714 this.search.removeClass("select2-active");
1718 // save context, if any
1719 this.context = (data.context===undefined) ? null : data.context;
1720 // create a default choice and prepend it to the list
1721 if (this.opts.createSearchChoice && search.val() !== "") {
1722 def = this.opts.createSearchChoice.call(self, search.val(), data.results);
1723 if (def !== undefined && def !== null && self.id(def) !== undefined && self.id(def) !== null) {
1724 if ($(data.results).filter(
1726 return equal(self.id(this), self.id(def));
1728 this.opts.createSearchChoicePosition(data.results, def);
1733 if (data.results.length === 0 && checkFormatter(opts.formatNoMatches, "formatNoMatches")) {
1734 render("<li class='select2-no-results'>" + evaluate(opts.formatNoMatches, search.val()) + "</li>");
1739 self.opts.populateResults.call(this, results, data.results, {term: search.val(), page: this.resultsPage, context:null});
1741 if (data.more === true && checkFormatter(opts.formatLoadMore, "formatLoadMore")) {
1742 results.append("<li class='select2-more-results'>" + self.opts.escapeMarkup(evaluate(opts.formatLoadMore, this.resultsPage)) + "</li>");
1743 window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10);
1746 this.postprocessResults(data, initial);
1750 this.opts.element.trigger({ type: "select2-loaded", items: data });
1755 cancel: function () {
1761 // if selectOnBlur == true, select the currently highlighted option
1762 if (this.opts.selectOnBlur)
1763 this.selectHighlighted({noFocus: true});
1766 this.container.removeClass("select2-container-active");
1767 // synonymous to .is(':focus'), which is available in jquery >= 1.6
1768 if (this.search[0] === document.activeElement) { this.search.blur(); }
1770 this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
1774 focusSearch: function () {
1779 selectHighlighted: function (options) {
1780 if (this._touchMoved) {
1781 this.clearTouchMoved();
1784 var index=this.highlight(),
1785 highlighted=this.results.find(".select2-highlighted"),
1786 data = highlighted.closest('.select2-result').data("select2-data");
1789 this.highlight(index);
1790 this.onSelect(data, options);
1791 } else if (options && options.noFocus) {
1797 getPlaceholder: function () {
1798 var placeholderOption;
1799 return this.opts.element.attr("placeholder") ||
1800 this.opts.element.attr("data-placeholder") || // jquery 1.4 compat
1801 this.opts.element.data("placeholder") ||
1802 this.opts.placeholder ||
1803 ((placeholderOption = this.getPlaceholderOption()) !== undefined ? placeholderOption.text() : undefined);
1807 getPlaceholderOption: function() {
1809 var firstOption = this.select.children('option').first();
1810 if (this.opts.placeholderOption !== undefined ) {
1811 //Determine the placeholder option based on the specified placeholderOption setting
1812 return (this.opts.placeholderOption === "first" && firstOption) ||
1813 (typeof this.opts.placeholderOption === "function" && this.opts.placeholderOption(this.select));
1814 } else if (firstOption.text() === "" && firstOption.val() === "") {
1815 //No explicit placeholder option specified, use the first if it's blank
1822 * Get the desired width for the container element. This is
1823 * derived first from option `width` passed to select2, then
1824 * the inline 'style' on the original element, and finally
1825 * falls back to the jQuery calculated element width.
1828 initContainerWidth: function () {
1829 function resolveContainerWidth() {
1830 var style, attrs, matches, i, l, attr;
1832 if (this.opts.width === "off") {
1834 } else if (this.opts.width === "element"){
1835 return this.opts.element.outerWidth(false) === 0 ? 'auto' : this.opts.element.outerWidth(false) + 'px';
1836 } else if (this.opts.width === "copy" || this.opts.width === "resolve") {
1837 // check if there is inline style on the element that contains width
1838 style = this.opts.element.attr('style');
1839 if (style !== undefined) {
1840 attrs = style.split(';');
1841 for (i = 0, l = attrs.length; i < l; i = i + 1) {
1842 attr = attrs[i].replace(/\s/g, '');
1843 matches = attr.match(/^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i);
1844 if (matches !== null && matches.length >= 1)
1849 if (this.opts.width === "resolve") {
1850 // next check if css('width') can resolve a width that is percent based, this is sometimes possible
1851 // when attached to input type=hidden or elements hidden via css
1852 style = this.opts.element.css('width');
1853 if (style.indexOf("%") > 0) return style;
1855 // finally, fallback on the calculated width of the element
1856 return (this.opts.element.outerWidth(false) === 0 ? 'auto' : this.opts.element.outerWidth(false) + 'px');
1860 } else if ($.isFunction(this.opts.width)) {
1861 return this.opts.width();
1863 return this.opts.width;
1867 var width = resolveContainerWidth.call(this);
1868 if (width !== null) {
1869 this.container.css("width", width);
1874 SingleSelect2 = clazz(AbstractSelect2, {
1878 createContainer: function () {
1879 var container = $(document.createElement("div")).attr({
1880 "class": "select2-container"
1882 "<a href='javascript:void(0)' class='select2-choice' tabindex='-1'>",
1883 " <span class='select2-chosen'> </span><abbr class='select2-search-choice-close'></abbr>",
1884 " <span class='select2-arrow' role='presentation'><b role='presentation'></b></span>",
1886 "<label for='' class='select2-offscreen'></label>",
1887 "<input class='select2-focusser select2-offscreen' type='text' aria-haspopup='true' role='button' />",
1888 "<div class='select2-drop select2-display-none'>",
1889 " <div class='select2-search'>",
1890 " <label for='' class='select2-offscreen'></label>",
1891 " <input type='text' autocomplete='off' autocorrect='off' autocapitalize='off' spellcheck='false' class='select2-input' role='combobox' aria-expanded='true'",
1892 " aria-autocomplete='list' />",
1894 " <ul class='select2-results' role='listbox'>",
1896 "</div>"].join(""));
1901 enableInterface: function() {
1902 if (this.parent.enableInterface.apply(this, arguments)) {
1903 this.focusser.prop("disabled", !this.isInterfaceEnabled());
1908 opening: function () {
1911 if (this.opts.minimumResultsForSearch >= 0) {
1912 this.showSearch(true);
1915 this.parent.opening.apply(this, arguments);
1917 if (this.showSearchInput !== false) {
1918 // IE appends focusser.val() at the end of field :/ so we manually insert it at the beginning using a range
1919 // all other browsers handle this just fine
1921 this.search.val(this.focusser.val());
1923 this.search.focus();
1924 // move the cursor to the end after focussing, otherwise it will be at the beginning and
1925 // new text will appear *before* focusser.val()
1926 el = this.search.get(0);
1927 if (el.createTextRange) {
1928 range = el.createTextRange();
1929 range.collapse(false);
1931 } else if (el.setSelectionRange) {
1932 len = this.search.val().length;
1933 el.setSelectionRange(len, len);
1936 // initializes search's value with nextSearchTerm (if defined by user)
1937 // ignore nextSearchTerm if the dropdown is opened by the user pressing a letter
1938 if(this.search.val() === "") {
1939 if(this.nextSearchTerm != undefined){
1940 this.search.val(this.nextSearchTerm);
1941 this.search.select();
1945 this.focusser.prop("disabled", true).val("");
1946 this.updateResults(true);
1947 this.opts.element.trigger($.Event("select2-open"));
1951 close: function () {
1952 if (!this.opened()) return;
1953 this.parent.close.apply(this, arguments);
1955 this.focusser.prop("disabled", false);
1957 if (this.opts.shouldFocusInput(this)) {
1958 this.focusser.focus();
1963 focus: function () {
1964 if (this.opened()) {
1967 this.focusser.prop("disabled", false);
1968 if (this.opts.shouldFocusInput(this)) {
1969 this.focusser.focus();
1975 isFocused: function () {
1976 return this.container.hasClass("select2-container-active");
1980 cancel: function () {
1981 this.parent.cancel.apply(this, arguments);
1982 this.focusser.prop("disabled", false);
1984 if (this.opts.shouldFocusInput(this)) {
1985 this.focusser.focus();
1990 destroy: function() {
1991 $("label[for='" + this.focusser.attr('id') + "']")
1992 .attr('for', this.opts.element.attr("id"));
1993 this.parent.destroy.apply(this, arguments);
1997 initContainer: function () {
2000 container = this.container,
2001 dropdown = this.dropdown,
2002 idSuffix = nextUid(),
2005 if (this.opts.minimumResultsForSearch < 0) {
2006 this.showSearch(false);
2008 this.showSearch(true);
2011 this.selection = selection = container.find(".select2-choice");
2013 this.focusser = container.find(".select2-focusser");
2015 // add aria associations
2016 selection.find(".select2-chosen").attr("id", "select2-chosen-"+idSuffix);
2017 this.focusser.attr("aria-labelledby", "select2-chosen-"+idSuffix);
2018 this.results.attr("id", "select2-results-"+idSuffix);
2019 this.search.attr("aria-owns", "select2-results-"+idSuffix);
2021 // rewrite labels from original element to focusser
2022 this.focusser.attr("id", "s2id_autogen"+idSuffix);
2024 elementLabel = $("label[for='" + this.opts.element.attr("id") + "']");
2026 this.focusser.prev()
2027 .text(elementLabel.text())
2028 .attr('for', this.focusser.attr('id'));
2030 // Ensure the original element retains an accessible name
2031 var originalTitle = this.opts.element.attr("title");
2032 this.opts.element.attr("title", (originalTitle || elementLabel.text()));
2034 this.focusser.attr("tabindex", this.elementTabIndex);
2036 // write label for search field using the label from the focusser element
2037 this.search.attr("id", this.focusser.attr('id') + '_search');
2040 .text($("label[for='" + this.focusser.attr('id') + "']").text())
2041 .attr('for', this.search.attr('id'));
2043 this.search.on("keydown", this.bind(function (e) {
2044 if (!this.isInterfaceEnabled()) return;
2046 if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) {
2047 // prevent the page from scrolling
2055 this.moveHighlight((e.which === KEY.UP) ? -1 : 1);
2059 this.selectHighlighted();
2063 this.selectHighlighted({noFocus: true});
2072 this.search.on("blur", this.bind(function(e) {
2073 // a workaround for chrome to keep the search field focussed when the scroll bar is used to scroll the dropdown.
2074 // without this the search field loses focus which is annoying
2075 if (document.activeElement === this.body().get(0)) {
2076 window.setTimeout(this.bind(function() {
2077 if (this.opened()) {
2078 this.search.focus();
2084 this.focusser.on("keydown", this.bind(function (e) {
2085 if (!this.isInterfaceEnabled()) return;
2087 if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) {
2091 if (this.opts.openOnEnter === false && e.which === KEY.ENTER) {
2096 if (e.which == KEY.DOWN || e.which == KEY.UP
2097 || (e.which == KEY.ENTER && this.opts.openOnEnter)) {
2099 if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) return;
2106 if (e.which == KEY.DELETE || e.which == KEY.BACKSPACE) {
2107 if (this.opts.allowClear) {
2116 installKeyUpChangeEvent(this.focusser);
2117 this.focusser.on("keyup-change input", this.bind(function(e) {
2118 if (this.opts.minimumResultsForSearch >= 0) {
2119 e.stopPropagation();
2120 if (this.opened()) return;
2125 selection.on("mousedown touchstart", "abbr", this.bind(function (e) {
2126 if (!this.isInterfaceEnabled()) return;
2128 killEventImmediately(e);
2130 this.selection.focus();
2133 selection.on("mousedown touchstart", this.bind(function (e) {
2134 // Prevent IE from generating a click event on the body
2135 reinsertElement(selection);
2137 if (!this.container.hasClass("select2-container-active")) {
2138 this.opts.element.trigger($.Event("select2-focus"));
2141 if (this.opened()) {
2143 } else if (this.isInterfaceEnabled()) {
2150 dropdown.on("mousedown touchstart", this.bind(function() { this.search.focus(); }));
2152 selection.on("focus", this.bind(function(e) {
2156 this.focusser.on("focus", this.bind(function(){
2157 if (!this.container.hasClass("select2-container-active")) {
2158 this.opts.element.trigger($.Event("select2-focus"));
2160 this.container.addClass("select2-container-active");
2161 })).on("blur", this.bind(function() {
2162 if (!this.opened()) {
2163 this.container.removeClass("select2-container-active");
2164 this.opts.element.trigger($.Event("select2-blur"));
2167 this.search.on("focus", this.bind(function(){
2168 if (!this.container.hasClass("select2-container-active")) {
2169 this.opts.element.trigger($.Event("select2-focus"));
2171 this.container.addClass("select2-container-active");
2174 this.initContainerWidth();
2175 this.opts.element.addClass("select2-offscreen");
2176 this.setPlaceholder();
2181 clear: function(triggerChange) {
2182 var data=this.selection.data("select2-data");
2183 if (data) { // guard against queued quick consecutive clicks
2184 var evt = $.Event("select2-clearing");
2185 this.opts.element.trigger(evt);
2186 if (evt.isDefaultPrevented()) {
2189 var placeholderOption = this.getPlaceholderOption();
2190 this.opts.element.val(placeholderOption ? placeholderOption.val() : "");
2191 this.selection.find(".select2-chosen").empty();
2192 this.selection.removeData("select2-data");
2193 this.setPlaceholder();
2195 if (triggerChange !== false){
2196 this.opts.element.trigger({ type: "select2-removed", val: this.id(data), choice: data });
2197 this.triggerChange({removed:data});
2203 * Sets selection based on source element's value
2206 initSelection: function () {
2208 if (this.isPlaceholderOptionSelected()) {
2209 this.updateSelection(null);
2211 this.setPlaceholder();
2214 this.opts.initSelection.call(null, this.opts.element, function(selected){
2215 if (selected !== undefined && selected !== null) {
2216 self.updateSelection(selected);
2218 self.setPlaceholder();
2219 self.nextSearchTerm = self.opts.nextSearchTerm(selected, self.search.val());
2225 isPlaceholderOptionSelected: function() {
2226 var placeholderOption;
2227 if (!this.getPlaceholder()) return false; // no placeholder specified so no option should be considered
2228 return ((placeholderOption = this.getPlaceholderOption()) !== undefined && placeholderOption.prop("selected"))
2229 || (this.opts.element.val() === "")
2230 || (this.opts.element.val() === undefined)
2231 || (this.opts.element.val() === null);
2235 prepareOpts: function () {
2236 var opts = this.parent.prepareOpts.apply(this, arguments),
2239 if (opts.element.get(0).tagName.toLowerCase() === "select") {
2240 // install the selection initializer
2241 opts.initSelection = function (element, callback) {
2242 var selected = element.find("option").filter(function() { return this.selected && !this.disabled });
2243 // a single select box always has a value, no need to null check 'selected'
2244 callback(self.optionToData(selected));
2246 } else if ("data" in opts) {
2247 // install default initSelection when applied to hidden input and data is local
2248 opts.initSelection = opts.initSelection || function (element, callback) {
2249 var id = element.val();
2250 //search in data by id, storing the actual matching item
2253 matcher: function(term, text, el){
2254 var is_match = equal(id, opts.id(el));
2260 callback: !$.isFunction(callback) ? $.noop : function() {
2271 getPlaceholder: function() {
2272 // if a placeholder is specified on a single select without a valid placeholder option ignore it
2274 if (this.getPlaceholderOption() === undefined) {
2279 return this.parent.getPlaceholder.apply(this, arguments);
2283 setPlaceholder: function () {
2284 var placeholder = this.getPlaceholder();
2286 if (this.isPlaceholderOptionSelected() && placeholder !== undefined) {
2288 // check for a placeholder option if attached to a select
2289 if (this.select && this.getPlaceholderOption() === undefined) return;
2291 this.selection.find(".select2-chosen").html(this.opts.escapeMarkup(placeholder));
2293 this.selection.addClass("select2-default");
2295 this.container.removeClass("select2-allowclear");
2300 postprocessResults: function (data, initial, noHighlightUpdate) {
2301 var selected = 0, self = this, showSearchInput = true;
2303 // find the selected element in the result list
2305 this.findHighlightableChoices().each2(function (i, elm) {
2306 if (equal(self.id(elm.data("select2-data")), self.opts.element.val())) {
2313 if (noHighlightUpdate !== false) {
2314 if (initial === true && selected >= 0) {
2315 this.highlight(selected);
2321 // hide the search box if this is the first we got the results and there are enough of them for search
2323 if (initial === true) {
2324 var min = this.opts.minimumResultsForSearch;
2326 this.showSearch(countResults(data.results) >= min);
2332 showSearch: function(showSearchInput) {
2333 if (this.showSearchInput === showSearchInput) return;
2335 this.showSearchInput = showSearchInput;
2337 this.dropdown.find(".select2-search").toggleClass("select2-search-hidden", !showSearchInput);
2338 this.dropdown.find(".select2-search").toggleClass("select2-offscreen", !showSearchInput);
2339 //add "select2-with-searchbox" to the container if search box is shown
2340 $(this.dropdown, this.container).toggleClass("select2-with-searchbox", showSearchInput);
2344 onSelect: function (data, options) {
2346 if (!this.triggerSelect(data)) { return; }
2348 var old = this.opts.element.val(),
2349 oldData = this.data();
2351 this.opts.element.val(this.id(data));
2352 this.updateSelection(data);
2354 this.opts.element.trigger({ type: "select2-selected", val: this.id(data), choice: data });
2356 this.nextSearchTerm = this.opts.nextSearchTerm(data, this.search.val());
2359 if ((!options || !options.noFocus) && this.opts.shouldFocusInput(this)) {
2360 this.focusser.focus();
2363 if (!equal(old, this.id(data))) {
2364 this.triggerChange({ added: data, removed: oldData });
2369 updateSelection: function (data) {
2371 var container=this.selection.find(".select2-chosen"), formatted, cssClass;
2373 this.selection.data("select2-data", data);
2376 if (data !== null) {
2377 formatted=this.opts.formatSelection(data, container, this.opts.escapeMarkup);
2379 if (formatted !== undefined) {
2380 container.append(formatted);
2382 cssClass=this.opts.formatSelectionCssClass(data, container);
2383 if (cssClass !== undefined) {
2384 container.addClass(cssClass);
2387 this.selection.removeClass("select2-default");
2389 if (this.opts.allowClear && this.getPlaceholder() !== undefined) {
2390 this.container.addClass("select2-allowclear");
2397 triggerChange = false,
2400 oldData = this.data();
2402 if (arguments.length === 0) {
2403 return this.opts.element.val();
2408 if (arguments.length > 1) {
2409 triggerChange = arguments[1];
2415 .find("option").filter(function() { return this.selected }).each2(function (i, elm) {
2416 data = self.optionToData(elm);
2419 this.updateSelection(data);
2420 this.setPlaceholder();
2421 if (triggerChange) {
2422 this.triggerChange({added: data, removed:oldData});
2425 // val is an id. !val is true for [undefined,null,'',0] - 0 is legal
2426 if (!val && val !== 0) {
2427 this.clear(triggerChange);
2430 if (this.opts.initSelection === undefined) {
2431 throw new Error("cannot call val() if initSelection() is not defined");
2433 this.opts.element.val(val);
2434 this.opts.initSelection(this.opts.element, function(data){
2435 self.opts.element.val(!data ? "" : self.id(data));
2436 self.updateSelection(data);
2437 self.setPlaceholder();
2438 if (triggerChange) {
2439 self.triggerChange({added: data, removed:oldData});
2446 clearSearch: function () {
2447 this.search.val("");
2448 this.focusser.val("");
2452 data: function(value) {
2454 triggerChange = false;
2456 if (arguments.length === 0) {
2457 data = this.selection.data("select2-data");
2458 if (data == undefined) data = null;
2461 if (arguments.length > 1) {
2462 triggerChange = arguments[1];
2465 this.clear(triggerChange);
2468 this.opts.element.val(!value ? "" : this.id(value));
2469 this.updateSelection(value);
2470 if (triggerChange) {
2471 this.triggerChange({added: value, removed:data});
2478 MultiSelect2 = clazz(AbstractSelect2, {
2481 createContainer: function () {
2482 var container = $(document.createElement("div")).attr({
2483 "class": "select2-container select2-container-multi"
2485 "<ul class='select2-choices'>",
2486 " <li class='select2-search-field'>",
2487 " <label for='' class='select2-offscreen'></label>",
2488 " <input type='text' autocomplete='off' autocorrect='off' autocapitalize='off' spellcheck='false' class='select2-input'>",
2491 "<div class='select2-drop select2-drop-multi select2-display-none'>",
2492 " <ul class='select2-results'>",
2494 "</div>"].join(""));
2499 prepareOpts: function () {
2500 var opts = this.parent.prepareOpts.apply(this, arguments),
2503 // TODO validate placeholder is a string if specified
2505 if (opts.element.get(0).tagName.toLowerCase() === "select") {
2506 // install the selection initializer
2507 opts.initSelection = function (element, callback) {
2511 element.find("option").filter(function() { return this.selected && !this.disabled }).each2(function (i, elm) {
2512 data.push(self.optionToData(elm));
2516 } else if ("data" in opts) {
2517 // install default initSelection when applied to hidden input and data is local
2518 opts.initSelection = opts.initSelection || function (element, callback) {
2519 var ids = splitVal(element.val(), opts.separator);
2520 //search in data by array of ids, storing matching items in a list
2523 matcher: function(term, text, el){
2524 var is_match = $.grep(ids, function(id) {
2525 return equal(id, opts.id(el));
2532 callback: !$.isFunction(callback) ? $.noop : function() {
2533 // reorder matches based on the order they appear in the ids array because right now
2534 // they are in the order in which they appear in data array
2536 for (var i = 0; i < ids.length; i++) {
2538 for (var j = 0; j < matches.length; j++) {
2539 var match = matches[j];
2540 if (equal(id, opts.id(match))) {
2541 ordered.push(match);
2542 matches.splice(j, 1);
2557 selectChoice: function (choice) {
2559 var selected = this.container.find(".select2-search-choice-focus");
2560 if (selected.length && choice && choice[0] == selected[0]) {
2563 if (selected.length) {
2564 this.opts.element.trigger("choice-deselected", selected);
2566 selected.removeClass("select2-search-choice-focus");
2567 if (choice && choice.length) {
2569 choice.addClass("select2-search-choice-focus");
2570 this.opts.element.trigger("choice-selected", choice);
2576 destroy: function() {
2577 $("label[for='" + this.search.attr('id') + "']")
2578 .attr('for', this.opts.element.attr("id"));
2579 this.parent.destroy.apply(this, arguments);
2583 initContainer: function () {
2585 var selector = ".select2-choices", selection;
2587 this.searchContainer = this.container.find(".select2-search-field");
2588 this.selection = selection = this.container.find(selector);
2591 this.selection.on("click", ".select2-search-choice:not(.select2-locked)", function (e) {
2593 _this.search[0].focus();
2594 _this.selectChoice($(this));
2597 // rewrite labels from original element to focusser
2598 this.search.attr("id", "s2id_autogen"+nextUid());
2601 .text($("label[for='" + this.opts.element.attr("id") + "']").text())
2602 .attr('for', this.search.attr('id'));
2604 this.search.on("input paste", this.bind(function() {
2605 if (!this.isInterfaceEnabled()) return;
2606 if (!this.opened()) {
2611 this.search.attr("tabindex", this.elementTabIndex);
2614 this.search.on("keydown", this.bind(function (e) {
2615 if (!this.isInterfaceEnabled()) return;
2618 var selected = selection.find(".select2-search-choice-focus");
2619 var prev = selected.prev(".select2-search-choice:not(.select2-locked)");
2620 var next = selected.next(".select2-search-choice:not(.select2-locked)");
2621 var pos = getCursorInfo(this.search);
2623 if (selected.length &&
2624 (e.which == KEY.LEFT || e.which == KEY.RIGHT || e.which == KEY.BACKSPACE || e.which == KEY.DELETE || e.which == KEY.ENTER)) {
2625 var selectedChoice = selected;
2626 if (e.which == KEY.LEFT && prev.length) {
2627 selectedChoice = prev;
2629 else if (e.which == KEY.RIGHT) {
2630 selectedChoice = next.length ? next : null;
2632 else if (e.which === KEY.BACKSPACE) {
2633 if (this.unselect(selected.first())) {
2634 this.search.width(10);
2635 selectedChoice = prev.length ? prev : next;
2637 } else if (e.which == KEY.DELETE) {
2638 if (this.unselect(selected.first())) {
2639 this.search.width(10);
2640 selectedChoice = next.length ? next : null;
2642 } else if (e.which == KEY.ENTER) {
2643 selectedChoice = null;
2646 this.selectChoice(selectedChoice);
2648 if (!selectedChoice || !selectedChoice.length) {
2652 } else if (((e.which === KEY.BACKSPACE && this.keydowns == 1)
2653 || e.which == KEY.LEFT) && (pos.offset == 0 && !pos.length)) {
2655 this.selectChoice(selection.find(".select2-search-choice:not(.select2-locked)").last());
2659 this.selectChoice(null);
2662 if (this.opened()) {
2666 this.moveHighlight((e.which === KEY.UP) ? -1 : 1);
2670 this.selectHighlighted();
2674 this.selectHighlighted({noFocus:true});
2684 if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e)
2685 || e.which === KEY.BACKSPACE || e.which === KEY.ESC) {
2689 if (e.which === KEY.ENTER) {
2690 if (this.opts.openOnEnter === false) {
2692 } else if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) {
2699 if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) {
2700 // prevent the page from scrolling
2704 if (e.which === KEY.ENTER) {
2705 // prevent form from being submitted
2711 this.search.on("keyup", this.bind(function (e) {
2713 this.resizeSearch();
2717 this.search.on("blur", this.bind(function(e) {
2718 this.container.removeClass("select2-container-active");
2719 this.search.removeClass("select2-focused");
2720 this.selectChoice(null);
2721 if (!this.opened()) this.clearSearch();
2722 e.stopImmediatePropagation();
2723 this.opts.element.trigger($.Event("select2-blur"));
2726 this.container.on("click", selector, this.bind(function (e) {
2727 if (!this.isInterfaceEnabled()) return;
2728 if ($(e.target).closest(".select2-search-choice").length > 0) {
2729 // clicked inside a select2 search choice, do not open
2732 this.selectChoice(null);
2733 this.clearPlaceholder();
2734 if (!this.container.hasClass("select2-container-active")) {
2735 this.opts.element.trigger($.Event("select2-focus"));
2742 this.container.on("focus", selector, this.bind(function () {
2743 if (!this.isInterfaceEnabled()) return;
2744 if (!this.container.hasClass("select2-container-active")) {
2745 this.opts.element.trigger($.Event("select2-focus"));
2747 this.container.addClass("select2-container-active");
2748 this.dropdown.addClass("select2-drop-active");
2749 this.clearPlaceholder();
2752 this.initContainerWidth();
2753 this.opts.element.addClass("select2-offscreen");
2755 // set the placeholder if necessary
2760 enableInterface: function() {
2761 if (this.parent.enableInterface.apply(this, arguments)) {
2762 this.search.prop("disabled", !this.isInterfaceEnabled());
2767 initSelection: function () {
2769 if (this.opts.element.val() === "" && this.opts.element.text() === "") {
2770 this.updateSelection([]);
2772 // set the placeholder if necessary
2775 if (this.select || this.opts.element.val() !== "") {
2777 this.opts.initSelection.call(null, this.opts.element, function(data){
2778 if (data !== undefined && data !== null) {
2779 self.updateSelection(data);
2781 // set the placeholder if necessary
2789 clearSearch: function () {
2790 var placeholder = this.getPlaceholder(),
2791 maxWidth = this.getMaxSearchWidth();
2793 if (placeholder !== undefined && this.getVal().length === 0 && this.search.hasClass("select2-focused") === false) {
2794 this.search.val(placeholder).addClass("select2-default");
2795 // stretch the search box to full width of the container so as much of the placeholder is visible as possible
2796 // 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
2797 this.search.width(maxWidth > 0 ? maxWidth : this.container.css("width"));
2799 this.search.val("").width(10);
2804 clearPlaceholder: function () {
2805 if (this.search.hasClass("select2-default")) {
2806 this.search.val("").removeClass("select2-default");
2811 opening: function () {
2812 this.clearPlaceholder(); // should be done before super so placeholder is not used to search
2813 this.resizeSearch();
2815 this.parent.opening.apply(this, arguments);
2819 // initializes search's value with nextSearchTerm (if defined by user)
2820 // ignore nextSearchTerm if the dropdown is opened by the user pressing a letter
2821 if(this.search.val() === "") {
2822 if(this.nextSearchTerm != undefined){
2823 this.search.val(this.nextSearchTerm);
2824 this.search.select();
2828 this.updateResults(true);
2829 this.search.focus();
2830 this.opts.element.trigger($.Event("select2-open"));
2834 close: function () {
2835 if (!this.opened()) return;
2836 this.parent.close.apply(this, arguments);
2840 focus: function () {
2842 this.search.focus();
2846 isFocused: function () {
2847 return this.search.hasClass("select2-focused");
2851 updateSelection: function (data) {
2852 var ids = [], filtered = [], self = this;
2854 // filter out duplicates
2855 $(data).each(function () {
2856 if (indexOf(self.id(this), ids) < 0) {
2857 ids.push(self.id(this));
2858 filtered.push(this);
2863 this.selection.find(".select2-search-choice").remove();
2864 $(data).each(function () {
2865 self.addSelectedChoice(this);
2867 self.postprocessResults();
2871 tokenize: function() {
2872 var input = this.search.val();
2873 input = this.opts.tokenizer.call(this, input, this.data(), this.bind(this.onSelect), this.opts);
2874 if (input != null && input != undefined) {
2875 this.search.val(input);
2876 if (input.length > 0) {
2884 onSelect: function (data, options) {
2886 if (!this.triggerSelect(data)) { return; }
2888 this.addSelectedChoice(data);
2890 this.opts.element.trigger({ type: "selected", val: this.id(data), choice: data });
2892 // keep track of the search's value before it gets cleared
2893 this.nextSearchTerm = this.opts.nextSearchTerm(data, this.search.val());
2896 this.updateResults();
2898 if (this.select || !this.opts.closeOnSelect) this.postprocessResults(data, false, this.opts.closeOnSelect===true);
2900 if (this.opts.closeOnSelect) {
2902 this.search.width(10);
2904 if (this.countSelectableResults()>0) {
2905 this.search.width(10);
2906 this.resizeSearch();
2907 if (this.getMaximumSelectionSize() > 0 && this.val().length >= this.getMaximumSelectionSize()) {
2908 // if we reached max selection size repaint the results so choices
2909 // are replaced with the max selection reached message
2910 this.updateResults(true);
2912 // initializes search's value with nextSearchTerm and update search result
2913 if(this.nextSearchTerm != undefined){
2914 this.search.val(this.nextSearchTerm);
2915 this.updateResults();
2916 this.search.select();
2919 this.positionDropdown();
2921 // if nothing left to select close
2923 this.search.width(10);
2927 // since its not possible to select an element that has already been
2928 // added we do not need to check if this is a new element before firing change
2929 this.triggerChange({ added: data });
2931 if (!options || !options.noFocus)
2936 cancel: function () {
2941 addSelectedChoice: function (data) {
2942 var enableChoice = !data.locked,
2944 "<li class='select2-search-choice'>" +
2946 " <a href='#' class='select2-search-choice-close' tabindex='-1'></a>" +
2949 "<li class='select2-search-choice select2-locked'>" +
2952 var choice = enableChoice ? enabledItem : disabledItem,
2954 val = this.getVal(),
2958 formatted=this.opts.formatSelection(data, choice.find("div"), this.opts.escapeMarkup);
2959 if (formatted != undefined) {
2960 choice.find("div").replaceWith("<div>"+formatted+"</div>");
2962 cssClass=this.opts.formatSelectionCssClass(data, choice.find("div"));
2963 if (cssClass != undefined) {
2964 choice.addClass(cssClass);
2968 choice.find(".select2-search-choice-close")
2969 .on("mousedown", killEvent)
2970 .on("click dblclick", this.bind(function (e) {
2971 if (!this.isInterfaceEnabled()) return;
2973 this.unselect($(e.target));
2974 this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
2978 })).on("focus", this.bind(function () {
2979 if (!this.isInterfaceEnabled()) return;
2980 this.container.addClass("select2-container-active");
2981 this.dropdown.addClass("select2-drop-active");
2985 choice.data("select2-data", data);
2986 choice.insertBefore(this.searchContainer);
2993 unselect: function (selected) {
2994 var val = this.getVal(),
2997 selected = selected.closest(".select2-search-choice");
2999 if (selected.length === 0) {
3000 throw "Invalid argument: " + selected + ". Must be .select2-search-choice";
3003 data = selected.data("select2-data");
3006 // prevent a race condition when the 'x' is clicked really fast repeatedly the event can be queued
3007 // and invoked on an element already removed
3011 var evt = $.Event("select2-removing");
3012 evt.val = this.id(data);
3014 this.opts.element.trigger(evt);
3016 if (evt.isDefaultPrevented()) {
3020 while((index = indexOf(this.id(data), val)) >= 0) {
3021 val.splice(index, 1);
3023 if (this.select) this.postprocessResults();
3028 this.opts.element.trigger({ type: "select2-removed", val: this.id(data), choice: data });
3029 this.triggerChange({ removed: data });
3035 postprocessResults: function (data, initial, noHighlightUpdate) {
3036 var val = this.getVal(),
3037 choices = this.results.find(".select2-result"),
3038 compound = this.results.find(".select2-result-with-children"),
3041 choices.each2(function (i, choice) {
3042 var id = self.id(choice.data("select2-data"));
3043 if (indexOf(id, val) >= 0) {
3044 choice.addClass("select2-selected");
3045 // mark all children of the selected parent as selected
3046 choice.find(".select2-result-selectable").addClass("select2-selected");
3050 compound.each2(function(i, choice) {
3051 // hide an optgroup if it doesn't have any selectable children
3052 if (!choice.is('.select2-result-selectable')
3053 && choice.find(".select2-result-selectable:not(.select2-selected)").length === 0) {
3054 choice.addClass("select2-selected");
3058 if (this.highlight() == -1 && noHighlightUpdate !== false){
3062 //If all results are chosen render formatNoMatches
3063 if(!this.opts.createSearchChoice && !choices.filter('.select2-result:not(.select2-selected)').length > 0){
3064 if(!data || data && !data.more && this.results.find(".select2-no-results").length === 0) {
3065 if (checkFormatter(self.opts.formatNoMatches, "formatNoMatches")) {
3066 this.results.append("<li class='select2-no-results'>" + evaluate(self.opts.formatNoMatches, self.search.val()) + "</li>");
3074 getMaxSearchWidth: function() {
3075 return this.selection.width() - getSideBorderPadding(this.search);
3079 resizeSearch: function () {
3080 var minimumWidth, left, maxWidth, containerLeft, searchWidth,
3081 sideBorderPadding = getSideBorderPadding(this.search);
3083 minimumWidth = measureTextWidth(this.search) + 10;
3085 left = this.search.offset().left;
3087 maxWidth = this.selection.width();
3088 containerLeft = this.selection.offset().left;
3090 searchWidth = maxWidth - (left - containerLeft) - sideBorderPadding;
3092 if (searchWidth < minimumWidth) {
3093 searchWidth = maxWidth - sideBorderPadding;
3096 if (searchWidth < 40) {
3097 searchWidth = maxWidth - sideBorderPadding;
3100 if (searchWidth <= 0) {
3101 searchWidth = minimumWidth;
3104 this.search.width(Math.floor(searchWidth));
3108 getVal: function () {
3111 val = this.select.val();
3112 return val === null ? [] : val;
3114 val = this.opts.element.val();
3115 return splitVal(val, this.opts.separator);
3120 setVal: function (val) {
3123 this.select.val(val);
3126 // filter out duplicates
3127 $(val).each(function () {
3128 if (indexOf(this, unique) < 0) unique.push(this);
3130 this.opts.element.val(unique.length === 0 ? "" : unique.join(this.opts.separator));
3135 buildChangeDetails: function (old, current) {
3136 var current = current.slice(0),
3139 // remove intersection from each array
3140 for (var i = 0; i < current.length; i++) {
3141 for (var j = 0; j < old.length; j++) {
3142 if (equal(this.opts.id(current[i]), this.opts.id(old[j]))) {
3143 current.splice(i, 1);
3153 return {added: current, removed: old};
3158 val: function (val, triggerChange) {
3159 var oldData, self=this;
3161 if (arguments.length === 0) {
3162 return this.getVal();
3165 oldData=this.data();
3166 if (!oldData.length) oldData=[];
3168 // val is an id. !val is true for [undefined,null,'',0] - 0 is legal
3169 if (!val && val !== 0) {
3170 this.opts.element.val("");
3171 this.updateSelection([]);
3173 if (triggerChange) {
3174 this.triggerChange({added: this.data(), removed: oldData});
3179 // val is a list of ids
3183 this.opts.initSelection(this.select, this.bind(this.updateSelection));
3184 if (triggerChange) {
3185 this.triggerChange(this.buildChangeDetails(oldData, this.data()));
3188 if (this.opts.initSelection === undefined) {
3189 throw new Error("val() cannot be called if initSelection() is not defined");
3192 this.opts.initSelection(this.opts.element, function(data){
3193 var ids=$.map(data, self.id);
3195 self.updateSelection(data);
3197 if (triggerChange) {
3198 self.triggerChange(self.buildChangeDetails(oldData, self.data()));
3206 onSortStart: function() {
3208 throw new Error("Sorting of elements is not supported when attached to <select>. Attach to <input type='hidden'/> instead.");
3211 // collapse search field into 0 width so its container can be collapsed as well
3212 this.search.width(0);
3213 // hide the container
3214 this.searchContainer.hide();
3218 onSortEnd:function() {
3220 var val=[], self=this;
3222 // show search and move it to the end of the list
3223 this.searchContainer.show();
3224 // make sure the search container is the last item in the list
3225 this.searchContainer.appendTo(this.searchContainer.parent());
3226 // since we collapsed the width in dragStarted, we resize it here
3227 this.resizeSearch();
3230 this.selection.find(".select2-search-choice").each(function() {
3231 val.push(self.opts.id($(this).data("select2-data")));
3234 this.triggerChange();
3238 data: function(values, triggerChange) {
3239 var self=this, ids, old;
3240 if (arguments.length === 0) {
3241 return this.selection
3242 .children(".select2-search-choice")
3243 .map(function() { return $(this).data("select2-data"); })
3247 if (!values) { values = []; }
3248 ids = $.map(values, function(e) { return self.opts.id(e); });
3250 this.updateSelection(values);
3252 if (triggerChange) {
3253 this.triggerChange(this.buildChangeDetails(old, this.data()));
3259 $.fn.select2 = function () {
3261 var args = Array.prototype.slice.call(arguments, 0),
3264 method, value, multiple,
3265 allowedMethods = ["val", "destroy", "opened", "open", "close", "focus", "isFocused", "container", "dropdown", "onSortStart", "onSortEnd", "enable", "disable", "readonly", "positionDropdown", "data", "search", "updateResults"],
3266 valueMethods = ["opened", "isFocused", "container", "dropdown"],
3267 propertyMethods = ["val", "data"],
3268 methodsMap = { search: "externalSearch" };
3270 this.each(function () {
3271 if (args.length === 0 || typeof(args[0]) === "object") {
3272 opts = args.length === 0 ? {} : $.extend({}, args[0]);
3273 opts.element = $(this);
3275 if (opts.element.get(0).tagName.toLowerCase() === "select") {
3276 multiple = opts.element.prop("multiple");
3278 multiple = opts.multiple || false;
3279 if ("tags" in opts) {opts.multiple = multiple = true;}
3282 select2 = multiple ? new window.Select2["class"].multi() : new window.Select2["class"].single();
3284 } else if (typeof(args[0]) === "string") {
3286 if (indexOf(args[0], allowedMethods) < 0) {
3287 throw "Unknown method: " + args[0];
3291 select2 = $(this).data("select2");
3292 if (select2 === undefined) return;
3296 if (method === "container") {
3297 value = select2.container;
3298 } else if (method === "dropdown") {
3299 value = select2.dropdown;
3301 if (methodsMap[method]) method = methodsMap[method];
3303 value = select2[method].apply(select2, args.slice(1));
3305 if (indexOf(args[0], valueMethods) >= 0
3306 || (indexOf(args[0], propertyMethods) && args.length == 1)) {
3307 return false; // abort the iteration, ready to return first matched value
3310 throw "Invalid arguments to select2 plugin: " + args;
3313 return (value === undefined) ? this : value;
3316 // plugin defaults, accessible to users
3317 $.fn.select2.defaults = {
3320 closeOnSelect: true,
3324 containerCssClass: "",
3325 dropdownCssClass: "",
3326 formatResult: function(result, container, query, escapeMarkup) {
3328 markMatch(result.text, query.term, markup, escapeMarkup);
3329 return markup.join("");
3331 formatSelection: function (data, container, escapeMarkup) {
3332 return data ? escapeMarkup(data.text) : undefined;
3334 sortResults: function (results, container, query) {
3337 formatResultCssClass: function(data) {return data.css;},
3338 formatSelectionCssClass: function(data, container) {return undefined;},
3339 formatMatches: function (matches) { return matches + " results are available, use up and down arrow keys to navigate."; },
3340 formatNoMatches: function () { return "No matches found"; },
3341 formatInputTooShort: function (input, min) { var n = min - input.length; return "Please enter " + n + " or more character" + (n == 1? "" : "s"); },
3342 formatInputTooLong: function (input, max) { var n = input.length - max; return "Please delete " + n + " character" + (n == 1? "" : "s"); },
3343 formatSelectionTooBig: function (limit) { return "You can only select " + limit + " item" + (limit == 1 ? "" : "s"); },
3344 formatLoadMore: function (pageNumber) { return "Loading more results…"; },
3345 formatSearching: function () { return "Searching…"; },
3346 minimumResultsForSearch: 0,
3347 minimumInputLength: 0,
3348 maximumInputLength: null,
3349 maximumSelectionSize: 0,
3350 id: function (e) { return e == undefined ? null : e.id; },
3351 matcher: function(term, text) {
3352 return stripDiacritics(''+text).toUpperCase().indexOf(stripDiacritics(''+term).toUpperCase()) >= 0;
3355 tokenSeparators: [],
3356 tokenizer: defaultTokenizer,
3357 escapeMarkup: defaultEscapeMarkup,
3358 blurOnChange: false,
3359 selectOnBlur: false,
3360 adaptContainerCssClass: function(c) { return c; },
3361 adaptDropdownCssClass: function(c) { return null; },
3362 nextSearchTerm: function(selectedObject, currentSearchTerm) { return undefined; },
3363 searchInputPlaceholder: '',
3364 createSearchChoicePosition: 'top',
3365 shouldFocusInput: function (instance) {
3366 // Never focus the input if search is disabled
3367 if (instance.opts.minimumResultsForSearch < 0) {
3375 $.fn.select2.ajaxDefaults = {
3392 markMatch: markMatch,
3393 escapeMarkup: defaultEscapeMarkup,
3394 stripDiacritics: stripDiacritics
3396 "abstract": AbstractSelect2,
3397 "single": SingleSelect2,
3398 "multi": MultiSelect2