2 Copyright 2012 Igor Vaynberg
4 Version: 3.5.1 Timestamp: Tue Jul 22 18:58:56 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","\u0386":"\u0391","\u0388":"\u0395","\u0389":"\u0397","\u038A":"\u0399","\u03AA":"\u0399","\u038C":"\u039F","\u038E":"\u03A5","\u03AB":"\u03A5","\u038F":"\u03A9","\u03AC":"\u03B1","\u03AD":"\u03B5","\u03AE":"\u03B7","\u03AF":"\u03B9","\u03CA":"\u03B9","\u0390":"\u03B9","\u03CC":"\u03BF","\u03CD":"\u03C5","\u03CB":"\u03C5","\u03B0":"\u03C5","\u03C9":"\u03C9","\u03C2":"\u03C3"};
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) {
117 // Used 'uni range + named function' from http://jsperf.com/diacritics/18
119 return DIACRITICS[a] || a;
122 return str.replace(/[^\u0000-\u007E]/g, match);
125 function indexOf(value, array) {
126 var i = 0, l = array.length;
127 for (; i < l; i = i + 1) {
128 if (equal(value, array[i])) return i;
133 function measureScrollbar () {
134 var $template = $( MEASURE_SCROLLBAR_TEMPLATE );
135 $template.appendTo('body');
138 width: $template.width() - $template[0].clientWidth,
139 height: $template.height() - $template[0].clientHeight
147 * Compares equality of a and b
151 function equal(a, b) {
152 if (a === b) return true;
153 if (a === undefined || b === undefined) return false;
154 if (a === null || b === null) return false;
155 // Check whether 'a' or 'b' is a string (primitive or object).
156 // The concatenation of an empty string (+'') converts its argument to a string's primitive.
157 if (a.constructor === String) return a+'' === b+''; // a+'' - in case 'a' is a String object
158 if (b.constructor === String) return b+'' === a+''; // b+'' - in case 'b' is a String object
163 * Splits the string into an array of values, trimming each value. An empty array is returned for nulls or empty
168 function splitVal(string, separator) {
170 if (string === null || string.length < 1) return [];
171 val = string.split(separator);
172 for (i = 0, l = val.length; i < l; i = i + 1) val[i] = $.trim(val[i]);
176 function getSideBorderPadding(element) {
177 return element.outerWidth(false) - element.width();
180 function installKeyUpChangeEvent(element) {
181 var key="keyup-change-value";
182 element.on("keydown", function () {
183 if ($.data(element, key) === undefined) {
184 $.data(element, key, element.val());
187 element.on("keyup", function () {
188 var val= $.data(element, key);
189 if (val !== undefined && element.val() !== val) {
190 $.removeData(element, key);
191 element.trigger("keyup-change");
198 * filters mouse events so an event is fired only if the mouse moved.
200 * filters out mouse events that occur when mouse is stationary but
201 * the elements under the pointer are scrolled.
203 function installFilteredMouseMove(element) {
204 element.on("mousemove", function (e) {
205 var lastpos = lastMousePosition;
206 if (lastpos === undefined || lastpos.x !== e.pageX || lastpos.y !== e.pageY) {
207 $(e.target).trigger("mousemove-filtered", e);
213 * Debounces a function. Returns a function that calls the original fn function only if no invocations have been made
214 * within the last quietMillis milliseconds.
216 * @param quietMillis number of milliseconds to wait before invoking fn
217 * @param fn function to be debounced
218 * @param ctx object to be used as this reference within fn
219 * @return debounced version of fn
221 function debounce(quietMillis, fn, ctx) {
222 ctx = ctx || undefined;
225 var args = arguments;
226 window.clearTimeout(timeout);
227 timeout = window.setTimeout(function() {
233 function installDebouncedScroll(threshold, element) {
234 var notify = debounce(threshold, function (e) { element.trigger("scroll-debounced", e);});
235 element.on("scroll", function (e) {
236 if (indexOf(e.target, element.get()) >= 0) notify(e);
240 function focus($el) {
241 if ($el[0] === document.activeElement) return;
243 /* set the focus in a 0 timeout - that way the focus is set after the processing
244 of the current event has finished - which seems like the only reliable way
246 window.setTimeout(function() {
247 var el=$el[0], pos=$el.val().length, range;
251 /* make sure el received focus so we do not error out when trying to manipulate the caret.
252 sometimes modals or others listeners may steal it after its set */
253 var isVisible = (el.offsetWidth > 0 || el.offsetHeight > 0);
254 if (isVisible && el === document.activeElement) {
256 /* after the focus is set move the caret to the end, necessary when we val()
257 just before setting focus */
258 if(el.setSelectionRange)
260 el.setSelectionRange(pos, pos);
262 else if (el.createTextRange) {
263 range = el.createTextRange();
264 range.collapse(false);
271 function getCursorInfo(el) {
275 if ('selectionStart' in el) {
276 offset = el.selectionStart;
277 length = el.selectionEnd - offset;
278 } else if ('selection' in document) {
280 var sel = document.selection.createRange();
281 length = document.selection.createRange().text.length;
282 sel.moveStart('character', -el.value.length);
283 offset = sel.text.length - length;
285 return { offset: offset, length: length };
288 function killEvent(event) {
289 event.preventDefault();
290 event.stopPropagation();
292 function killEventImmediately(event) {
293 event.preventDefault();
294 event.stopImmediatePropagation();
297 function measureTextWidth(e) {
299 var style = e[0].currentStyle || window.getComputedStyle(e[0], null);
300 sizer = $(document.createElement("div")).css({
301 position: "absolute",
305 fontSize: style.fontSize,
306 fontFamily: style.fontFamily,
307 fontStyle: style.fontStyle,
308 fontWeight: style.fontWeight,
309 letterSpacing: style.letterSpacing,
310 textTransform: style.textTransform,
313 sizer.attr("class","select2-sizer");
314 $("body").append(sizer);
317 return sizer.width();
320 function syncCssClasses(dest, src, adapter) {
321 var classes, replacements = [], adapted;
323 classes = $.trim(dest.attr("class"));
326 classes = '' + classes; // for IE which returns object
328 $(classes.split(/\s+/)).each2(function() {
329 if (this.indexOf("select2-") === 0) {
330 replacements.push(this);
335 classes = $.trim(src.attr("class"));
338 classes = '' + classes; // for IE which returns object
340 $(classes.split(/\s+/)).each2(function() {
341 if (this.indexOf("select2-") !== 0) {
342 adapted = adapter(this);
345 replacements.push(adapted);
351 dest.attr("class", replacements.join(" "));
355 function markMatch(text, term, markup, escapeMarkup) {
356 var match=stripDiacritics(text.toUpperCase()).indexOf(stripDiacritics(term.toUpperCase())),
360 markup.push(escapeMarkup(text));
364 markup.push(escapeMarkup(text.substring(0, match)));
365 markup.push("<span class='select2-match'>");
366 markup.push(escapeMarkup(text.substring(match, match + tl)));
367 markup.push("</span>");
368 markup.push(escapeMarkup(text.substring(match + tl, text.length)));
371 function defaultEscapeMarkup(markup) {
382 return String(markup).replace(/[&<>"'\/\\]/g, function (match) {
383 return replace_map[match];
388 * Produces an ajax-based query function
390 * @param options object containing configuration parameters
391 * @param options.params parameter map for the transport ajax call, can contain such options as cache, jsonpCallback, etc. see $.ajax
392 * @param options.transport function that will be used to execute the ajax request. must be compatible with parameters supported by $.ajax
393 * @param options.url url for the data
394 * @param options.data a function(searchTerm, pageNumber, context) that should return an object containing query string parameters for the above url.
395 * @param options.dataType request data type: ajax, jsonp, other datatypes supported by jQuery's $.ajax function or the transport function if specified
396 * @param options.quietMillis (optional) milliseconds to wait before making the ajaxRequest, helps debounce the ajax function if invoked too often
397 * @param options.results a function(remoteData, pageNumber, query) that converts data returned form the remote request to the format expected by Select2.
398 * The expected format is an object containing the following keys:
399 * results array of objects that will be used as choices
400 * more (optional) boolean indicating whether there are more results available
401 * Example: {results:[{id:1, text:'Red'},{id:2, text:'Blue'}], more:true}
403 function ajax(options) {
404 var timeout, // current scheduled but not yet executed request
406 quietMillis = options.quietMillis || 100,
407 ajaxUrl = options.url,
410 return function (query) {
411 window.clearTimeout(timeout);
412 timeout = window.setTimeout(function () {
413 var data = options.data, // ajax data function
414 url = ajaxUrl, // ajax url string or function
415 transport = options.transport || $.fn.select2.ajaxDefaults.transport,
416 // deprecated - to be removed in 4.0 - use params instead
418 type: options.type || 'GET', // set type of request (GET or POST)
419 cache: options.cache || false,
420 jsonpCallback: options.jsonpCallback||undefined,
421 dataType: options.dataType||"json"
423 params = $.extend({}, $.fn.select2.ajaxDefaults.params, deprecated);
425 data = data ? data.call(self, query.term, query.page, query.context) : null;
426 url = (typeof url === 'function') ? url.call(self, query.term, query.page, query.context) : url;
428 if (handler && typeof handler.abort === "function") { handler.abort(); }
430 if (options.params) {
431 if ($.isFunction(options.params)) {
432 $.extend(params, options.params.call(self));
434 $.extend(params, options.params);
440 dataType: options.dataType,
442 success: function (data) {
443 // TODO - replace query.page with query so users have access to term, page, etc.
444 // added query as third paramter to keep backwards compatibility
445 var results = options.results(data, query.page, query);
446 query.callback(results);
448 error: function(jqXHR, textStatus, errorThrown){
452 textStatus: textStatus,
453 errorThrown: errorThrown,
456 query.callback(results);
459 handler = transport.call(self, params);
465 * Produces a query function that works with a local array
467 * @param options object containing configuration parameters. The options parameter can either be an array or an
470 * If the array form is used it is assumed that it contains objects with 'id' and 'text' keys.
472 * If the object form is used it is assumed that it contains 'data' and 'text' keys. The 'data' key should contain
473 * an array of objects that will be used as choices. These objects must contain at least an 'id' key. The 'text'
474 * key can either be a String in which case it is expected that each element in the 'data' array has a key with the
475 * value of 'text' which will be used to match choices. Alternatively, text can be a function(item) that can extract
478 function local(options) {
479 var data = options, // data elements
482 text = function (item) { return ""+item.text; }; // function used to retrieve the text portion of a data item that is matched against the search
484 if ($.isArray(data)) {
486 data = { results: tmp };
489 if ($.isFunction(data) === false) {
491 data = function() { return tmp; };
494 var dataItem = data();
496 text = dataItem.text;
497 // if text is not a function we assume it to be a key name
498 if (!$.isFunction(text)) {
499 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
500 text = function (item) { return item[dataText]; };
504 return function (query) {
505 var t = query.term, filtered = { results: [] }, process;
507 query.callback(data());
511 process = function(datum, collection) {
514 if (datum.children) {
516 for (attr in datum) {
517 if (datum.hasOwnProperty(attr)) group[attr]=datum[attr];
520 $(datum.children).each2(function(i, childDatum) { process(childDatum, group.children); });
521 if (group.children.length || query.matcher(t, text(group), datum)) {
522 collection.push(group);
525 if (query.matcher(t, text(datum), datum)) {
526 collection.push(datum);
531 $(data().results).each2(function(i, datum) { process(datum, filtered.results); });
532 query.callback(filtered);
537 function tags(data) {
538 var isFunc = $.isFunction(data);
539 return function (query) {
540 var t = query.term, filtered = {results: []};
541 var result = isFunc ? data(query) : data;
542 if ($.isArray(result)) {
543 $(result).each(function () {
544 var isObject = this.text !== undefined,
545 text = isObject ? this.text : this;
546 if (t === "" || query.matcher(t, text)) {
547 filtered.results.push(isObject ? this : {id: this, text: this});
550 query.callback(filtered);
556 * Checks if the formatter function should be used.
558 * Throws an error if it is not a function. Returns true if it should be used,
559 * false if no formatting should be performed.
563 function checkFormatter(formatter, formatterName) {
564 if ($.isFunction(formatter)) return true;
565 if (!formatter) return false;
566 if (typeof(formatter) === 'string') return true;
567 throw new Error(formatterName +" must be a string, function, or falsy value");
571 * Returns a given value
572 * If given a function, returns its output
574 * @param val string|function
575 * @param context value of "this" to be passed to function
578 function evaluate(val, context) {
579 if ($.isFunction(val)) {
580 var args = Array.prototype.slice.call(arguments, 2);
581 return val.apply(context, args);
586 function countResults(results) {
588 $.each(results, function(i, item) {
590 count += countResults(item.children);
599 * Default tokenizer. This function uses breaks the input on substring match of any string from the
600 * opts.tokenSeparators array and uses opts.createSearchChoice to create the choice object. Both of those
601 * two options have to be defined in order for the tokenizer to work.
603 * @param input text user has typed so far or pasted into the search field
604 * @param selection currently selected choices
605 * @param selectCallback function(choice) callback tho add the choice to selection
606 * @param opts select2's opts
607 * @return undefined/null to leave the current input unchanged, or a string to change the input to the returned value
609 function defaultTokenizer(input, selection, selectCallback, opts) {
610 var original = input, // store the original so we can compare and know if we need to tell the search to update its text
611 dupe = false, // check for whether a token we extracted represents a duplicate selected choice
613 index, // position at which the separator was found
614 i, l, // looping variables
615 separator; // the matched separator
617 if (!opts.createSearchChoice || !opts.tokenSeparators || opts.tokenSeparators.length < 1) return undefined;
622 for (i = 0, l = opts.tokenSeparators.length; i < l; i++) {
623 separator = opts.tokenSeparators[i];
624 index = input.indexOf(separator);
625 if (index >= 0) break;
628 if (index < 0) break; // did not find any token separator in the input string, bail
630 token = input.substring(0, index);
631 input = input.substring(index + separator.length);
633 if (token.length > 0) {
634 token = opts.createSearchChoice.call(this, token, selection);
635 if (token !== undefined && token !== null && opts.id(token) !== undefined && opts.id(token) !== null) {
637 for (i = 0, l = selection.length; i < l; i++) {
638 if (equal(opts.id(token), opts.id(selection[i]))) {
643 if (!dupe) selectCallback(token);
648 if (original!==input) return input;
651 function cleanupJQueryElements() {
654 $.each(arguments, function (i, element) {
655 self[element].remove();
656 self[element] = null;
661 * Creates a new class
666 function clazz(SuperClass, methods) {
667 var constructor = function () {};
668 constructor.prototype = new SuperClass;
669 constructor.prototype.constructor = constructor;
670 constructor.prototype.parent = SuperClass.prototype;
671 constructor.prototype = $.extend(constructor.prototype, methods);
675 AbstractSelect2 = clazz(Object, {
678 bind: function (func) {
681 func.apply(self, arguments);
686 init: function (opts) {
687 var results, search, resultsSelector = ".select2-results";
690 this.opts = opts = this.prepareOpts(opts);
694 // destroy if called on an existing component
695 if (opts.element.data("select2") !== undefined &&
696 opts.element.data("select2") !== null) {
697 opts.element.data("select2").destroy();
700 this.container = this.createContainer();
702 this.liveRegion = $("<span>", {
704 "aria-live": "polite"
706 .addClass("select2-hidden-accessible")
707 .appendTo(document.body);
709 this.containerId="s2id_"+(opts.element.attr("id") || "autogen"+nextUid());
710 this.containerEventName= this.containerId
711 .replace(/([.])/g, '_')
712 .replace(/([;&,\-\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '\\$1');
713 this.container.attr("id", this.containerId);
715 this.container.attr("title", opts.element.attr("title"));
717 this.body = $("body");
719 syncCssClasses(this.container, this.opts.element, this.opts.adaptContainerCssClass);
721 this.container.attr("style", opts.element.attr("style"));
722 this.container.css(evaluate(opts.containerCss, this.opts.element));
723 this.container.addClass(evaluate(opts.containerCssClass, this.opts.element));
725 this.elementTabIndex = this.opts.element.attr("tabindex");
727 // swap container for the element
729 .data("select2", this)
730 .attr("tabindex", "-1")
731 .before(this.container)
732 .on("click.select2", killEvent); // do not leak click events
734 this.container.data("select2", this);
736 this.dropdown = this.container.find(".select2-drop");
738 syncCssClasses(this.dropdown, this.opts.element, this.opts.adaptDropdownCssClass);
740 this.dropdown.addClass(evaluate(opts.dropdownCssClass, this.opts.element));
741 this.dropdown.data("select2", this);
742 this.dropdown.on("click", killEvent);
744 this.results = results = this.container.find(resultsSelector);
745 this.search = search = this.container.find("input.select2-input");
748 this.resultsPage = 0;
751 // initialize the container
752 this.initContainer();
754 this.container.on("click", killEvent);
756 installFilteredMouseMove(this.results);
758 this.dropdown.on("mousemove-filtered", resultsSelector, this.bind(this.highlightUnderEvent));
759 this.dropdown.on("touchstart touchmove touchend", resultsSelector, this.bind(function (event) {
760 this._touchEvent = true;
761 this.highlightUnderEvent(event);
763 this.dropdown.on("touchmove", resultsSelector, this.bind(this.touchMoved));
764 this.dropdown.on("touchstart touchend", resultsSelector, this.bind(this.clearTouchMoved));
766 // Waiting for a click event on touch devices to select option and hide dropdown
767 // otherwise click will be triggered on an underlying element
768 this.dropdown.on('click', this.bind(function (event) {
769 if (this._touchEvent) {
770 this._touchEvent = false;
771 this.selectHighlighted();
775 installDebouncedScroll(80, this.results);
776 this.dropdown.on("scroll-debounced", resultsSelector, this.bind(this.loadMoreIfNeeded));
778 // do not propagate change event from the search field out of the component
779 $(this.container).on("change", ".select2-input", function(e) {e.stopPropagation();});
780 $(this.dropdown).on("change", ".select2-input", function(e) {e.stopPropagation();});
782 // if jquery.mousewheel plugin is installed we can prevent out-of-bounds scrolling of results via mousewheel
783 if ($.fn.mousewheel) {
784 results.mousewheel(function (e, delta, deltaX, deltaY) {
785 var top = results.scrollTop();
786 if (deltaY > 0 && top - deltaY <= 0) {
787 results.scrollTop(0);
789 } else if (deltaY < 0 && results.get(0).scrollHeight - results.scrollTop() + deltaY <= results.height()) {
790 results.scrollTop(results.get(0).scrollHeight - results.height());
796 installKeyUpChangeEvent(search);
797 search.on("keyup-change input paste", this.bind(this.updateResults));
798 search.on("focus", function () { search.addClass("select2-focused"); });
799 search.on("blur", function () { search.removeClass("select2-focused");});
801 this.dropdown.on("mouseup", resultsSelector, this.bind(function (e) {
802 if ($(e.target).closest(".select2-result-selectable").length > 0) {
803 this.highlightUnderEvent(e);
804 this.selectHighlighted(e);
808 // trap all mouse events from leaving the dropdown. sometimes there may be a modal that is listening
809 // for mouse events outside of itself so it can close itself. since the dropdown is now outside the select2's
810 // dom it will trigger the popup close, which is not what we want
811 // focusin can cause focus wars between modals and select2 since the dropdown is outside the modal.
812 this.dropdown.on("click mouseup mousedown touchstart touchend focusin", function (e) { e.stopPropagation(); });
814 this.nextSearchTerm = undefined;
816 if ($.isFunction(this.opts.initSelection)) {
817 // initialize selection based on the current value of the source element
818 this.initSelection();
820 // if the user has provided a function that can set selection based on the value of the source element
821 // we monitor the change event on the element and trigger it, allowing for two way synchronization
822 this.monitorSource();
825 if (opts.maximumInputLength !== null) {
826 this.search.attr("maxlength", opts.maximumInputLength);
829 var disabled = opts.element.prop("disabled");
830 if (disabled === undefined) disabled = false;
831 this.enable(!disabled);
833 var readonly = opts.element.prop("readonly");
834 if (readonly === undefined) readonly = false;
835 this.readonly(readonly);
837 // Calculate size of scrollbar
838 scrollBarDimensions = scrollBarDimensions || measureScrollbar();
840 this.autofocus = opts.element.prop("autofocus");
841 opts.element.prop("autofocus", false);
842 if (this.autofocus) this.focus();
844 this.search.attr("placeholder", opts.searchInputPlaceholder);
848 destroy: function () {
849 var element=this.opts.element, select2 = element.data("select2"), self = this;
853 if (element.length && element[0].detachEvent) {
854 element.each(function () {
855 this.detachEvent("onpropertychange", self._sync);
858 if (this.propertyObserver) {
859 this.propertyObserver.disconnect();
860 this.propertyObserver = null;
864 if (select2 !== undefined) {
865 select2.container.remove();
866 select2.liveRegion.remove();
867 select2.dropdown.remove();
869 .removeClass("select2-offscreen")
870 .removeData("select2")
872 .prop("autofocus", this.autofocus || false);
873 if (this.elementTabIndex) {
874 element.attr({tabindex: this.elementTabIndex});
876 element.removeAttr("tabindex");
881 cleanupJQueryElements.call(this,
891 optionToData: function(element) {
892 if (element.is("option")) {
894 id:element.prop("value"),
896 element: element.get(),
897 css: element.attr("class"),
898 disabled: element.prop("disabled"),
899 locked: equal(element.attr("locked"), "locked") || equal(element.data("locked"), true)
901 } else if (element.is("optgroup")) {
903 text:element.attr("label"),
905 element: element.get(),
906 css: element.attr("class")
912 prepareOpts: function (opts) {
913 var element, select, idKey, ajaxUrl, self = this;
915 element = opts.element;
917 if (element.get(0).tagName.toLowerCase() === "select") {
918 this.select = select = opts.element;
922 // these options are not allowed when attached to a select because they are picked up off the element itself
923 $.each(["id", "multiple", "ajax", "query", "createSearchChoice", "initSelection", "data", "tags"], function () {
925 throw new Error("Option '" + this + "' is not allowed for Select2 when attached to a <select> element.");
930 opts = $.extend({}, {
931 populateResults: function(container, results, query) {
932 var populate, id=this.opts.id, liveRegion=this.liveRegion;
934 populate=function(results, container, depth) {
936 var i, l, result, selectable, disabled, compound, node, label, innerContainer, formatted;
938 results = opts.sortResults(results, container, query);
940 // collect the created nodes for bulk append
942 for (i = 0, l = results.length; i < l; i = i + 1) {
946 disabled = (result.disabled === true);
947 selectable = (!disabled) && (id(result) !== undefined);
949 compound=result.children && result.children.length > 0;
952 node.addClass("select2-results-dept-"+depth);
953 node.addClass("select2-result");
954 node.addClass(selectable ? "select2-result-selectable" : "select2-result-unselectable");
955 if (disabled) { node.addClass("select2-disabled"); }
956 if (compound) { node.addClass("select2-result-with-children"); }
957 node.addClass(self.opts.formatResultCssClass(result));
958 node.attr("role", "presentation");
960 label=$(document.createElement("div"));
961 label.addClass("select2-result-label");
962 label.attr("id", "select2-result-label-" + nextUid());
963 label.attr("role", "option");
965 formatted=opts.formatResult(result, label, query, self.opts.escapeMarkup);
966 if (formatted!==undefined) {
967 label.html(formatted);
974 innerContainer=$("<ul></ul>");
975 innerContainer.addClass("select2-result-sub");
976 populate(result.children, innerContainer, depth+1);
977 node.append(innerContainer);
980 node.data("select2-data", result);
984 // bulk append the created nodes
985 container.append(nodes);
986 liveRegion.text(opts.formatMatches(results.length));
989 populate(results, container, 0);
991 }, $.fn.select2.defaults, opts);
993 if (typeof(opts.id) !== "function") {
995 opts.id = function (e) { return e[idKey]; };
998 if ($.isArray(opts.element.data("select2Tags"))) {
999 if ("tags" in opts) {
1000 throw "tags specified as both an attribute 'data-select2-tags' and in options of Select2 " + opts.element.attr("id");
1002 opts.tags=opts.element.data("select2Tags");
1006 opts.query = this.bind(function (query) {
1007 var data = { results: [], more: false },
1009 children, placeholderOption, process;
1011 process=function(element, collection) {
1013 if (element.is("option")) {
1014 if (query.matcher(term, element.text(), element)) {
1015 collection.push(self.optionToData(element));
1017 } else if (element.is("optgroup")) {
1018 group=self.optionToData(element);
1019 element.children().each2(function(i, elm) { process(elm, group.children); });
1020 if (group.children.length>0) {
1021 collection.push(group);
1026 children=element.children();
1028 // ignore the placeholder option if there is one
1029 if (this.getPlaceholder() !== undefined && children.length > 0) {
1030 placeholderOption = this.getPlaceholderOption();
1031 if (placeholderOption) {
1032 children=children.not(placeholderOption);
1036 children.each2(function(i, elm) { process(elm, data.results); });
1038 query.callback(data);
1040 // this is needed because inside val() we construct choices from options and their id is hardcoded
1041 opts.id=function(e) { return e.id; };
1043 if (!("query" in opts)) {
1045 if ("ajax" in opts) {
1046 ajaxUrl = opts.element.data("ajax-url");
1047 if (ajaxUrl && ajaxUrl.length > 0) {
1048 opts.ajax.url = ajaxUrl;
1050 opts.query = ajax.call(opts.element, opts.ajax);
1051 } else if ("data" in opts) {
1052 opts.query = local(opts.data);
1053 } else if ("tags" in opts) {
1054 opts.query = tags(opts.tags);
1055 if (opts.createSearchChoice === undefined) {
1056 opts.createSearchChoice = function (term) { return {id: $.trim(term), text: $.trim(term)}; };
1058 if (opts.initSelection === undefined) {
1059 opts.initSelection = function (element, callback) {
1061 $(splitVal(element.val(), opts.separator)).each(function () {
1062 var obj = { id: this, text: this },
1064 if ($.isFunction(tags)) tags=tags();
1065 $(tags).each(function() { if (equal(this.id, obj.id)) { obj = this; return false; } });
1075 if (typeof(opts.query) !== "function") {
1076 throw "query function not defined for Select2 " + opts.element.attr("id");
1079 if (opts.createSearchChoicePosition === 'top') {
1080 opts.createSearchChoicePosition = function(list, item) { list.unshift(item); };
1082 else if (opts.createSearchChoicePosition === 'bottom') {
1083 opts.createSearchChoicePosition = function(list, item) { list.push(item); };
1085 else if (typeof(opts.createSearchChoicePosition) !== "function") {
1086 throw "invalid createSearchChoicePosition option must be 'top', 'bottom' or a custom function";
1093 * Monitor the original element for changes and update select2 accordingly
1096 monitorSource: function () {
1097 var el = this.opts.element, observer, self = this;
1099 el.on("change.select2", this.bind(function (e) {
1100 if (this.opts.element.data("select2-change-triggered") !== true) {
1101 this.initSelection();
1105 this._sync = this.bind(function () {
1107 // sync enabled state
1108 var disabled = el.prop("disabled");
1109 if (disabled === undefined) disabled = false;
1110 this.enable(!disabled);
1112 var readonly = el.prop("readonly");
1113 if (readonly === undefined) readonly = false;
1114 this.readonly(readonly);
1116 syncCssClasses(this.container, this.opts.element, this.opts.adaptContainerCssClass);
1117 this.container.addClass(evaluate(this.opts.containerCssClass, this.opts.element));
1119 syncCssClasses(this.dropdown, this.opts.element, this.opts.adaptDropdownCssClass);
1120 this.dropdown.addClass(evaluate(this.opts.dropdownCssClass, this.opts.element));
1124 // IE8-10 (IE9/10 won't fire propertyChange via attachEventListener)
1125 if (el.length && el[0].attachEvent) {
1126 el.each(function() {
1127 this.attachEvent("onpropertychange", self._sync);
1131 // safari, chrome, firefox, IE11
1132 observer = window.MutationObserver || window.WebKitMutationObserver|| window.MozMutationObserver;
1133 if (observer !== undefined) {
1134 if (this.propertyObserver) { delete this.propertyObserver; this.propertyObserver = null; }
1135 this.propertyObserver = new observer(function (mutations) {
1136 $.each(mutations, self._sync);
1138 this.propertyObserver.observe(el.get(0), { attributes:true, subtree:false });
1143 triggerSelect: function(data) {
1144 var evt = $.Event("select2-selecting", { val: this.id(data), object: data, choice: data });
1145 this.opts.element.trigger(evt);
1146 return !evt.isDefaultPrevented();
1150 * Triggers the change event on the source element
1153 triggerChange: function (details) {
1155 details = details || {};
1156 details= $.extend({}, details, { type: "change", val: this.val() });
1157 // prevents recursive triggering
1158 this.opts.element.data("select2-change-triggered", true);
1159 this.opts.element.trigger(details);
1160 this.opts.element.data("select2-change-triggered", false);
1162 // some validation frameworks ignore the change event and listen instead to keyup, click for selects
1163 // so here we trigger the click event manually
1164 this.opts.element.click();
1166 // ValidationEngine ignores the change event and listens instead to blur
1167 // so here we trigger the blur event manually if so desired
1168 if (this.opts.blurOnChange)
1169 this.opts.element.blur();
1173 isInterfaceEnabled: function()
1175 return this.enabledInterface === true;
1179 enableInterface: function() {
1180 var enabled = this._enabled && !this._readonly,
1181 disabled = !enabled;
1183 if (enabled === this.enabledInterface) return false;
1185 this.container.toggleClass("select2-container-disabled", disabled);
1187 this.enabledInterface = enabled;
1193 enable: function(enabled) {
1194 if (enabled === undefined) enabled = true;
1195 if (this._enabled === enabled) return;
1196 this._enabled = enabled;
1198 this.opts.element.prop("disabled", !enabled);
1199 this.enableInterface();
1203 disable: function() {
1208 readonly: function(enabled) {
1209 if (enabled === undefined) enabled = false;
1210 if (this._readonly === enabled) return;
1211 this._readonly = enabled;
1213 this.opts.element.prop("readonly", enabled);
1214 this.enableInterface();
1218 opened: function () {
1219 return (this.container) ? this.container.hasClass("select2-dropdown-open") : false;
1223 positionDropdown: function() {
1224 var $dropdown = this.dropdown,
1225 offset = this.container.offset(),
1226 height = this.container.outerHeight(false),
1227 width = this.container.outerWidth(false),
1228 dropHeight = $dropdown.outerHeight(false),
1229 $window = $(window),
1230 windowWidth = $window.width(),
1231 windowHeight = $window.height(),
1232 viewPortRight = $window.scrollLeft() + windowWidth,
1233 viewportBottom = $window.scrollTop() + windowHeight,
1234 dropTop = offset.top + height,
1235 dropLeft = offset.left,
1236 enoughRoomBelow = dropTop + dropHeight <= viewportBottom,
1237 enoughRoomAbove = (offset.top - dropHeight) >= $window.scrollTop(),
1238 dropWidth = $dropdown.outerWidth(false),
1239 enoughRoomOnRight = dropLeft + dropWidth <= viewPortRight,
1240 aboveNow = $dropdown.hasClass("select2-drop-above"),
1247 // always prefer the current above/below alignment, unless there is not enough room
1250 if (!enoughRoomAbove && enoughRoomBelow) {
1251 changeDirection = true;
1256 if (!enoughRoomBelow && enoughRoomAbove) {
1257 changeDirection = true;
1262 //if we are changing direction we need to get positions when dropdown is hidden;
1263 if (changeDirection) {
1265 offset = this.container.offset();
1266 height = this.container.outerHeight(false);
1267 width = this.container.outerWidth(false);
1268 dropHeight = $dropdown.outerHeight(false);
1269 viewPortRight = $window.scrollLeft() + windowWidth;
1270 viewportBottom = $window.scrollTop() + windowHeight;
1271 dropTop = offset.top + height;
1272 dropLeft = offset.left;
1273 dropWidth = $dropdown.outerWidth(false);
1274 enoughRoomOnRight = dropLeft + dropWidth <= viewPortRight;
1277 // fix so the cursor does not move to the left within the search-textbox in IE
1281 if (this.opts.dropdownAutoWidth) {
1282 resultsListNode = $('.select2-results', $dropdown)[0];
1283 $dropdown.addClass('select2-drop-auto-width');
1284 $dropdown.css('width', '');
1285 // Add scrollbar width to dropdown if vertical scrollbar is present
1286 dropWidth = $dropdown.outerWidth(false) + (resultsListNode.scrollHeight === resultsListNode.clientHeight ? 0 : scrollBarDimensions.width);
1287 dropWidth > width ? width = dropWidth : dropWidth = width;
1288 dropHeight = $dropdown.outerHeight(false);
1289 enoughRoomOnRight = dropLeft + dropWidth <= viewPortRight;
1292 this.container.removeClass('select2-drop-auto-width');
1295 //console.log("below/ droptop:", dropTop, "dropHeight", dropHeight, "sum", (dropTop+dropHeight)+" viewport bottom", viewportBottom, "enough?", enoughRoomBelow);
1296 //console.log("above/ offset.top", offset.top, "dropHeight", dropHeight, "top", (offset.top-dropHeight), "scrollTop", this.body.scrollTop(), "enough?", enoughRoomAbove);
1298 // fix positioning when body has an offset and is not position: static
1299 if (this.body.css('position') !== 'static') {
1300 bodyOffset = this.body.offset();
1301 dropTop -= bodyOffset.top;
1302 dropLeft -= bodyOffset.left;
1305 if (!enoughRoomOnRight) {
1306 dropLeft = offset.left + this.container.outerWidth(false) - dropWidth;
1315 css.top = offset.top - dropHeight;
1316 css.bottom = 'auto';
1317 this.container.addClass("select2-drop-above");
1318 $dropdown.addClass("select2-drop-above");
1322 css.bottom = 'auto';
1323 this.container.removeClass("select2-drop-above");
1324 $dropdown.removeClass("select2-drop-above");
1326 css = $.extend(css, evaluate(this.opts.dropdownCss, this.opts.element));
1332 shouldOpen: function() {
1335 if (this.opened()) return false;
1337 if (this._enabled === false || this._readonly === true) return false;
1339 event = $.Event("select2-opening");
1340 this.opts.element.trigger(event);
1341 return !event.isDefaultPrevented();
1345 clearDropdownAlignmentPreference: function() {
1346 // clear the classes used to figure out the preference of where the dropdown should be opened
1347 this.container.removeClass("select2-drop-above");
1348 this.dropdown.removeClass("select2-drop-above");
1352 * Opens the dropdown
1354 * @return {Boolean} whether or not dropdown was opened. This method will return false if, for example,
1355 * the dropdown is already open, or if the 'open' event listener on the element called preventDefault().
1360 if (!this.shouldOpen()) return false;
1364 // Only bind the document mousemove when the dropdown is visible
1365 $document.on("mousemove.select2Event", function (e) {
1366 lastMousePosition.x = e.pageX;
1367 lastMousePosition.y = e.pageY;
1374 * Performs the opening of the dropdown
1377 opening: function() {
1378 var cid = this.containerEventName,
1379 scroll = "scroll." + cid,
1380 resize = "resize."+cid,
1381 orient = "orientationchange."+cid,
1384 this.container.addClass("select2-dropdown-open").addClass("select2-container-active");
1386 this.clearDropdownAlignmentPreference();
1388 if(this.dropdown[0] !== this.body.children().last()[0]) {
1389 this.dropdown.detach().appendTo(this.body);
1392 // create the dropdown mask if doesn't already exist
1393 mask = $("#select2-drop-mask");
1394 if (mask.length == 0) {
1395 mask = $(document.createElement("div"));
1396 mask.attr("id","select2-drop-mask").attr("class","select2-drop-mask");
1398 mask.appendTo(this.body);
1399 mask.on("mousedown touchstart click", function (e) {
1400 // Prevent IE from generating a click event on the body
1401 reinsertElement(mask);
1403 var dropdown = $("#select2-drop"), self;
1404 if (dropdown.length > 0) {
1405 self=dropdown.data("select2");
1406 if (self.opts.selectOnBlur) {
1407 self.selectHighlighted({noFocus: true});
1411 e.stopPropagation();
1416 // ensure the mask is always right before the dropdown
1417 if (this.dropdown.prev()[0] !== mask[0]) {
1418 this.dropdown.before(mask);
1421 // move the global id to the correct dropdown
1422 $("#select2-drop").removeAttr("id");
1423 this.dropdown.attr("id", "select2-drop");
1425 // show the elements
1428 this.positionDropdown();
1429 this.dropdown.show();
1430 this.positionDropdown();
1432 this.dropdown.addClass("select2-drop-active");
1434 // attach listeners to events that can change the position of the container and thus require
1435 // the position of the dropdown to be updated as well so it does not come unglued from the container
1437 this.container.parents().add(window).each(function () {
1438 $(this).on(resize+" "+scroll+" "+orient, function (e) {
1439 if (that.opened()) that.positionDropdown();
1447 close: function () {
1448 if (!this.opened()) return;
1450 var cid = this.containerEventName,
1451 scroll = "scroll." + cid,
1452 resize = "resize."+cid,
1453 orient = "orientationchange."+cid;
1455 // unbind event listeners
1456 this.container.parents().add(window).each(function () { $(this).off(scroll).off(resize).off(orient); });
1458 this.clearDropdownAlignmentPreference();
1460 $("#select2-drop-mask").hide();
1461 this.dropdown.removeAttr("id"); // only the active dropdown has the select2-drop id
1462 this.dropdown.hide();
1463 this.container.removeClass("select2-dropdown-open").removeClass("select2-container-active");
1464 this.results.empty();
1466 // Now that the dropdown is closed, unbind the global document mousemove event
1467 $document.off("mousemove.select2Event");
1470 this.search.removeClass("select2-active");
1471 this.opts.element.trigger($.Event("select2-close"));
1475 * Opens control, sets input value, and updates results.
1478 externalSearch: function (term) {
1480 this.search.val(term);
1481 this.updateResults(false);
1485 clearSearch: function () {
1490 getMaximumSelectionSize: function() {
1491 return evaluate(this.opts.maximumSelectionSize, this.opts.element);
1495 ensureHighlightVisible: function () {
1496 var results = this.results, children, index, child, hb, rb, y, more, topOffset;
1498 index = this.highlight();
1500 if (index < 0) return;
1504 // if the first element is highlighted scroll all the way to the top,
1505 // that way any unselectable headers above it will also be scrolled
1508 results.scrollTop(0);
1512 children = this.findHighlightableChoices().find('.select2-result-label');
1514 child = $(children[index]);
1516 topOffset = (child.offset() || {}).top || 0;
1518 hb = topOffset + child.outerHeight(true);
1520 // if this is the last child lets also make sure select2-more-results is visible
1521 if (index === children.length - 1) {
1522 more = results.find("li.select2-more-results");
1523 if (more.length > 0) {
1524 hb = more.offset().top + more.outerHeight(true);
1528 rb = results.offset().top + results.outerHeight(true);
1530 results.scrollTop(results.scrollTop() + (hb - rb));
1532 y = topOffset - results.offset().top;
1534 // make sure the top of the element is visible
1535 if (y < 0 && child.css('display') != 'none' ) {
1536 results.scrollTop(results.scrollTop() + y); // y is negative
1541 findHighlightableChoices: function() {
1542 return this.results.find(".select2-result-selectable:not(.select2-disabled):not(.select2-selected)");
1546 moveHighlight: function (delta) {
1547 var choices = this.findHighlightableChoices(),
1548 index = this.highlight();
1550 while (index > -1 && index < choices.length) {
1552 var choice = $(choices[index]);
1553 if (choice.hasClass("select2-result-selectable") && !choice.hasClass("select2-disabled") && !choice.hasClass("select2-selected")) {
1554 this.highlight(index);
1561 highlight: function (index) {
1562 var choices = this.findHighlightableChoices(),
1566 if (arguments.length === 0) {
1567 return indexOf(choices.filter(".select2-highlighted")[0], choices.get());
1570 if (index >= choices.length) index = choices.length - 1;
1571 if (index < 0) index = 0;
1573 this.removeHighlight();
1575 choice = $(choices[index]);
1576 choice.addClass("select2-highlighted");
1578 // ensure assistive technology can determine the active choice
1579 this.search.attr("aria-activedescendant", choice.find(".select2-result-label").attr("id"));
1581 this.ensureHighlightVisible();
1583 this.liveRegion.text(choice.text());
1585 data = choice.data("select2-data");
1587 this.opts.element.trigger({ type: "select2-highlight", val: this.id(data), choice: data });
1591 removeHighlight: function() {
1592 this.results.find(".select2-highlighted").removeClass("select2-highlighted");
1595 touchMoved: function() {
1596 this._touchMoved = true;
1599 clearTouchMoved: function() {
1600 this._touchMoved = false;
1604 countSelectableResults: function() {
1605 return this.findHighlightableChoices().length;
1609 highlightUnderEvent: function (event) {
1610 var el = $(event.target).closest(".select2-result-selectable");
1611 if (el.length > 0 && !el.is(".select2-highlighted")) {
1612 var choices = this.findHighlightableChoices();
1613 this.highlight(choices.index(el));
1614 } else if (el.length == 0) {
1615 // if we are over an unselectable item remove all highlights
1616 this.removeHighlight();
1621 loadMoreIfNeeded: function () {
1622 var results = this.results,
1623 more = results.find("li.select2-more-results"),
1624 below, // pixels the element is below the scroll fold, below==0 is when the element is starting to be visible
1625 page = this.resultsPage + 1,
1627 term=this.search.val(),
1628 context=this.context;
1630 if (more.length === 0) return;
1631 below = more.offset().top - results.offset().top - results.height();
1633 if (below <= this.opts.loadMorePadding) {
1634 more.addClass("select2-active");
1636 element: this.opts.element,
1640 matcher: this.opts.matcher,
1641 callback: this.bind(function (data) {
1643 // ignore a response if the select2 has been closed before it was received
1644 if (!self.opened()) return;
1647 self.opts.populateResults.call(this, results, data.results, {term: term, page: page, context:context});
1648 self.postprocessResults(data, false, false);
1650 if (data.more===true) {
1651 more.detach().appendTo(results).text(evaluate(self.opts.formatLoadMore, self.opts.element, page+1));
1652 window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10);
1656 self.positionDropdown();
1657 self.resultsPage = page;
1658 self.context = data.context;
1659 this.opts.element.trigger({ type: "select2-loaded", items: data });
1665 * Default tokenizer function which does nothing
1667 tokenize: function() {
1672 * @param initial whether or not this is the call to this method right after the dropdown has been opened
1675 updateResults: function (initial) {
1676 var search = this.search,
1677 results = this.results,
1682 term = search.val(),
1683 lastTerm = $.data(this.container, "select2-last-term"),
1684 // sequence number used to drop out-of-order responses
1687 // prevent duplicate queries against the same term
1688 if (initial !== true && lastTerm && equal(term, lastTerm)) return;
1690 $.data(this.container, "select2-last-term", term);
1692 // if the search is currently hidden we do not alter the results
1693 if (initial !== true && (this.showSearchInput === false || !this.opened())) {
1697 function postRender() {
1698 search.removeClass("select2-active");
1699 self.positionDropdown();
1700 if (results.find('.select2-no-results,.select2-selection-limit,.select2-searching').length) {
1701 self.liveRegion.text(results.text());
1704 self.liveRegion.text(self.opts.formatMatches(results.find('.select2-result-selectable').length));
1708 function render(html) {
1713 queryNumber = ++this.queryCount;
1715 var maxSelSize = this.getMaximumSelectionSize();
1716 if (maxSelSize >=1) {
1718 if ($.isArray(data) && data.length >= maxSelSize && checkFormatter(opts.formatSelectionTooBig, "formatSelectionTooBig")) {
1719 render("<li class='select2-selection-limit'>" + evaluate(opts.formatSelectionTooBig, opts.element, maxSelSize) + "</li>");
1724 if (search.val().length < opts.minimumInputLength) {
1725 if (checkFormatter(opts.formatInputTooShort, "formatInputTooShort")) {
1726 render("<li class='select2-no-results'>" + evaluate(opts.formatInputTooShort, opts.element, search.val(), opts.minimumInputLength) + "</li>");
1730 if (initial && this.showSearch) this.showSearch(true);
1734 if (opts.maximumInputLength && search.val().length > opts.maximumInputLength) {
1735 if (checkFormatter(opts.formatInputTooLong, "formatInputTooLong")) {
1736 render("<li class='select2-no-results'>" + evaluate(opts.formatInputTooLong, opts.element, search.val(), opts.maximumInputLength) + "</li>");
1743 if (opts.formatSearching && this.findHighlightableChoices().length === 0) {
1744 render("<li class='select2-searching'>" + evaluate(opts.formatSearching, opts.element) + "</li>");
1747 search.addClass("select2-active");
1749 this.removeHighlight();
1751 // give the tokenizer a chance to pre-process the input
1752 input = this.tokenize();
1753 if (input != undefined && input != null) {
1757 this.resultsPage = 1;
1760 element: opts.element,
1762 page: this.resultsPage,
1764 matcher: opts.matcher,
1765 callback: this.bind(function (data) {
1766 var def; // default choice
1768 // ignore old responses
1769 if (queryNumber != this.queryCount) {
1773 // ignore a response if the select2 has been closed before it was received
1774 if (!this.opened()) {
1775 this.search.removeClass("select2-active");
1779 // handle ajax error
1780 if(data.hasError !== undefined && checkFormatter(opts.formatAjaxError, "formatAjaxError")) {
1781 render("<li class='select2-ajax-error'>" + evaluate(opts.formatAjaxError, opts.element, data.jqXHR, data.textStatus, data.errorThrown) + "</li>");
1785 // save context, if any
1786 this.context = (data.context===undefined) ? null : data.context;
1787 // create a default choice and prepend it to the list
1788 if (this.opts.createSearchChoice && search.val() !== "") {
1789 def = this.opts.createSearchChoice.call(self, search.val(), data.results);
1790 if (def !== undefined && def !== null && self.id(def) !== undefined && self.id(def) !== null) {
1791 if ($(data.results).filter(
1793 return equal(self.id(this), self.id(def));
1795 this.opts.createSearchChoicePosition(data.results, def);
1800 if (data.results.length === 0 && checkFormatter(opts.formatNoMatches, "formatNoMatches")) {
1801 render("<li class='select2-no-results'>" + evaluate(opts.formatNoMatches, opts.element, search.val()) + "</li>");
1806 self.opts.populateResults.call(this, results, data.results, {term: search.val(), page: this.resultsPage, context:null});
1808 if (data.more === true && checkFormatter(opts.formatLoadMore, "formatLoadMore")) {
1809 results.append("<li class='select2-more-results'>" + opts.escapeMarkup(evaluate(opts.formatLoadMore, opts.element, this.resultsPage)) + "</li>");
1810 window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10);
1813 this.postprocessResults(data, initial);
1817 this.opts.element.trigger({ type: "select2-loaded", items: data });
1822 cancel: function () {
1828 // if selectOnBlur == true, select the currently highlighted option
1829 if (this.opts.selectOnBlur)
1830 this.selectHighlighted({noFocus: true});
1833 this.container.removeClass("select2-container-active");
1834 // synonymous to .is(':focus'), which is available in jquery >= 1.6
1835 if (this.search[0] === document.activeElement) { this.search.blur(); }
1837 this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
1841 focusSearch: function () {
1846 selectHighlighted: function (options) {
1847 if (this._touchMoved) {
1848 this.clearTouchMoved();
1851 var index=this.highlight(),
1852 highlighted=this.results.find(".select2-highlighted"),
1853 data = highlighted.closest('.select2-result').data("select2-data");
1856 this.highlight(index);
1857 this.onSelect(data, options);
1858 } else if (options && options.noFocus) {
1864 getPlaceholder: function () {
1865 var placeholderOption;
1866 return this.opts.element.attr("placeholder") ||
1867 this.opts.element.attr("data-placeholder") || // jquery 1.4 compat
1868 this.opts.element.data("placeholder") ||
1869 this.opts.placeholder ||
1870 ((placeholderOption = this.getPlaceholderOption()) !== undefined ? placeholderOption.text() : undefined);
1874 getPlaceholderOption: function() {
1876 var firstOption = this.select.children('option').first();
1877 if (this.opts.placeholderOption !== undefined ) {
1878 //Determine the placeholder option based on the specified placeholderOption setting
1879 return (this.opts.placeholderOption === "first" && firstOption) ||
1880 (typeof this.opts.placeholderOption === "function" && this.opts.placeholderOption(this.select));
1881 } else if ($.trim(firstOption.text()) === "" && firstOption.val() === "") {
1882 //No explicit placeholder option specified, use the first if it's blank
1889 * Get the desired width for the container element. This is
1890 * derived first from option `width` passed to select2, then
1891 * the inline 'style' on the original element, and finally
1892 * falls back to the jQuery calculated element width.
1895 initContainerWidth: function () {
1896 function resolveContainerWidth() {
1897 var style, attrs, matches, i, l, attr;
1899 if (this.opts.width === "off") {
1901 } else if (this.opts.width === "element"){
1902 return this.opts.element.outerWidth(false) === 0 ? 'auto' : this.opts.element.outerWidth(false) + 'px';
1903 } else if (this.opts.width === "copy" || this.opts.width === "resolve") {
1904 // check if there is inline style on the element that contains width
1905 style = this.opts.element.attr('style');
1906 if (style !== undefined) {
1907 attrs = style.split(';');
1908 for (i = 0, l = attrs.length; i < l; i = i + 1) {
1909 attr = attrs[i].replace(/\s/g, '');
1910 matches = attr.match(/^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i);
1911 if (matches !== null && matches.length >= 1)
1916 if (this.opts.width === "resolve") {
1917 // next check if css('width') can resolve a width that is percent based, this is sometimes possible
1918 // when attached to input type=hidden or elements hidden via css
1919 style = this.opts.element.css('width');
1920 if (style.indexOf("%") > 0) return style;
1922 // finally, fallback on the calculated width of the element
1923 return (this.opts.element.outerWidth(false) === 0 ? 'auto' : this.opts.element.outerWidth(false) + 'px');
1927 } else if ($.isFunction(this.opts.width)) {
1928 return this.opts.width();
1930 return this.opts.width;
1934 var width = resolveContainerWidth.call(this);
1935 if (width !== null) {
1936 this.container.css("width", width);
1941 SingleSelect2 = clazz(AbstractSelect2, {
1945 createContainer: function () {
1946 var container = $(document.createElement("div")).attr({
1947 "class": "select2-container"
1949 "<a href='javascript:void(0)' class='select2-choice' tabindex='-1'>",
1950 " <span class='select2-chosen'> </span><abbr class='select2-search-choice-close'></abbr>",
1951 " <span class='select2-arrow' role='presentation'><b role='presentation'></b></span>",
1953 "<label for='' class='select2-offscreen'></label>",
1954 "<input class='select2-focusser select2-offscreen' type='text' aria-haspopup='true' role='button' />",
1955 "<div class='select2-drop select2-display-none'>",
1956 " <div class='select2-search'>",
1957 " <label for='' class='select2-offscreen'></label>",
1958 " <input type='text' autocomplete='off' autocorrect='off' autocapitalize='off' spellcheck='false' class='select2-input' role='combobox' aria-expanded='true'",
1959 " aria-autocomplete='list' />",
1961 " <ul class='select2-results' role='listbox'>",
1963 "</div>"].join(""));
1968 enableInterface: function() {
1969 if (this.parent.enableInterface.apply(this, arguments)) {
1970 this.focusser.prop("disabled", !this.isInterfaceEnabled());
1975 opening: function () {
1978 if (this.opts.minimumResultsForSearch >= 0) {
1979 this.showSearch(true);
1982 this.parent.opening.apply(this, arguments);
1984 if (this.showSearchInput !== false) {
1985 // IE appends focusser.val() at the end of field :/ so we manually insert it at the beginning using a range
1986 // all other browsers handle this just fine
1988 this.search.val(this.focusser.val());
1990 if (this.opts.shouldFocusInput(this)) {
1991 this.search.focus();
1992 // move the cursor to the end after focussing, otherwise it will be at the beginning and
1993 // new text will appear *before* focusser.val()
1994 el = this.search.get(0);
1995 if (el.createTextRange) {
1996 range = el.createTextRange();
1997 range.collapse(false);
1999 } else if (el.setSelectionRange) {
2000 len = this.search.val().length;
2001 el.setSelectionRange(len, len);
2005 // initializes search's value with nextSearchTerm (if defined by user)
2006 // ignore nextSearchTerm if the dropdown is opened by the user pressing a letter
2007 if(this.search.val() === "") {
2008 if(this.nextSearchTerm != undefined){
2009 this.search.val(this.nextSearchTerm);
2010 this.search.select();
2014 this.focusser.prop("disabled", true).val("");
2015 this.updateResults(true);
2016 this.opts.element.trigger($.Event("select2-open"));
2020 close: function () {
2021 if (!this.opened()) return;
2022 this.parent.close.apply(this, arguments);
2024 this.focusser.prop("disabled", false);
2026 if (this.opts.shouldFocusInput(this)) {
2027 this.focusser.focus();
2032 focus: function () {
2033 if (this.opened()) {
2036 this.focusser.prop("disabled", false);
2037 if (this.opts.shouldFocusInput(this)) {
2038 this.focusser.focus();
2044 isFocused: function () {
2045 return this.container.hasClass("select2-container-active");
2049 cancel: function () {
2050 this.parent.cancel.apply(this, arguments);
2051 this.focusser.prop("disabled", false);
2053 if (this.opts.shouldFocusInput(this)) {
2054 this.focusser.focus();
2059 destroy: function() {
2060 $("label[for='" + this.focusser.attr('id') + "']")
2061 .attr('for', this.opts.element.attr("id"));
2062 this.parent.destroy.apply(this, arguments);
2064 cleanupJQueryElements.call(this,
2071 initContainer: function () {
2074 container = this.container,
2075 dropdown = this.dropdown,
2076 idSuffix = nextUid(),
2079 if (this.opts.minimumResultsForSearch < 0) {
2080 this.showSearch(false);
2082 this.showSearch(true);
2085 this.selection = selection = container.find(".select2-choice");
2087 this.focusser = container.find(".select2-focusser");
2089 // add aria associations
2090 selection.find(".select2-chosen").attr("id", "select2-chosen-"+idSuffix);
2091 this.focusser.attr("aria-labelledby", "select2-chosen-"+idSuffix);
2092 this.results.attr("id", "select2-results-"+idSuffix);
2093 this.search.attr("aria-owns", "select2-results-"+idSuffix);
2095 // rewrite labels from original element to focusser
2096 this.focusser.attr("id", "s2id_autogen"+idSuffix);
2098 elementLabel = $("label[for='" + this.opts.element.attr("id") + "']");
2100 this.focusser.prev()
2101 .text(elementLabel.text())
2102 .attr('for', this.focusser.attr('id'));
2104 // Ensure the original element retains an accessible name
2105 var originalTitle = this.opts.element.attr("title");
2106 this.opts.element.attr("title", (originalTitle || elementLabel.text()));
2108 this.focusser.attr("tabindex", this.elementTabIndex);
2110 // write label for search field using the label from the focusser element
2111 this.search.attr("id", this.focusser.attr('id') + '_search');
2114 .text($("label[for='" + this.focusser.attr('id') + "']").text())
2115 .attr('for', this.search.attr('id'));
2117 this.search.on("keydown", this.bind(function (e) {
2118 if (!this.isInterfaceEnabled()) return;
2120 // filter 229 keyCodes (input method editor is processing key input)
2121 if (229 == e.keyCode) return;
2123 if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) {
2124 // prevent the page from scrolling
2132 this.moveHighlight((e.which === KEY.UP) ? -1 : 1);
2136 this.selectHighlighted();
2140 this.selectHighlighted({noFocus: true});
2149 this.search.on("blur", this.bind(function(e) {
2150 // a workaround for chrome to keep the search field focussed when the scroll bar is used to scroll the dropdown.
2151 // without this the search field loses focus which is annoying
2152 if (document.activeElement === this.body.get(0)) {
2153 window.setTimeout(this.bind(function() {
2154 if (this.opened()) {
2155 this.search.focus();
2161 this.focusser.on("keydown", this.bind(function (e) {
2162 if (!this.isInterfaceEnabled()) return;
2164 if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) {
2168 if (this.opts.openOnEnter === false && e.which === KEY.ENTER) {
2173 if (e.which == KEY.DOWN || e.which == KEY.UP
2174 || (e.which == KEY.ENTER && this.opts.openOnEnter)) {
2176 if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) return;
2183 if (e.which == KEY.DELETE || e.which == KEY.BACKSPACE) {
2184 if (this.opts.allowClear) {
2193 installKeyUpChangeEvent(this.focusser);
2194 this.focusser.on("keyup-change input", this.bind(function(e) {
2195 if (this.opts.minimumResultsForSearch >= 0) {
2196 e.stopPropagation();
2197 if (this.opened()) return;
2202 selection.on("mousedown touchstart", "abbr", this.bind(function (e) {
2203 if (!this.isInterfaceEnabled()) return;
2205 killEventImmediately(e);
2207 this.selection.focus();
2210 selection.on("mousedown touchstart", this.bind(function (e) {
2211 // Prevent IE from generating a click event on the body
2212 reinsertElement(selection);
2214 if (!this.container.hasClass("select2-container-active")) {
2215 this.opts.element.trigger($.Event("select2-focus"));
2218 if (this.opened()) {
2220 } else if (this.isInterfaceEnabled()) {
2227 dropdown.on("mousedown touchstart", this.bind(function() {
2228 if (this.opts.shouldFocusInput(this)) {
2229 this.search.focus();
2233 selection.on("focus", this.bind(function(e) {
2237 this.focusser.on("focus", this.bind(function(){
2238 if (!this.container.hasClass("select2-container-active")) {
2239 this.opts.element.trigger($.Event("select2-focus"));
2241 this.container.addClass("select2-container-active");
2242 })).on("blur", this.bind(function() {
2243 if (!this.opened()) {
2244 this.container.removeClass("select2-container-active");
2245 this.opts.element.trigger($.Event("select2-blur"));
2248 this.search.on("focus", this.bind(function(){
2249 if (!this.container.hasClass("select2-container-active")) {
2250 this.opts.element.trigger($.Event("select2-focus"));
2252 this.container.addClass("select2-container-active");
2255 this.initContainerWidth();
2256 this.opts.element.addClass("select2-offscreen");
2257 this.setPlaceholder();
2262 clear: function(triggerChange) {
2263 var data=this.selection.data("select2-data");
2264 if (data) { // guard against queued quick consecutive clicks
2265 var evt = $.Event("select2-clearing");
2266 this.opts.element.trigger(evt);
2267 if (evt.isDefaultPrevented()) {
2270 var placeholderOption = this.getPlaceholderOption();
2271 this.opts.element.val(placeholderOption ? placeholderOption.val() : "");
2272 this.selection.find(".select2-chosen").empty();
2273 this.selection.removeData("select2-data");
2274 this.setPlaceholder();
2276 if (triggerChange !== false){
2277 this.opts.element.trigger({ type: "select2-removed", val: this.id(data), choice: data });
2278 this.triggerChange({removed:data});
2284 * Sets selection based on source element's value
2287 initSelection: function () {
2289 if (this.isPlaceholderOptionSelected()) {
2290 this.updateSelection(null);
2292 this.setPlaceholder();
2295 this.opts.initSelection.call(null, this.opts.element, function(selected){
2296 if (selected !== undefined && selected !== null) {
2297 self.updateSelection(selected);
2299 self.setPlaceholder();
2300 self.nextSearchTerm = self.opts.nextSearchTerm(selected, self.search.val());
2306 isPlaceholderOptionSelected: function() {
2307 var placeholderOption;
2308 if (this.getPlaceholder() === undefined) return false; // no placeholder specified so no option should be considered
2309 return ((placeholderOption = this.getPlaceholderOption()) !== undefined && placeholderOption.prop("selected"))
2310 || (this.opts.element.val() === "")
2311 || (this.opts.element.val() === undefined)
2312 || (this.opts.element.val() === null);
2316 prepareOpts: function () {
2317 var opts = this.parent.prepareOpts.apply(this, arguments),
2320 if (opts.element.get(0).tagName.toLowerCase() === "select") {
2321 // install the selection initializer
2322 opts.initSelection = function (element, callback) {
2323 var selected = element.find("option").filter(function() { return this.selected && !this.disabled });
2324 // a single select box always has a value, no need to null check 'selected'
2325 callback(self.optionToData(selected));
2327 } else if ("data" in opts) {
2328 // install default initSelection when applied to hidden input and data is local
2329 opts.initSelection = opts.initSelection || function (element, callback) {
2330 var id = element.val();
2331 //search in data by id, storing the actual matching item
2334 matcher: function(term, text, el){
2335 var is_match = equal(id, opts.id(el));
2341 callback: !$.isFunction(callback) ? $.noop : function() {
2352 getPlaceholder: function() {
2353 // if a placeholder is specified on a single select without a valid placeholder option ignore it
2355 if (this.getPlaceholderOption() === undefined) {
2360 return this.parent.getPlaceholder.apply(this, arguments);
2364 setPlaceholder: function () {
2365 var placeholder = this.getPlaceholder();
2367 if (this.isPlaceholderOptionSelected() && placeholder !== undefined) {
2369 // check for a placeholder option if attached to a select
2370 if (this.select && this.getPlaceholderOption() === undefined) return;
2372 this.selection.find(".select2-chosen").html(this.opts.escapeMarkup(placeholder));
2374 this.selection.addClass("select2-default");
2376 this.container.removeClass("select2-allowclear");
2381 postprocessResults: function (data, initial, noHighlightUpdate) {
2382 var selected = 0, self = this, showSearchInput = true;
2384 // find the selected element in the result list
2386 this.findHighlightableChoices().each2(function (i, elm) {
2387 if (equal(self.id(elm.data("select2-data")), self.opts.element.val())) {
2394 if (noHighlightUpdate !== false) {
2395 if (initial === true && selected >= 0) {
2396 this.highlight(selected);
2402 // hide the search box if this is the first we got the results and there are enough of them for search
2404 if (initial === true) {
2405 var min = this.opts.minimumResultsForSearch;
2407 this.showSearch(countResults(data.results) >= min);
2413 showSearch: function(showSearchInput) {
2414 if (this.showSearchInput === showSearchInput) return;
2416 this.showSearchInput = showSearchInput;
2418 this.dropdown.find(".select2-search").toggleClass("select2-search-hidden", !showSearchInput);
2419 this.dropdown.find(".select2-search").toggleClass("select2-offscreen", !showSearchInput);
2420 //add "select2-with-searchbox" to the container if search box is shown
2421 $(this.dropdown, this.container).toggleClass("select2-with-searchbox", showSearchInput);
2425 onSelect: function (data, options) {
2427 if (!this.triggerSelect(data)) { return; }
2429 var old = this.opts.element.val(),
2430 oldData = this.data();
2432 this.opts.element.val(this.id(data));
2433 this.updateSelection(data);
2435 this.opts.element.trigger({ type: "select2-selected", val: this.id(data), choice: data });
2437 this.nextSearchTerm = this.opts.nextSearchTerm(data, this.search.val());
2440 if ((!options || !options.noFocus) && this.opts.shouldFocusInput(this)) {
2441 this.focusser.focus();
2444 if (!equal(old, this.id(data))) {
2445 this.triggerChange({ added: data, removed: oldData });
2450 updateSelection: function (data) {
2452 var container=this.selection.find(".select2-chosen"), formatted, cssClass;
2454 this.selection.data("select2-data", data);
2457 if (data !== null) {
2458 formatted=this.opts.formatSelection(data, container, this.opts.escapeMarkup);
2460 if (formatted !== undefined) {
2461 container.append(formatted);
2463 cssClass=this.opts.formatSelectionCssClass(data, container);
2464 if (cssClass !== undefined) {
2465 container.addClass(cssClass);
2468 this.selection.removeClass("select2-default");
2470 if (this.opts.allowClear && this.getPlaceholder() !== undefined) {
2471 this.container.addClass("select2-allowclear");
2478 triggerChange = false,
2481 oldData = this.data();
2483 if (arguments.length === 0) {
2484 return this.opts.element.val();
2489 if (arguments.length > 1) {
2490 triggerChange = arguments[1];
2496 .find("option").filter(function() { return this.selected }).each2(function (i, elm) {
2497 data = self.optionToData(elm);
2500 this.updateSelection(data);
2501 this.setPlaceholder();
2502 if (triggerChange) {
2503 this.triggerChange({added: data, removed:oldData});
2506 // val is an id. !val is true for [undefined,null,'',0] - 0 is legal
2507 if (!val && val !== 0) {
2508 this.clear(triggerChange);
2511 if (this.opts.initSelection === undefined) {
2512 throw new Error("cannot call val() if initSelection() is not defined");
2514 this.opts.element.val(val);
2515 this.opts.initSelection(this.opts.element, function(data){
2516 self.opts.element.val(!data ? "" : self.id(data));
2517 self.updateSelection(data);
2518 self.setPlaceholder();
2519 if (triggerChange) {
2520 self.triggerChange({added: data, removed:oldData});
2527 clearSearch: function () {
2528 this.search.val("");
2529 this.focusser.val("");
2533 data: function(value) {
2535 triggerChange = false;
2537 if (arguments.length === 0) {
2538 data = this.selection.data("select2-data");
2539 if (data == undefined) data = null;
2542 if (arguments.length > 1) {
2543 triggerChange = arguments[1];
2546 this.clear(triggerChange);
2549 this.opts.element.val(!value ? "" : this.id(value));
2550 this.updateSelection(value);
2551 if (triggerChange) {
2552 this.triggerChange({added: value, removed:data});
2559 MultiSelect2 = clazz(AbstractSelect2, {
2562 createContainer: function () {
2563 var container = $(document.createElement("div")).attr({
2564 "class": "select2-container select2-container-multi"
2566 "<ul class='select2-choices'>",
2567 " <li class='select2-search-field'>",
2568 " <label for='' class='select2-offscreen'></label>",
2569 " <input type='text' autocomplete='off' autocorrect='off' autocapitalize='off' spellcheck='false' class='select2-input'>",
2572 "<div class='select2-drop select2-drop-multi select2-display-none'>",
2573 " <ul class='select2-results'>",
2575 "</div>"].join(""));
2580 prepareOpts: function () {
2581 var opts = this.parent.prepareOpts.apply(this, arguments),
2584 // TODO validate placeholder is a string if specified
2586 if (opts.element.get(0).tagName.toLowerCase() === "select") {
2587 // install the selection initializer
2588 opts.initSelection = function (element, callback) {
2592 element.find("option").filter(function() { return this.selected && !this.disabled }).each2(function (i, elm) {
2593 data.push(self.optionToData(elm));
2597 } else if ("data" in opts) {
2598 // install default initSelection when applied to hidden input and data is local
2599 opts.initSelection = opts.initSelection || function (element, callback) {
2600 var ids = splitVal(element.val(), opts.separator);
2601 //search in data by array of ids, storing matching items in a list
2604 matcher: function(term, text, el){
2605 var is_match = $.grep(ids, function(id) {
2606 return equal(id, opts.id(el));
2613 callback: !$.isFunction(callback) ? $.noop : function() {
2614 // reorder matches based on the order they appear in the ids array because right now
2615 // they are in the order in which they appear in data array
2617 for (var i = 0; i < ids.length; i++) {
2619 for (var j = 0; j < matches.length; j++) {
2620 var match = matches[j];
2621 if (equal(id, opts.id(match))) {
2622 ordered.push(match);
2623 matches.splice(j, 1);
2638 selectChoice: function (choice) {
2640 var selected = this.container.find(".select2-search-choice-focus");
2641 if (selected.length && choice && choice[0] == selected[0]) {
2644 if (selected.length) {
2645 this.opts.element.trigger("choice-deselected", selected);
2647 selected.removeClass("select2-search-choice-focus");
2648 if (choice && choice.length) {
2650 choice.addClass("select2-search-choice-focus");
2651 this.opts.element.trigger("choice-selected", choice);
2657 destroy: function() {
2658 $("label[for='" + this.search.attr('id') + "']")
2659 .attr('for', this.opts.element.attr("id"));
2660 this.parent.destroy.apply(this, arguments);
2662 cleanupJQueryElements.call(this,
2669 initContainer: function () {
2671 var selector = ".select2-choices", selection;
2673 this.searchContainer = this.container.find(".select2-search-field");
2674 this.selection = selection = this.container.find(selector);
2677 this.selection.on("click", ".select2-search-choice:not(.select2-locked)", function (e) {
2679 _this.search[0].focus();
2680 _this.selectChoice($(this));
2683 // rewrite labels from original element to focusser
2684 this.search.attr("id", "s2id_autogen"+nextUid());
2687 .text($("label[for='" + this.opts.element.attr("id") + "']").text())
2688 .attr('for', this.search.attr('id'));
2690 this.search.on("input paste", this.bind(function() {
2691 if (this.search.attr('placeholder') && this.search.val().length == 0) return;
2692 if (!this.isInterfaceEnabled()) return;
2693 if (!this.opened()) {
2698 this.search.attr("tabindex", this.elementTabIndex);
2701 this.search.on("keydown", this.bind(function (e) {
2702 if (!this.isInterfaceEnabled()) return;
2705 var selected = selection.find(".select2-search-choice-focus");
2706 var prev = selected.prev(".select2-search-choice:not(.select2-locked)");
2707 var next = selected.next(".select2-search-choice:not(.select2-locked)");
2708 var pos = getCursorInfo(this.search);
2710 if (selected.length &&
2711 (e.which == KEY.LEFT || e.which == KEY.RIGHT || e.which == KEY.BACKSPACE || e.which == KEY.DELETE || e.which == KEY.ENTER)) {
2712 var selectedChoice = selected;
2713 if (e.which == KEY.LEFT && prev.length) {
2714 selectedChoice = prev;
2716 else if (e.which == KEY.RIGHT) {
2717 selectedChoice = next.length ? next : null;
2719 else if (e.which === KEY.BACKSPACE) {
2720 if (this.unselect(selected.first())) {
2721 this.search.width(10);
2722 selectedChoice = prev.length ? prev : next;
2724 } else if (e.which == KEY.DELETE) {
2725 if (this.unselect(selected.first())) {
2726 this.search.width(10);
2727 selectedChoice = next.length ? next : null;
2729 } else if (e.which == KEY.ENTER) {
2730 selectedChoice = null;
2733 this.selectChoice(selectedChoice);
2735 if (!selectedChoice || !selectedChoice.length) {
2739 } else if (((e.which === KEY.BACKSPACE && this.keydowns == 1)
2740 || e.which == KEY.LEFT) && (pos.offset == 0 && !pos.length)) {
2742 this.selectChoice(selection.find(".select2-search-choice:not(.select2-locked)").last());
2746 this.selectChoice(null);
2749 if (this.opened()) {
2753 this.moveHighlight((e.which === KEY.UP) ? -1 : 1);
2757 this.selectHighlighted();
2761 this.selectHighlighted({noFocus:true});
2771 if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e)
2772 || e.which === KEY.BACKSPACE || e.which === KEY.ESC) {
2776 if (e.which === KEY.ENTER) {
2777 if (this.opts.openOnEnter === false) {
2779 } else if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) {
2786 if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) {
2787 // prevent the page from scrolling
2791 if (e.which === KEY.ENTER) {
2792 // prevent form from being submitted
2798 this.search.on("keyup", this.bind(function (e) {
2800 this.resizeSearch();
2804 this.search.on("blur", this.bind(function(e) {
2805 this.container.removeClass("select2-container-active");
2806 this.search.removeClass("select2-focused");
2807 this.selectChoice(null);
2808 if (!this.opened()) this.clearSearch();
2809 e.stopImmediatePropagation();
2810 this.opts.element.trigger($.Event("select2-blur"));
2813 this.container.on("click", selector, this.bind(function (e) {
2814 if (!this.isInterfaceEnabled()) return;
2815 if ($(e.target).closest(".select2-search-choice").length > 0) {
2816 // clicked inside a select2 search choice, do not open
2819 this.selectChoice(null);
2820 this.clearPlaceholder();
2821 if (!this.container.hasClass("select2-container-active")) {
2822 this.opts.element.trigger($.Event("select2-focus"));
2829 this.container.on("focus", selector, this.bind(function () {
2830 if (!this.isInterfaceEnabled()) return;
2831 if (!this.container.hasClass("select2-container-active")) {
2832 this.opts.element.trigger($.Event("select2-focus"));
2834 this.container.addClass("select2-container-active");
2835 this.dropdown.addClass("select2-drop-active");
2836 this.clearPlaceholder();
2839 this.initContainerWidth();
2840 this.opts.element.addClass("select2-offscreen");
2842 // set the placeholder if necessary
2847 enableInterface: function() {
2848 if (this.parent.enableInterface.apply(this, arguments)) {
2849 this.search.prop("disabled", !this.isInterfaceEnabled());
2854 initSelection: function () {
2856 if (this.opts.element.val() === "" && this.opts.element.text() === "") {
2857 this.updateSelection([]);
2859 // set the placeholder if necessary
2862 if (this.select || this.opts.element.val() !== "") {
2864 this.opts.initSelection.call(null, this.opts.element, function(data){
2865 if (data !== undefined && data !== null) {
2866 self.updateSelection(data);
2868 // set the placeholder if necessary
2876 clearSearch: function () {
2877 var placeholder = this.getPlaceholder(),
2878 maxWidth = this.getMaxSearchWidth();
2880 if (placeholder !== undefined && this.getVal().length === 0 && this.search.hasClass("select2-focused") === false) {
2881 this.search.val(placeholder).addClass("select2-default");
2882 // stretch the search box to full width of the container so as much of the placeholder is visible as possible
2883 // 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
2884 this.search.width(maxWidth > 0 ? maxWidth : this.container.css("width"));
2886 this.search.val("").width(10);
2891 clearPlaceholder: function () {
2892 if (this.search.hasClass("select2-default")) {
2893 this.search.val("").removeClass("select2-default");
2898 opening: function () {
2899 this.clearPlaceholder(); // should be done before super so placeholder is not used to search
2900 this.resizeSearch();
2902 this.parent.opening.apply(this, arguments);
2906 // initializes search's value with nextSearchTerm (if defined by user)
2907 // ignore nextSearchTerm if the dropdown is opened by the user pressing a letter
2908 if(this.search.val() === "") {
2909 if(this.nextSearchTerm != undefined){
2910 this.search.val(this.nextSearchTerm);
2911 this.search.select();
2915 this.updateResults(true);
2916 if (this.opts.shouldFocusInput(this)) {
2917 this.search.focus();
2919 this.opts.element.trigger($.Event("select2-open"));
2923 close: function () {
2924 if (!this.opened()) return;
2925 this.parent.close.apply(this, arguments);
2929 focus: function () {
2931 this.search.focus();
2935 isFocused: function () {
2936 return this.search.hasClass("select2-focused");
2940 updateSelection: function (data) {
2941 var ids = [], filtered = [], self = this;
2943 // filter out duplicates
2944 $(data).each(function () {
2945 if (indexOf(self.id(this), ids) < 0) {
2946 ids.push(self.id(this));
2947 filtered.push(this);
2952 this.selection.find(".select2-search-choice").remove();
2953 $(data).each(function () {
2954 self.addSelectedChoice(this);
2956 self.postprocessResults();
2960 tokenize: function() {
2961 var input = this.search.val();
2962 input = this.opts.tokenizer.call(this, input, this.data(), this.bind(this.onSelect), this.opts);
2963 if (input != null && input != undefined) {
2964 this.search.val(input);
2965 if (input.length > 0) {
2973 onSelect: function (data, options) {
2975 if (!this.triggerSelect(data) || data.text === "") { return; }
2977 this.addSelectedChoice(data);
2979 this.opts.element.trigger({ type: "selected", val: this.id(data), choice: data });
2981 // keep track of the search's value before it gets cleared
2982 this.nextSearchTerm = this.opts.nextSearchTerm(data, this.search.val());
2985 this.updateResults();
2987 if (this.select || !this.opts.closeOnSelect) this.postprocessResults(data, false, this.opts.closeOnSelect===true);
2989 if (this.opts.closeOnSelect) {
2991 this.search.width(10);
2993 if (this.countSelectableResults()>0) {
2994 this.search.width(10);
2995 this.resizeSearch();
2996 if (this.getMaximumSelectionSize() > 0 && this.val().length >= this.getMaximumSelectionSize()) {
2997 // if we reached max selection size repaint the results so choices
2998 // are replaced with the max selection reached message
2999 this.updateResults(true);
3001 // initializes search's value with nextSearchTerm and update search result
3002 if(this.nextSearchTerm != undefined){
3003 this.search.val(this.nextSearchTerm);
3004 this.updateResults();
3005 this.search.select();
3008 this.positionDropdown();
3010 // if nothing left to select close
3012 this.search.width(10);
3016 // since its not possible to select an element that has already been
3017 // added we do not need to check if this is a new element before firing change
3018 this.triggerChange({ added: data });
3020 if (!options || !options.noFocus)
3025 cancel: function () {
3030 addSelectedChoice: function (data) {
3031 var enableChoice = !data.locked,
3033 "<li class='select2-search-choice'>" +
3035 " <a href='#' class='select2-search-choice-close' tabindex='-1'></a>" +
3038 "<li class='select2-search-choice select2-locked'>" +
3041 var choice = enableChoice ? enabledItem : disabledItem,
3043 val = this.getVal(),
3047 formatted=this.opts.formatSelection(data, choice.find("div"), this.opts.escapeMarkup);
3048 if (formatted != undefined) {
3049 choice.find("div").replaceWith("<div>"+formatted+"</div>");
3051 cssClass=this.opts.formatSelectionCssClass(data, choice.find("div"));
3052 if (cssClass != undefined) {
3053 choice.addClass(cssClass);
3057 choice.find(".select2-search-choice-close")
3058 .on("mousedown", killEvent)
3059 .on("click dblclick", this.bind(function (e) {
3060 if (!this.isInterfaceEnabled()) return;
3062 this.unselect($(e.target));
3063 this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
3067 })).on("focus", this.bind(function () {
3068 if (!this.isInterfaceEnabled()) return;
3069 this.container.addClass("select2-container-active");
3070 this.dropdown.addClass("select2-drop-active");
3074 choice.data("select2-data", data);
3075 choice.insertBefore(this.searchContainer);
3082 unselect: function (selected) {
3083 var val = this.getVal(),
3086 selected = selected.closest(".select2-search-choice");
3088 if (selected.length === 0) {
3089 throw "Invalid argument: " + selected + ". Must be .select2-search-choice";
3092 data = selected.data("select2-data");
3095 // prevent a race condition when the 'x' is clicked really fast repeatedly the event can be queued
3096 // and invoked on an element already removed
3100 var evt = $.Event("select2-removing");
3101 evt.val = this.id(data);
3103 this.opts.element.trigger(evt);
3105 if (evt.isDefaultPrevented()) {
3109 while((index = indexOf(this.id(data), val)) >= 0) {
3110 val.splice(index, 1);
3112 if (this.select) this.postprocessResults();
3117 this.opts.element.trigger({ type: "select2-removed", val: this.id(data), choice: data });
3118 this.triggerChange({ removed: data });
3124 postprocessResults: function (data, initial, noHighlightUpdate) {
3125 var val = this.getVal(),
3126 choices = this.results.find(".select2-result"),
3127 compound = this.results.find(".select2-result-with-children"),
3130 choices.each2(function (i, choice) {
3131 var id = self.id(choice.data("select2-data"));
3132 if (indexOf(id, val) >= 0) {
3133 choice.addClass("select2-selected");
3134 // mark all children of the selected parent as selected
3135 choice.find(".select2-result-selectable").addClass("select2-selected");
3139 compound.each2(function(i, choice) {
3140 // hide an optgroup if it doesn't have any selectable children
3141 if (!choice.is('.select2-result-selectable')
3142 && choice.find(".select2-result-selectable:not(.select2-selected)").length === 0) {
3143 choice.addClass("select2-selected");
3147 if (this.highlight() == -1 && noHighlightUpdate !== false){
3151 //If all results are chosen render formatNoMatches
3152 if(!this.opts.createSearchChoice && !choices.filter('.select2-result:not(.select2-selected)').length > 0){
3153 if(!data || data && !data.more && this.results.find(".select2-no-results").length === 0) {
3154 if (checkFormatter(self.opts.formatNoMatches, "formatNoMatches")) {
3155 this.results.append("<li class='select2-no-results'>" + evaluate(self.opts.formatNoMatches, self.opts.element, self.search.val()) + "</li>");
3163 getMaxSearchWidth: function() {
3164 return this.selection.width() - getSideBorderPadding(this.search);
3168 resizeSearch: function () {
3169 var minimumWidth, left, maxWidth, containerLeft, searchWidth,
3170 sideBorderPadding = getSideBorderPadding(this.search);
3172 minimumWidth = measureTextWidth(this.search) + 10;
3174 left = this.search.offset().left;
3176 maxWidth = this.selection.width();
3177 containerLeft = this.selection.offset().left;
3179 searchWidth = maxWidth - (left - containerLeft) - sideBorderPadding;
3181 if (searchWidth < minimumWidth) {
3182 searchWidth = maxWidth - sideBorderPadding;
3185 if (searchWidth < 40) {
3186 searchWidth = maxWidth - sideBorderPadding;
3189 if (searchWidth <= 0) {
3190 searchWidth = minimumWidth;
3193 this.search.width(Math.floor(searchWidth));
3197 getVal: function () {
3200 val = this.select.val();
3201 return val === null ? [] : val;
3203 val = this.opts.element.val();
3204 return splitVal(val, this.opts.separator);
3209 setVal: function (val) {
3212 this.select.val(val);
3215 // filter out duplicates
3216 $(val).each(function () {
3217 if (indexOf(this, unique) < 0) unique.push(this);
3219 this.opts.element.val(unique.length === 0 ? "" : unique.join(this.opts.separator));
3224 buildChangeDetails: function (old, current) {
3225 var current = current.slice(0),
3228 // remove intersection from each array
3229 for (var i = 0; i < current.length; i++) {
3230 for (var j = 0; j < old.length; j++) {
3231 if (equal(this.opts.id(current[i]), this.opts.id(old[j]))) {
3232 current.splice(i, 1);
3242 return {added: current, removed: old};
3247 val: function (val, triggerChange) {
3248 var oldData, self=this;
3250 if (arguments.length === 0) {
3251 return this.getVal();
3254 oldData=this.data();
3255 if (!oldData.length) oldData=[];
3257 // val is an id. !val is true for [undefined,null,'',0] - 0 is legal
3258 if (!val && val !== 0) {
3259 this.opts.element.val("");
3260 this.updateSelection([]);
3262 if (triggerChange) {
3263 this.triggerChange({added: this.data(), removed: oldData});
3268 // val is a list of ids
3272 this.opts.initSelection(this.select, this.bind(this.updateSelection));
3273 if (triggerChange) {
3274 this.triggerChange(this.buildChangeDetails(oldData, this.data()));
3277 if (this.opts.initSelection === undefined) {
3278 throw new Error("val() cannot be called if initSelection() is not defined");
3281 this.opts.initSelection(this.opts.element, function(data){
3282 var ids=$.map(data, self.id);
3284 self.updateSelection(data);
3286 if (triggerChange) {
3287 self.triggerChange(self.buildChangeDetails(oldData, self.data()));
3295 onSortStart: function() {
3297 throw new Error("Sorting of elements is not supported when attached to <select>. Attach to <input type='hidden'/> instead.");
3300 // collapse search field into 0 width so its container can be collapsed as well
3301 this.search.width(0);
3302 // hide the container
3303 this.searchContainer.hide();
3307 onSortEnd:function() {
3309 var val=[], self=this;
3311 // show search and move it to the end of the list
3312 this.searchContainer.show();
3313 // make sure the search container is the last item in the list
3314 this.searchContainer.appendTo(this.searchContainer.parent());
3315 // since we collapsed the width in dragStarted, we resize it here
3316 this.resizeSearch();
3319 this.selection.find(".select2-search-choice").each(function() {
3320 val.push(self.opts.id($(this).data("select2-data")));
3323 this.triggerChange();
3327 data: function(values, triggerChange) {
3328 var self=this, ids, old;
3329 if (arguments.length === 0) {
3330 return this.selection
3331 .children(".select2-search-choice")
3332 .map(function() { return $(this).data("select2-data"); })
3336 if (!values) { values = []; }
3337 ids = $.map(values, function(e) { return self.opts.id(e); });
3339 this.updateSelection(values);
3341 if (triggerChange) {
3342 this.triggerChange(this.buildChangeDetails(old, this.data()));
3348 $.fn.select2 = function () {
3350 var args = Array.prototype.slice.call(arguments, 0),
3353 method, value, multiple,
3354 allowedMethods = ["val", "destroy", "opened", "open", "close", "focus", "isFocused", "container", "dropdown", "onSortStart", "onSortEnd", "enable", "disable", "readonly", "positionDropdown", "data", "search"],
3355 valueMethods = ["opened", "isFocused", "container", "dropdown"],
3356 propertyMethods = ["val", "data"],
3357 methodsMap = { search: "externalSearch" };
3359 this.each(function () {
3360 if (args.length === 0 || typeof(args[0]) === "object") {
3361 opts = args.length === 0 ? {} : $.extend({}, args[0]);
3362 opts.element = $(this);
3364 if (opts.element.get(0).tagName.toLowerCase() === "select") {
3365 multiple = opts.element.prop("multiple");
3367 multiple = opts.multiple || false;
3368 if ("tags" in opts) {opts.multiple = multiple = true;}
3371 select2 = multiple ? new window.Select2["class"].multi() : new window.Select2["class"].single();
3373 } else if (typeof(args[0]) === "string") {
3375 if (indexOf(args[0], allowedMethods) < 0) {
3376 throw "Unknown method: " + args[0];
3380 select2 = $(this).data("select2");
3381 if (select2 === undefined) return;
3385 if (method === "container") {
3386 value = select2.container;
3387 } else if (method === "dropdown") {
3388 value = select2.dropdown;
3390 if (methodsMap[method]) method = methodsMap[method];
3392 value = select2[method].apply(select2, args.slice(1));
3394 if (indexOf(args[0], valueMethods) >= 0
3395 || (indexOf(args[0], propertyMethods) >= 0 && args.length == 1)) {
3396 return false; // abort the iteration, ready to return first matched value
3399 throw "Invalid arguments to select2 plugin: " + args;
3402 return (value === undefined) ? this : value;
3405 // plugin defaults, accessible to users
3406 $.fn.select2.defaults = {
3409 closeOnSelect: true,
3413 containerCssClass: "",
3414 dropdownCssClass: "",
3415 formatResult: function(result, container, query, escapeMarkup) {
3417 markMatch(result.text, query.term, markup, escapeMarkup);
3418 return markup.join("");
3420 formatSelection: function (data, container, escapeMarkup) {
3421 return data ? escapeMarkup(data.text) : undefined;
3423 sortResults: function (results, container, query) {
3426 formatResultCssClass: function(data) {return data.css;},
3427 formatSelectionCssClass: function(data, container) {return undefined;},
3428 minimumResultsForSearch: 0,
3429 minimumInputLength: 0,
3430 maximumInputLength: null,
3431 maximumSelectionSize: 0,
3432 id: function (e) { return e == undefined ? null : e.id; },
3433 matcher: function(term, text) {
3434 return stripDiacritics(''+text).toUpperCase().indexOf(stripDiacritics(''+term).toUpperCase()) >= 0;
3437 tokenSeparators: [],
3438 tokenizer: defaultTokenizer,
3439 escapeMarkup: defaultEscapeMarkup,
3440 blurOnChange: false,
3441 selectOnBlur: false,
3442 adaptContainerCssClass: function(c) { return c; },
3443 adaptDropdownCssClass: function(c) { return null; },
3444 nextSearchTerm: function(selectedObject, currentSearchTerm) { return undefined; },
3445 searchInputPlaceholder: '',
3446 createSearchChoicePosition: 'top',
3447 shouldFocusInput: function (instance) {
3448 // Attempt to detect touch devices
3449 var supportsTouchEvents = (('ontouchstart' in window) ||
3450 (navigator.msMaxTouchPoints > 0));
3452 // Only devices which support touch events should be special cased
3453 if (!supportsTouchEvents) {
3457 // Never focus the input if search is disabled
3458 if (instance.opts.minimumResultsForSearch < 0) {
3466 $.fn.select2.locales = [];
3468 $.fn.select2.locales['en'] = {
3469 formatMatches: function (matches) { if (matches === 1) { return "One result is available, press enter to select it."; } return matches + " results are available, use up and down arrow keys to navigate."; },
3470 formatNoMatches: function () { return "No matches found"; },
3471 formatAjaxError: function (jqXHR, textStatus, errorThrown) { return "Loading failed"; },
3472 formatInputTooShort: function (input, min) { var n = min - input.length; return "Please enter " + n + " or more character" + (n == 1 ? "" : "s"); },
3473 formatInputTooLong: function (input, max) { var n = input.length - max; return "Please delete " + n + " character" + (n == 1 ? "" : "s"); },
3474 formatSelectionTooBig: function (limit) { return "You can only select " + limit + " item" + (limit == 1 ? "" : "s"); },
3475 formatLoadMore: function (pageNumber) { return "Loading more results…"; },
3476 formatSearching: function () { return "Searching…"; },
3479 $.extend($.fn.select2.defaults, $.fn.select2.locales['en']);
3481 $.fn.select2.ajaxDefaults = {
3498 markMatch: markMatch,
3499 escapeMarkup: defaultEscapeMarkup,
3500 stripDiacritics: stripDiacritics
3502 "abstract": AbstractSelect2,
3503 "single": SingleSelect2,
3504 "multi": MultiSelect2