PluginProbe
Customify / 1.2.3
Customify v1.2.3
2.10.9 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.7.1 1.3.0 1.3.1 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.6.0 1.6.0.1 1.6.5 1.7.0 1.7.1 All 77 releases
customify / js / select2 / select2.js

select2.js in Customify 1.2.3, at js/select2/select2.js

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