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