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