1/* 2Copyright 2012 Igor Vaynberg 3 4Version: 3.4.6 Timestamp: Sat Mar 22 22:30:15 EDT 2014 5 6This software is licensed under the Apache License, Version 2.0 (the "Apache License") or the GNU 7General Public License version 2 (the "GPL License"). You may choose either license to govern your 8use of this software only upon the condition that you accept all of the terms of either the Apache 9License or the GPL License. 10 11You may obtain a copy of the Apache License and the GPL License at: 12 13 http://www.apache.org/licenses/LICENSE-2.0 14 http://www.gnu.org/licenses/gpl-2.0.html 15 16Unless required by applicable law or agreed to in writing, software distributed under the 17Apache License or the GPL License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 18CONDITIONS OF ANY KIND, either express or implied. See the Apache License and the GPL License for 19the specific language governing permissions and limitations under the Apache License and the GPL License. 20*/ 21(function ($) { 22 if(typeof $.fn.each2 == "undefined") { 23 $.extend($.fn, { 24 /* 25 * 4-10 times faster .each replacement 26 * use it carefully, as it overrides jQuery context of element on each iteration 27 */ 28 each2 : function (c) { 29 var j = $([0]), i = -1, l = this.length; 30 while ( 31 ++i < l 32 && (j.context = j[0] = this[i]) 33 && c.call(j[0], i, j) !== false //"this"=DOM, i=index, j=jQuery object 34 ); 35 return this; 36 } 37 }); 38 } 39})(jQuery); 40 41(function ($, undefined) { 42 "use strict"; 43 /*global document, window, jQuery, console */ 44 45 if (window.Select2 !== undefined) { 46 return; 47 } 48 49 var KEY, AbstractSelect2, SingleSelect2, MultiSelect2, nextUid, sizer, 50 lastMousePosition={x:0,y:0}, $document, scrollBarDimensions, 51 52 KEY = { 53 TAB: 9, 54 ENTER: 13, 55 ESC: 27, 56 SPACE: 32, 57 LEFT: 37, 58 UP: 38, 59 RIGHT: 39, 60 DOWN: 40, 61 SHIFT: 16, 62 CTRL: 17, 63 ALT: 18, 64 PAGE_UP: 33, 65 PAGE_DOWN: 34, 66 HOME: 36, 67 END: 35, 68 BACKSPACE: 8, 69 DELETE: 46, 70 isArrow: function (k) { 71 k = k.which ? k.which : k; 72 switch (k) { 73 case KEY.LEFT: 74 case KEY.RIGHT: 75 case KEY.UP: 76 case KEY.DOWN: 77 return true; 78 } 79 return false; 80 }, 81 isControl: function (e) { 82 var k = e.which; 83 switch (k) { 84 case KEY.SHIFT: 85 case KEY.CTRL: 86 case KEY.ALT: 87 return true; 88 } 89 90 if (e.metaKey) return true; 91 92 return false; 93 }, 94 isFunctionKey: function (k) { 95 k = k.which ? k.which : k; 96 return k >= 112 && k <= 123; 97 } 98 }, 99 MEASURE_SCROLLBAR_TEMPLATE = "<div class='select2-measure-scrollbar'></div>", 100 101 DIACRITICS = {"\u24B6":"A","\uFF21":"A","\u00C0":"A","\u00C1":"A","\u00C2":"A","\u1EA6":"A","\u1EA4":"A","\u1EAA":"A","\u1EA8":"A","\u00C3":"A","\u0100":"A","\u0102":"A","\u1EB0":"A","\u1EAE":"A","\u1EB4":"A","\u1EB2":"A","\u0226":"A","\u01E0":"A","\u00C4":"A","\u01DE":"A","\u1EA2":"A","\u00C5":"A","\u01FA":"A","\u01CD":"A","\u0200":"A","\u0202":"A","\u1EA0":"A","\u1EAC":"A","\u1EB6":"A","\u1E00":"A","\u0104":"A","\u023A":"A","\u2C6F":"A","\uA732":"AA","\u00C6":"AE","\u01FC":"AE","\u01E2":"AE","\uA734":"AO","\uA736":"AU","\uA738":"AV","\uA73A":"AV","\uA73C":"AY","\u24B7":"B","\uFF22":"B","\u1E02":"B","\u1E04":"B","\u1E06":"B","\u0243":"B","\u0182":"B","\u0181":"B","\u24B8":"C","\uFF23":"C","\u0106":"C","\u0108":"C","\u010A":"C","\u010C":"C","\u00C7":"C","\u1E08":"C","\u0187":"C","\u023B":"C","\uA73E":"C","\u24B9":"D","\uFF24":"D","\u1E0A":"D","\u010E":"D","\u1E0C":"D","\u1E10":"D","\u1E12":"D","\u1E0E":"D","\u0110":"D","\u018B":"D","\u018A":"D","\u0189":"D","\uA779":"D","\u01F1":"DZ","\u01C4":"DZ","\u01F2":"Dz","\u01C5":"Dz","\u24BA":"E","\uFF25":"E","\u00C8":"E","\u00C9":"E","\u00CA":"E","\u1EC0":"E","\u1EBE":"E","\u1EC4":"E","\u1EC2":"E","\u1EBC":"E","\u0112":"E","\u1E14":"E","\u1E16":"E","\u0114":"E","\u0116":"E","\u00CB":"E","\u1EBA":"E","\u011A":"E","\u0204":"E","\u0206":"E","\u1EB8":"E","\u1EC6":"E","\u0228":"E","\u1E1C":"E","\u0118":"E","\u1E18":"E","\u1E1A":"E","\u0190":"E","\u018E":"E","\u24BB":"F","\uFF26":"F","\u1E1E":"F","\u0191":"F","\uA77B":"F","\u24BC":"G","\uFF27":"G","\u01F4":"G","\u011C":"G","\u1E20":"G","\u011E":"G","\u0120":"G","\u01E6":"G","\u0122":"G","\u01E4":"G","\u0193":"G","\uA7A0":"G","\uA77D":"G","\uA77E":"G","\u24BD":"H","\uFF28":"H","\u0124":"H","\u1E22":"H","\u1E26":"H","\u021E":"H","\u1E24":"H","\u1E28":"H","\u1E2A":"H","\u0126":"H","\u2C67":"H","\u2C75":"H","\uA78D":"H","\u24BE":"I","\uFF29":"I","\u00CC":"I","\u00CD":"I","\u00CE":"I","\u0128":"I","\u012A":"I","\u012C":"I","\u0130":"I","\u00CF":"I","\u1E2E":"I","\u1EC8":"I","\u01CF":"I","\u0208":"I","\u020A":"I","\u1ECA":"I","\u012E":"I","\u1E2C":"I","\u0197":"I","\u24BF":"J","\uFF2A":"J","\u0134":"J","\u0248":"J","\u24C0":"K","\uFF2B":"K","\u1E30":"K","\u01E8":"K","\u1E32":"K","\u0136":"K","\u1E34":"K","\u0198":"K","\u2C69":"K","\uA740":"K","\uA742":"K","\uA744":"K","\uA7A2":"K","\u24C1":"L","\uFF2C":"L","\u013F":"L","\u0139":"L","\u013D":"L","\u1E36":"L","\u1E38":"L","\u013B":"L","\u1E3C":"L","\u1E3A":"L","\u0141":"L","\u023D":"L","\u2C62":"L","\u2C60":"L","\uA748":"L","\uA746":"L","\uA780":"L","\u01C7":"LJ","\u01C8":"Lj","\u24C2":"M","\uFF2D":"M","\u1E3E":"M","\u1E40":"M","\u1E42":"M","\u2C6E":"M","\u019C":"M","\u24C3":"N","\uFF2E":"N","\u01F8":"N","\u0143":"N","\u00D1":"N","\u1E44":"N","\u0147":"N","\u1E46":"N","\u0145":"N","\u1E4A":"N","\u1E48":"N","\u0220":"N","\u019D":"N","\uA790":"N","\uA7A4":"N","\u01CA":"NJ","\u01CB":"Nj","\u24C4":"O","\uFF2F":"O","\u00D2":"O","\u00D3":"O","\u00D4":"O","\u1ED2":"O","\u1ED0":"O","\u1ED6":"O","\u1ED4":"O","\u00D5":"O","\u1E4C":"O","\u022C":"O","\u1E4E":"O","\u014C":"O","\u1E50":"O","\u1E52":"O","\u014E":"O","\u022E":"O","\u0230":"O","\u00D6":"O","\u022A":"O","\u1ECE":"O","\u0150":"O","\u01D1":"O","\u020C":"O","\u020E":"O","\u01A0":"O","\u1EDC":"O","\u1EDA":"O","\u1EE0":"O","\u1EDE":"O","\u1EE2":"O","\u1ECC":"O","\u1ED8":"O","\u01EA":"O","\u01EC":"O","\u00D8":"O","\u01FE":"O","\u0186":"O","\u019F":"O","\uA74A":"O","\uA74C":"O","\u01A2":"OI","\uA74E":"OO","\u0222":"OU","\u24C5":"P","\uFF30":"P","\u1E54":"P","\u1E56":"P","\u01A4":"P","\u2C63":"P","\uA750":"P","\uA752":"P","\uA754":"P","\u24C6":"Q","\uFF31":"Q","\uA756":"Q","\uA758":"Q","\u024A":"Q","\u24C7":"R","\uFF32":"R","\u0154":"R","\u1E58":"R","\u0158":"R","\u0210":"R","\u0212":"R","\u1E5A":"R","\u1E5C":"R","\u0156":"R","\u1E5E":"R","\u024C":"R","\u2C64":"R","\uA75A":"R","\uA7A6":"R","\uA782":"R","\u24C8":"S","\uFF33":"S","\u1E9E":"S","\u015A":"S","\u1E64":"S","\u015C":"S","\u1E60":"S","\u0160":"S","\u1E66":"S","\u1E62":"S","\u1E68":"S","\u0218":"S","\u015E":"S","\u2C7E":"S","\uA7A8":"S","\uA784":"S","\u24C9":"T","\uFF34":"T","\u1E6A":"T","\u0164":"T","\u1E6C":"T","\u021A":"T","\u0162":"T","\u1E70":"T","\u1E6E":"T","\u0166":"T","\u01AC":"T","\u01AE":"T","\u023E":"T","\uA786":"T","\uA728":"TZ","\u24CA":"U","\uFF35":"U","\u00D9":"U","\u00DA":"U","\u00DB":"U","\u0168":"U","\u1E78":"U","\u016A":"U","\u1E7A":"U","\u016C":"U","\u00DC":"U","\u01DB":"U","\u01D7":"U","\u01D5":"U","\u01D9":"U","\u1EE6":"U","\u016E":"U","\u0170":"U","\u01D3":"U","\u0214":"U","\u0216":"U","\u01AF":"U","\u1EEA":"U","\u1EE8":"U","\u1EEE":"U","\u1EEC":"U","\u1EF0":"U","\u1EE4":"U","\u1E72":"U","\u0172":"U","\u1E76":"U","\u1E74":"U","\u0244":"U","\u24CB":"V","\uFF36":"V","\u1E7C":"V","\u1E7E":"V","\u01B2":"V","\uA75E":"V","\u0245":"V","\uA760":"VY","\u24CC":"W","\uFF37":"W","\u1E80":"W","\u1E82":"W","\u0174":"W","\u1E86":"W","\u1E84":"W","\u1E88":"W","\u2C72":"W","\u24CD":"X","\uFF38":"X","\u1E8A":"X","\u1E8C":"X","\u24CE":"Y","\uFF39":"Y","\u1EF2":"Y","\u00DD":"Y","\u0176":"Y","\u1EF8":"Y","\u0232":"Y","\u1E8E":"Y","\u0178":"Y","\u1EF6":"Y","\u1EF4":"Y","\u01B3":"Y","\u024E":"Y","\u1EFE":"Y","\u24CF":"Z","\uFF3A":"Z","\u0179":"Z","\u1E90":"Z","\u017B":"Z","\u017D":"Z","\u1E92":"Z","\u1E94":"Z","\u01B5":"Z","\u0224":"Z","\u2C7F":"Z","\u2C6B":"Z","\uA762":"Z","\u24D0":"a","\uFF41":"a","\u1E9A":"a","\u00E0":"a","\u00E1":"a","\u00E2":"a","\u1EA7":"a","\u1EA5":"a","\u1EAB":"a","\u1EA9":"a","\u00E3":"a","\u0101":"a","\u0103":"a","\u1EB1":"a","\u1EAF":"a","\u1EB5":"a","\u1EB3":"a","\u0227":"a","\u01E1":"a","\u00E4":"a","\u01DF":"a","\u1EA3":"a","\u00E5":"a","\u01FB":"a","\u01CE":"a","\u0201":"a","\u0203":"a","\u1EA1":"a","\u1EAD":"a","\u1EB7":"a","\u1E01":"a","\u0105":"a","\u2C65":"a","\u0250":"a","\uA733":"aa","\u00E6":"ae","\u01FD":"ae","\u01E3":"ae","\uA735":"ao","\uA737":"au","\uA739":"av","\uA73B":"av","\uA73D":"ay","\u24D1":"b","\uFF42":"b","\u1E03":"b","\u1E05":"b","\u1E07":"b","\u0180":"b","\u0183":"b","\u0253":"b","\u24D2":"c","\uFF43":"c","\u0107":"c","\u0109":"c","\u010B":"c","\u010D":"c","\u00E7":"c","\u1E09":"c","\u0188":"c","\u023C":"c","\uA73F":"c","\u2184":"c","\u24D3":"d","\uFF44":"d","\u1E0B":"d","\u010F":"d","\u1E0D":"d","\u1E11":"d","\u1E13":"d","\u1E0F":"d","\u0111":"d","\u018C":"d","\u0256":"d","\u0257":"d","\uA77A":"d","\u01F3":"dz","\u01C6":"dz","\u24D4":"e","\uFF45":"e","\u00E8":"e","\u00E9":"e","\u00EA":"e","\u1EC1":"e","\u1EBF":"e","\u1EC5":"e","\u1EC3":"e","\u1EBD":"e","\u0113":"e","\u1E15":"e","\u1E17":"e","\u0115":"e","\u0117":"e","\u00EB":"e","\u1EBB":"e","\u011B":"e","\u0205":"e","\u0207":"e","\u1EB9":"e","\u1EC7":"e","\u0229":"e","\u1E1D":"e","\u0119":"e","\u1E19":"e","\u1E1B":"e","\u0247":"e","\u025B":"e","\u01DD":"e","\u24D5":"f","\uFF46":"f","\u1E1F":"f","\u0192":"f","\uA77C":"f","\u24D6":"g","\uFF47":"g","\u01F5":"g","\u011D":"g","\u1E21":"g","\u011F":"g","\u0121":"g","\u01E7":"g","\u0123":"g","\u01E5":"g","\u0260":"g","\uA7A1":"g","\u1D79":"g","\uA77F":"g","\u24D7":"h","\uFF48":"h","\u0125":"h","\u1E23":"h","\u1E27":"h","\u021F":"h","\u1E25":"h","\u1E29":"h","\u1E2B":"h","\u1E96":"h","\u0127":"h","\u2C68":"h","\u2C76":"h","\u0265":"h","\u0195":"hv","\u24D8":"i","\uFF49":"i","\u00EC":"i","\u00ED":"i","\u00EE":"i","\u0129":"i","\u012B":"i","\u012D":"i","\u00EF":"i","\u1E2F":"i","\u1EC9":"i","\u01D0":"i","\u0209":"i","\u020B":"i","\u1ECB":"i","\u012F":"i","\u1E2D":"i","\u0268":"i","\u0131":"i","\u24D9":"j","\uFF4A":"j","\u0135":"j","\u01F0":"j","\u0249":"j","\u24DA":"k","\uFF4B":"k","\u1E31":"k","\u01E9":"k","\u1E33":"k","\u0137":"k","\u1E35":"k","\u0199":"k","\u2C6A":"k","\uA741":"k","\uA743":"k","\uA745":"k","\uA7A3":"k","\u24DB":"l","\uFF4C":"l","\u0140":"l","\u013A":"l","\u013E":"l","\u1E37":"l","\u1E39":"l","\u013C":"l","\u1E3D":"l","\u1E3B":"l","\u017F":"l","\u0142":"l","\u019A":"l","\u026B":"l","\u2C61":"l","\uA749":"l","\uA781":"l","\uA747":"l","\u01C9":"lj","\u24DC":"m","\uFF4D":"m","\u1E3F":"m","\u1E41":"m","\u1E43":"m","\u0271":"m","\u026F":"m","\u24DD":"n","\uFF4E":"n","\u01F9":"n","\u0144":"n","\u00F1":"n","\u1E45":"n","\u0148":"n","\u1E47":"n","\u0146":"n","\u1E4B":"n","\u1E49":"n","\u019E":"n","\u0272":"n","\u0149":"n","\uA791":"n","\uA7A5":"n","\u01CC":"nj","\u24DE":"o","\uFF4F":"o","\u00F2":"o","\u00F3":"o","\u00F4":"o","\u1ED3":"o","\u1ED1":"o","\u1ED7":"o","\u1ED5":"o","\u00F5":"o","\u1E4D":"o","\u022D":"o","\u1E4F":"o","\u014D":"o","\u1E51":"o","\u1E53":"o","\u014F":"o","\u022F":"o","\u0231":"o","\u00F6":"o","\u022B":"o","\u1ECF":"o","\u0151":"o","\u01D2":"o","\u020D":"o","\u020F":"o","\u01A1":"o","\u1EDD":"o","\u1EDB":"o","\u1EE1":"o","\u1EDF":"o","\u1EE3":"o","\u1ECD":"o","\u1ED9":"o","\u01EB":"o","\u01ED":"o","\u00F8":"o","\u01FF":"o","\u0254":"o","\uA74B":"o","\uA74D":"o","\u0275":"o","\u01A3":"oi","\u0223":"ou","\uA74F":"oo","\u24DF":"p","\uFF50":"p","\u1E55":"p","\u1E57":"p","\u01A5":"p","\u1D7D":"p","\uA751":"p","\uA753":"p","\uA755":"p","\u24E0":"q","\uFF51":"q","\u024B":"q","\uA757":"q","\uA759":"q","\u24E1":"r","\uFF52":"r","\u0155":"r","\u1E59":"r","\u0159":"r","\u0211":"r","\u0213":"r","\u1E5B":"r","\u1E5D":"r","\u0157":"r","\u1E5F":"r","\u024D":"r","\u027D":"r","\uA75B":"r","\uA7A7":"r","\uA783":"r","\u24E2":"s","\uFF53":"s","\u00DF":"s","\u015B":"s","\u1E65":"s","\u015D":"s","\u1E61":"s","\u0161":"s","\u1E67":"s","\u1E63":"s","\u1E69":"s","\u0219":"s","\u015F":"s","\u023F":"s","\uA7A9":"s","\uA785":"s","\u1E9B":"s","\u24E3":"t","\uFF54":"t","\u1E6B":"t","\u1E97":"t","\u0165":"t","\u1E6D":"t","\u021B":"t","\u0163":"t","\u1E71":"t","\u1E6F":"t","\u0167":"t","\u01AD":"t","\u0288":"t","\u2C66":"t","\uA787":"t","\uA729":"tz","\u24E4":"u","\uFF55":"u","\u00F9":"u","\u00FA":"u","\u00FB":"u","\u0169":"u","\u1E79":"u","\u016B":"u","\u1E7B":"u","\u016D":"u","\u00FC":"u","\u01DC":"u","\u01D8":"u","\u01D6":"u","\u01DA":"u","\u1EE7":"u","\u016F":"u","\u0171":"u","\u01D4":"u","\u0215":"u","\u0217":"u","\u01B0":"u","\u1EEB":"u","\u1EE9":"u","\u1EEF":"u","\u1EED":"u","\u1EF1":"u","\u1EE5":"u","\u1E73":"u","\u0173":"u","\u1E77":"u","\u1E75":"u","\u0289":"u","\u24E5":"v","\uFF56":"v","\u1E7D":"v","\u1E7F":"v","\u028B":"v","\uA75F":"v","\u028C":"v","\uA761":"vy","\u24E6":"w","\uFF57":"w","\u1E81":"w","\u1E83":"w","\u0175":"w","\u1E87":"w","\u1E85":"w","\u1E98":"w","\u1E89":"w","\u2C73":"w","\u24E7":"x","\uFF58":"x","\u1E8B":"x","\u1E8D":"x","\u24E8":"y","\uFF59":"y","\u1EF3":"y","\u00FD":"y","\u0177":"y","\u1EF9":"y","\u0233":"y","\u1E8F":"y","\u00FF":"y","\u1EF7":"y","\u1E99":"y","\u1EF5":"y","\u01B4":"y","\u024F":"y","\u1EFF":"y","\u24E9":"z","\uFF5A":"z","\u017A":"z","\u1E91":"z","\u017C":"z","\u017E":"z","\u1E93":"z","\u1E95":"z","\u01B6":"z","\u0225":"z","\u0240":"z","\u2C6C":"z","\uA763":"z"}; 102 103 $document = $(document); 104 105 nextUid=(function() { var counter=1; return function() { return counter++; }; }()); 106 107 108 function reinsertElement(element) { 109 var placeholder = $(document.createTextNode('')); 110 111 element.before(placeholder); 112 placeholder.before(element); 113 placeholder.remove(); 114 } 115 116 function stripDiacritics(str) { 117 var ret, i, l, c; 118 119 if (!str || str.length < 1) return str; 120 121 ret = ""; 122 for (i = 0, l = str.length; i < l; i++) { 123 c = str.charAt(i); 124 ret += DIACRITICS[c] || c; 125 } 126 return ret; 127 } 128 129 function indexOf(value, array) { 130 var i = 0, l = array.length; 131 for (; i < l; i = i + 1) { 132 if (equal(value, array[i])) return i; 133 } 134 return -1; 135 } 136 137 function measureScrollbar () { 138 var $template = $( MEASURE_SCROLLBAR_TEMPLATE ); 139 $template.appendTo('body'); 140 141 var dim = { 142 width: $template.width() - $template[0].clientWidth, 143 height: $template.height() - $template[0].clientHeight 144 }; 145 $template.remove(); 146 147 return dim; 148 } 149 150 /** 151 * Compares equality of a and b 152 * @param a 153 * @param b 154 */ 155 function equal(a, b) { 156 if (a === b) return true; 157 if (a === undefined || b === undefined) return false; 158 if (a === null || b === null) return false; 159 // Check whether 'a' or 'b' is a string (primitive or object). 160 // The concatenation of an empty string (+'') converts its argument to a string's primitive. 161 if (a.constructor === String) return a+'' === b+''; // a+'' - in case 'a' is a String object 162 if (b.constructor === String) return b+'' === a+''; // b+'' - in case 'b' is a String object 163 return false; 164 } 165 166 /** 167 * Splits the string into an array of values, trimming each value. An empty array is returned for nulls or empty 168 * strings 169 * @param string 170 * @param separator 171 */ 172 function splitVal(string, separator) { 173 var val, i, l; 174 if (string === null || string.length < 1) return []; 175 val = string.split(separator); 176 for (i = 0, l = val.length; i < l; i = i + 1) val[i] = $.trim(val[i]); 177 return val; 178 } 179 180 function getSideBorderPadding(element) { 181 return element.outerWidth(false) - element.width(); 182 } 183 184 function installKeyUpChangeEvent(element) { 185 var key="keyup-change-value"; 186 element.on("keydown", function () { 187 if ($.data(element, key) === undefined) { 188 $.data(element, key, element.val()); 189 } 190 }); 191 element.on("keyup", function () { 192 var val= $.data(element, key); 193 if (val !== undefined && element.val() !== val) { 194 $.removeData(element, key); 195 element.trigger("keyup-change"); 196 } 197 }); 198 } 199 200 $document.on("mousemove", function (e) { 201 lastMousePosition.x = e.pageX; 202 lastMousePosition.y = e.pageY; 203 }); 204 205 /** 206 * filters mouse events so an event is fired only if the mouse moved. 207 * 208 * filters out mouse events that occur when mouse is stationary but 209 * the elements under the pointer are scrolled. 210 */ 211 function installFilteredMouseMove(element) { 212 element.on("mousemove", function (e) { 213 var lastpos = lastMousePosition; 214 if (lastpos === undefined || lastpos.x !== e.pageX || lastpos.y !== e.pageY) { 215 $(e.target).trigger("mousemove-filtered", e); 216 } 217 }); 218 } 219 220 /** 221 * Debounces a function. Returns a function that calls the original fn function only if no invocations have been made 222 * within the last quietMillis milliseconds. 223 * 224 * @param quietMillis number of milliseconds to wait before invoking fn 225 * @param fn function to be debounced 226 * @param ctx object to be used as this reference within fn 227 * @return debounced version of fn 228 */ 229 function debounce(quietMillis, fn, ctx) { 230 ctx = ctx || undefined; 231 var timeout; 232 return function () { 233 var args = arguments; 234 window.clearTimeout(timeout); 235 timeout = window.setTimeout(function() { 236 fn.apply(ctx, args); 237 }, quietMillis); 238 }; 239 } 240 241 /** 242 * A simple implementation of a thunk 243 * @param formula function used to lazily initialize the thunk 244 * @return {Function} 245 */ 246 function thunk(formula) { 247 var evaluated = false, 248 value; 249 return function() { 250 if (evaluated === false) { value = formula(); evaluated = true; } 251 return value; 252 }; 253 }; 254 255 function installDebouncedScroll(threshold, element) { 256 var notify = debounce(threshold, function (e) { element.trigger("scroll-debounced", e);}); 257 element.on("scroll", function (e) { 258 if (indexOf(e.target, element.get()) >= 0) notify(e); 259 }); 260 } 261 262 function focus($el) { 263 if ($el[0] === document.activeElement) return; 264 265 /* set the focus in a 0 timeout - that way the focus is set after the processing 266 of the current event has finished - which seems like the only reliable way 267 to set focus */ 268 window.setTimeout(function() { 269 var el=$el[0], pos=$el.val().length, range; 270 271 $el.focus(); 272 273 /* make sure el received focus so we do not error out when trying to manipulate the caret. 274 sometimes modals or others listeners may steal it after its set */ 275 var isVisible = (el.offsetWidth > 0 || el.offsetHeight > 0); 276 if (isVisible && el === document.activeElement) { 277 278 /* after the focus is set move the caret to the end, necessary when we val() 279 just before setting focus */ 280 if(el.setSelectionRange) 281 { 282 el.setSelectionRange(pos, pos); 283 } 284 else if (el.createTextRange) { 285 range = el.createTextRange(); 286 range.collapse(false); 287 range.select(); 288 } 289 } 290 }, 0); 291 } 292 293 function getCursorInfo(el) { 294 el = $(el)[0]; 295 var offset = 0; 296 var length = 0; 297 if ('selectionStart' in el) { 298 offset = el.selectionStart; 299 length = el.selectionEnd - offset; 300 } else if ('selection' in document) { 301 el.focus(); 302 var sel = document.selection.createRange(); 303 length = document.selection.createRange().text.length; 304 sel.moveStart('character', -el.value.length); 305 offset = sel.text.length - length; 306 } 307 return { offset: offset, length: length }; 308 } 309 310 function killEvent(event) { 311 event.preventDefault(); 312 event.stopPropagation(); 313 } 314 function killEventImmediately(event) { 315 event.preventDefault(); 316 event.stopImmediatePropagation(); 317 } 318 319 function measureTextWidth(e) { 320 if (!sizer){ 321 var style = e[0].currentStyle || window.getComputedStyle(e[0], null); 322 sizer = $(document.createElement("div")).css({ 323 position: "absolute", 324 left: "-10000px", 325 top: "-10000px", 326 display: "none", 327 fontSize: style.fontSize, 328 fontFamily: style.fontFamily, 329 fontStyle: style.fontStyle, 330 fontWeight: style.fontWeight, 331 letterSpacing: style.letterSpacing, 332 textTransform: style.textTransform, 333 whiteSpace: "nowrap" 334 }); 335 sizer.attr("class","select2-sizer"); 336 $("body").append(sizer); 337 } 338 sizer.text(e.val()); 339 return sizer.width(); 340 } 341 342 function syncCssClasses(dest, src, adapter) { 343 var classes, replacements = [], adapted; 344 345 classes = dest.attr("class"); 346 if (classes) { 347 classes = '' + classes; // for IE which returns object 348 $(classes.split(" ")).each2(function() { 349 if (this.indexOf("select2-") === 0) { 350 replacements.push(this); 351 } 352 }); 353 } 354 classes = src.attr("class"); 355 if (classes) { 356 classes = '' + classes; // for IE which returns object 357 $(classes.split(" ")).each2(function() { 358 if (this.indexOf("select2-") !== 0) { 359 adapted = adapter(this); 360 if (adapted) { 361 replacements.push(adapted); 362 } 363 } 364 }); 365 } 366 dest.attr("class", replacements.join(" ")); 367 } 368 369 370 function markMatch(text, term, markup, escapeMarkup) { 371 var match=stripDiacritics(text.toUpperCase()).indexOf(stripDiacritics(term.toUpperCase())), 372 tl=term.length; 373 374 if (match<0) { 375 markup.push(escapeMarkup(text)); 376 return; 377 } 378 379 markup.push(escapeMarkup(text.substring(0, match))); 380 markup.push("<span class='select2-match'>"); 381 markup.push(escapeMarkup(text.substring(match, match + tl))); 382 markup.push("</span>"); 383 markup.push(escapeMarkup(text.substring(match + tl, text.length))); 384 } 385 386 function defaultEscapeMarkup(markup) { 387 var replace_map = { 388 '\\': '\', 389 '&': '&', 390 '<': '<', 391 '>': '>', 392 '"': '"', 393 "'": ''', 394 "/": '/' 395 }; 396 397 return String(markup).replace(/[&<>"'\/\\]/g, function (match) { 398 return replace_map[match]; 399 }); 400 } 401 402 /** 403 * Produces an ajax-based query function 404 * 405 * @param options object containing configuration parameters 406 * @param options.params parameter map for the transport ajax call, can contain such options as cache, jsonpCallback, etc. see $.ajax 407 * @param options.transport function that will be used to execute the ajax request. must be compatible with parameters supported by $.ajax 408 * @param options.url url for the data 409 * @param options.data a function(searchTerm, pageNumber, context) that should return an object containing query string parameters for the above url. 410 * @param options.dataType request data type: ajax, jsonp, other datatypes supported by jQuery's $.ajax function or the transport function if specified 411 * @param options.quietMillis (optional) milliseconds to wait before making the ajaxRequest, helps debounce the ajax function if invoked too often 412 * @param options.results a function(remoteData, pageNumber) that converts data returned form the remote request to the format expected by Select2. 413 * The expected format is an object containing the following keys: 414 * results array of objects that will be used as choices 415 * more (optional) boolean indicating whether there are more results available 416 * Example: {results:[{id:1, text:'Red'},{id:2, text:'Blue'}], more:true} 417 */ 418 function ajax(options) { 419 var timeout, // current scheduled but not yet executed request 420 handler = null, 421 quietMillis = options.quietMillis || 100, 422 ajaxUrl = options.url, 423 self = this; 424 425 return function (query) { 426 window.clearTimeout(timeout); 427 timeout = window.setTimeout(function () { 428 var data = options.data, // ajax data function 429 url = ajaxUrl, // ajax url string or function 430 transport = options.transport || $.fn.select2.ajaxDefaults.transport, 431 // deprecated - to be removed in 4.0 - use params instead 432 deprecated = { 433 type: options.type || 'GET', // set type of request (GET or POST) 434 cache: options.cache || false, 435 jsonpCallback: options.jsonpCallback||undefined, 436 dataType: options.dataType||"json" 437 }, 438 params = $.extend({}, $.fn.select2.ajaxDefaults.params, deprecated); 439 440 data = data ? data.call(self, query.term, query.page, query.context) : null; 441 url = (typeof url === 'function') ? url.call(self, query.term, query.page, query.context) : url; 442 443 if (handler && typeof handler.abort === "function") { handler.abort(); } 444 445 if (options.params) { 446 if ($.isFunction(options.params)) { 447 $.extend(params, options.params.call(self)); 448 } else { 449 $.extend(params, options.params); 450 } 451 } 452 453 $.extend(params, { 454 url: url, 455 dataType: options.dataType, 456 data: data, 457 success: function (data) { 458 // TODO - replace query.page with query so users have access to term, page, etc. 459 var results = options.results(data, query.page); 460 query.callback(results); 461 } 462 }); 463 handler = transport.call(self, params); 464 }, quietMillis); 465 }; 466 } 467 468 /** 469 * Produces a query function that works with a local array 470 * 471 * @param options object containing configuration parameters. The options parameter can either be an array or an 472 * object. 473 * 474 * If the array form is used it is assumed that it contains objects with 'id' and 'text' keys. 475 * 476 * If the object form is used ti is assumed that it contains 'data' and 'text' keys. The 'data' key should contain 477 * an array of objects that will be used as choices. These objects must contain at least an 'id' key. The 'text' 478 * key can either be a String in which case it is expected that each element in the 'data' array has a key with the 479 * value of 'text' which will be used to match choices. Alternatively, text can be a function(item) that can extract 480 * the text. 481 */ 482 function local(options) { 483 var data = options, // data elements 484 dataText, 485 tmp, 486 text = function (item) { return ""+item.text; }; // function used to retrieve the text portion of a data item that is matched against the search 487 488 if ($.isArray(data)) { 489 tmp = data; 490 data = { results: tmp }; 491 } 492 493 if ($.isFunction(data) === false) { 494 tmp = data; 495 data = function() { return tmp; }; 496 } 497 498 var dataItem = data(); 499 if (dataItem.text) { 500 text = dataItem.text; 501 // if text is not a function we assume it to be a key name 502 if (!$.isFunction(text)) { 503 dataText = dataItem.text; // we need to store this in a separate variable because in the next step data gets reset and data.text is no longer available 504 text = function (item) { return item[dataText]; }; 505 } 506 } 507 508 return function (query) { 509 var t = query.term, filtered = { results: [] }, process; 510 if (t === "") { 511 query.callback(data()); 512 return; 513 } 514 515 process = function(datum, collection) { 516 var group, attr; 517 datum = datum[0]; 518 if (datum.children) { 519 group = {}; 520 for (attr in datum) { 521 if (datum.hasOwnProperty(attr)) group[attr]=datum[attr]; 522 } 523 group.children=[]; 524 $(datum.children).each2(function(i, childDatum) { process(childDatum, group.children); }); 525 if (group.children.length || query.matcher(t, text(group), datum)) { 526 collection.push(group); 527 } 528 } else { 529 if (query.matcher(t, text(datum), datum)) { 530 collection.push(datum); 531 } 532 } 533 }; 534 535 $(data().results).each2(function(i, datum) { process(datum, filtered.results); }); 536 query.callback(filtered); 537 }; 538 } 539 540 // TODO javadoc 541 function tags(data) { 542 var isFunc = $.isFunction(data); 543 return function (query) { 544 var t = query.term, filtered = {results: []}; 545 $(isFunc ? data() : data).each(function () { 546 var isObject = this.text !== undefined, 547 text = isObject ? this.text : this; 548 if (t === "" || query.matcher(t, text)) { 549 filtered.results.push(isObject ? this : {id: this, text: this}); 550 } 551 }); 552 query.callback(filtered); 553 }; 554 } 555 556 /** 557 * Checks if the formatter function should be used. 558 * 559 * Throws an error if it is not a function. Returns true if it should be used, 560 * false if no formatting should be performed. 561 * 562 * @param formatter 563 */ 564 function checkFormatter(formatter, formatterName) { 565 if ($.isFunction(formatter)) return true; 566 if (!formatter) return false; 567 if (typeof(formatter) === 'string') return true; 568 throw new Error(formatterName +" must be a string, function, or falsy value"); 569 } 570 571 function evaluate(val) { 572 if ($.isFunction(val)) { 573 var args = Array.prototype.slice.call(arguments, 1); 574 return val.apply(null, args); 575 } 576 return val; 577 } 578 579 function countResults(results) { 580 var count = 0; 581 $.each(results, function(i, item) { 582 if (item.children) { 583 count += countResults(item.children); 584 } else { 585 count++; 586 } 587 }); 588 return count; 589 } 590 591 /** 592 * Default tokenizer. This function uses breaks the input on substring match of any string from the 593 * opts.tokenSeparators array and uses opts.createSearchChoice to create the choice object. Both of those 594 * two options have to be defined in order for the tokenizer to work. 595 * 596 * @param input text user has typed so far or pasted into the search field 597 * @param selection currently selected choices 598 * @param selectCallback function(choice) callback tho add the choice to selection 599 * @param opts select2's opts 600 * @return undefined/null to leave the current input unchanged, or a string to change the input to the returned value 601 */ 602 function defaultTokenizer(input, selection, selectCallback, opts) { 603 var original = input, // store the original so we can compare and know if we need to tell the search to update its text 604 dupe = false, // check for whether a token we extracted represents a duplicate selected choice 605 token, // token 606 index, // position at which the separator was found 607 i, l, // looping variables 608 separator; // the matched separator 609 610 if (!opts.createSearchChoice || !opts.tokenSeparators || opts.tokenSeparators.length < 1) return undefined; 611 612 while (true) { 613 index = -1; 614 615 for (i = 0, l = opts.tokenSeparators.length; i < l; i++) { 616 separator = opts.tokenSeparators[i]; 617 index = input.indexOf(separator); 618 if (index >= 0) break; 619 } 620 621 if (index < 0) break; // did not find any token separator in the input string, bail 622 623 token = input.substring(0, index); 624 input = input.substring(index + separator.length); 625 626 if (token.length > 0) { 627 token = opts.createSearchChoice.call(this, token, selection); 628 if (token !== undefined && token !== null && opts.id(token) !== undefined && opts.id(token) !== null) { 629 dupe = false; 630 for (i = 0, l = selection.length; i < l; i++) { 631 if (equal(opts.id(token), opts.id(selection[i]))) { 632 dupe = true; break; 633 } 634 } 635 636 if (!dupe) selectCallback(token); 637 } 638 } 639 } 640 641 if (original!==input) return input; 642 } 643 644 /** 645 * Creates a new class 646 * 647 * @param superClass 648 * @param methods 649 */ 650 function clazz(SuperClass, methods) { 651 var constructor = function () {}; 652 constructor.prototype = new SuperClass; 653 constructor.prototype.constructor = constructor; 654 constructor.prototype.parent = SuperClass.prototype; 655 constructor.prototype = $.extend(constructor.prototype, methods); 656 return constructor; 657 } 658 659 AbstractSelect2 = clazz(Object, { 660 661 // abstract 662 bind: function (func) { 663 var self = this; 664 return function () { 665 func.apply(self, arguments); 666 }; 667 }, 668 669 // abstract 670 init: function (opts) { 671 var results, search, resultsSelector = ".select2-results"; 672 673 // prepare options 674 this.opts = opts = this.prepareOpts(opts); 675 676 this.id=opts.id; 677 678 // destroy if called on an existing component 679 if (opts.element.data("select2") !== undefined && 680 opts.element.data("select2") !== null) { 681 opts.element.data("select2").destroy(); 682 } 683 684 this.container = this.createContainer(); 685 686 this.liveRegion = $("<span>", { 687 role: "status", 688 "aria-live": "polite" 689 }) 690 .addClass("select2-hidden-accessible") 691 .appendTo(document.body); 692 693 this.containerId="s2id_"+(opts.element.attr("id") || "autogen"+nextUid()).replace(/([;&,\-\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '\\$1'); 694 this.containerSelector="#"+this.containerId; 695 this.container.attr("id", this.containerId); 696 697 // cache the body so future lookups are cheap 698 this.body = thunk(function() { return opts.element.closest("body"); }); 699 700 syncCssClasses(this.container, this.opts.element, this.opts.adaptContainerCssClass); 701 702 this.container.attr("style", opts.element.attr("style")); 703 this.container.css(evaluate(opts.containerCss)); 704 this.container.addClass(evaluate(opts.containerCssClass)); 705 706 this.elementTabIndex = this.opts.element.attr("tabindex"); 707 708 // swap container for the element 709 this.opts.element 710 .data("select2", this) 711 .attr("tabindex", "-1") 712 .before(this.container) 713 .on("click.select2", killEvent); // do not leak click events 714 715 this.container.data("select2", this); 716 717 this.dropdown = this.container.find(".select2-drop"); 718 719 syncCssClasses(this.dropdown, this.opts.element, this.opts.adaptDropdownCssClass); 720 721 this.dropdown.addClass(evaluate(opts.dropdownCssClass)); 722 this.dropdown.data("select2", this); 723 this.dropdown.on("click", killEvent); 724 725 this.results = results = this.container.find(resultsSelector); 726 this.search = search = this.container.find("input.select2-input"); 727 728 this.queryCount = 0; 729 this.resultsPage = 0; 730 this.context = null; 731 732 // initialize the container 733 this.initContainer(); 734 735 this.container.on("click", killEvent); 736 737 installFilteredMouseMove(this.results); 738 this.dropdown.on("mousemove-filtered touchstart touchmove touchend", resultsSelector, this.bind(this.highlightUnderEvent)); 739 this.dropdown.on("touchend", resultsSelector, this.bind(this.selectHighlighted)); 740 this.dropdown.on("touchmove", resultsSelector, this.bind(this.touchMoved)); 741 this.dropdown.on("touchstart touchend", resultsSelector, this.bind(this.clearTouchMoved)); 742 743 installDebouncedScroll(80, this.results); 744 this.dropdown.on("scroll-debounced", resultsSelector, this.bind(this.loadMoreIfNeeded)); 745 746 // do not propagate change event from the search field out of the component 747 $(this.container).on("change", ".select2-input", function(e) {e.stopPropagation();}); 748 $(this.dropdown).on("change", ".select2-input", function(e) {e.stopPropagation();}); 749 750 // if jquery.mousewheel plugin is installed we can prevent out-of-bounds scrolling of results via mousewheel 751 if ($.fn.mousewheel) { 752 results.mousewheel(function (e, delta, deltaX, deltaY) { 753 var top = results.scrollTop(); 754 if (deltaY > 0 && top - deltaY <= 0) { 755 results.scrollTop(0); 756 killEvent(e); 757 } else if (deltaY < 0 && results.get(0).scrollHeight - results.scrollTop() + deltaY <= results.height()) { 758 results.scrollTop(results.get(0).scrollHeight - results.height()); 759 killEvent(e); 760 } 761 }); 762 } 763 764 installKeyUpChangeEvent(search); 765 search.on("keyup-change input paste", this.bind(this.updateResults)); 766 search.on("focus", function () { search.addClass("select2-focused"); }); 767 search.on("blur", function () { search.removeClass("select2-focused");}); 768 769 this.dropdown.on("mouseup", resultsSelector, this.bind(function (e) { 770 if ($(e.target).closest(".select2-result-selectable").length > 0) { 771 this.highlightUnderEvent(e); 772 this.selectHighlighted(e); 773 } 774 })); 775 776 // trap all mouse events from leaving the dropdown. sometimes there may be a modal that is listening 777 // for mouse events outside of itself so it can close itself. since the dropdown is now outside the select2's 778 // dom it will trigger the popup close, which is not what we want 779 // focusin can cause focus wars between modals and select2 since the dropdown is outside the modal. 780 this.dropdown.on("click mouseup mousedown focusin", function (e) { e.stopPropagation(); }); 781 782 this.nextSearchTerm = undefined; 783 784 if ($.isFunction(this.opts.initSelection)) { 785 // initialize selection based on the current value of the source element 786 this.initSelection(); 787 788 // if the user has provided a function that can set selection based on the value of the source element 789 // we monitor the change event on the element and trigger it, allowing for two way synchronization 790 this.monitorSource(); 791 } 792 793 if (opts.maximumInputLength !== null) { 794 this.search.attr("maxlength", opts.maximumInputLength); 795 } 796 797 var disabled = opts.element.prop("disabled"); 798 if (disabled === undefined) disabled = false; 799 this.enable(!disabled); 800 801 var readonly = opts.element.prop("readonly"); 802 if (readonly === undefined) readonly = false; 803 this.readonly(readonly); 804 805 // Calculate size of scrollbar 806 scrollBarDimensions = scrollBarDimensions || measureScrollbar(); 807 808 this.autofocus = opts.element.prop("autofocus"); 809 opts.element.prop("autofocus", false); 810 if (this.autofocus) this.focus(); 811 812 this.search.attr("placeholder", opts.searchInputPlaceholder); 813 }, 814 815 // abstract 816 destroy: function () { 817 var element=this.opts.element, select2 = element.data("select2"); 818 819 this.close(); 820 821 if (this.propertyObserver) { delete this.propertyObserver; this.propertyObserver = null; } 822 823 if (select2 !== undefined) { 824 select2.container.remove(); 825 select2.liveRegion.remove(); 826 select2.dropdown.remove(); 827 element 828 .removeClass("select2-offscreen") 829 .removeData("select2") 830 .off(".select2") 831 .prop("autofocus", this.autofocus || false); 832 if (this.elementTabIndex) { 833 element.attr({tabindex: this.elementTabIndex}); 834 } else { 835 element.removeAttr("tabindex"); 836 } 837 element.show(); 838 } 839 }, 840 841 // abstract 842 optionToData: function(element) { 843 if (element.is("option")) { 844 return { 845 id:element.prop("value"), 846 text:element.text(), 847 element: element.get(), 848 css: element.attr("class"), 849 disabled: element.prop("disabled"), 850 locked: equal(element.attr("locked"), "locked") || equal(element.data("locked"), true) 851 }; 852 } else if (element.is("optgroup")) { 853 return { 854 text:element.attr("label"), 855 children:[], 856 element: element.get(), 857 css: element.attr("class") 858 }; 859 } 860 }, 861 862 // abstract 863 prepareOpts: function (opts) { 864 var element, select, idKey, ajaxUrl, self = this; 865 866 element = opts.element; 867 868 if (element.get(0).tagName.toLowerCase() === "select") { 869 this.select = select = opts.element; 870 } 871 872 if (select) { 873 // these options are not allowed when attached to a select because they are picked up off the element itself 874 $.each(["id", "multiple", "ajax", "query", "createSearchChoice", "initSelection", "data", "tags"], function () { 875 if (this in opts) { 876 throw new Error("Option '" + this + "' is not allowed for Select2 when attached to a <select> element."); 877 } 878 }); 879 } 880 881 opts = $.extend({}, { 882 populateResults: function(container, results, query) { 883 var populate, id=this.opts.id, liveRegion=this.liveRegion; 884 885 populate=function(results, container, depth) { 886 887 var i, l, result, selectable, disabled, compound, node, label, innerContainer, formatted; 888 889 results = opts.sortResults(results, container, query); 890 891 for (i = 0, l = results.length; i < l; i = i + 1) { 892 893 result=results[i]; 894 895 disabled = (result.disabled === true); 896 selectable = (!disabled) && (id(result) !== undefined); 897 898 compound=result.children && result.children.length > 0; 899 900 node=$("<li></li>"); 901 node.addClass("select2-results-dept-"+depth); 902 node.addClass("select2-result"); 903 node.addClass(selectable ? "select2-result-selectable" : "select2-result-unselectable"); 904 if (disabled) { node.addClass("select2-disabled"); } 905 if (compound) { node.addClass("select2-result-with-children"); } 906 node.addClass(self.opts.formatResultCssClass(result)); 907 node.attr("role", "presentation"); 908 909 label=$(document.createElement("div")); 910 label.addClass("select2-result-label"); 911 label.attr("id", "select2-result-label-" + nextUid()); 912 label.attr("role", "option"); 913 914 formatted=opts.formatResult(result, label, query, self.opts.escapeMarkup); 915 if (formatted!==undefined) { 916 label.html(formatted); 917 node.append(label); 918 } 919 920 921 if (compound) { 922 923 innerContainer=$("<ul></ul>"); 924 innerContainer.addClass("select2-result-sub"); 925 populate(result.children, innerContainer, depth+1); 926 node.append(innerContainer); 927 } 928 929 node.data("select2-data", result); 930 container.append(node); 931 } 932 933 liveRegion.text(opts.formatMatches(results.length)); 934 }; 935 936 populate(results, container, 0); 937 } 938 }, $.fn.select2.defaults, opts); 939 940 if (typeof(opts.id) !== "function") { 941 idKey = opts.id; 942 opts.id = function (e) { return e[idKey]; }; 943 } 944 945 if ($.isArray(opts.element.data("select2Tags"))) { 946 if ("tags" in opts) { 947 throw "tags specified as both an attribute 'data-select2-tags' and in options of Select2 " + opts.element.attr("id"); 948 } 949 opts.tags=opts.element.data("select2Tags"); 950 } 951 952 if (select) { 953 opts.query = this.bind(function (query) { 954 var data = { results: [], more: false }, 955 term = query.term, 956 children, placeholderOption, process; 957 958 process=function(element, collection) { 959 var group; 960 if (element.is("option")) { 961 if (query.matcher(term, element.text(), element)) { 962 collection.push(self.optionToData(element)); 963 } 964 } else if (element.is("optgroup")) { 965 group=self.optionToData(element); 966 element.children().each2(function(i, elm) { process(elm, group.children); }); 967 if (group.children.length>0) { 968 collection.push(group); 969 } 970 } 971 }; 972 973 children=element.children(); 974 975 // ignore the placeholder option if there is one 976 if (this.getPlaceholder() !== undefined && children.length > 0) { 977 placeholderOption = this.getPlaceholderOption(); 978 if (placeholderOption) { 979 children=children.not(placeholderOption); 980 } 981 } 982 983 children.each2(function(i, elm) { process(elm, data.results); }); 984 985 query.callback(data); 986 }); 987 // this is needed because inside val() we construct choices from options and there id is hardcoded 988 opts.id=function(e) { return e.id; }; 989 } else { 990 if (!("query" in opts)) { 991 992 if ("ajax" in opts) { 993 ajaxUrl = opts.element.data("ajax-url"); 994 if (ajaxUrl && ajaxUrl.length > 0) { 995 opts.ajax.url = ajaxUrl; 996 } 997 opts.query = ajax.call(opts.element, opts.ajax); 998 } else if ("data" in opts) { 999 opts.query = local(opts.data); 1000 } else if ("tags" in opts) { 1001 opts.query = tags(opts.tags); 1002 if (opts.createSearchChoice === undefined) { 1003 opts.createSearchChoice = function (term) { return {id: $.trim(term), text: $.trim(term)}; }; 1004 } 1005 if (opts.initSelection === undefined) { 1006 opts.initSelection = function (element, callback) { 1007 var data = []; 1008 $(splitVal(element.val(), opts.separator)).each(function () { 1009 var obj = { id: this, text: this }, 1010 tags = opts.tags; 1011 if ($.isFunction(tags)) tags=tags(); 1012 $(tags).each(function() { if (equal(this.id, obj.id)) { obj = this; return false; } }); 1013 data.push(obj); 1014 }); 1015 1016 callback(data); 1017 }; 1018 } 1019 } 1020 } 1021 } 1022 if (typeof(opts.query) !== "function") { 1023 throw "query function not defined for Select2 " + opts.element.attr("id"); 1024 } 1025 1026 if (opts.createSearchChoicePosition === 'top') { 1027 opts.createSearchChoicePosition = function(list, item) { list.unshift(item); }; 1028 } 1029 else if (opts.createSearchChoicePosition === 'bottom') { 1030 opts.createSearchChoicePosition = function(list, item) { list.push(item); }; 1031 } 1032 else if (typeof(opts.createSearchChoicePosition) !== "function") { 1033 throw "invalid createSearchChoicePosition option must be 'top', 'bottom' or a custom function"; 1034 } 1035 1036 return opts; 1037 }, 1038 1039 /** 1040 * Monitor the original element for changes and update select2 accordingly 1041 */ 1042 // abstract 1043 monitorSource: function () { 1044 var el = this.opts.element, sync, observer; 1045 1046 el.on("change.select2", this.bind(function (e) { 1047 if (this.opts.element.data("select2-change-triggered") !== true) { 1048 this.initSelection(); 1049 } 1050 })); 1051 1052 sync = this.bind(function () { 1053 1054 // sync enabled state 1055 var disabled = el.prop("disabled"); 1056 if (disabled === undefined) disabled = false; 1057 this.enable(!disabled); 1058 1059 var readonly = el.prop("readonly"); 1060 if (readonly === undefined) readonly = false; 1061 this.readonly(readonly); 1062 1063 syncCssClasses(this.container, this.opts.element, this.opts.adaptContainerCssClass); 1064 this.container.addClass(evaluate(this.opts.containerCssClass)); 1065 1066 syncCssClasses(this.dropdown, this.opts.element, this.opts.adaptDropdownCssClass); 1067 this.dropdown.addClass(evaluate(this.opts.dropdownCssClass)); 1068 1069 }); 1070 1071 // IE8-10 1072 el.on("propertychange.select2", sync); 1073 1074 // hold onto a reference of the callback to work around a chromium bug 1075 if (this.mutationCallback === undefined) { 1076 this.mutationCallback = function (mutations) { 1077 mutations.forEach(sync); 1078 } 1079 } 1080 1081 // safari, chrome, firefox, IE11 1082 observer = window.MutationObserver || window.WebKitMutationObserver|| window.MozMutationObserver; 1083 if (observer !== undefined) { 1084 if (this.propertyObserver) { delete this.propertyObserver; this.propertyObserver = null; } 1085 this.propertyObserver = new observer(this.mutationCallback); 1086 this.propertyObserver.observe(el.get(0), { attributes:true, subtree:false }); 1087 } 1088 }, 1089 1090 // abstract 1091 triggerSelect: function(data) { 1092 var evt = $.Event("select2-selecting", { val: this.id(data), object: data }); 1093 this.opts.element.trigger(evt); 1094 return !evt.isDefaultPrevented(); 1095 }, 1096 1097 /** 1098 * Triggers the change event on the source element 1099 */ 1100 // abstract 1101 triggerChange: function (details) { 1102 1103 details = details || {}; 1104 details= $.extend({}, details, { type: "change", val: this.val() }); 1105 // prevents recursive triggering 1106 this.opts.element.data("select2-change-triggered", true); 1107 this.opts.element.trigger(details); 1108 this.opts.element.data("select2-change-triggered", false); 1109 1110 // some validation frameworks ignore the change event and listen instead to keyup, click for selects 1111 // so here we trigger the click event manually 1112 this.opts.element.click(); 1113 1114 // ValidationEngine ignores the change event and listens instead to blur 1115 // so here we trigger the blur event manually if so desired 1116 if (this.opts.blurOnChange) 1117 this.opts.element.blur(); 1118 }, 1119 1120 //abstract 1121 isInterfaceEnabled: function() 1122 { 1123 return this.enabledInterface === true; 1124 }, 1125 1126 // abstract 1127 enableInterface: function() { 1128 var enabled = this._enabled && !this._readonly, 1129 disabled = !enabled; 1130 1131 if (enabled === this.enabledInterface) return false; 1132 1133 this.container.toggleClass("select2-container-disabled", disabled); 1134 this.close(); 1135 this.enabledInterface = enabled; 1136 1137 return true; 1138 }, 1139 1140 // abstract 1141 enable: function(enabled) { 1142 if (enabled === undefined) enabled = true; 1143 if (this._enabled === enabled) return; 1144 this._enabled = enabled; 1145 1146 this.opts.element.prop("disabled", !enabled); 1147 this.enableInterface(); 1148 }, 1149 1150 // abstract 1151 disable: function() { 1152 this.enable(false); 1153 }, 1154 1155 // abstract 1156 readonly: function(enabled) { 1157 if (enabled === undefined) enabled = false; 1158 if (this._readonly === enabled) return; 1159 this._readonly = enabled; 1160 1161 this.opts.element.prop("readonly", enabled); 1162 this.enableInterface(); 1163 }, 1164 1165 // abstract 1166 opened: function () { 1167 return this.container.hasClass("select2-dropdown-open"); 1168 }, 1169 1170 // abstract 1171 positionDropdown: function() { 1172 var $dropdown = this.dropdown, 1173 offset = this.container.offset(), 1174 height = this.container.outerHeight(false), 1175 width = this.container.outerWidth(false), 1176 dropHeight = $dropdown.outerHeight(false), 1177 $window = $(window), 1178 windowWidth = $window.width(), 1179 windowHeight = $window.height(), 1180 viewPortRight = $window.scrollLeft() + windowWidth, 1181 viewportBottom = $window.scrollTop() + windowHeight, 1182 dropTop = offset.top + height, 1183 dropLeft = offset.left, 1184 enoughRoomBelow = dropTop + dropHeight <= viewportBottom, 1185 enoughRoomAbove = (offset.top - dropHeight) >= $window.scrollTop(), 1186 dropWidth = $dropdown.outerWidth(false), 1187 enoughRoomOnRight = dropLeft + dropWidth <= viewPortRight, 1188 aboveNow = $dropdown.hasClass("select2-drop-above"), 1189 bodyOffset, 1190 above, 1191 changeDirection, 1192 css, 1193 resultsListNode; 1194 1195 // always prefer the current above/below alignment, unless there is not enough room 1196 if (aboveNow) { 1197 above = true; 1198 if (!enoughRoomAbove && enoughRoomBelow) { 1199 changeDirection = true; 1200 above = false; 1201 } 1202 } else { 1203 above = false; 1204 if (!enoughRoomBelow && enoughRoomAbove) { 1205 changeDirection = true; 1206 above = true; 1207 } 1208 } 1209 1210 //if we are changing direction we need to get positions when dropdown is hidden; 1211 if (changeDirection) { 1212 $dropdown.hide(); 1213 offset = this.container.offset(); 1214 height = this.container.outerHeight(false); 1215 width = this.container.outerWidth(false); 1216 dropHeight = $dropdown.outerHeight(false); 1217 viewPortRight = $window.scrollLeft() + windowWidth; 1218 viewportBottom = $window.scrollTop() + windowHeight; 1219 dropTop = offset.top + height; 1220 dropLeft = offset.left; 1221 dropWidth = $dropdown.outerWidth(false); 1222 enoughRoomOnRight = dropLeft + dropWidth <= viewPortRight; 1223 $dropdown.show(); 1224 } 1225 1226 if (this.opts.dropdownAutoWidth) { 1227 resultsListNode = $('.select2-results', $dropdown)[0]; 1228 $dropdown.addClass('select2-drop-auto-width'); 1229 $dropdown.css('width', ''); 1230 // Add scrollbar width to dropdown if vertical scrollbar is present 1231 dropWidth = $dropdown.outerWidth(false) + (resultsListNode.scrollHeight === resultsListNode.clientHeight ? 0 : scrollBarDimensions.width); 1232 dropWidth > width ? width = dropWidth : dropWidth = width; 1233 enoughRoomOnRight = dropLeft + dropWidth <= viewPortRight; 1234 } 1235 else { 1236 this.container.removeClass('select2-drop-auto-width'); 1237 } 1238 1239 //console.log("below/ droptop:", dropTop, "dropHeight", dropHeight, "sum", (dropTop+dropHeight)+" viewport bottom", viewportBottom, "enough?", enoughRoomBelow); 1240 //console.log("above/ offset.top", offset.top, "dropHeight", dropHeight, "top", (offset.top-dropHeight), "scrollTop", this.body().scrollTop(), "enough?", enoughRoomAbove); 1241 1242 // fix positioning when body has an offset and is not position: static 1243 if (this.body().css('position') !== 'static') { 1244 bodyOffset = this.body().offset(); 1245 dropTop -= bodyOffset.top; 1246 dropLeft -= bodyOffset.left; 1247 } 1248 1249 if (!enoughRoomOnRight) { 1250 dropLeft = offset.left + this.container.outerWidth(false) - dropWidth; 1251 } 1252 1253 css = { 1254 left: dropLeft, 1255 width: width 1256 }; 1257 1258 if (above) { 1259 css.top = offset.top - dropHeight; 1260 css.bottom = 'auto'; 1261 this.container.addClass("select2-drop-above"); 1262 $dropdown.addClass("select2-drop-above"); 1263 } 1264 else { 1265 css.top = dropTop; 1266 css.bottom = 'auto'; 1267 this.container.removeClass("select2-drop-above"); 1268 $dropdown.removeClass("select2-drop-above"); 1269 } 1270 css = $.extend(css, evaluate(this.opts.dropdownCss)); 1271 1272 $dropdown.css(css); 1273 }, 1274 1275 // abstract 1276 shouldOpen: function() { 1277 var event; 1278 1279 if (this.opened()) return false; 1280 1281 if (this._enabled === false || this._readonly === true) return false; 1282 1283 event = $.Event("select2-opening"); 1284 this.opts.element.trigger(event); 1285 return !event.isDefaultPrevented(); 1286 }, 1287 1288 // abstract 1289 clearDropdownAlignmentPreference: function() { 1290 // clear the classes used to figure out the preference of where the dropdown should be opened 1291 this.container.removeClass("select2-drop-above"); 1292 this.dropdown.removeClass("select2-drop-above"); 1293 }, 1294 1295 /** 1296 * Opens the dropdown 1297 * 1298 * @return {Boolean} whether or not dropdown was opened. This method will return false if, for example, 1299 * the dropdown is already open, or if the 'open' event listener on the element called preventDefault(). 1300 */ 1301 // abstract 1302 open: function () { 1303 1304 if (!this.shouldOpen()) return false; 1305 1306 this.opening(); 1307 1308 return true; 1309 }, 1310 1311 /** 1312 * Performs the opening of the dropdown 1313 */ 1314 // abstract 1315 opening: function() { 1316 var cid = this.containerId, 1317 scroll = "scroll." + cid, 1318 resize = "resize."+cid, 1319 orient = "orientationchange."+cid, 1320 mask; 1321 1322 this.container.addClass("select2-dropdown-open").addClass("select2-container-active"); 1323 1324 this.clearDropdownAlignmentPreference(); 1325 1326 if(this.dropdown[0] !== this.body().children().last()[0]) { 1327 this.dropdown.detach().appendTo(this.body()); 1328 } 1329 1330 // create the dropdown mask if doesn't already exist 1331 mask = $("#select2-drop-mask"); 1332 if (mask.length == 0) { 1333 mask = $(document.createElement("div")); 1334 mask.attr("id","select2-drop-mask").attr("class","select2-drop-mask"); 1335 mask.hide(); 1336 mask.appendTo(this.body()); 1337 mask.on("mousedown touchstart click", function (e) { 1338 // Prevent IE from generating a click event on the body 1339 reinsertElement(mask); 1340 1341 var dropdown = $("#select2-drop"), self; 1342 if (dropdown.length > 0) { 1343 self=dropdown.data("select2"); 1344 if (self.opts.selectOnBlur) { 1345 self.selectHighlighted({noFocus: true}); 1346 } 1347 self.close(); 1348 e.preventDefault(); 1349 e.stopPropagation(); 1350 } 1351 }); 1352 } 1353 1354 // ensure the mask is always right before the dropdown 1355 if (this.dropdown.prev()[0] !== mask[0]) { 1356 this.dropdown.before(mask); 1357 } 1358 1359 // move the global id to the correct dropdown 1360 $("#select2-drop").removeAttr("id"); 1361 this.dropdown.attr("id", "select2-drop"); 1362 1363 // show the elements 1364 mask.show(); 1365 1366 this.positionDropdown(); 1367 this.dropdown.show(); 1368 this.positionDropdown(); 1369 1370 this.dropdown.addClass("select2-drop-active"); 1371 1372 // attach listeners to events that can change the position of the container and thus require 1373 // the position of the dropdown to be updated as well so it does not come unglued from the container 1374 var that = this; 1375 this.container.parents().add(window).each(function () { 1376 $(this).on(resize+" "+scroll+" "+orient, function (e) { 1377 that.positionDropdown(); 1378 }); 1379 }); 1380 1381 1382 }, 1383 1384 // abstract 1385 close: function () { 1386 if (!this.opened()) return; 1387 1388 var cid = this.containerId, 1389 scroll = "scroll." + cid, 1390 resize = "resize."+cid, 1391 orient = "orientationchange."+cid; 1392 1393 // unbind event listeners 1394 this.container.parents().add(window).each(function () { $(this).off(scroll).off(resize).off(orient); }); 1395 1396 this.clearDropdownAlignmentPreference(); 1397 1398 $("#select2-drop-mask").hide(); 1399 this.dropdown.removeAttr("id"); // only the active dropdown has the select2-drop id 1400 this.dropdown.hide(); 1401 this.container.removeClass("select2-dropdown-open").removeClass("select2-container-active"); 1402 this.results.empty(); 1403 1404 1405 this.clearSearch(); 1406 this.search.removeClass("select2-active"); 1407 this.opts.element.trigger($.Event("select2-close")); 1408 }, 1409 1410 /** 1411 * Opens control, sets input value, and updates results. 1412 */ 1413 // abstract 1414 externalSearch: function (term) { 1415 this.open(); 1416 this.search.val(term); 1417 this.updateResults(false); 1418 }, 1419 1420 // abstract 1421 clearSearch: function () { 1422 1423 }, 1424 1425 //abstract 1426 getMaximumSelectionSize: function() { 1427 return evaluate(this.opts.maximumSelectionSize); 1428 }, 1429 1430 // abstract 1431 ensureHighlightVisible: function () { 1432 var results = this.results, children, index, child, hb, rb, y, more; 1433 1434 index = this.highlight(); 1435 1436 if (index < 0) return; 1437 1438 if (index == 0) { 1439 1440 // if the first element is highlighted scroll all the way to the top, 1441 // that way any unselectable headers above it will also be scrolled 1442 // into view 1443 1444 results.scrollTop(0); 1445 return; 1446 } 1447 1448 children = this.findHighlightableChoices().find('.select2-result-label'); 1449 1450 child = $(children[index]); 1451 1452 hb = child.offset().top + child.outerHeight(true); 1453 1454 // if this is the last child lets also make sure select2-more-results is visible 1455 if (index === children.length - 1) { 1456 more = results.find("li.select2-more-results"); 1457 if (more.length > 0) { 1458 hb = more.offset().top + more.outerHeight(true); 1459 } 1460 } 1461 1462 rb = results.offset().top + results.outerHeight(true); 1463 if (hb > rb) { 1464 results.scrollTop(results.scrollTop() + (hb - rb)); 1465 } 1466 y = child.offset().top - results.offset().top; 1467 1468 // make sure the top of the element is visible 1469 if (y < 0 && child.css('display') != 'none' ) { 1470 results.scrollTop(results.scrollTop() + y); // y is negative 1471 } 1472 }, 1473 1474 // abstract 1475 findHighlightableChoices: function() { 1476 return this.results.find(".select2-result-selectable:not(.select2-disabled):not(.select2-selected)"); 1477 }, 1478 1479 // abstract 1480 moveHighlight: function (delta) { 1481 var choices = this.findHighlightableChoices(), 1482 index = this.highlight(); 1483 1484 while (index > -1 && index < choices.length) { 1485 index += delta; 1486 var choice = $(choices[index]); 1487 if (choice.hasClass("select2-result-selectable") && !choice.hasClass("select2-disabled") && !choice.hasClass("select2-selected")) { 1488 this.highlight(index); 1489 break; 1490 } 1491 } 1492 }, 1493 1494 // abstract 1495 highlight: function (index) { 1496 var choices = this.findHighlightableChoices(), 1497 choice, 1498 data; 1499 1500 if (arguments.length === 0) { 1501 return indexOf(choices.filter(".select2-highlighted")[0], choices.get()); 1502 } 1503 1504 if (index >= choices.length) index = choices.length - 1; 1505 if (index < 0) index = 0; 1506 1507 this.removeHighlight(); 1508 1509 choice = $(choices[index]); 1510 choice.addClass("select2-highlighted"); 1511 1512 // ensure assistive technology can determine the active choice 1513 this.search.attr("aria-activedescendant", choice.find(".select2-result-label").attr("id")); 1514 1515 this.ensureHighlightVisible(); 1516 1517 this.liveRegion.text(choice.text()); 1518 1519 data = choice.data("select2-data"); 1520 if (data) { 1521 this.opts.element.trigger({ type: "select2-highlight", val: this.id(data), choice: data }); 1522 } 1523 }, 1524 1525 removeHighlight: function() { 1526 this.results.find(".select2-highlighted").removeClass("select2-highlighted"); 1527 }, 1528 1529 touchMoved: function() { 1530 this._touchMoved = true; 1531 }, 1532 1533 clearTouchMoved: function() { 1534 this._touchMoved = false; 1535 }, 1536 1537 // abstract 1538 countSelectableResults: function() { 1539 return this.findHighlightableChoices().length; 1540 }, 1541 1542 // abstract 1543 highlightUnderEvent: function (event) { 1544 var el = $(event.target).closest(".select2-result-selectable"); 1545 if (el.length > 0 && !el.is(".select2-highlighted")) { 1546 var choices = this.findHighlightableChoices(); 1547 this.highlight(choices.index(el)); 1548 } else if (el.length == 0) { 1549 // if we are over an unselectable item remove all highlights 1550 this.removeHighlight(); 1551 } 1552 }, 1553 1554 // abstract 1555 loadMoreIfNeeded: function () { 1556 var results = this.results, 1557 more = results.find("li.select2-more-results"), 1558 below, // pixels the element is below the scroll fold, below==0 is when the element is starting to be visible 1559 page = this.resultsPage + 1, 1560 self=this, 1561 term=this.search.val(), 1562 context=this.context; 1563 1564 if (more.length === 0) return; 1565 below = more.offset().top - results.offset().top - results.height(); 1566 1567 if (below <= this.opts.loadMorePadding) { 1568 more.addClass("select2-active"); 1569 this.opts.query({ 1570 element: this.opts.element, 1571 term: term, 1572 page: page, 1573 context: context, 1574 matcher: this.opts.matcher, 1575 callback: this.bind(function (data) { 1576 1577 // ignore a response if the select2 has been closed before it was received 1578 if (!self.opened()) return; 1579 1580 1581 self.opts.populateResults.call(this, results, data.results, {term: term, page: page, context:context}); 1582 self.postprocessResults(data, false, false); 1583 1584 if (data.more===true) { 1585 more.detach().appendTo(results).text(evaluate(self.opts.formatLoadMore, page+1)); 1586 window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10); 1587 } else { 1588 more.remove(); 1589 } 1590 self.positionDropdown(); 1591 self.resultsPage = page; 1592 self.context = data.context; 1593 this.opts.element.trigger({ type: "select2-loaded", items: data }); 1594 })}); 1595 } 1596 }, 1597 1598 /** 1599 * Default tokenizer function which does nothing 1600 */ 1601 tokenize: function() { 1602 1603 }, 1604 1605 /** 1606 * @param initial whether or not this is the call to this method right after the dropdown has been opened 1607 */ 1608 // abstract 1609 updateResults: function (initial) { 1610 var search = this.search, 1611 results = this.results, 1612 opts = this.opts, 1613 data, 1614 self = this, 1615 input, 1616 term = search.val(), 1617 lastTerm = $.data(this.container, "select2-last-term"), 1618 // sequence number used to drop out-of-order responses 1619 queryNumber; 1620 1621 // prevent duplicate queries against the same term 1622 if (initial !== true && lastTerm && equal(term, lastTerm)) return; 1623 1624 $.data(this.container, "select2-last-term", term); 1625 1626 // if the search is currently hidden we do not alter the results 1627 if (initial !== true && (this.showSearchInput === false || !this.opened())) { 1628 return; 1629 } 1630 1631 function postRender() { 1632 search.removeClass("select2-active"); 1633 self.positionDropdown(); 1634 if (results.find('.select2-no-results,.select2-selection-limit,.select2-searching').length) { 1635 self.liveRegion.text(results.text()); 1636 } 1637 else { 1638 self.liveRegion.text(self.opts.formatMatches(results.find('.select2-result-selectable').length)); 1639 } 1640 } 1641 1642 function render(html) { 1643 results.html(html); 1644 postRender(); 1645 } 1646 1647 queryNumber = ++this.queryCount; 1648 1649 var maxSelSize = this.getMaximumSelectionSize(); 1650 if (maxSelSize >=1) { 1651 data = this.data(); 1652 if ($.isArray(data) && data.length >= maxSelSize && checkFormatter(opts.formatSelectionTooBig, "formatSelectionTooBig")) { 1653 render("<li class='select2-selection-limit'>" + evaluate(opts.formatSelectionTooBig, maxSelSize) + "</li>"); 1654 return; 1655 } 1656 } 1657 1658 if (search.val().length < opts.minimumInputLength) { 1659 if (checkFormatter(opts.formatInputTooShort, "formatInputTooShort")) { 1660 render("<li class='select2-no-results'>" + evaluate(opts.formatInputTooShort, search.val(), opts.minimumInputLength) + "</li>"); 1661 } else { 1662 render(""); 1663 } 1664 if (initial && this.showSearch) this.showSearch(true); 1665 return; 1666 } 1667 1668 if (opts.maximumInputLength && search.val().length > opts.maximumInputLength) { 1669 if (checkFormatter(opts.formatInputTooLong, "formatInputTooLong")) { 1670 render("<li class='select2-no-results'>" + evaluate(opts.formatInputTooLong, search.val(), opts.maximumInputLength) + "</li>"); 1671 } else { 1672 render(""); 1673 } 1674 return; 1675 } 1676 1677 if (opts.formatSearching && this.findHighlightableChoices().length === 0) { 1678 render("<li class='select2-searching'>" + evaluate(opts.formatSearching) + "</li>"); 1679 } 1680 1681 search.addClass("select2-active"); 1682 1683 this.removeHighlight(); 1684 1685 // give the tokenizer a chance to pre-process the input 1686 input = this.tokenize(); 1687 if (input != undefined && input != null) { 1688 search.val(input); 1689 } 1690 1691 this.resultsPage = 1; 1692 1693 opts.query({ 1694 element: opts.element, 1695 term: search.val(), 1696 page: this.resultsPage, 1697 context: null, 1698 matcher: opts.matcher, 1699 callback: this.bind(function (data) { 1700 var def; // default choice 1701 1702 // ignore old responses 1703 if (queryNumber != this.queryCount) { 1704 return; 1705 } 1706 1707 // ignore a response if the select2 has been closed before it was received 1708 if (!this.opened()) { 1709 this.search.removeClass("select2-active"); 1710 return; 1711 } 1712 1713 // save context, if any 1714 this.context = (data.context===undefined) ? null : data.context; 1715 // create a default choice and prepend it to the list 1716 if (this.opts.createSearchChoice && search.val() !== "") { 1717 def = this.opts.createSearchChoice.call(self, search.val(), data.results); 1718 if (def !== undefined && def !== null && self.id(def) !== undefined && self.id(def) !== null) { 1719 if ($(data.results).filter( 1720 function () { 1721 return equal(self.id(this), self.id(def)); 1722 }).length === 0) { 1723 this.opts.createSearchChoicePosition(data.results, def); 1724 } 1725 } 1726 } 1727 1728 if (data.results.length === 0 && checkFormatter(opts.formatNoMatches, "formatNoMatches")) { 1729 render("<li class='select2-no-results'>" + evaluate(opts.formatNoMatches, search.val()) + "</li>"); 1730 return; 1731 } 1732 1733 results.empty(); 1734 self.opts.populateResults.call(this, results, data.results, {term: search.val(), page: this.resultsPage, context:null}); 1735 1736 if (data.more === true && checkFormatter(opts.formatLoadMore, "formatLoadMore")) { 1737 results.append("<li class='select2-more-results'>" + self.opts.escapeMarkup(evaluate(opts.formatLoadMore, this.resultsPage)) + "</li>"); 1738 window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10); 1739 } 1740 1741 this.postprocessResults(data, initial); 1742 1743 postRender(); 1744 1745 this.opts.element.trigger({ type: "select2-loaded", items: data }); 1746 })}); 1747 }, 1748 1749 // abstract 1750 cancel: function () { 1751 this.close(); 1752 }, 1753 1754 // abstract 1755 blur: function () { 1756 // if selectOnBlur == true, select the currently highlighted option 1757 if (this.opts.selectOnBlur) 1758 this.selectHighlighted({noFocus: true}); 1759 1760 this.close(); 1761 this.container.removeClass("select2-container-active"); 1762 // synonymous to .is(':focus'), which is available in jquery >= 1.6 1763 if (this.search[0] === document.activeElement) { this.search.blur(); } 1764 this.clearSearch(); 1765 this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus"); 1766 }, 1767 1768 // abstract 1769 focusSearch: function () { 1770 focus(this.search); 1771 }, 1772 1773 // abstract 1774 selectHighlighted: function (options) { 1775 if (this._touchMoved) { 1776 this.clearTouchMoved(); 1777 return; 1778 } 1779 var index=this.highlight(), 1780 highlighted=this.results.find(".select2-highlighted"), 1781 data = highlighted.closest('.select2-result').data("select2-data"); 1782 1783 if (data) { 1784 this.highlight(index); 1785 this.onSelect(data, options); 1786 } else if (options && options.noFocus) { 1787 this.close(); 1788 } 1789 }, 1790 1791 // abstract 1792 getPlaceholder: function () { 1793 var placeholderOption; 1794 return this.opts.element.attr("placeholder") || 1795 this.opts.element.attr("data-placeholder") || // jquery 1.4 compat 1796 this.opts.element.data("placeholder") || 1797 this.opts.placeholder || 1798 ((placeholderOption = this.getPlaceholderOption()) !== undefined ? placeholderOption.text() : undefined); 1799 }, 1800 1801 // abstract 1802 getPlaceholderOption: function() { 1803 if (this.select) { 1804 var firstOption = this.select.children('option').first(); 1805 if (this.opts.placeholderOption !== undefined ) { 1806 //Determine the placeholder option based on the specified placeholderOption setting 1807 return (this.opts.placeholderOption === "first" && firstOption) || 1808 (typeof this.opts.placeholderOption === "function" && this.opts.placeholderOption(this.select)); 1809 } else if (firstOption.text() === "" && firstOption.val() === "") { 1810 //No explicit placeholder option specified, use the first if it's blank 1811 return firstOption; 1812 } 1813 } 1814 }, 1815 1816 /** 1817 * Get the desired width for the container element. This is 1818 * derived first from option `width` passed to select2, then 1819 * the inline 'style' on the original element, and finally 1820 * falls back to the jQuery calculated element width. 1821 */ 1822 // abstract 1823 initContainerWidth: function () { 1824 function resolveContainerWidth() { 1825 var style, attrs, matches, i, l, attr; 1826 1827 if (this.opts.width === "off") { 1828 return null; 1829 } else if (this.opts.width === "element"){ 1830 return this.opts.element.outerWidth(false) === 0 ? 'auto' : this.opts.element.outerWidth(false) + 'px'; 1831 } else if (this.opts.width === "copy" || this.opts.width === "resolve") { 1832 // check if there is inline style on the element that contains width 1833 style = this.opts.element.attr('style'); 1834 if (style !== undefined) { 1835 attrs = style.split(';'); 1836 for (i = 0, l = attrs.length; i < l; i = i + 1) { 1837 attr = attrs[i].replace(/\s/g, ''); 1838 matches = attr.match(/^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i); 1839 if (matches !== null && matches.length >= 1) 1840 return matches[1]; 1841 } 1842 } 1843 1844 if (this.opts.width === "resolve") { 1845 // next check if css('width') can resolve a width that is percent based, this is sometimes possible 1846 // when attached to input type=hidden or elements hidden via css 1847 style = this.opts.element.css('width'); 1848 if (style.indexOf("%") > 0) return style; 1849 1850 // finally, fallback on the calculated width of the element 1851 return (this.opts.element.outerWidth(false) === 0 ? 'auto' : this.opts.element.outerWidth(false) + 'px'); 1852 } 1853 1854 return null; 1855 } else if ($.isFunction(this.opts.width)) { 1856 return this.opts.width(); 1857 } else { 1858 return this.opts.width; 1859 } 1860 }; 1861 1862 var width = resolveContainerWidth.call(this); 1863 if (width !== null) { 1864 this.container.css("width", width); 1865 } 1866 } 1867 }); 1868 1869 SingleSelect2 = clazz(AbstractSelect2, { 1870 1871 // single 1872 1873 createContainer: function () { 1874 var container = $(document.createElement("div")).attr({ 1875 "class": "select2-container" 1876 }).html([ 1877 "<a href='javascript:void(0)' class='select2-choice' tabindex='-1'>", 1878 " <span class='select2-chosen'> </span><abbr class='select2-search-choice-close'></abbr>", 1879 " <span class='select2-arrow' role='presentation'><b role='presentation'></b></span>", 1880 "</a>", 1881 "<label for='' class='select2-offscreen'></label>", 1882 "<input class='select2-focusser select2-offscreen' type='text' aria-haspopup='true' role='button' />", 1883 "<div class='select2-drop select2-display-none'>", 1884 " <div class='select2-search'>", 1885 " <label for='' class='select2-offscreen'></label>", 1886 " <input type='text' autocomplete='off' autocorrect='off' autocapitalize='off' spellcheck='false' class='select2-input' role='combobox' aria-expanded='true'", 1887 " aria-autocomplete='list' />", 1888 " </div>", 1889 " <ul class='select2-results' role='listbox'>", 1890 " </ul>", 1891 "</div>"].join("")); 1892 return container; 1893 }, 1894 1895 // single 1896 enableInterface: function() { 1897 if (this.parent.enableInterface.apply(this, arguments)) { 1898 this.focusser.prop("disabled", !this.isInterfaceEnabled()); 1899 } 1900 }, 1901 1902 // single 1903 opening: function () { 1904 var el, range, len; 1905 1906 if (this.opts.minimumResultsForSearch >= 0) { 1907 this.showSearch(true); 1908 } 1909 1910 this.parent.opening.apply(this, arguments); 1911 1912 if (this.showSearchInput !== false) { 1913 // IE appends focusser.val() at the end of field :/ so we manually insert it at the beginning using a range 1914 // all other browsers handle this just fine 1915 1916 this.search.val(this.focusser.val()); 1917 } 1918 this.search.focus(); 1919 // move the cursor to the end after focussing, otherwise it will be at the beginning and 1920 // new text will appear *before* focusser.val() 1921 el = this.search.get(0); 1922 if (el.createTextRange) { 1923 range = el.createTextRange(); 1924 range.collapse(false); 1925 range.select(); 1926 } else if (el.setSelectionRange) { 1927 len = this.search.val().length; 1928 el.setSelectionRange(len, len); 1929 } 1930 1931 // initializes search's value with nextSearchTerm (if defined by user) 1932 // ignore nextSearchTerm if the dropdown is opened by the user pressing a letter 1933 if(this.search.val() === "") { 1934 if(this.nextSearchTerm != undefined){ 1935 this.search.val(this.nextSearchTerm); 1936 this.search.select(); 1937 } 1938 } 1939 1940 this.focusser.prop("disabled", true).val(""); 1941 this.updateResults(true); 1942 this.opts.element.trigger($.Event("select2-open")); 1943 }, 1944 1945 // single 1946 close: function () { 1947 if (!this.opened()) return; 1948 this.parent.close.apply(this, arguments); 1949 1950 this.focusser.prop("disabled", false); 1951 1952 if (this.opts.shouldFocusInput(this)) { 1953 this.focusser.focus(); 1954 } 1955 }, 1956 1957 // single 1958 focus: function () { 1959 if (this.opened()) { 1960 this.close(); 1961 } else { 1962 this.focusser.prop("disabled", false); 1963 if (this.opts.shouldFocusInput(this)) { 1964 this.focusser.focus(); 1965 } 1966 } 1967 }, 1968 1969 // single 1970 isFocused: function () { 1971 return this.container.hasClass("select2-container-active"); 1972 }, 1973 1974 // single 1975 cancel: function () { 1976 this.parent.cancel.apply(this, arguments); 1977 this.focusser.prop("disabled", false); 1978 1979 if (this.opts.shouldFocusInput(this)) { 1980 this.focusser.focus(); 1981 } 1982 }, 1983 1984 // single 1985 destroy: function() { 1986 $("label[for='" + this.focusser.attr('id') + "']") 1987 .attr('for', this.opts.element.attr("id")); 1988 this.parent.destroy.apply(this, arguments); 1989 }, 1990 1991 // single 1992 initContainer: function () { 1993 1994 var selection, 1995 container = this.container, 1996 dropdown = this.dropdown, 1997 idSuffix = nextUid(), 1998 elementLabel; 1999 2000 if (this.opts.minimumResultsForSearch < 0) { 2001 this.showSearch(false); 2002 } else { 2003 this.showSearch(true); 2004 } 2005 2006 this.selection = selection = container.find(".select2-choice"); 2007 2008 this.focusser = container.find(".select2-focusser"); 2009 2010 // add aria associations 2011 selection.find(".select2-chosen").attr("id", "select2-chosen-"+idSuffix); 2012 this.focusser.attr("aria-labelledby", "select2-chosen-"+idSuffix); 2013 this.results.attr("id", "select2-results-"+idSuffix); 2014 this.search.attr("aria-owns", "select2-results-"+idSuffix); 2015 2016 // rewrite labels from original element to focusser 2017 this.focusser.attr("id", "s2id_autogen"+idSuffix); 2018 2019 elementLabel = $("label[for='" + this.opts.element.attr("id") + "']"); 2020 2021 this.focusser.prev() 2022 .text(elementLabel.text()) 2023 .attr('for', this.focusser.attr('id')); 2024 2025 // Ensure the original element retains an accessible name 2026 var originalTitle = this.opts.element.attr("title"); 2027 this.opts.element.attr("title", (originalTitle || elementLabel.text())); 2028 2029 this.focusser.attr("tabindex", this.elementTabIndex); 2030 2031 // write label for search field using the label from the focusser element 2032 this.search.attr("id", this.focusser.attr('id') + '_search'); 2033 2034 this.search.prev() 2035 .text($("label[for='" + this.focusser.attr('id') + "']").text()) 2036 .attr('for', this.search.attr('id')); 2037 2038 this.search.on("keydown", this.bind(function (e) { 2039 if (!this.isInterfaceEnabled()) return; 2040 2041 if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) { 2042 // prevent the page from scrolling 2043 killEvent(e); 2044 return; 2045 } 2046 2047 switch (e.which) { 2048 case KEY.UP: 2049 case KEY.DOWN: 2050 this.moveHighlight((e.which === KEY.UP) ? -1 : 1); 2051 killEvent(e); 2052 return; 2053 case KEY.ENTER: 2054 this.selectHighlighted(); 2055 killEvent(e); 2056 return; 2057 case KEY.TAB: 2058 this.selectHighlighted({noFocus: true}); 2059 return; 2060 case KEY.ESC: 2061 this.cancel(e); 2062 killEvent(e); 2063 return; 2064 } 2065 })); 2066 2067 this.search.on("blur", this.bind(function(e) { 2068 // a workaround for chrome to keep the search field focussed when the scroll bar is used to scroll the dropdown. 2069 // without this the search field loses focus which is annoying 2070 if (document.activeElement === this.body().get(0)) { 2071 window.setTimeout(this.bind(function() { 2072 if (this.opened()) { 2073 this.search.focus(); 2074 } 2075 }), 0); 2076 } 2077 })); 2078 2079 this.focusser.on("keydown", this.bind(function (e) { 2080 if (!this.isInterfaceEnabled()) return; 2081 2082 if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) { 2083 return; 2084 } 2085 2086 if (this.opts.openOnEnter === false && e.which === KEY.ENTER) { 2087 killEvent(e); 2088 return; 2089 } 2090 2091 if (e.which == KEY.DOWN || e.which == KEY.UP 2092 || (e.which == KEY.ENTER && this.opts.openOnEnter)) { 2093 2094 if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) return; 2095 2096 this.open(); 2097 killEvent(e); 2098 return; 2099 } 2100 2101 if (e.which == KEY.DELETE || e.which == KEY.BACKSPACE) { 2102 if (this.opts.allowClear) { 2103 this.clear(); 2104 } 2105 killEvent(e); 2106 return; 2107 } 2108 })); 2109 2110 2111 installKeyUpChangeEvent(this.focusser); 2112 this.focusser.on("keyup-change input", this.bind(function(e) { 2113 if (this.opts.minimumResultsForSearch >= 0) { 2114 e.stopPropagation(); 2115 if (this.opened()) return; 2116 this.open(); 2117 } 2118 })); 2119 2120 selection.on("mousedown touchstart", "abbr", this.bind(function (e) { 2121 if (!this.isInterfaceEnabled()) return; 2122 this.clear(); 2123 killEventImmediately(e); 2124 this.close(); 2125 this.selection.focus(); 2126 })); 2127 2128 selection.on("mousedown touchstart", this.bind(function (e) { 2129 // Prevent IE from generating a click event on the body 2130 reinsertElement(selection); 2131 2132 if (!this.container.hasClass("select2-container-active")) { 2133 this.opts.element.trigger($.Event("select2-focus")); 2134 } 2135 2136 if (this.opened()) { 2137 this.close(); 2138 } else if (this.isInterfaceEnabled()) { 2139 this.open(); 2140 } 2141 2142 killEvent(e); 2143 })); 2144 2145 dropdown.on("mousedown touchstart", this.bind(function() { this.search.focus(); })); 2146 2147 selection.on("focus", this.bind(function(e) { 2148 killEvent(e); 2149 })); 2150 2151 this.focusser.on("focus", this.bind(function(){ 2152 if (!this.container.hasClass("select2-container-active")) { 2153 this.opts.element.trigger($.Event("select2-focus")); 2154 } 2155 this.container.addClass("select2-container-active"); 2156 })).on("blur", this.bind(function() { 2157 if (!this.opened()) { 2158 this.container.removeClass("select2-container-active"); 2159 this.opts.element.trigger($.Event("select2-blur")); 2160 } 2161 })); 2162 this.search.on("focus", this.bind(function(){ 2163 if (!this.container.hasClass("select2-container-active")) { 2164 this.opts.element.trigger($.Event("select2-focus")); 2165 } 2166 this.container.addClass("select2-container-active"); 2167 })); 2168 2169 this.initContainerWidth(); 2170 this.opts.element.addClass("select2-offscreen"); 2171 this.setPlaceholder(); 2172 2173 }, 2174 2175 // single 2176 clear: function(triggerChange) { 2177 var data=this.selection.data("select2-data"); 2178 if (data) { // guard against queued quick consecutive clicks 2179 var evt = $.Event("select2-clearing"); 2180 this.opts.element.trigger(evt); 2181 if (evt.isDefaultPrevented()) { 2182 return; 2183 } 2184 var placeholderOption = this.getPlaceholderOption(); 2185 this.opts.element.val(placeholderOption ? placeholderOption.val() : ""); 2186 this.selection.find(".select2-chosen").empty(); 2187 this.selection.removeData("select2-data"); 2188 this.setPlaceholder(); 2189 2190 if (triggerChange !== false){ 2191 this.opts.element.trigger({ type: "select2-removed", val: this.id(data), choice: data }); 2192 this.triggerChange({removed:data}); 2193 } 2194 } 2195 }, 2196 2197 /** 2198 * Sets selection based on source element's value 2199 */ 2200 // single 2201 initSelection: function () { 2202 var selected; 2203 if (this.isPlaceholderOptionSelected()) { 2204 this.updateSelection(null); 2205 this.close(); 2206 this.setPlaceholder(); 2207 } else { 2208 var self = this; 2209 this.opts.initSelection.call(null, this.opts.element, function(selected){ 2210 if (selected !== undefined && selected !== null) { 2211 self.updateSelection(selected); 2212 self.close(); 2213 self.setPlaceholder(); 2214 self.nextSearchTerm = self.opts.nextSearchTerm(selected, self.search.val()); 2215 } 2216 }); 2217 } 2218 }, 2219 2220 isPlaceholderOptionSelected: function() { 2221 var placeholderOption; 2222 if (!this.getPlaceholder()) return false; // no placeholder specified so no option should be considered 2223 return ((placeholderOption = this.getPlaceholderOption()) !== undefined && placeholderOption.prop("selected")) 2224 || (this.opts.element.val() === "") 2225 || (this.opts.element.val() === undefined) 2226 || (this.opts.element.val() === null); 2227 }, 2228 2229 // single 2230 prepareOpts: function () { 2231 var opts = this.parent.prepareOpts.apply(this, arguments), 2232 self=this; 2233 2234 if (opts.element.get(0).tagName.toLowerCase() === "select") { 2235 // install the selection initializer 2236 opts.initSelection = function (element, callback) { 2237 var selected = element.find("option").filter(function() { return this.selected && !this.disabled }); 2238 // a single select box always has a value, no need to null check 'selected' 2239 callback(self.optionToData(selected)); 2240 }; 2241 } else if ("data" in opts) { 2242 // install default initSelection when applied to hidden input and data is local 2243 opts.initSelection = opts.initSelection || function (element, callback) { 2244 var id = element.val(); 2245 //search in data by id, storing the actual matching item 2246 var match = null; 2247 opts.query({ 2248 matcher: function(term, text, el){ 2249 var is_match = equal(id, opts.id(el)); 2250 if (is_match) { 2251 match = el; 2252 } 2253 return is_match; 2254 }, 2255 callback: !$.isFunction(callback) ? $.noop : function() { 2256 callback(match); 2257 } 2258 }); 2259 }; 2260 } 2261 2262 return opts; 2263 }, 2264 2265 // single 2266 getPlaceholder: function() { 2267 // if a placeholder is specified on a single select without a valid placeholder option ignore it 2268 if (this.select) { 2269 if (this.getPlaceholderOption() === undefined) { 2270 return undefined; 2271 } 2272 } 2273 2274 return this.parent.getPlaceholder.apply(this, arguments); 2275 }, 2276 2277 // single 2278 setPlaceholder: function () { 2279 var placeholder = this.getPlaceholder(); 2280 2281 if (this.isPlaceholderOptionSelected() && placeholder !== undefined) { 2282 2283 // check for a placeholder option if attached to a select 2284 if (this.select && this.getPlaceholderOption() === undefined) return; 2285 2286 this.selection.find(".select2-chosen").html(this.opts.escapeMarkup(placeholder)); 2287 2288 this.selection.addClass("select2-default"); 2289 2290 this.container.removeClass("select2-allowclear"); 2291 } 2292 }, 2293 2294 // single 2295 postprocessResults: function (data, initial, noHighlightUpdate) { 2296 var selected = 0, self = this, showSearchInput = true; 2297 2298 // find the selected element in the result list 2299 2300 this.findHighlightableChoices().each2(function (i, elm) { 2301 if (equal(self.id(elm.data("select2-data")), self.opts.element.val())) { 2302 selected = i; 2303 return false; 2304 } 2305 }); 2306 2307 // and highlight it 2308 if (noHighlightUpdate !== false) { 2309 if (initial === true && selected >= 0) { 2310 this.highlight(selected); 2311 } else { 2312 this.highlight(0); 2313 } 2314 } 2315 2316 // hide the search box if this is the first we got the results and there are enough of them for search 2317 2318 if (initial === true) { 2319 var min = this.opts.minimumResultsForSearch; 2320 if (min >= 0) { 2321 this.showSearch(countResults(data.results) >= min); 2322 } 2323 } 2324 }, 2325 2326 // single 2327 showSearch: function(showSearchInput) { 2328 if (this.showSearchInput === showSearchInput) return; 2329 2330 this.showSearchInput = showSearchInput; 2331 2332 this.dropdown.find(".select2-search").toggleClass("select2-search-hidden", !showSearchInput); 2333 this.dropdown.find(".select2-search").toggleClass("select2-offscreen", !showSearchInput); 2334 //add "select2-with-searchbox" to the container if search box is shown 2335 $(this.dropdown, this.container).toggleClass("select2-with-searchbox", showSearchInput); 2336 }, 2337 2338 // single 2339 onSelect: function (data, options) { 2340 2341 if (!this.triggerSelect(data)) { return; } 2342 2343 var old = this.opts.element.val(), 2344 oldData = this.data(); 2345 2346 this.opts.element.val(this.id(data)); 2347 this.updateSelection(data); 2348 2349 this.opts.element.trigger({ type: "select2-selected", val: this.id(data), choice: data }); 2350 2351 this.nextSearchTerm = this.opts.nextSearchTerm(data, this.search.val()); 2352 this.close(); 2353 2354 if ((!options || !options.noFocus) && this.opts.shouldFocusInput(this)) { 2355 this.focusser.focus(); 2356 } 2357 2358 if (!equal(old, this.id(data))) { 2359 this.triggerChange({ added: data, removed: oldData }); 2360 } 2361 }, 2362 2363 // single 2364 updateSelection: function (data) { 2365 2366 var container=this.selection.find(".select2-chosen"), formatted, cssClass; 2367 2368 this.selection.data("select2-data", data); 2369 2370 container.empty(); 2371 if (data !== null) { 2372 formatted=this.opts.formatSelection(data, container, this.opts.escapeMarkup); 2373 } 2374 if (formatted !== undefined) { 2375 container.append(formatted); 2376 } 2377 cssClass=this.opts.formatSelectionCssClass(data, container); 2378 if (cssClass !== undefined) { 2379 container.addClass(cssClass); 2380 } 2381 2382 this.selection.removeClass("select2-default"); 2383 2384 if (this.opts.allowClear && this.getPlaceholder() !== undefined) { 2385 this.container.addClass("select2-allowclear"); 2386 } 2387 }, 2388 2389 // single 2390 val: function () { 2391 var val, 2392 triggerChange = false, 2393 data = null, 2394 self = this, 2395 oldData = this.data(); 2396 2397 if (arguments.length === 0) { 2398 return this.opts.element.val(); 2399 } 2400 2401 val = arguments[0]; 2402 2403 if (arguments.length > 1) { 2404 triggerChange = arguments[1]; 2405 } 2406 2407 if (this.select) { 2408 this.select 2409 .val(val) 2410 .find("option").filter(function() { return this.selected }).each2(function (i, elm) { 2411 data = self.optionToData(elm); 2412 return false; 2413 }); 2414 this.updateSelection(data); 2415 this.setPlaceholder(); 2416 if (triggerChange) { 2417 this.triggerChange({added: data, removed:oldData}); 2418 } 2419 } else { 2420 // val is an id. !val is true for [undefined,null,'',0] - 0 is legal 2421 if (!val && val !== 0) { 2422 this.clear(triggerChange); 2423 return; 2424 } 2425 if (this.opts.initSelection === undefined) { 2426 throw new Error("cannot call val() if initSelection() is not defined"); 2427 } 2428 this.opts.element.val(val); 2429 this.opts.initSelection(this.opts.element, function(data){ 2430 self.opts.element.val(!data ? "" : self.id(data)); 2431 self.updateSelection(data); 2432 self.setPlaceholder(); 2433 if (triggerChange) { 2434 self.triggerChange({added: data, removed:oldData}); 2435 } 2436 }); 2437 } 2438 }, 2439 2440 // single 2441 clearSearch: function () { 2442 this.search.val(""); 2443 this.focusser.val(""); 2444 }, 2445 2446 // single 2447 data: function(value) { 2448 var data, 2449 triggerChange = false; 2450 2451 if (arguments.length === 0) { 2452 data = this.selection.data("select2-data"); 2453 if (data == undefined) data = null; 2454 return data; 2455 } else { 2456 if (arguments.length > 1) { 2457 triggerChange = arguments[1]; 2458 } 2459 if (!value) { 2460 this.clear(triggerChange); 2461 } else { 2462 data = this.data(); 2463 this.opts.element.val(!value ? "" : this.id(value)); 2464 this.updateSelection(value); 2465 if (triggerChange) { 2466 this.triggerChange({added: value, removed:data}); 2467 } 2468 } 2469 } 2470 } 2471 }); 2472 2473 MultiSelect2 = clazz(AbstractSelect2, { 2474 2475 // multi 2476 createContainer: function () { 2477 var container = $(document.createElement("div")).attr({ 2478 "class": "select2-container select2-container-multi" 2479 }).html([ 2480 "<ul class='select2-choices'>", 2481 " <li class='select2-search-field'>", 2482 " <label for='' class='select2-offscreen'></label>", 2483 " <input type='text' autocomplete='off' autocorrect='off' autocapitalize='off' spellcheck='false' class='select2-input'>", 2484 " </li>", 2485 "</ul>", 2486 "<div class='select2-drop select2-drop-multi select2-display-none'>", 2487 " <ul class='select2-results'>", 2488 " </ul>", 2489 "</div>"].join("")); 2490 return container; 2491 }, 2492 2493 // multi 2494 prepareOpts: function () { 2495 var opts = this.parent.prepareOpts.apply(this, arguments), 2496 self=this; 2497 2498 // TODO validate placeholder is a string if specified 2499 2500 if (opts.element.get(0).tagName.toLowerCase() === "select") { 2501 // install the selection initializer 2502 opts.initSelection = function (element, callback) { 2503 2504 var data = []; 2505 2506 element.find("option").filter(function() { return this.selected && !this.disabled }).each2(function (i, elm) { 2507 data.push(self.optionToData(elm)); 2508 }); 2509 callback(data); 2510 }; 2511 } else if ("data" in opts) { 2512 // install default initSelection when applied to hidden input and data is local 2513 opts.initSelection = opts.initSelection || function (element, callback) { 2514 var ids = splitVal(element.val(), opts.separator); 2515 //search in data by array of ids, storing matching items in a list 2516 var matches = []; 2517 opts.query({ 2518 matcher: function(term, text, el){ 2519 var is_match = $.grep(ids, function(id) { 2520 return equal(id, opts.id(el)); 2521 }).length; 2522 if (is_match) { 2523 matches.push(el); 2524 } 2525 return is_match; 2526 }, 2527 callback: !$.isFunction(callback) ? $.noop : function() { 2528 // reorder matches based on the order they appear in the ids array because right now 2529 // they are in the order in which they appear in data array 2530 var ordered = []; 2531 for (var i = 0; i < ids.length; i++) { 2532 var id = ids[i]; 2533 for (var j = 0; j < matches.length; j++) { 2534 var match = matches[j]; 2535 if (equal(id, opts.id(match))) { 2536 ordered.push(match); 2537 matches.splice(j, 1); 2538 break; 2539 } 2540 } 2541 } 2542 callback(ordered); 2543 } 2544 }); 2545 }; 2546 } 2547 2548 return opts; 2549 }, 2550 2551 // multi 2552 selectChoice: function (choice) { 2553 2554 var selected = this.container.find(".select2-search-choice-focus"); 2555 if (selected.length && choice && choice[0] == selected[0]) { 2556 2557 } else { 2558 if (selected.length) { 2559 this.opts.element.trigger("choice-deselected", selected); 2560 } 2561 selected.removeClass("select2-search-choice-focus"); 2562 if (choice && choice.length) { 2563 this.close(); 2564 choice.addClass("select2-search-choice-focus"); 2565 this.opts.element.trigger("choice-selected", choice); 2566 } 2567 } 2568 }, 2569 2570 // multi 2571 destroy: function() { 2572 $("label[for='" + this.search.attr('id') + "']") 2573 .attr('for', this.opts.element.attr("id")); 2574 this.parent.destroy.apply(this, arguments); 2575 }, 2576 2577 // multi 2578 initContainer: function () { 2579 2580 var selector = ".select2-choices", selection; 2581 2582 this.searchContainer = this.container.find(".select2-search-field"); 2583 this.selection = selection = this.container.find(selector); 2584 2585 var _this = this; 2586 this.selection.on("click", ".select2-search-choice:not(.select2-locked)", function (e) { 2587 //killEvent(e); 2588 _this.search[0].focus(); 2589 _this.selectChoice($(this)); 2590 }); 2591 2592 // rewrite labels from original element to focusser 2593 this.search.attr("id", "s2id_autogen"+nextUid()); 2594 2595 this.search.prev() 2596 .text($("label[for='" + this.opts.element.attr("id") + "']").text()) 2597 .attr('for', this.search.attr('id')); 2598 2599 this.search.on("input paste", this.bind(function() { 2600 if (!this.isInterfaceEnabled()) return; 2601 if (!this.opened()) { 2602 this.open(); 2603 } 2604 })); 2605 2606 this.search.attr("tabindex", this.elementTabIndex); 2607 2608 this.keydowns = 0; 2609 this.search.on("keydown", this.bind(function (e) { 2610 if (!this.isInterfaceEnabled()) return; 2611 2612 ++this.keydowns; 2613 var selected = selection.find(".select2-search-choice-focus"); 2614 var prev = selected.prev(".select2-search-choice:not(.select2-locked)"); 2615 var next = selected.next(".select2-search-choice:not(.select2-locked)"); 2616 var pos = getCursorInfo(this.search); 2617 2618 if (selected.length && 2619 (e.which == KEY.LEFT || e.which == KEY.RIGHT || e.which == KEY.BACKSPACE || e.which == KEY.DELETE || e.which == KEY.ENTER)) { 2620 var selectedChoice = selected; 2621 if (e.which == KEY.LEFT && prev.length) { 2622 selectedChoice = prev; 2623 } 2624 else if (e.which == KEY.RIGHT) { 2625 selectedChoice = next.length ? next : null; 2626 } 2627 else if (e.which === KEY.BACKSPACE) { 2628 if (this.unselect(selected.first())) { 2629 this.search.width(10); 2630 selectedChoice = prev.length ? prev : next; 2631 } 2632 } else if (e.which == KEY.DELETE) { 2633 if (this.unselect(selected.first())) { 2634 this.search.width(10); 2635 selectedChoice = next.length ? next : null; 2636 } 2637 } else if (e.which == KEY.ENTER) { 2638 selectedChoice = null; 2639 } 2640 2641 this.selectChoice(selectedChoice); 2642 killEvent(e); 2643 if (!selectedChoice || !selectedChoice.length) { 2644 this.open(); 2645 } 2646 return; 2647 } else if (((e.which === KEY.BACKSPACE && this.keydowns == 1) 2648 || e.which == KEY.LEFT) && (pos.offset == 0 && !pos.length)) { 2649 2650 this.selectChoice(selection.find(".select2-search-choice:not(.select2-locked)").last()); 2651 killEvent(e); 2652 return; 2653 } else { 2654 this.selectChoice(null); 2655 } 2656 2657 if (this.opened()) { 2658 switch (e.which) { 2659 case KEY.UP: 2660 case KEY.DOWN: 2661 this.moveHighlight((e.which === KEY.UP) ? -1 : 1); 2662 killEvent(e); 2663 return; 2664 case KEY.ENTER: 2665 this.selectHighlighted(); 2666 killEvent(e); 2667 return; 2668 case KEY.TAB: 2669 this.selectHighlighted({noFocus:true}); 2670 this.close(); 2671 return; 2672 case KEY.ESC: 2673 this.cancel(e); 2674 killEvent(e); 2675 return; 2676 } 2677 } 2678 2679 if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) 2680 || e.which === KEY.BACKSPACE || e.which === KEY.ESC) { 2681 return; 2682 } 2683 2684 if (e.which === KEY.ENTER) { 2685 if (this.opts.openOnEnter === false) { 2686 return; 2687 } else if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) { 2688 return; 2689 } 2690 } 2691 2692 this.open(); 2693 2694 if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) { 2695 // prevent the page from scrolling 2696 killEvent(e); 2697 } 2698 2699 if (e.which === KEY.ENTER) { 2700 // prevent form from being submitted 2701 killEvent(e); 2702 } 2703 2704 })); 2705 2706 this.search.on("keyup", this.bind(function (e) { 2707 this.keydowns = 0; 2708 this.resizeSearch(); 2709 }) 2710 ); 2711 2712 this.search.on("blur", this.bind(function(e) { 2713 this.container.removeClass("select2-container-active"); 2714 this.search.removeClass("select2-focused"); 2715 this.selectChoice(null); 2716 if (!this.opened()) this.clearSearch(); 2717 e.stopImmediatePropagation(); 2718 this.opts.element.trigger($.Event("select2-blur")); 2719 })); 2720 2721 this.container.on("click", selector, this.bind(function (e) { 2722 if (!this.isInterfaceEnabled()) return; 2723 if ($(e.target).closest(".select2-search-choice").length > 0) { 2724 // clicked inside a select2 search choice, do not open 2725 return; 2726 } 2727 this.selectChoice(null); 2728 this.clearPlaceholder(); 2729 if (!this.container.hasClass("select2-container-active")) { 2730 this.opts.element.trigger($.Event("select2-focus")); 2731 } 2732 this.open(); 2733 this.focusSearch(); 2734 e.preventDefault(); 2735 })); 2736 2737 this.container.on("focus", selector, this.bind(function () { 2738 if (!this.isInterfaceEnabled()) return; 2739 if (!this.container.hasClass("select2-container-active")) { 2740 this.opts.element.trigger($.Event("select2-focus")); 2741 } 2742 this.container.addClass("select2-container-active"); 2743 this.dropdown.addClass("select2-drop-active"); 2744 this.clearPlaceholder(); 2745 })); 2746 2747 this.initContainerWidth(); 2748 this.opts.element.addClass("select2-offscreen"); 2749 2750 // set the placeholder if necessary 2751 this.clearSearch(); 2752 }, 2753 2754 // multi 2755 enableInterface: function() { 2756 if (this.parent.enableInterface.apply(this, arguments)) { 2757 this.search.prop("disabled", !this.isInterfaceEnabled()); 2758 } 2759 }, 2760 2761 // multi 2762 initSelection: function () { 2763 var data; 2764 if (this.opts.element.val() === "" && this.opts.element.text() === "") { 2765 this.updateSelection([]); 2766 this.close(); 2767 // set the placeholder if necessary 2768 this.clearSearch(); 2769 } 2770 if (this.select || this.opts.element.val() !== "") { 2771 var self = this; 2772 this.opts.initSelection.call(null, this.opts.element, function(data){ 2773 if (data !== undefined && data !== null) { 2774 self.updateSelection(data); 2775 self.close(); 2776 // set the placeholder if necessary 2777 self.clearSearch(); 2778 } 2779 }); 2780 } 2781 }, 2782 2783 // multi 2784 clearSearch: function () { 2785 var placeholder = this.getPlaceholder(), 2786 maxWidth = this.getMaxSearchWidth(); 2787 2788 if (placeholder !== undefined && this.getVal().length === 0 && this.search.hasClass("select2-focused") === false) { 2789 this.search.val(placeholder).addClass("select2-default"); 2790 // stretch the search box to full width of the container so as much of the placeholder is visible as possible 2791 // 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 2792 this.search.width(maxWidth > 0 ? maxWidth : this.container.css("width")); 2793 } else { 2794 this.search.val("").width(10); 2795 } 2796 }, 2797 2798 // multi 2799 clearPlaceholder: function () { 2800 if (this.search.hasClass("select2-default")) { 2801 this.search.val("").removeClass("select2-default"); 2802 } 2803 }, 2804 2805 // multi 2806 opening: function () { 2807 this.clearPlaceholder(); // should be done before super so placeholder is not used to search 2808 this.resizeSearch(); 2809 2810 this.parent.opening.apply(this, arguments); 2811 2812 this.focusSearch(); 2813 2814 // initializes search's value with nextSearchTerm (if defined by user) 2815 // ignore nextSearchTerm if the dropdown is opened by the user pressing a letter 2816 if(this.search.val() === "") { 2817 if(this.nextSearchTerm != undefined){ 2818 this.search.val(this.nextSearchTerm); 2819 this.search.select(); 2820 } 2821 } 2822 2823 this.updateResults(true); 2824 this.search.focus(); 2825 this.opts.element.trigger($.Event("select2-open")); 2826 }, 2827 2828 // multi 2829 close: function () { 2830 if (!this.opened()) return; 2831 this.parent.close.apply(this, arguments); 2832 }, 2833 2834 // multi 2835 focus: function () { 2836 this.close(); 2837 this.search.focus(); 2838 }, 2839 2840 // multi 2841 isFocused: function () { 2842 return this.search.hasClass("select2-focused"); 2843 }, 2844 2845 // multi 2846 updateSelection: function (data) { 2847 var ids = [], filtered = [], self = this; 2848 2849 // filter out duplicates 2850 $(data).each(function () { 2851 if (indexOf(self.id(this), ids) < 0) { 2852 ids.push(self.id(this)); 2853 filtered.push(this); 2854 } 2855 }); 2856 data = filtered; 2857 2858 this.selection.find(".select2-search-choice").remove(); 2859 $(data).each(function () { 2860 self.addSelectedChoice(this); 2861 }); 2862 self.postprocessResults(); 2863 }, 2864 2865 // multi 2866 tokenize: function() { 2867 var input = this.search.val(); 2868 input = this.opts.tokenizer.call(this, input, this.data(), this.bind(this.onSelect), this.opts); 2869 if (input != null && input != undefined) { 2870 this.search.val(input); 2871 if (input.length > 0) { 2872 this.open(); 2873 } 2874 } 2875 2876 }, 2877 2878 // multi 2879 onSelect: function (data, options) { 2880 2881 if (!this.triggerSelect(data)) { return; } 2882 2883 this.addSelectedChoice(data); 2884 2885 this.opts.element.trigger({ type: "selected", val: this.id(data), choice: data }); 2886 2887 // keep track of the search's value before it gets cleared 2888 this.nextSearchTerm = this.opts.nextSearchTerm(data, this.search.val()); 2889 2890 this.clearSearch(); 2891 this.updateResults(); 2892 2893 if (this.select || !this.opts.closeOnSelect) this.postprocessResults(data, false, this.opts.closeOnSelect===true); 2894 2895 if (this.opts.closeOnSelect) { 2896 this.close(); 2897 this.search.width(10); 2898 } else { 2899 if (this.countSelectableResults()>0) { 2900 this.search.width(10); 2901 this.resizeSearch(); 2902 if (this.getMaximumSelectionSize() > 0 && this.val().length >= this.getMaximumSelectionSize()) { 2903 // if we reached max selection size repaint the results so choices 2904 // are replaced with the max selection reached message 2905 this.updateResults(true); 2906 } else { 2907 // initializes search's value with nextSearchTerm and update search result 2908 if(this.nextSearchTerm != undefined){ 2909 this.search.val(this.nextSearchTerm); 2910 this.updateResults(); 2911 this.search.select(); 2912 } 2913 } 2914 this.positionDropdown(); 2915 } else { 2916 // if nothing left to select close 2917 this.close(); 2918 this.search.width(10); 2919 } 2920 } 2921 2922 // since its not possible to select an element that has already been 2923 // added we do not need to check if this is a new element before firing change 2924 this.triggerChange({ added: data }); 2925 2926 if (!options || !options.noFocus) 2927 this.focusSearch(); 2928 }, 2929 2930 // multi 2931 cancel: function () { 2932 this.close(); 2933 this.focusSearch(); 2934 }, 2935 2936 addSelectedChoice: function (data) { 2937 var enableChoice = !data.locked, 2938 enabledItem = $( 2939 "<li class='select2-search-choice'>" + 2940 " <div></div>" + 2941 " <a href='#' class='select2-search-choice-close' tabindex='-1'></a>" + 2942 "</li>"), 2943 disabledItem = $( 2944 "<li class='select2-search-choice select2-locked'>" + 2945 "<div></div>" + 2946 "</li>"); 2947 var choice = enableChoice ? enabledItem : disabledItem, 2948 id = this.id(data), 2949 val = this.getVal(), 2950 formatted, 2951 cssClass; 2952 2953 formatted=this.opts.formatSelection(data, choice.find("div"), this.opts.escapeMarkup); 2954 if (formatted != undefined) { 2955 choice.find("div").replaceWith("<div>"+formatted+"</div>"); 2956 } 2957 cssClass=this.opts.formatSelectionCssClass(data, choice.find("div")); 2958 if (cssClass != undefined) { 2959 choice.addClass(cssClass); 2960 } 2961 2962 if(enableChoice){ 2963 choice.find(".select2-search-choice-close") 2964 .on("mousedown", killEvent) 2965 .on("click dblclick", this.bind(function (e) { 2966 if (!this.isInterfaceEnabled()) return; 2967 2968 this.unselect($(e.target)); 2969 this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus"); 2970 killEvent(e); 2971 this.close(); 2972 this.focusSearch(); 2973 })).on("focus", this.bind(function () { 2974 if (!this.isInterfaceEnabled()) return; 2975 this.container.addClass("select2-container-active"); 2976 this.dropdown.addClass("select2-drop-active"); 2977 })); 2978 } 2979 2980 choice.data("select2-data", data); 2981 choice.insertBefore(this.searchContainer); 2982 2983 val.push(id); 2984 this.setVal(val); 2985 }, 2986 2987 // multi 2988 unselect: function (selected) { 2989 var val = this.getVal(), 2990 data, 2991 index; 2992 selected = selected.closest(".select2-search-choice"); 2993 2994 if (selected.length === 0) { 2995 throw "Invalid argument: " + selected + ". Must be .select2-search-choice"; 2996 } 2997 2998 data = selected.data("select2-data"); 2999 3000 if (!data) { 3001 // prevent a race condition when the 'x' is clicked really fast repeatedly the event can be queued 3002 // and invoked on an element already removed 3003 return; 3004 } 3005 3006 var evt = $.Event("select2-removing"); 3007 evt.val = this.id(data); 3008 evt.choice = data; 3009 this.opts.element.trigger(evt); 3010 3011 if (evt.isDefaultPrevented()) { 3012 return false; 3013 } 3014 3015 while((index = indexOf(this.id(data), val)) >= 0) { 3016 val.splice(index, 1); 3017 this.setVal(val); 3018 if (this.select) this.postprocessResults(); 3019 } 3020 3021 selected.remove(); 3022 3023 this.opts.element.trigger({ type: "select2-removed", val: this.id(data), choice: data }); 3024 this.triggerChange({ removed: data }); 3025 3026 return true; 3027 }, 3028 3029 // multi 3030 postprocessResults: function (data, initial, noHighlightUpdate) { 3031 var val = this.getVal(), 3032 choices = this.results.find(".select2-result"), 3033 compound = this.results.find(".select2-result-with-children"), 3034 self = this; 3035 3036 choices.each2(function (i, choice) { 3037 var id = self.id(choice.data("select2-data")); 3038 if (indexOf(id, val) >= 0) { 3039 choice.addClass("select2-selected"); 3040 // mark all children of the selected parent as selected 3041 choice.find(".select2-result-selectable").addClass("select2-selected"); 3042 } 3043 }); 3044 3045 compound.each2(function(i, choice) { 3046 // hide an optgroup if it doesn't have any selectable children 3047 if (!choice.is('.select2-result-selectable') 3048 && choice.find(".select2-result-selectable:not(.select2-selected)").length === 0) { 3049 choice.addClass("select2-selected"); 3050 } 3051 }); 3052 3053 if (this.highlight() == -1 && noHighlightUpdate !== false){ 3054 self.highlight(0); 3055 } 3056 3057 //If all results are chosen render formatNoMatches 3058 if(!this.opts.createSearchChoice && !choices.filter('.select2-result:not(.select2-selected)').length > 0){ 3059 if(!data || data && !data.more && this.results.find(".select2-no-results").length === 0) { 3060 if (checkFormatter(self.opts.formatNoMatches, "formatNoMatches")) { 3061 this.results.append("<li class='select2-no-results'>" + evaluate(self.opts.formatNoMatches, self.search.val()) + "</li>"); 3062 } 3063 } 3064 } 3065 3066 }, 3067 3068 // multi 3069 getMaxSearchWidth: function() { 3070 return this.selection.width() - getSideBorderPadding(this.search); 3071 }, 3072 3073 // multi 3074 resizeSearch: function () { 3075 var minimumWidth, left, maxWidth, containerLeft, searchWidth, 3076 sideBorderPadding = getSideBorderPadding(this.search); 3077 3078 minimumWidth = measureTextWidth(this.search) + 10; 3079 3080 left = this.search.offset().left; 3081 3082 maxWidth = this.selection.width(); 3083 containerLeft = this.selection.offset().left; 3084 3085 searchWidth = maxWidth - (left - containerLeft) - sideBorderPadding; 3086 3087 if (searchWidth < minimumWidth) { 3088 searchWidth = maxWidth - sideBorderPadding; 3089 } 3090 3091 if (searchWidth < 40) { 3092 searchWidth = maxWidth - sideBorderPadding; 3093 } 3094 3095 if (searchWidth <= 0) { 3096 searchWidth = minimumWidth; 3097 } 3098 3099 this.search.width(Math.floor(searchWidth)); 3100 }, 3101 3102 // multi 3103 getVal: function () { 3104 var val; 3105 if (this.select) { 3106 val = this.select.val(); 3107 return val === null ? [] : val; 3108 } else { 3109 val = this.opts.element.val(); 3110 return splitVal(val, this.opts.separator); 3111 } 3112 }, 3113 3114 // multi 3115 setVal: function (val) { 3116 var unique; 3117 if (this.select) { 3118 this.select.val(val); 3119 } else { 3120 unique = []; 3121 // filter out duplicates 3122 $(val).each(function () { 3123 if (indexOf(this, unique) < 0) unique.push(this); 3124 }); 3125 this.opts.element.val(unique.length === 0 ? "" : unique.join(this.opts.separator)); 3126 } 3127 }, 3128 3129 // multi 3130 buildChangeDetails: function (old, current) { 3131 var current = current.slice(0), 3132 old = old.slice(0); 3133 3134 // remove intersection from each array 3135 for (var i = 0; i < current.length; i++) { 3136 for (var j = 0; j < old.length; j++) { 3137 if (equal(this.opts.id(current[i]), this.opts.id(old[j]))) { 3138 current.splice(i, 1); 3139 if(i>0){ 3140 i--; 3141 } 3142 old.splice(j, 1); 3143 j--; 3144 } 3145 } 3146 } 3147 3148 return {added: current, removed: old}; 3149 }, 3150 3151 3152 // multi 3153 val: function (val, triggerChange) { 3154 var oldData, self=this; 3155 3156 if (arguments.length === 0) { 3157 return this.getVal(); 3158 } 3159 3160 oldData=this.data(); 3161 if (!oldData.length) oldData=[]; 3162 3163 // val is an id. !val is true for [undefined,null,'',0] - 0 is legal 3164 if (!val && val !== 0) { 3165 this.opts.element.val(""); 3166 this.updateSelection([]); 3167 this.clearSearch(); 3168 if (triggerChange) { 3169 this.triggerChange({added: this.data(), removed: oldData}); 3170 } 3171 return; 3172 } 3173 3174 // val is a list of ids 3175 this.setVal(val); 3176 3177 if (this.select) { 3178 this.opts.initSelection(this.select, this.bind(this.updateSelection)); 3179 if (triggerChange) { 3180 this.triggerChange(this.buildChangeDetails(oldData, this.data())); 3181 } 3182 } else { 3183 if (this.opts.initSelection === undefined) { 3184 throw new Error("val() cannot be called if initSelection() is not defined"); 3185 } 3186 3187 this.opts.initSelection(this.opts.element, function(data){ 3188 var ids=$.map(data, self.id); 3189 self.setVal(ids); 3190 self.updateSelection(data); 3191 self.clearSearch(); 3192 if (triggerChange) { 3193 self.triggerChange(self.buildChangeDetails(oldData, self.data())); 3194 } 3195 }); 3196 } 3197 this.clearSearch(); 3198 }, 3199 3200 // multi 3201 onSortStart: function() { 3202 if (this.select) { 3203 throw new Error("Sorting of elements is not supported when attached to <select>. Attach to <input type='hidden'/> instead."); 3204 } 3205 3206 // collapse search field into 0 width so its container can be collapsed as well 3207 this.search.width(0); 3208 // hide the container 3209 this.searchContainer.hide(); 3210 }, 3211 3212 // multi 3213 onSortEnd:function() { 3214 3215 var val=[], self=this; 3216 3217 // show search and move it to the end of the list 3218 this.searchContainer.show(); 3219 // make sure the search container is the last item in the list 3220 this.searchContainer.appendTo(this.searchContainer.parent()); 3221 // since we collapsed the width in dragStarted, we resize it here 3222 this.resizeSearch(); 3223 3224 // update selection 3225 this.selection.find(".select2-search-choice").each(function() { 3226 val.push(self.opts.id($(this).data("select2-data"))); 3227 }); 3228 this.setVal(val); 3229 this.triggerChange(); 3230 }, 3231 3232 // multi 3233 data: function(values, triggerChange) { 3234 var self=this, ids, old; 3235 if (arguments.length === 0) { 3236 return this.selection 3237 .children(".select2-search-choice") 3238 .map(function() { return $(this).data("select2-data"); }) 3239 .get(); 3240 } else { 3241 old = this.data(); 3242 if (!values) { values = []; } 3243 ids = $.map(values, function(e) { return self.opts.id(e); }); 3244 this.setVal(ids); 3245 this.updateSelection(values); 3246 this.clearSearch(); 3247 if (triggerChange) { 3248 this.triggerChange(this.buildChangeDetails(old, this.data())); 3249 } 3250 } 3251 } 3252 }); 3253 3254 $.fn.select2 = function () { 3255 3256 var args = Array.prototype.slice.call(arguments, 0), 3257 opts, 3258 select2, 3259 method, value, multiple, 3260 allowedMethods = ["val", "destroy", "opened", "open", "close", "focus", "isFocused", "container", "dropdown", "onSortStart", "onSortEnd", "enable", "disable", "readonly", "positionDropdown", "data", "search"], 3261 valueMethods = ["opened", "isFocused", "container", "dropdown"], 3262 propertyMethods = ["val", "data"], 3263 methodsMap = { search: "externalSearch" }; 3264 3265 this.each(function () { 3266 if (args.length === 0 || typeof(args[0]) === "object") { 3267 opts = args.length === 0 ? {} : $.extend({}, args[0]); 3268 opts.element = $(this); 3269 3270 if (opts.element.get(0).tagName.toLowerCase() === "select") { 3271 multiple = opts.element.prop("multiple"); 3272 } else { 3273 multiple = opts.multiple || false; 3274 if ("tags" in opts) {opts.multiple = multiple = true;} 3275 } 3276 3277 select2 = multiple ? new window.Select2["class"].multi() : new window.Select2["class"].single(); 3278 select2.init(opts); 3279 } else if (typeof(args[0]) === "string") { 3280 3281 if (indexOf(args[0], allowedMethods) < 0) { 3282 throw "Unknown method: " + args[0]; 3283 } 3284 3285 value = undefined; 3286 select2 = $(this).data("select2"); 3287 if (select2 === undefined) return; 3288 3289 method=args[0]; 3290 3291 if (method === "container") { 3292 value = select2.container; 3293 } else if (method === "dropdown") { 3294 value = select2.dropdown; 3295 } else { 3296 if (methodsMap[method]) method = methodsMap[method]; 3297 3298 value = select2[method].apply(select2, args.slice(1)); 3299 } 3300 if (indexOf(args[0], valueMethods) >= 0 3301 || (indexOf(args[0], propertyMethods) && args.length == 1)) { 3302 return false; // abort the iteration, ready to return first matched value 3303 } 3304 } else { 3305 throw "Invalid arguments to select2 plugin: " + args; 3306 } 3307 }); 3308 return (value === undefined) ? this : value; 3309 }; 3310 3311 // plugin defaults, accessible to users 3312 $.fn.select2.defaults = { 3313 width: "copy", 3314 loadMorePadding: 0, 3315 closeOnSelect: true, 3316 openOnEnter: true, 3317 containerCss: {}, 3318 dropdownCss: {}, 3319 containerCssClass: "", 3320 dropdownCssClass: "", 3321 formatResult: function(result, container, query, escapeMarkup) { 3322 var markup=[]; 3323 markMatch(result.text, query.term, markup, escapeMarkup); 3324 return markup.join(""); 3325 }, 3326 formatSelection: function (data, container, escapeMarkup) { 3327 return data ? escapeMarkup(data.text) : undefined; 3328 }, 3329 sortResults: function (results, container, query) { 3330 return results; 3331 }, 3332 formatResultCssClass: function(data) {return data.css;}, 3333 formatSelectionCssClass: function(data, container) {return undefined;}, 3334 formatMatches: function (matches) { return matches + " results are available, use up and down arrow keys to navigate."; }, 3335 formatNoMatches: function () { return "No matches found"; }, 3336 formatInputTooShort: function (input, min) { var n = min - input.length; return "Please enter " + n + " or more character" + (n == 1? "" : "s"); }, 3337 formatInputTooLong: function (input, max) { var n = input.length - max; return "Please delete " + n + " character" + (n == 1? "" : "s"); }, 3338 formatSelectionTooBig: function (limit) { return "You can only select " + limit + " item" + (limit == 1 ? "" : "s"); }, 3339 formatLoadMore: function (pageNumber) { return "Loading more results…"; }, 3340 formatSearching: function () { return "Searching…"; }, 3341 minimumResultsForSearch: 0, 3342 minimumInputLength: 0, 3343 maximumInputLength: null, 3344 maximumSelectionSize: 0, 3345 id: function (e) { return e == undefined ? null : e.id; }, 3346 matcher: function(term, text) { 3347 return stripDiacritics(''+text).toUpperCase().indexOf(stripDiacritics(''+term).toUpperCase()) >= 0; 3348 }, 3349 separator: ",", 3350 tokenSeparators: [], 3351 tokenizer: defaultTokenizer, 3352 escapeMarkup: defaultEscapeMarkup, 3353 blurOnChange: false, 3354 selectOnBlur: false, 3355 adaptContainerCssClass: function(c) { return c; }, 3356 adaptDropdownCssClass: function(c) { return null; }, 3357 nextSearchTerm: function(selectedObject, currentSearchTerm) { return undefined; }, 3358 searchInputPlaceholder: '', 3359 createSearchChoicePosition: 'top', 3360 shouldFocusInput: function (instance) { 3361 // Never focus the input if search is disabled 3362 if (instance.opts.minimumResultsForSearch < 0) { 3363 return false; 3364 } 3365 3366 return true; 3367 } 3368 }; 3369 3370 $.fn.select2.ajaxDefaults = { 3371 transport: $.ajax, 3372 params: { 3373 type: "GET", 3374 cache: false, 3375 dataType: "json" 3376 } 3377 }; 3378 3379 // exports 3380 window.Select2 = { 3381 query: { 3382 ajax: ajax, 3383 local: local, 3384 tags: tags 3385 }, util: { 3386 debounce: debounce, 3387 markMatch: markMatch, 3388 escapeMarkup: defaultEscapeMarkup, 3389 stripDiacritics: stripDiacritics 3390 }, "class": { 3391 "abstract": AbstractSelect2, 3392 "single": SingleSelect2, 3393 "multi": MultiSelect2 3394 } 3395 }; 3396 3397}(jQuery)); 3398