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(opts); 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 (opts) { 1946 var html = [ 1947 "<a href='javascript:void(0)' class='select2-choice' tabindex='-1'>", 1948 " <span class='select2-chosen'> </span><abbr class='select2-search-choice-close'></abbr>", 1949 " <span class='select2-arrow' role='presentation'><b role='presentation'></b></span>", 1950 "</a>", 1951 "<label for='' class='select2-offscreen'></label>", 1952 "<input class='select2-focusser select2-offscreen' type='text' aria-haspopup='true' role='button' />", 1953 "<div class='select2-drop select2-display-none'>"]; 1954 1955 1956 if (opts.searchBar) { 1957 var html2 = [" <div class='select2-search'>"]; 1958 } else { 1959 var html2 = [" <div class='select2-search' style='display:none;'>"]; 1960 } 1961 html = $.merge(html, html2); 1962 1963 var html2 = [ 1964 " <label for='' class='select2-offscreen'></label>", 1965 " <input type='text' autocomplete='off' autocorrect='off' autocapitalize='off' spellcheck='false' class='select2-input' role='combobox' aria-expanded='true'", 1966 " aria-autocomplete='list' />", 1967 " </div>", 1968 " <ul class='select2-results' role='listbox'>", 1969 " </ul>", 1970 "</div>"]; 1971 1972 html = $.merge(html, html2); 1973 1974 var container = $(document.createElement("div")).attr({ 1975 "class": "select2-container" 1976 }).html(html.join("")); 1977 return container; 1978 }, 1979 1980 // single 1981 enableInterface: function() { 1982 if (this.parent.enableInterface.apply(this, arguments)) { 1983 this.focusser.prop("disabled", !this.isInterfaceEnabled()); 1984 } 1985 }, 1986 1987 // single 1988 opening: function () { 1989 var el, range, len;
1990 1991 if (this.opts.minimumResultsForSearch >= 0) { 1992 this.showSearch(true); 1993 } 1994 1995 this.parent.opening.apply(this, arguments); 1996 1997 if (this.showSearchInput !== false) { 1998 // IE appends focusser.val() at the end of field :/ so we manually insert it at the beginning using a range 1999 // all other browsers handle this just fine 2000 2001 this.search.val(this.focusser.val()); 2002 } 2003 if (this.opts.shouldFocusInput(this)) { 2004 this.search.focus(); 2005 // move the cursor to the end after focussing, otherwise it will be at the beginning and 2006 // new text will appear *before* focusser.val() 2007 el = this.search.get(0); 2008 if (el.createTextRange) { 2009 range = el.createTextRange(); 2010 range.collapse(false); 2011 range.select(); 2012 } else if (el.setSelectionRange) { 2013 len = this.search.val().length; 2014 el.setSelectionRange(len, len); 2015 } 2016 } 2017 2018 // initializes search's value with nextSearchTerm (if defined by user) 2019 // ignore nextSearchTerm if the dropdown is opened by the user pressing a letter 2020 if(this.search.val() === "") { 2021 if(this.nextSearchTerm != undefined){ 2022 this.search.val(this.nextSearchTerm); 2023 this.search.select(); 2024 } 2025 } 2026 2027 this.focusser.prop("disabled", true).val(""); 2028 this.updateResults(true); 2029 this.opts.element.trigger($.Event("select2-open")); 2030 }, 2031 2032 // single 2033 close: function () { 2034 if (!this.opened()) return; 2035 this.parent.close.apply(this, arguments); 2036 2037 this.focusser.prop("disabled", false); 2038 2039 if (this.opts.shouldFocusInput(this)) { 2040 this.focusser.focus(); 2041 } 2042 }, 2043 2044 // single 2045 focus: function () { 2046 if (this.opened()) { 2047 this.close(); 2048 } else { 2049 this.focusser.prop("disabled", false); 2050 if (this.opts.shouldFocusInput(this)) { 2051 this.focusser.focus(); 2052 } 2053 } 2054 }, 2055 2056 // single 2057 isFocused: function () { 2058 return this.container.hasClass("select2-container-active"); 2059 }, 2060 2061 // single 2062 cancel: function () { 2063 this.parent.cancel.apply(this, arguments); 2064 this.focusser.prop("disabled", false); 2065 2066 if (this.opts.shouldFocusInput(this)) { 2067 this.focusser.focus(); 2068 } 2069 }, 2070 2071 // single 2072 destroy: function() { 2073 $("label[for='" + this.focusser.attr('id') + "']") 2074 .attr('for', this.opts.element.attr("id")); 2075 this.parent.destroy.apply(this, arguments); 2076 2077 cleanupJQueryElements.call(this, 2078 "selection", 2079 "focusser" 2080 ); 2081 }, 2082 2083 // single 2084 initContainer: function () { 2085 2086 var selection, 2087 container = this.container, 2088 dropdown = this.dropdown, 2089 idSuffix = nextUid(), 2090 elementLabel; 2091 2092 if (this.opts.minimumResultsForSearch < 0) { 2093 this.showSearch(false); 2094 } else { 2095 this.showSearch(true); 2096 } 2097 2098 this.selection = selection = container.find(".select2-choice"); 2099 2100 this.focusser = container.find(".select2-focusser"); 2101 2102 // add aria associations 2103 selection.find(".select2-chosen").attr("id", "select2-chosen-"+idSuffix); 2104 this.focusser.attr("aria-labelledby", "select2-chosen-"+idSuffix); 2105 this.results.attr("id", "select2-results-"+idSuffix); 2106 this.search.attr("aria-owns", "select2-results-"+idSuffix); 2107 2108 // rewrite labels from original element to focusser 2109 this.focusser.attr("id", "s2id_autogen"+idSuffix); 2110 2111 elementLabel = $("label[for='" + this.opts.element.attr("id") + "']"); 2112 2113 this.focusser.prev() 2114 .text(elementLabel.text()) 2115 .attr('for', this.focusser.attr('id')); 2116 2117 // Ensure the original element retains an accessible name 2118 var originalTitle = this.opts.element.attr("title"); 2119 this.opts.element.attr("title", (originalTitle || elementLabel.text())); 2120 2121 this.focusser.attr("tabindex", this.elementTabIndex); 2122
2123 // write label for search field using the label from the focusser element 2124 this.search.attr("id", this.focusser.attr('id') + '_search'); 2125 2126 this.search.prev() 2127 .text($("label[for='" + this.focusser.attr('id') + "']").text()) 2128 .attr('for', this.search.attr('id')); 2129 2130 this.search.on("keydown", this.bind(function (e) { 2131 if (!this.isInterfaceEnabled()) return; 2132 2133 // filter 229 keyCodes (input method editor is processing key input) 2134 if (229 == e.keyCode) return; 2135 2136 if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) { 2137 // prevent the page from scrolling 2138 killEvent(e); 2139 return; 2140 } 2141 2142 switch (e.which) { 2143 case KEY.UP: 2144 case KEY.DOWN: 2145 this.moveHighlight((e.which === KEY.UP) ? -1 : 1); 2146 killEvent(e); 2147 return; 2148 case KEY.ENTER: 2149 this.selectHighlighted(); 2150 killEvent(e); 2151 return; 2152 case KEY.TAB: 2153 this.selectHighlighted({noFocus: true}); 2154 return; 2155 case KEY.ESC: 2156 this.cancel(e); 2157 killEvent(e); 2158 return; 2159 } 2160 })); 2161 2162 this.search.on("blur", this.bind(function(e) { 2163 // a workaround for chrome to keep the search field focussed when the scroll bar is used to scroll the dropdown. 2164 // without this the search field loses focus which is annoying 2165 if (document.activeElement === this.body.get(0)) { 2166 window.setTimeout(this.bind(function() { 2167 if (this.opened()) { 2168 this.search.focus(); 2169 } 2170 }), 0); 2171 } 2172 })); 2173 2174 this.focusser.on("keydown", this.bind(function (e) { 2175 if (!this.isInterfaceEnabled()) return; 2176 2177 if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) { 2178 return; 2179 } 2180 2181 if (this.opts.openOnEnter === false && e.which === KEY.ENTER) { 2182 killEvent(e); 2183 return; 2184 } 2185 2186 if (e.which == KEY.DOWN || e.which == KEY.UP 2187 || (e.which == KEY.ENTER && this.opts.openOnEnter)) { 2188 2189 if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) return; 2190 2191 this.open(); 2192 killEvent(e); 2193 return; 2194 } 2195 2196 if (e.which == KEY.DELETE || e.which == KEY.BACKSPACE) { 2197 if (this.opts.allowClear) { 2198 this.clear(); 2199 } 2200 killEvent(e); 2201 return; 2202 } 2203 })); 2204 2205 2206 installKeyUpChangeEvent(this.focusser); 2207 this.focusser.on("keyup-change input", this.bind(function(e) { 2208 if (this.opts.minimumResultsForSearch >= 0) { 2209 e.stopPropagation(); 2210 if (this.opened()) return; 2211 this.open(); 2212 } 2213 })); 2214 2215 selection.on("mousedown touchstart", "abbr", this.bind(function (e) { 2216 if (!this.isInterfaceEnabled()) return; 2217 this.clear(); 2218 killEventImmediately(e); 2219 this.close(); 2220 this.selection.focus(); 2221 })); 2222 2223 selection.on("mousedown touchstart", this.bind(function (e) { 2224 // Prevent IE from generating a click event on the body 2225 reinsertElement(selection); 2226 2227 if (!this.container.hasClass("select2-container-active")) { 2228 this.opts.element.trigger($.Event("select2-focus")); 2229 } 2230 2231 if (this.opened()) { 2232 this.close(); 2233 } else if (this.isInterfaceEnabled()) { 2234 this.open(); 2235 } 2236 2237 killEvent(e); 2238 })); 2239 2240 dropdown.on("mousedown touchstart", this.bind(function() { 2241 if (this.opts.shouldFocusInput(this)) { 2242 this.search.focus(); 2243 } 2244 })); 2245 2246 selection.on("focus", this.bind(function(e) { 2247 killEvent(e); 2248 })); 2249 2250 this.focusser.on("focus", this.bind(function(){ 2251 if (!this.container.hasClass("select2-container-active")) { 2252 this.opts.element.trigger($.Event("select2-focus")); 2253 } 2254 this.container.addClass("select2-container-active"); 2255 })).on("blur", this.bind(function() { 2256 if (!this.opened()) { 2257 this.container.removeClass("select2-container-active"); 2258 this.opts.element.trigger($.Event("select2-blur")); 2259 } 2260 })); 2261 this.search.on("focus", this.bind(function(){ 2262 if (!this.container.hasClass("select2-container-active")) { 2263 this.opts.element.trigger($.Event("select2-focus")); 2264 } 2265 this.container.addClass("select2-container-active"); 2266 })); 2267 2268 this.initContainerWidth(); 2269 this.opts.element.addClass("select2-offscreen"); 2270 this.setPlaceholder(); 2271 2272 }, 2273 2274 // single 2275 clear: function(triggerChange) { 2276 var data=this.selection.data("select2-data"); 2277 if (data) { // guard against queued quick consecutive clicks 2278 var evt = $.Event("select2-clearing"); 2279 this.opts.element.trigger(evt); 2280 if (evt.isDefaultPrevented()) { 2281 return; 2282 } 2283 var placeholderOption = this.getPlaceholderOption(); 2284 this.opts.element.val(placeholderOption ? placeholderOption.val() : ""); 2285 this.selection.find(".select2-chosen").empty(); 2286 this.selection.removeData("select2-data"); 2287 this.setPlaceholder(); 2288 2289 if (triggerChange !== false){ 2290 this.opts.element.trigger({ type: "select2-removed", val: this.id(data), choice: data }); 2291 this.triggerChange({removed:data}); 2292 } 2293 } 2294 }, 2295 2296 /** 2297 * Sets selection based on source element's value 2298 */ 2299 // single 2300 initSelection: function () { 2301 var selected; 2302 if (this.isPlaceholderOptionSelected()) { 2303 this.updateSelection(null); 2304 this.close(); 2305 this.setPlaceholder(); 2306 } else { 2307 var self = this; 2308 this.opts.initSelection.call(null, this.opts.element, function(selected){ 2309 if (selected !== undefined && selected !== null) { 2310 self.updateSelection(selected); 2311 self.close(); 2312 self.setPlaceholder(); 2313 self.nextSearchTerm = self.opts.nextSearchTerm(selected, self.search.val()); 2314 } 2315 }); 2316 } 2317 }, 2318 2319 isPlaceholderOptionSelected: function() { 2320 var placeholderOption; 2321 if (this.getPlaceholder() === undefined) return false; // no placeholder specified so no option should be considered 2322 return ((placeholderOption = this.getPlaceholderOption()) !== undefined && placeholderOption.prop("selected")) 2323 || (this.opts.element.val() === "") 2324 || (this.opts.element.val() === undefined) 2325 || (this.opts.element.val() === null); 2326 }, 2327 2328 // single 2329 prepareOpts: function () { 2330 var opts = this.parent.prepareOpts.apply(this, arguments), 2331 self=this; 2332 2333 if (opts.element.get(0).tagName.toLowerCase() === "select") { 2334 // install the selection initializer 2335 opts.initSelection = function (element, callback) { 2336 var selected = element.find("option").filter(function() { return this.selected && !this.disabled }); 2337 // a single select box always has a value, no need to null check 'selected' 2338 callback(self.optionToData(selected)); 2339 }; 2340 } else if ("data" in opts) { 2341 // install default initSelection when applied to hidden input and data is local 2342 opts.initSelection = opts.initSelection || function (element, callback) { 2343 var id = element.val(); 2344 //search in data by id, storing the actual matching item 2345 var match = null; 2346 opts.query({ 2347 matcher: function(term, text, el){ 2348 var is_match = equal(id, opts.id(el)); 2349 if (is_match) { 2350 match = el; 2351 } 2352 return is_match; 2353 },
2354 callback: !$.isFunction(callback) ? $.noop : function() { 2355 callback(match); 2356 } 2357 }); 2358 }; 2359 } 2360 2361 return opts; 2362 }, 2363 2364 // single 2365 getPlaceholder: function() { 2366 // if a placeholder is specified on a single select without a valid placeholder option ignore it 2367 if (this.select) { 2368 if (this.getPlaceholderOption() === undefined) { 2369 return undefined; 2370 } 2371 } 2372 2373 return this.parent.getPlaceholder.apply(this, arguments); 2374 }, 2375 2376 // single 2377 setPlaceholder: function () { 2378 var placeholder = this.getPlaceholder(); 2379 2380 if (this.isPlaceholderOptionSelected() && placeholder !== undefined) { 2381 2382 // check for a placeholder option if attached to a select 2383 if (this.select && this.getPlaceholderOption() === undefined) return; 2384 2385 this.selection.find(".select2-chosen").html(this.opts.escapeMarkup(placeholder)); 2386 2387 this.selection.addClass("select2-default"); 2388 2389 this.container.removeClass("select2-allowclear"); 2390 } 2391 }, 2392 2393 // single 2394 postprocessResults: function (data, initial, noHighlightUpdate) { 2395 var selected = 0, self = this, showSearchInput = true; 2396 2397 // find the selected element in the result list 2398 2399 this.findHighlightableChoices().each2(function (i, elm) { 2400 if (equal(self.id(elm.data("select2-data")), self.opts.element.val())) { 2401 selected = i; 2402 return false; 2403 } 2404 }); 2405 2406 // and highlight it 2407 if (noHighlightUpdate !== false) { 2408 if (initial === true && selected >= 0) { 2409 this.highlight(selected); 2410 } else { 2411 this.highlight(0); 2412 } 2413 } 2414 2415 // hide the search box if this is the first we got the results and there are enough of them for search 2416 2417 if (initial === true) { 2418 var min = this.opts.minimumResultsForSearch; 2419 if (min >= 0) { 2420 this.showSearch(countResults(data.results) >= min); 2421 } 2422 } 2423 }, 2424 2425 // single 2426 showSearch: function(showSearchInput) { 2427 if (this.showSearchInput === showSearchInput) return; 2428 2429 this.showSearchInput = showSearchInput; 2430 2431 this.dropdown.find(".select2-search").toggleClass("select2-search-hidden", !showSearchInput); 2432 this.dropdown.find(".select2-search").toggleClass("select2-offscreen", !showSearchInput); 2433 //add "select2-with-searchbox" to the container if search box is shown 2434 $(this.dropdown, this.container).toggleClass("select2-with-searchbox", showSearchInput); 2435 }, 2436 2437 // single 2438 onSelect: function (data, options) { 2439 2440 if (!this.triggerSelect(data)) { return; } 2441 2442 var old = this.opts.element.val(), 2443 oldData = this.data(); 2444 2445 this.opts.element.val(this.id(data)); 2446 this.updateSelection(data); 2447 2448 this.opts.element.trigger({ type: "select2-selected", val: this.id(data), choice: data }); 2449 2450 this.nextSearchTerm = this.opts.nextSearchTerm(data, this.search.val()); 2451 this.close(); 2452 2453 if ((!options || !options.noFocus) && this.opts.shouldFocusInput(this)) { 2454 this.focusser.focus(); 2455 } 2456 2457 if (!equal(old, this.id(data))) { 2458 this.triggerChange({ added: data, removed: oldData }); 2459 } 2460 }, 2461 2462 // single 2463 updateSelection: function (data) { 2464 2465 var container=this.selection.find(".select2-chosen"), formatted, cssClass; 2466 2467 this.selection.data("select2-data", data); 2468 2469 container.empty(); 2470 if (data !== null) { 2471 formatted=this.opts.formatSelection(data, container, this.opts.escapeMarkup); 2472 } 2473 if (formatted !== undefined) { 2474 container.append(formatted); 2475 } 2476 cssClass=this.opts.formatSelectionCssClass(data, container); 2477 if (cssClass !== undefined) { 2478 container.addClass(cssClass); 2479 } 2480 2481 this.selection.removeClass("select2-default"); 2482 2483 if (this.opts.allowClear && this.getPlaceholder() !== undefined) { 2484 this.container.addClass("select2-allowclear"); 2485 } 2486 }, 2487 2488 // single 2489 val: function () { 2490 var val, 2491 triggerChange = false, 2492 data = null, 2493 self = this, 2494 oldData = this.data(); 2495 2496 if (arguments.length === 0) { 2497 return this.opts.element.val(); 2498 } 2499 2500 val = arguments[0]; 2501 2502 if (arguments.length > 1) { 2503 triggerChange = arguments[1]; 2504 } 2505 2506 if (this.select) { 2507 this.select 2508 .val(val) 2509 .find("option").filter(function() { return this.selected }).each2(function (i, elm) { 2510 data = self.optionToData(elm); 2511 return false; 2512 }); 2513 this.updateSelection(data); 2514 this.setPlaceholder(); 2515 if (triggerChange) { 2516 this.triggerChange({added: data, removed:oldData}
2516); 2517 } 2518 } else { 2519 // val is an id. !val is true for [undefined,null,'',0] - 0 is legal 2520 if (!val && val !== 0) { 2521 this.clear(triggerChange); 2522 return; 2523 } 2524 if (this.opts.initSelection === undefined) { 2525 throw new Error("cannot call val() if initSelection() is not defined"); 2526 } 2527 this.opts.element.val(val); 2528 this.opts.initSelection(this.opts.element, function(data){ 2529 self.opts.element.val(!data ? "" : self.id(data)); 2530 self.updateSelection(data); 2531 self.setPlaceholder(); 2532 if (triggerChange) { 2533 self.triggerChange({added: data, removed:oldData}); 2534 } 2535 }); 2536 } 2537 }, 2538 2539 // single 2540 clearSearch: function () { 2541 this.search.val(""); 2542 this.focusser.val(""); 2543 }, 2544 2545 // single 2546 data: function(value) { 2547 var data, 2548 triggerChange = false; 2549 2550 if (arguments.length === 0) { 2551 data = this.selection.data("select2-data"); 2552 if (data == undefined) data = null; 2553 return data; 2554 } else { 2555 if (arguments.length > 1) { 2556 triggerChange = arguments[1]; 2557 } 2558 if (!value) { 2559 this.clear(triggerChange); 2560 } else { 2561 data = this.data(); 2562 this.opts.element.val(!value ? "" : this.id(value)); 2563 this.updateSelection(value); 2564 if (triggerChange) { 2565 this.triggerChange({added: value, removed:data}); 2566 } 2567 } 2568 } 2569 } 2570 }); 2571 2572 MultiSelect2 = clazz(AbstractSelect2, { 2573 2574 // multi 2575 createContainer: function (opts) { 2576 var container = $(document.createElement("div")).attr({ 2577 "class": "select2-container select2-container-multi" 2578 }).html([ 2579 "<ul class='select2-choices'>", 2580 " <li class='select2-search-field'>", 2581 " <label for='' class='select2-offscreen'></label>", 2582 " <input type='text' autocomplete='off' autocorrect='off' autocapitalize='off' spellcheck='false' class='select2-input'>", 2583 " </li>", 2584 "</ul>", 2585 "<div class='select2-drop select2-drop-multi select2-display-none'>", 2586 " <ul class='select2-results'>", 2587 " </ul>", 2588 "</div>"].join("")); 2589 return container; 2590 }, 2591 2592 // multi 2593 prepareOpts: function () { 2594 var opts = this.parent.prepareOpts.apply(this, arguments), 2595 self=this; 2596 2597 // TODO validate placeholder is a string if specified 2598 2599 if (opts.element.get(0).tagName.toLowerCase() === "select") { 2600 // install the selection initializer 2601 opts.initSelection = function (element, callback) { 2602 2603 var data = []; 2604 2605 element.find("option").filter(function() { return this.selected && !this.disabled }).each2(function (i, elm) { 2606 data.push(self.optionToData(elm)); 2607 }); 2608 callback(data); 2609 }; 2610 } else if ("data" in opts) { 2611 // install default initSelection when applied to hidden input and data is local 2612 opts.initSelection = opts.initSelection || function (element, callback) { 2613 var ids = splitVal(element.val(), opts.separator); 2614 //search in data by array of ids, storing matching items in a list 2615 var matches = []; 2616 opts.query({ 2617 matcher: function(term, text, el){ 2618 var is_match = $.grep(ids, function(id) { 2619 return equal(id, opts.id(el)); 2620 }).length; 2621 if (is_match) { 2622 matches.push(el); 2623 } 2624 return is_match; 2625 },
2626 callback: !$.isFunction(callback) ? $.noop : function() { 2627 // reorder matches based on the order they appear in the ids array because right now 2628 // they are in the order in which they appear in data array 2629 var ordered = []; 2630 for (var i = 0; i < ids.length; i++) { 2631 var id = ids[i]; 2632 for (var j = 0; j < matches.length; j++) { 2633 var match = matches[j]; 2634 if (equal(id, opts.id(match))) { 2635 ordered.push(match); 2636 matches.splice(j, 1); 2637 break; 2638 } 2639 } 2640 } 2641 callback(ordered); 2642 } 2643 }); 2644 }; 2645 } 2646 2647 return opts; 2648 }, 2649 2650 // multi 2651 selectChoice: function (choice) { 2652 2653 var selected = this.container.find(".select2-search-choice-focus"); 2654 if (selected.length && choice && choice[0] == selected[0]) { 2655 2656 } else { 2657 if (selected.length) { 2658 this.opts.element.trigger("choice-deselected", selected); 2659 } 2660 selected.removeClass("select2-search-choice-focus"); 2661 if (choice && choice.length) { 2662 this.close(); 2663 choice.addClass("select2-search-choice-focus"); 2664 this.opts.element.trigger("choice-selected", choice); 2665 } 2666 } 2667 }, 2668 2669 // multi 2670 destroy: function() { 2671 $("label[for='" + this.search.attr('id') + "']") 2672 .attr('for', this.opts.element.attr("id")); 2673 this.parent.destroy.apply(this, arguments); 2674 2675 cleanupJQueryElements.call(this, 2676 "searchContainer", 2677 "selection" 2678 ); 2679 }, 2680 2681 // multi 2682 initContainer: function () { 2683 2684 var selector = ".select2-choices", selection; 2685 2686 this.searchContainer = this.container.find(".select2-search-field"); 2687 this.selection = selection = this.container.find(selector); 2688 2689 var _this = this; 2690 this.selection.on("click", ".select2-search-choice:not(.select2-locked)", function (e) { 2691 //killEvent(e); 2692 _this.search[0].focus(); 2693 _this.selectChoice($(this)); 2694 }); 2695 2696 // rewrite labels from original element to focusser 2697 this.search.attr("id", "s2id_autogen"+nextUid()); 2698 2699 this.search.prev() 2700 .text($("label[for='" + this.opts.element.attr("id") + "']").text()) 2701 .attr('for', this.search.attr('id')); 2702 2703 this.search.on("input paste", this.bind(function() { 2704 if (this.search.attr('placeholder') && this.search.val().length == 0) return; 2705 if (!this.isInterfaceEnabled()) return; 2706 if (!this.opened()) { 2707 this.open(); 2708 } 2709 })); 2710 2711 this.search.attr("tabindex", this.elementTabIndex); 2712 2713 this.keydowns = 0; 2714 this.search.on("keydown", this.bind(function (e) { 2715 if (!this.isInterfaceEnabled()) return; 2716 2717 ++this.keydowns; 2718 var selected = selection.find(".select2-search-choice-focus"); 2719 var prev = selected.prev(".select2-search-choice:not(.select2-locked)"); 2720 var next = selected.next(".select2-search-choice:not(.select2-locked)"); 2721 var pos = getCursorInfo(this.search); 2722 2723 if (selected.length && 2724 (e.which == KEY.LEFT || e.which == KEY.RIGHT || e.which == KEY.BACKSPACE || e.which == KEY.DELETE || e.which == KEY.ENTER)) { 2725 var selectedChoice = selected; 2726 if (e.which == KEY.LEFT && prev.length) { 2727 selectedChoice = prev; 2728 } 2729 else if (e.which == KEY.RIGHT) { 2730 selectedChoice = next.length ? next : null; 2731 } 2732 else if (e.which === KEY.BACKSPACE) { 2733 if (this.unselect(selected.first())) { 2734 this.search.width(10); 2735 selectedChoice = prev.length ? prev : next; 2736 } 2737 } else if (e.which == KEY.DELETE) { 2738 if (this.unselect(selected.first())) { 2739 this.search.width(10); 2740 selectedChoice = next.length ? next : null; 2741 } 2742 } else if (e.which == KEY.ENTER) { 2743 selectedChoice = null; 2744 } 2745 2746 this.selectChoice(selectedChoice); 2747 killEvent(e); 2748 if (!selectedChoice || !selectedChoice.length) { 2749 this.open(); 2750 } 2751 return; 2752 } else if (((e.which === KEY.BACKSPACE && this.keydowns == 1) 2753 || e.which == KEY.LEFT) && (pos.offset == 0 && !pos.length)) { 2754 2755 this.selectChoice(selection.find(".select2-search-choice:not(.select2-locked)").last()); 2756 killEvent(e); 2757 return; 2758 } else { 2759 this.selectChoice(null); 2760 } 2761 2762 if (this.opened()) { 2763 switch (e.which) { 2764 case KEY.UP: 2765 case KEY.DOWN: 2766 this.moveHighlight((e.which === KEY.UP) ? -1 : 1); 2767 killEvent(e); 2768 return; 2769 case KEY.ENTER: 2770 this.selectHighlighted(); 2771 killEvent(e); 2772 return; 2773 case KEY.TAB: 2774 this.selectHighlighted({noFocus:true}); 2775 this.close(); 2776 return; 2777 case KEY.ESC: 2778 this.cancel(e); 2779 killEvent(e); 2780 return; 2781 } 2782 } 2783 2784 if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) 2785 || e.which === KEY.BACKSPACE || e.which === KEY.ESC) { 2786 return; 2787 } 2788 2789 if (e.which === KEY.ENTER) { 2790 if (this.opts.openOnEnter === false) { 2791 return; 2792 } else if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) { 2793 return; 2794 } 2795 } 2796 2797 this.open(); 2798 2799 if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) { 2800 // prevent the page from scrolling 2801 killEvent(e); 2802 } 2803 2804 if (e.which === KEY.ENTER) { 2805 // prevent form from being submitted 2806 killEvent(e); 2807 } 2808 2809 })); 2810 2811 this.search.on("keyup", this.bind(function (e) { 2812 this.keydowns = 0; 2813 this.resizeSearch(); 2814 }) 2815 ); 2816 2817 this.search.on("blur", this.bind(function(e) { 2818 this.container.removeClass("select2-container-active"); 2819 this.search.removeClass("select2-focused"); 2820 this.selectChoice(null); 2821 if (!this.opened()) this.clearSearch(); 2822 e.stopImmediatePropagation(); 2823 this.opts.element.trigger($.Event("select2-blur")); 2824 })); 2825 2826 this.container.on("click", selector, this.bind(function (e) { 2827 if (!this.isInterfaceEnabled()) return; 2828 if ($(e.target).closest(".select2-search-choice").length > 0) { 2829 // clicked inside a select2 search choice, do not open 2830 return; 2831 } 2832 this.selectChoice(null); 2833 this.clearPlaceholder(); 2834 if (!this.container.hasClass("select2-container-active")) { 2835 this.opts.element.trigger($.Event("select2-focus")); 2836 } 2837 this.open(); 2838 this.focusSearch(); 2839 e.preventDefault(); 2840 })); 2841 2842 this.container.on("focus", selector, this.bind(function () { 2843 if (!this.isInterfaceEnabled()) return; 2844 if (!this.container.hasClass("select2-container-active")) { 2845 this.opts.element.trigger($.Event("select2-focus")); 2846 } 2847 this.container.addClass("select2-container-active"); 2848 this.dropdown.addClass("select2-drop-active"); 2849 this.clearPlaceholder(); 2850 })); 2851 2852 this.initContainerWidth(); 2853 this.opts.element.addClass("select2-offscreen"); 2854 2855 // set the placeholder if necessary 2856 this.clearSearch(); 2857 }, 2858 2859 // multi 2860 enableInterface: function() { 2861 if (this.parent.enableInterface.apply(this, arguments)) { 2862 this.search.prop("disabled", !this.isInterfaceEnabled()); 2863 } 2864 }, 2865 2866 // multi 2867 initSelection: function () { 2868 var data; 2869 if (this.opts.element.val() === "" && this.opts.element.text() === "") { 2870 this.updateSelection([]); 2871 this.close(); 2872 // set the placeholder if necessary 2873 this.clearSearch(); 2874 } 2875 if (this.select || this.opts.element.val() !== "") { 2876 var self = this; 2877 this.opts.initSelection.call(null, this.opts.element, function(data){ 2878 if (data !== undefined && data !== null) { 2879 self.updateSelection(data); 2880 self.close(); 2881 // set the placeholder if necessary 2882 self.clearSearch(); 2883 } 2884 }); 2885 } 2886 }, 2887 2888 // multi 2889 clearSearch: function () { 2890 var placeholder = this.getPlaceholder(), 2891 maxWidth = this.getMaxSearchWidth(); 2892 2893 if (placeholder !== undefined && this.getVal().length === 0 && this.search.hasClass("select2-focused") === false) { 2894 this.search.val(placeholder).addClass("select2-default"); 2895 // stretch the search box to full width of the container so as much of the placeholder is visible as possible 2896 // 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 2897 this.search.width(maxWidth > 0 ? maxWidth : this.container.css("width")); 2898 } else { 2899 this.search.val("").width(10); 2900 } 2901 }, 2902 2903 // multi 2904 clearPlaceholder: function () { 2905 if (this.search.hasClass("select2-default")) { 2906 this.search.val("").removeClass("select2-default"); 2907 } 2908 }, 2909 2910 // multi 2911 opening: function () { 2912 this.clearPlaceholder(); // should be done before super so placeholder is not used to search 2913 this.resizeSearch(); 2914 2915 this.parent.opening.apply(this, arguments); 2916 2917 this.focusSearch(); 2918 2919 // initializes search's value with nextSearchTerm (if defined by user) 2920 // ignore nextSearchTerm if the dropdown is opened by the user pressing a letter 2921 if(this.search.val() === "") { 2922 if(this.nextSearchTerm != undefined){ 2923 this.search.val(this.nextSearchTerm); 2924 this.search.select(); 2925 } 2926 } 2927 2928 this.updateResults(true); 2929 if (this.opts.shouldFocusInput(this)) { 2930 this.search.focus(); 2931 } 2932 this.opts.element.trigger($.Event("select2-open")); 2933 }, 2934 2935 // multi 2936 close: function () { 2937 if (!this.opened()) return; 2938 this.parent.close.apply(this, arguments); 2939 }, 2940 2941 // multi 2942 focus: function () { 2943 this.close(); 2944 this.search.focus(); 2945 }, 2946 2947 // multi 2948 isFocused: function () { 2949 return this.search.hasClass("select2-focused"); 2950 }, 2951 2952 // multi 2953 updateSelection: function (data) { 2954 var ids = [], filtered = [], self = this; 2955 2956 // filter out duplicates 2957 $(data).each(function () { 2958 if (indexOf(self.id(this), ids) < 0) { 2959 ids.push(self.id(this)); 2960 filtered.push(this); 2961 } 2962 }); 2963 data = filtered; 2964 2965 this.selection.find(".select2-search-choice").remove(); 2966 $(data).each(function () { 2967 self.addSelectedChoice(this); 2968 }); 2969 self.postprocessResults(); 2970 }, 2971 2972 // multi 2973 tokenize: function() { 2974 var input = this.search.val(); 2975 input = this.opts.tokenizer.call(this, input, this.data(), this.bind(this.onSelect), this.opts); 2976 if (input != null && input != undefined) { 2977 this.search.val(input); 2978 if (input.length > 0) { 2979 this.open(); 2980 } 2981 } 2982 2983 }, 2984 2985 // multi 2986 onSelect: function (data, options) { 2987 2988 if (!this.triggerSelect(data) || data.text === "") { return; } 2989 2990 this.addSelectedChoice(data); 2991 2992 this.opts.element.trigger({ type: "selected", val: this.id(data), choice: data }); 2993 2994 // keep track of the search's value before it gets cleared
2995 this.nextSearchTerm = this.opts.nextSearchTerm(data, this.search.val()); 2996 2997 this.clearSearch(); 2998 this.updateResults(); 2999 3000 if (this.select || !this.opts.closeOnSelect) this.postprocessResults(data, false, this.opts.closeOnSelect===true); 3001 3002 if (this.opts.closeOnSelect) { 3003 this.close(); 3004 this.search.width(10); 3005 } else { 3006 if (this.countSelectableResults()>0) { 3007 this.search.width(10); 3008 this.resizeSearch(); 3009 if (this.getMaximumSelectionSize() > 0 && this.val().length >= this.getMaximumSelectionSize()) { 3010 // if we reached max selection size repaint the results so choices 3011 // are replaced with the max selection reached message 3012 this.updateResults(true); 3013 } else { 3014 // initializes search's value with nextSearchTerm and update search result 3015 if(this.nextSearchTerm != undefined){ 3016 this.search.val(this.nextSearchTerm); 3017 this.updateResults(); 3018 this.search.select(); 3019 } 3020 } 3021 this.positionDropdown(); 3022 } else { 3023 // if nothing left to select close 3024 this.close(); 3025 this.search.width(10); 3026 } 3027 } 3028 3029 // since its not possible to select an element that has already been 3030 // added we do not need to check if this is a new element before firing change 3031 this.triggerChange({ added: data }); 3032 3033 if (!options || !options.noFocus) 3034 this.focusSearch(); 3035 }, 3036 3037 // multi 3038 cancel: function () { 3039 this.close(); 3040 this.focusSearch(); 3041 }, 3042 3043 addSelectedChoice: function (data) { 3044 var enableChoice = !data.locked, 3045 enabledItem = $( 3046 "<li class='select2-search-choice'>" + 3047 " <div></div>" + 3048 " <a href='#' class='select2-search-choice-close' tabindex='-1'></a>" + 3049 "</li>"), 3050 disabledItem = $( 3051 "<li class='select2-search-choice select2-locked'>" + 3052 "<div></div>" + 3053 "</li>"); 3054 var choice = enableChoice ? enabledItem : disabledItem, 3055 id = this.id(data), 3056 val = this.getVal(), 3057 formatted, 3058 cssClass; 3059 3060 formatted=this.opts.formatSelection(data, choice.find("div"), this.opts.escapeMarkup); 3061 if (formatted != undefined) { 3062 choice.find("div").replaceWith("<div>"+formatted+"</div>"); 3063 } 3064 cssClass=this.opts.formatSelectionCssClass(data, choice.find("div")); 3065 if (cssClass != undefined) { 3066 choice.addClass(cssClass); 3067 } 3068 3069 if(enableChoice){ 3070 choice.find(".select2-search-choice-close") 3071 .on("mousedown", killEvent) 3072 .on("click dblclick", this.bind(function (e) { 3073 if (!this.isInterfaceEnabled()) return; 3074 3075 this.unselect($(e.target)); 3076 this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus"); 3077 killEvent(e); 3078 this.close(); 3079 this.focusSearch(); 3080 })).on("focus", this.bind(function () { 3081 if (!this.isInterfaceEnabled()) return; 3082 this.container.addClass("select2-container-active"); 3083 this.dropdown.addClass("select2-drop-active"); 3084 })); 3085 } 3086 3087 choice.data("select2-data", data); 3088 choice.insertBefore(this.searchContainer); 3089 3090 val.push(id); 3091 this.setVal(val); 3092 }, 3093 3094 // multi 3095 unselect: function (selected) { 3096 var val = this.getVal(), 3097 data, 3098 index; 3099 selected = selected.closest(".select2-search-choice"); 3100 3101 if (selected.length === 0) { 3102 throw "Invalid argument: " + selected + ". Must be .select2-search-choice"; 3103 } 3104 3105 data = selected.data("select2-data"); 3106 3107 if (!data) { 3108 // prevent a race condition when the 'x' is clicked really fast repeatedly the event can be queued 3109 // and invoked on an element already removed 3110 return; 3111 } 3112 3113 var evt = $.Event("select2-removing"); 3114 evt.val = this.id(data); 3115 evt.choice = data; 3116 this.opts.element.trigger(evt); 3117 3118 if (evt.isDefaultPrevented()) { 3119 return false; 3120 } 3121 3122 while((index = indexOf(this.id(data), val)) >= 0) { 3123 val.splice(index, 1); 3124 this.setVal(val); 3125 if (this.select) this.postprocessResults(); 3126 } 3127 3128 selected.remove(); 3129 3130 this.opts.element.trigger({ type: "select2-removed", val: this.id(data), choice: data }); 3131 this.triggerChange({ removed: data }); 3132 3133 return true; 3134 }, 3135 3136 // multi 3137 postprocessResults: function (data, initial, noHighlightUpdate) { 3138 var val = this.getVal(), 3139 choices = this.results.find(".select2-result"), 3140 compound = this.results.find(".select2-result-with-children"), 3141 self = this; 3142 3143 choices.each2(function (i, choice) { 3144 var id = self.id(choice.data("select2-data")); 3145 if (indexOf(id, val) >= 0) { 3146 choice.addClass("select2-selected"); 3147 // mark all children of the selected parent as selected 3148 choice.find(".select2-result-selectable").addClass("select2-selected"); 3149 } 3150 }); 3151 3152 compound.each2(function(i, choice) { 3153 // hide an optgroup if it doesn't have any selectable children 3154 if (!choice.is('.select2-result-selectable') 3155 && choice.find(".select2-result-selectable:not(.select2-selected)").length === 0) { 3156 choice.addClass("select2-selected"); 3157 } 3158 }); 3159 3160 if (this.highlight() == -1 && noHighlightUpdate !== false){ 3161 self.highlight(0); 3162 } 3163 3164 //If all results are chosen render formatNoMatches 3165 if(!this.opts.createSearchChoice && !choices.filter('.select2-result:not(.select2-selected)').length > 0){ 3166 if(!data || data && !data.more && this.results.find(".select2-no-results").length === 0) { 3167 if (checkFormatter(self.opts.formatNoMatches, "formatNoMatches")) { 3168 this.results.append("<li class='select2-no-results'>" + evaluate(self.opts.formatNoMatches, self.opts.element, self.search.val()) + "</li>"); 3169 } 3170 } 3171 } 3172 3173 }, 3174 3175 // multi 3176 getMaxSearchWidth: function() { 3177 return this.selection.width() - getSideBorderPadding(this.search); 3178 }, 3179 3180 // multi 3181 resizeSearch: function () { 3182 var minimumWidth, left, maxWidth, containerLeft, searchWidth, 3183 sideBorderPadding = getSideBorderPadding(this.search); 3184 3185 minimumWidth = measureTextWidth(this.search) + 10; 3186 3187 left = this.search.offset().left; 3188 3189 maxWidth = this.selection.width(); 3190 containerLeft = this.selection.offset().left; 3191 3192 searchWidth = maxWidth - (left - containerLeft) - sideBorderPadding; 3193 3194 if (searchWidth < minimumWidth) { 3195 searchWidth = maxWidth - sideBorderPadding; 3196 } 3197 3198 if (searchWidth < 40) { 3199 searchWidth = maxWidth - sideBorderPadding; 3200 } 3201 3202 if (searchWidth <= 0) { 3203 searchWidth = minimumWidth; 3204 } 3205 3206 this.search.width(Math.floor(searchWidth)); 3207 }, 3208 3209 // multi 3210 getVal: function () { 3211 var val; 3212 if (this.select) { 3213 val = this.select.val(); 3214 return val === null ? [] : val; 3215 } else { 3216 val = this.opts.element.val(); 3217 return splitVal(val, this.opts.separator); 3218 } 3219 }, 3220 3221 // multi 3222 setVal: function (val) { 3223 var unique; 3224 if (this.select) { 3225 this.select.val(val); 3226 } else { 3227 unique = []; 3228 // filter out duplicates 3229 $(val).each(function () { 3230 if (indexOf(this, unique) < 0) unique.push(this); 3231 }); 3232 this.opts.element.val(unique.length === 0 ? "" : unique.join(this.opts.separator)); 3233 } 3234 }, 3235 3236 // multi 3237 buildChangeDetails: function (old, current) { 3238 var current = current.slice(0), 3239 old = old.slice(0); 3240 3241 // remove intersection from each array 3242 for (var i = 0; i < current.length; i++) { 3243 for (var j = 0; j < old.length; j++) { 3244 if (equal(this.opts.id(current[i]), this.opts.id(old[j]))) { 3245 current.splice(i, 1); 3246 if(i>0){ 3247 i--; 3248 } 3249 old.splice(j, 1); 3250 j--; 3251 } 3252 } 3253 } 3254 3255 return {added: current, removed: old}; 3256 }, 3257 3258 3259 // multi 3260 val: function (val, triggerChange) { 3261 var oldData, self=this; 3262 3263 if (arguments.length === 0) { 3264 return this.getVal(); 3265 } 3266 3267 oldData=this.data(); 3268 if (!oldData.length) oldData=[]; 3269 3270 // val is an id. !val is true for [undefined,null,'',0] - 0 is legal 3271 if (!val && val !== 0) { 3272 this.opts.element.val(""); 3273 this.updateSelection([]); 3274 this.clearSearch(); 3275 if (triggerChange) { 3276 this.triggerChange({added: this.data(), removed: oldData}); 3277 } 3278 return; 3279 } 3280 3281 // val is a list of ids 3282 this.setVal(val); 3283 3284 if (this.select) { 3285 this.opts.initSelection(this.select, this.bind(this.updateSelection)); 3286 if (triggerChange) { 3287 this.triggerChange(this.buildChangeDetails(oldData, this.data())); 3288 } 3289 } else { 3290 if (this.opts.initSelection === undefined) { 3291 throw new Error("val() cannot be called if initSelection() is not defined"); 3292 } 3293 3294 this.opts.initSelection(this.opts.element, function(data){ 3295 var ids=$.map(data, self.id); 3296 self.setVal(ids); 3297 self.updateSelection(data); 3298 self.clearSearch(); 3299 if (triggerChange) { 3300 self.triggerChange(self.buildChangeDetails(oldData, self.data())); 3301 } 3302 }); 3303 } 3304 this.clearSearch(); 3305 }, 3306 3307 // multi 3308 onSortStart: function() { 3309 if (this.select) { 3310 throw new Error("Sorting of elements is not supported when attached to <select>. Attach to <input type='hidden'/> instead."); 3311 } 3312
3313 // collapse search field into 0 width so its container can be collapsed as well 3314 this.search.width(0); 3315 // hide the container 3316 this.searchContainer.hide(); 3317 }, 3318 3319 // multi 3320 onSortEnd:function() { 3321 3322 var val=[], self=this; 3323 3324 // show search and move it to the end of the list 3325 this.searchContainer.show(); 3326 // make sure the search container is the last item in the list 3327 this.searchContainer.appendTo(this.searchContainer.parent()); 3328 // since we collapsed the width in dragStarted, we resize it here 3329 this.resizeSearch(); 3330 3331 // update selection 3332 this.selection.find(".select2-search-choice").each(function() { 3333 val.push(self.opts.id($(this).data("select2-data"))); 3334 }); 3335 this.setVal(val); 3336 this.triggerChange(); 3337 }, 3338 3339 // multi 3340 data: function(values, triggerChange) { 3341 var self=this, ids, old; 3342 if (arguments.length === 0) { 3343 return this.selection 3344 .children(".select2-search-choice") 3345 .map(function() { return $(this).data("select2-data"); }) 3346 .get(); 3347 } else { 3348 old = this.data(); 3349 if (!values) { values = []; } 3350 ids = $.map(values, function(e) { return self.opts.id(e); }); 3351 this.setVal(ids); 3352 this.updateSelection(values); 3353 this.clearSearch(); 3354 if (triggerChange) { 3355 this.triggerChange(this.buildChangeDetails(old, this.data())); 3356 } 3357 } 3358 } 3359 }); 3360 3361 $.fn.select2 = function () { 3362 3363 var args = Array.prototype.slice.call(arguments, 0), 3364 opts, 3365 select2, 3366 method, value, multiple, 3367 allowedMethods = ["val", "destroy", "opened", "open", "close", "focu
3367s", "isFocused", "container", "dropdown", "onSortStart", "onSortEnd", "enable", "disable", "readonly", "positionDropdown", "data", "search"], 3368 valueMethods = ["opened", "isFocused", "container", "dropdown"], 3369 propertyMethods = ["val", "data"], 3370 methodsMap = { search: "externalSearch" }; 3371 3372 this.each(function () { 3373 if (args.length === 0 || typeof(args[0]) === "object") { 3374 opts = args.length === 0 ? {} : $.extend({}, args[0]); 3375 opts.element = $(this); 3376 3377 if (opts.element.get(0).tagName.toLowerCase() === "select") { 3378 multiple = opts.element.prop("multiple"); 3379 } else { 3380 multiple = opts.multiple || false; 3381 if ("tags" in opts) {opts.multiple = multiple = true;} 3382 } 3383 3384 select2 = multiple ? new window.Select2["class"].multi() : new window.Select2["class"].single(); 3385 select2.init(opts); 3386 } else if (typeof(args[0]) === "string") { 3387 3388 if (indexOf(args[0], allowedMethods) < 0) { 3389 throw "Unknown method: " + args[0]; 3390 } 3391 3392 value = undefined; 3393 select2 = $(this).data("select2"); 3394 if (select2 === undefined) return; 3395 3396 method=args[0]; 3397 3398 if (method === "container") { 3399 value = select2.container; 3400 } else if (method === "dropdown") { 3401 value = select2.dropdown; 3402 } else { 3403 if (methodsMap[method]) method = methodsMap[method]; 3404 3405 value = select2[method].apply(select2, args.slice(1)); 3406 } 3407 if (indexOf(args[0], valueMethods) >= 0 3408 || (indexOf(args[0], propertyMethods) >= 0 && args.length == 1)) { 3409 return false; // abort the iteration, ready to return first matched value 3410 } 3411 } else { 3412 throw "Invalid arguments to select2 plugin: " + args; 3413 } 3414 }); 3415 return (value === undefined) ? this : value; 3416 }; 3417 3418 // plugin defaults, accessible to users 3419 $.fn.select2.defaults = { 3420 width: "copy", 3421 loadMorePadding: 0, 3422 closeOnSelect: true, 3423 openOnEnter: true, 3424 containerCss: {}, 3425 dropdownCss: {}, 3426 containerCssClass: "", 3427 dropdownCssClass: "", 3428 formatResult: function(result, container, query, escapeMarkup) { 3429 var markup=[]; 3430 markMatch(result.text, query.term, markup, escapeMarkup); 3431 return markup.join(""); 3432 }, 3433 formatSelection: function (data, container, escapeMarkup) { 3434 return data ? escapeMarkup(data.text) : undefined; 3435 }, 3436 sortResults: function (results, container, query) { 3437 return results; 3438 }, 3439 formatResultCssClass: function(data) {return data.css;}, 3440 formatSelectionCssClass: function(data, container) {return undefined;}, 3441 minimumResultsForSearch: 0, 3442 minimumInputLength: 0, 3443 maximumInputLength: null, 3444 maximumSelectionSize: 0, 3445 id: function (e) { return e == undefined ? null : e.id; }, 3446 matcher: function(term, text) { 3447 return stripDiacritics(''+text).toUpperCase().indexOf(stripDiacritics(''+term).toUpperCase()) >= 0; 3448 }, 3449 separator: ",", 3450 tokenSeparators: [], 3451 tokenizer: defaultTokenizer, 3452 escapeMarkup: defaultEscapeMarkup, 3453 blurOnChange: false, 3454 selectOnBlur: false, 3455 searchBar: true, 3456 adaptContainerCssClass: function(c) { return c; }, 3457 adaptDropdownCssClass: function(c) { return null; }, 3458 nextSearchTerm: function(selectedObject, currentSearchTerm) { return undefined; }, 3459 searchInputPlaceholder: '', 3460 createSearchChoicePosition: 'top', 3461 shouldFocusInput: function (instance) { 3462 // Attempt to detect touch devices 3463 var supportsTouchEvents = (('ontouchstart' in window) || 3464 (navigator.msMaxTouchPoints > 0)); 3465 3466 // Only devices which support touch events should be special cased 3467 if (!supportsTouchEvents) { 3468 return true; 3469 } 3470 3471 // Never focus the input if search is disabled 3472 if (instance.opts.minimumResultsForSearch < 0) { 3473 return false; 3474 } 3475 3476 return true; 3477 } 3478 }; 3479 3480 $.fn.select2.locales = []; 3481 3482 $.fn.select2.locales['en'] = { 3483 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."; }, 3484 formatNoMatches: function () { return "Aucun résultat"; }, 3485 formatAjaxError: function (jqXHR, textStatus, errorThrown) { return "Loading failed"; }, 3486 formatInputTooShort: function (input, min) { var n = min - input.length; return "Please enter " + n + " or more character" + (n == 1 ? "" : "s"); }, 3487 formatInputTooLong: function (input, max) { var n = input.length - max; return "Please delete " + n + " character" + (n == 1 ? "" : "s"); }, 3488 formatSelectionTooBig: function (limit) { return "You can only select " + limit + " item" + (limit == 1 ? "" : "s"); }, 3489 formatLoadMore: function (pageNumber) { return "Loading more resultsâ¦"; }, 3490 formatSearching: function () { return "Searchingâ¦"; }, 3491 }; 3492 3493 $.extend($.fn.select2.defaults, $.fn.select2.locales['en']);
3494 3495 $.fn.select2.ajaxDefaults = { 3496 transport: $.ajax, 3497 params: { 3498 type: "GET", 3499 cache: false, 3500 dataType: "json" 3501 } 3502 }; 3503 3504 // exports 3505 window.Select2 = { 3506 query: { 3507 ajax: ajax, 3508 local: local, 3509 tags: tags 3510 }, util: { 3511 debounce: debounce, 3512 markMatch: markMatch, 3513 escapeMarkup: defaultEscapeMarkup, 3514 stripDiacritics: stripDiacritics 3515 }, "class": { 3516 "abstract": AbstractSelect2, 3517 "single": SingleSelect2, 3518 "multi": MultiSelect2 3519 } 3520 }; 3521 3522}(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.