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