PluginProbe
Stream – Activity Log & Audit Trail / 3.0.2
Stream – Activity Log & Audit Trail v3.0.2
4.4.0 4.3.0 4.2.2 4.2.1 trunk 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.1 3.1.1 3.10.0 3.2.0 3.2.1 3.2.2 3.2.3 All 50 releases
stream / ui / lib / select2 / select2.js

select2.js in Stream – Activity Log & Audit Trail 3.0.2, at ui/lib/select2/select2.js

3,559 lines 145.4 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: @@ver@@ Timestamp: @@timestamp@@
5
6 This software is licensed under the Apache License, Version 2.0 (the "Apache License") or the GNU
7 General Public License version 2 (the "GPL License"). You may choose either license to govern your
8 use of this software only upon the condition that you accept all of the terms of either the Apache
9 License or the GPL License.
10
11 You may obtain a copy of the Apache License and the GPL License at:
12
13 http://www.apache.org/licenses/LICENSE-2.0
14 http://www.gnu.org/licenses/gpl-2.0.html
15
16 Unless required by applicable law or agreed to in writing, software distributed under the
17 Apache License or the GPL License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
18 CONDITIONS OF ANY KIND, either express or implied. See the Apache License and the GPL License for
19 the specific language governing permissions and limitations under the Apache License and the GPL License.
20 */
21 (function ($) {
22 if(typeof $.fn.each2 == "undefined") {
23 $.extend($.fn, {
24 /*
25 * 4-10 times faster .each replacement
26 * use it carefully, as it overrides jQuery context of element on each iteration
27 */
28 each2 : function (c) {
29 var j = $([0]), i = -1, l = this.length;
30 while (
31 ++i < l
32 && (j.context = j[0] = this[i])
33 && c.call(j[0], i, j) !== false //"this"=DOM, i=index, j=jQuery object
34 );
35 return this;
36 }
37 });
38 }
39 })(jQuery);
40
41 (function ($, undefined) {
42 "use strict";
43 /*global document, window, 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.lastSearchTerm = 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.removeData("select2")
874 .off("select2");
875 if (!element.is("input[type='hidden']")) {
876 element
877 .show()
878 .prop("autofocus", this.autofocus || false);
879 if (this.elementTabIndex) {
880 element.attr({tabindex: this.elementTabIndex});
881 } else {
882 element.removeAttr("tabindex");
883 }
884 element.show();
885 } else {
886 element.css("display", "");
887 }
888 }
889
890 cleanupJQueryElements.call(this,
891 "container",
892 "liveRegion",
893 "dropdown",
894 "results",
895 "search"
896 );
897 },
898
899 // abstract
900 optionToData: function(element) {
901 if (element.is("option")) {
902 return {
903 id:element.prop("value"),
904 text:element.text(),
905 element: element.get(),
906 css: element.attr("class"),
907 disabled: element.prop("disabled"),
908 locked: equal(element.attr("locked"), "locked") || equal(element.data("locked"), true)
909 };
910 } else if (element.is("optgroup")) {
911 return {
912 text:element.attr("label"),
913 children:[],
914 element: element.get(),
915 css: element.attr("class")
916 };
917 }
918 },
919
920 // abstract
921 prepareOpts: function (opts) {
922 var element, select, idKey, ajaxUrl, self = this;
923
924 element = opts.element;
925
926 if (element.get(0).tagName.toLowerCase() === "select") {
927 this.select = select = opts.element;
928 }
929
930 if (select) {
931 // these options are not allowed when attached to a select because they are picked up off the element itself
932 $.each(["id", "multiple", "ajax", "query", "createSearchChoice", "initSelection", "data", "tags"], function () {
933 if (this in opts) {
934 throw new Error("Option '" + this + "' is not allowed for Select2 when attached to a <select> element.");
935 }
936 });
937 }
938
939 opts = $.extend({}, {
940 populateResults: function(container, results, query) {
941 var populate, id=this.opts.id, liveRegion=this.liveRegion;
942
943 populate=function(results, container, depth) {
944
945 var i, l, result, selectable, disabled, compound, node, label, innerContainer, formatted;
946
947 results = opts.sortResults(results, container, query);
948
949 // collect the created nodes for bulk append
950 var nodes = [];
951 for (i = 0, l = results.length; i < l; i = i + 1) {
952
953 result=results[i];
954
955 disabled = (result.disabled === true);
956 selectable = (!disabled) && (id(result) !== undefined);
957
958 compound=result.children && result.children.length > 0;
959
960 node=$("<li></li>");
961 node.addClass("select2-results-dept-"+depth);
962 node.addClass("select2-result");
963 node.addClass(selectable ? "select2-result-selectable" : "select2-result-unselectable");
964 if (disabled) { node.addClass("select2-disabled"); }
965 if (compound) { node.addClass("select2-result-with-children"); }
966 node.addClass(self.opts.formatResultCssClass(result));
967 node.attr("role", "presentation");
968
969 label=$(document.createElement("div"));
970 label.addClass("select2-result-label");
971 label.attr("id", "select2-result-label-" + nextUid());
972 label.attr("role", "option");
973
974 formatted=opts.formatResult(result, label, query, self.opts.escapeMarkup);
975 if (formatted!==undefined) {
976 label.html(formatted);
977 node.append(label);
978 }
979
980
981 if (compound) {
982
983 innerContainer=$("<ul></ul>");
984 innerContainer.addClass("select2-result-sub");
985 populate(result.children, innerContainer, depth+1);
986 node.append(innerContainer);
987 }
988
989 node.data("select2-data", result);
990 nodes.push(node[0]);
991 }
992
993 // bulk append the created nodes
994 container.append(nodes);
995 liveRegion.text(opts.formatMatches(results.length));
996 };
997
998 populate(results, container, 0);
999 }
1000 }, $.fn.select2.defaults, opts);
1001
1002 if (typeof(opts.id) !== "function") {
1003 idKey = opts.id;
1004 opts.id = function (e) { return e[idKey]; };
1005 }
1006
1007 if ($.isArray(opts.element.data("select2Tags"))) {
1008 if ("tags" in opts) {
1009 throw "tags specified as both an attribute 'data-select2-tags' and in options of Select2 " + opts.element.attr("id");
1010 }
1011 opts.tags=opts.element.data("select2Tags");
1012 }
1013
1014 if (select) {
1015 opts.query = this.bind(function (query) {
1016 var data = { results: [], more: false },
1017 term = query.term,
1018 children, placeholderOption, process;
1019
1020 process=function(element, collection) {
1021 var group;
1022 if (element.is("option")) {
1023 if (query.matcher(term, element.text(), element)) {
1024 collection.push(self.optionToData(element));
1025 }
1026 } else if (element.is("optgroup")) {
1027 group=self.optionToData(element);
1028 element.children().each2(function(i, elm) { process(elm, group.children); });
1029 if (group.children.length>0) {
1030 collection.push(group);
1031 }
1032 }
1033 };
1034
1035 children=element.children();
1036
1037 // ignore the placeholder option if there is one
1038 if (this.getPlaceholder() !== undefined && children.length > 0) {
1039 placeholderOption = this.getPlaceholderOption();
1040 if (placeholderOption) {
1041 children=children.not(placeholderOption);
1042 }
1043 }
1044
1045 children.each2(function(i, elm) { process(elm, data.results); });
1046
1047 query.callback(data);
1048 });
1049 // this is needed because inside val() we construct choices from options and their id is hardcoded
1050 opts.id=function(e) { return e.id; };
1051 } else {
1052 if (!("query" in opts)) {
1053
1054 if ("ajax" in opts) {
1055 ajaxUrl = opts.element.data("ajax-url");
1056 if (ajaxUrl && ajaxUrl.length > 0) {
1057 opts.ajax.url = ajaxUrl;
1058 }
1059 opts.query = ajax.call(opts.element, opts.ajax);
1060 } else if ("data" in opts) {
1061 opts.query = local(opts.data);
1062 } else if ("tags" in opts) {
1063 opts.query = tags(opts.tags);
1064 if (opts.createSearchChoice === undefined) {
1065 opts.createSearchChoice = function (term) { return {id: $.trim(term), text: $.trim(term)}; };
1066 }
1067 if (opts.initSelection === undefined) {
1068 opts.initSelection = function (element, callback) {
1069 var data = [];
1070 $(splitVal(element.val(), opts.separator, opts.transformVal)).each(function () {
1071 var obj = { id: this, text: this },
1072 tags = opts.tags;
1073 if ($.isFunction(tags)) tags=tags();
1074 $(tags).each(function() { if (equal(this.id, obj.id)) { obj = this; return false; } });
1075 data.push(obj);
1076 });
1077
1078 callback(data);
1079 };
1080 }
1081 }
1082 }
1083 }
1084 if (typeof(opts.query) !== "function") {
1085 throw "query function not defined for Select2 " + opts.element.attr("id");
1086 }
1087
1088 if (opts.createSearchChoicePosition === 'top') {
1089 opts.createSearchChoicePosition = function(list, item) { list.unshift(item); };
1090 }
1091 else if (opts.createSearchChoicePosition === 'bottom') {
1092 opts.createSearchChoicePosition = function(list, item) { list.push(item); };
1093 }
1094 else if (typeof(opts.createSearchChoicePosition) !== "function") {
1095 throw "invalid createSearchChoicePosition option must be 'top', 'bottom' or a custom function";
1096 }
1097
1098 return opts;
1099 },
1100
1101 /**
1102 * Monitor the original element for changes and update select2 accordingly
1103 */
1104 // abstract
1105 monitorSource: function () {
1106 var el = this.opts.element, observer, self = this;
1107
1108 el.on("change.select2", this.bind(function (e) {
1109 if (this.opts.element.data("select2-change-triggered") !== true) {
1110 this.initSelection();
1111 }
1112 }));
1113
1114 this._sync = this.bind(function () {
1115
1116 // sync enabled state
1117 var disabled = el.prop("disabled");
1118 if (disabled === undefined) disabled = false;
1119 this.enable(!disabled);
1120
1121 var readonly = el.prop("readonly");
1122 if (readonly === undefined) readonly = false;
1123 this.readonly(readonly);
1124
1125 if (this.container) {
1126 syncCssClasses(this.container, this.opts.element, this.opts.adaptContainerCssClass);
1127 this.container.addClass(evaluate(this.opts.containerCssClass, this.opts.element));
1128 }
1129
1130 if (this.dropdown) {
1131 syncCssClasses(this.dropdown, this.opts.element, this.opts.adaptDropdownCssClass);
1132 this.dropdown.addClass(evaluate(this.opts.dropdownCssClass, this.opts.element));
1133 }
1134
1135 });
1136
1137 // IE8-10 (IE9/10 won't fire propertyChange via attachEventListener)
1138 if (el.length && el[0].attachEvent) {
1139 el.each(function() {
1140 this.attachEvent("onpropertychange", self._sync);
1141 });
1142 }
1143
1144 // safari, chrome, firefox, IE11
1145 observer = window.MutationObserver || window.WebKitMutationObserver|| window.MozMutationObserver;
1146 if (observer !== undefined) {
1147 if (this.propertyObserver) { delete this.propertyObserver; this.propertyObserver = null; }
1148 this.propertyObserver = new observer(function (mutations) {
1149 $.each(mutations, self._sync);
1150 });
1151 this.propertyObserver.observe(el.get(0), { attributes:true, subtree:false });
1152 }
1153 },
1154
1155 // abstract
1156 triggerSelect: function(data) {
1157 var evt = $.Event("select2-selecting", { val: this.id(data), object: data, choice: data });
1158 this.opts.element.trigger(evt);
1159 return !evt.isDefaultPrevented();
1160 },
1161
1162 /**
1163 * Triggers the change event on the source element
1164 */
1165 // abstract
1166 triggerChange: function (details) {
1167
1168 details = details || {};
1169 details= $.extend({}, details, { type: "change", val: this.val() });
1170 // prevents recursive triggering
1171 this.opts.element.data("select2-change-triggered", true);
1172 this.opts.element.trigger(details);
1173 this.opts.element.data("select2-change-triggered", false);
1174
1175 // some validation frameworks ignore the change event and listen instead to keyup, click for selects
1176 // so here we trigger the click event manually
1177 this.opts.element.click();
1178
1179 // ValidationEngine ignores the change event and listens instead to blur
1180 // so here we trigger the blur event manually if so desired
1181 if (this.opts.blurOnChange)
1182 this.opts.element.blur();
1183 },
1184
1185 //abstract
1186 isInterfaceEnabled: function()
1187 {
1188 return this.enabledInterface === true;
1189 },
1190
1191 // abstract
1192 enableInterface: function() {
1193 var enabled = this._enabled && !this._readonly,
1194 disabled = !enabled;
1195
1196 if (enabled === this.enabledInterface) return false;
1197
1198 this.container.toggleClass("select2-container-disabled", disabled);
1199 this.close();
1200 this.enabledInterface = enabled;
1201
1202 return true;
1203 },
1204
1205 // abstract
1206 enable: function(enabled) {
1207 if (enabled === undefined) enabled = true;
1208 if (this._enabled === enabled) return;
1209 this._enabled = enabled;
1210
1211 this.opts.element.prop("disabled", !enabled);
1212 this.enableInterface();
1213 },
1214
1215 // abstract
1216 disable: function() {
1217 this.enable(false);
1218 },
1219
1220 // abstract
1221 readonly: function(enabled) {
1222 if (enabled === undefined) enabled = false;
1223 if (this._readonly === enabled) return;
1224 this._readonly = enabled;
1225
1226 this.opts.element.prop("readonly", enabled);
1227 this.enableInterface();
1228 },
1229
1230 // abstract
1231 opened: function () {
1232 return (this.container) ? this.container.hasClass("select2-dropdown-open") : false;
1233 },
1234
1235 // abstract
1236 positionDropdown: function() {
1237 var $dropdown = this.dropdown,
1238 container = this.container,
1239 offset = container.offset(),
1240 height = container.outerHeight(false),
1241 width = container.outerWidth(false),
1242 dropHeight = $dropdown.outerHeight(false),
1243 $window = $(window),
1244 windowWidth = $window.width(),
1245 windowHeight = $window.height(),
1246 viewPortRight = $window.scrollLeft() + windowWidth,
1247 viewportBottom = $window.scrollTop() + windowHeight,
1248 dropTop = offset.top + height,
1249 dropLeft = offset.left,
1250 enoughRoomBelow = dropTop + dropHeight <= viewportBottom,
1251 enoughRoomAbove = (offset.top - dropHeight) >= $window.scrollTop(),
1252 dropWidth = $dropdown.outerWidth(false),
1253 enoughRoomOnRight = function() {
1254 return dropLeft + dropWidth <= viewPortRight;
1255 },
1256 enoughRoomOnLeft = function() {
1257 return offset.left + viewPortRight + container.outerWidth(false) > dropWidth;
1258 },
1259 aboveNow = $dropdown.hasClass("select2-drop-above"),
1260 bodyOffset,
1261 above,
1262 changeDirection,
1263 css,
1264 resultsListNode;
1265
1266 // always prefer the current above/below alignment, unless there is not enough room
1267 if (aboveNow) {
1268 above = true;
1269 if (!enoughRoomAbove && enoughRoomBelow) {
1270 changeDirection = true;
1271 above = false;
1272 }
1273 } else {
1274 above = false;
1275 if (!enoughRoomBelow && enoughRoomAbove) {
1276 changeDirection = true;
1277 above = true;
1278 }
1279 }
1280
1281 //if we are changing direction we need to get positions when dropdown is hidden;
1282 if (changeDirection) {
1283 $dropdown.hide();
1284 offset = this.container.offset();
1285 height = this.container.outerHeight(false);
1286 width = this.container.outerWidth(false);
1287 dropHeight = $dropdown.outerHeight(false);
1288 viewPortRight = $window.scrollLeft() + windowWidth;
1289 viewportBottom = $window.scrollTop() + windowHeight;
1290 dropTop = offset.top + height;
1291 dropLeft = offset.left;
1292 dropWidth = $dropdown.outerWidth(false);
1293 $dropdown.show();
1294
1295 // fix so the cursor does not move to the left within the search-textbox in IE
1296 this.focusSearch();
1297 }
1298
1299 if (this.opts.dropdownAutoWidth) {
1300 resultsListNode = $('.select2-results', $dropdown)[0];
1301 $dropdown.addClass('select2-drop-auto-width');
1302 $dropdown.css('width', '');
1303 // Add scrollbar width to dropdown if vertical scrollbar is present
1304 dropWidth = $dropdown.outerWidth(false) + (resultsListNode.scrollHeight === resultsListNode.clientHeight ? 0 : scrollBarDimensions.width);
1305 dropWidth > width ? width = dropWidth : dropWidth = width;
1306 dropHeight = $dropdown.outerHeight(false);
1307 }
1308 else {
1309 this.container.removeClass('select2-drop-auto-width');
1310 }
1311
1312 //console.log("below/ droptop:", dropTop, "dropHeight", dropHeight, "sum", (dropTop+dropHeight)+" viewport bottom", viewportBottom, "enough?", enoughRoomBelow);
1313 //console.log("above/ offset.top", offset.top, "dropHeight", dropHeight, "top", (offset.top-dropHeight), "scrollTop", this.body.scrollTop(), "enough?", enoughRoomAbove);
1314
1315 // fix positioning when body has an offset and is not position: static
1316 if (this.body.css('position') !== 'static') {
1317 bodyOffset = this.body.offset();
1318 dropTop -= bodyOffset.top;
1319 dropLeft -= bodyOffset.left;
1320 }
1321
1322 if (!enoughRoomOnRight() && enoughRoomOnLeft()) {
1323 dropLeft = offset.left + this.container.outerWidth(false) - dropWidth;
1324 }
1325
1326 css = {
1327 left: dropLeft,
1328 width: width
1329 };
1330
1331 if (above) {
1332 this.container.addClass("select2-drop-above");
1333 $dropdown.addClass("select2-drop-above");
1334 dropHeight = $dropdown.outerHeight(false);
1335 css.top = offset.top - dropHeight;
1336 css.bottom = 'auto';
1337 }
1338 else {
1339 css.top = dropTop;
1340 css.bottom = 'auto';
1341 this.container.removeClass("select2-drop-above");
1342 $dropdown.removeClass("select2-drop-above");
1343 }
1344 css = $.extend(css, evaluate(this.opts.dropdownCss, this.opts.element));
1345
1346 $dropdown.css(css);
1347 },
1348
1349 // abstract
1350 shouldOpen: function() {
1351 var event;
1352
1353 if (this.opened()) return false;
1354
1355 if (this._enabled === false || this._readonly === true) return false;
1356
1357 event = $.Event("select2-opening");
1358 this.opts.element.trigger(event);
1359 return !event.isDefaultPrevented();
1360 },
1361
1362 // abstract
1363 clearDropdownAlignmentPreference: function() {
1364 // clear the classes used to figure out the preference of where the dropdown should be opened
1365 this.container.removeClass("select2-drop-above");
1366 this.dropdown.removeClass("select2-drop-above");
1367 },
1368
1369 /**
1370 * Opens the dropdown
1371 *
1372 * @return {Boolean} whether or not dropdown was opened. This method will return false if, for example,
1373 * the dropdown is already open, or if the 'open' event listener on the element called preventDefault().
1374 */
1375 // abstract
1376 open: function () {
1377
1378 if (!this.shouldOpen()) return false;
1379
1380 this.opening();
1381
1382 // Only bind the document mousemove when the dropdown is visible
1383 $document.on("mousemove.select2Event", function (e) {
1384 lastMousePosition.x = e.pageX;
1385 lastMousePosition.y = e.pageY;
1386 });
1387
1388 return true;
1389 },
1390
1391 /**
1392 * Performs the opening of the dropdown
1393 */
1394 // abstract
1395 opening: function() {
1396 var cid = this.containerEventName,
1397 scroll = "scroll." + cid,
1398 resize = "resize."+cid,
1399 orient = "orientationchange."+cid,
1400 mask;
1401
1402 this.container.addClass("select2-dropdown-open").addClass("select2-container-active");
1403
1404 this.clearDropdownAlignmentPreference();
1405
1406 if(this.dropdown[0] !== this.body.children().last()[0]) {
1407 this.dropdown.detach().appendTo(this.body);
1408 }
1409
1410 // create the dropdown mask if doesn't already exist
1411 mask = $("#select2-drop-mask");
1412 if (mask.length === 0) {
1413 mask = $(document.createElement("div"));
1414 mask.attr("id","select2-drop-mask").attr("class","select2-drop-mask");
1415 mask.hide();
1416 mask.appendTo(this.body);
1417 mask.on("mousedown touchstart click", function (e) {
1418 // Prevent IE from generating a click event on the body
1419 reinsertElement(mask);
1420
1421 var dropdown = $("#select2-drop"), self;
1422 if (dropdown.length > 0) {
1423 self=dropdown.data("select2");
1424 if (self.opts.selectOnBlur) {
1425 self.selectHighlighted({noFocus: true});
1426 }
1427 self.close();
1428 e.preventDefault();
1429 e.stopPropagation();
1430 }
1431 });
1432 }
1433
1434 // ensure the mask is always right before the dropdown
1435 if (this.dropdown.prev()[0] !== mask[0]) {
1436 this.dropdown.before(mask);
1437 }
1438
1439 // move the global id to the correct dropdown
1440 $("#select2-drop").removeAttr("id");
1441 this.dropdown.attr("id", "select2-drop");
1442
1443 // show the elements
1444 mask.show();
1445
1446 this.positionDropdown();
1447 this.dropdown.show();
1448 this.positionDropdown();
1449
1450 this.dropdown.addClass("select2-drop-active");
1451
1452 // attach listeners to events that can change the position of the container and thus require
1453 // the position of the dropdown to be updated as well so it does not come unglued from the container
1454 var that = this;
1455 this.container.parents().add(window).each(function () {
1456 $(this).on(resize+" "+scroll+" "+orient, function (e) {
1457 if (that.opened()) that.positionDropdown();
1458 });
1459 });
1460
1461
1462 },
1463
1464 // abstract
1465 close: function () {
1466 if (!this.opened()) return;
1467
1468 var cid = this.containerEventName,
1469 scroll = "scroll." + cid,
1470 resize = "resize."+cid,
1471 orient = "orientationchange."+cid;
1472
1473 // unbind event listeners
1474 this.container.parents().add(window).each(function () { $(this).off(scroll).off(resize).off(orient); });
1475
1476 this.clearDropdownAlignmentPreference();
1477
1478 $("#select2-drop-mask").hide();
1479 this.dropdown.removeAttr("id"); // only the active dropdown has the select2-drop id
1480 this.dropdown.hide();
1481 this.container.removeClass("select2-dropdown-open").removeClass("select2-container-active");
1482 this.results.empty();
1483
1484 // Now that the dropdown is closed, unbind the global document mousemove event
1485 $document.off("mousemove.select2Event");
1486
1487 this.clearSearch();
1488 this.search.removeClass("select2-active");
1489
1490 // Remove the aria active descendant for highlighted element
1491 this.search.removeAttr("aria-activedescendant");
1492 this.opts.element.trigger($.Event("select2-close"));
1493 },
1494
1495 /**
1496 * Opens control, sets input value, and updates results.
1497 */
1498 // abstract
1499 externalSearch: function (term) {
1500 this.open();
1501 this.search.val(term);
1502 this.updateResults(false);
1503 },
1504
1505 // abstract
1506 clearSearch: function () {
1507
1508 },
1509
1510 /**
1511 * @return {Boolean} Whether or not search value was changed.
1512 * @private
1513 */
1514 prefillNextSearchTerm: function () {
1515 // initializes search's value with nextSearchTerm (if defined by user)
1516 // ignore nextSearchTerm if the dropdown is opened by the user pressing a letter
1517 if(this.search.val() !== "") {
1518 return false;
1519 }
1520
1521 var nextSearchTerm = this.opts.nextSearchTerm(this.data(), this.lastSearchTerm);
1522 if(nextSearchTerm !== undefined){
1523 this.search.val(nextSearchTerm);
1524 this.search.select();
1525 return true;
1526 }
1527
1528 return false;
1529 },
1530
1531 //abstract
1532 getMaximumSelectionSize: function() {
1533 return evaluate(this.opts.maximumSelectionSize, this.opts.element);
1534 },
1535
1536 // abstract
1537 ensureHighlightVisible: function () {
1538 var results = this.results, children, index, child, hb, rb, y, more, topOffset;
1539
1540 index = this.highlight();
1541
1542 if (index < 0) return;
1543
1544 if (index == 0) {
1545
1546 // if the first element is highlighted scroll all the way to the top,
1547 // that way any unselectable headers above it will also be scrolled
1548 // into view
1549
1550 results.scrollTop(0);
1551 return;
1552 }
1553
1554 children = this.findHighlightableChoices().find('.select2-result-label');
1555
1556 child = $(children[index]);
1557
1558 topOffset = (child.offset() || {}).top || 0;
1559
1560 hb = topOffset + child.outerHeight(true);
1561
1562 // if this is the last child lets also make sure select2-more-results is visible
1563 if (index === children.length - 1) {
1564 more = results.find("li.select2-more-results");
1565 if (more.length > 0) {
1566 hb = more.offset().top + more.outerHeight(true);
1567 }
1568 }
1569
1570 rb = results.offset().top + results.outerHeight(false);
1571 if (hb > rb) {
1572 results.scrollTop(results.scrollTop() + (hb - rb));
1573 }
1574 y = topOffset - results.offset().top;
1575
1576 // make sure the top of the element is visible
1577 if (y < 0 && child.css('display') != 'none' ) {
1578 results.scrollTop(results.scrollTop() + y); // y is negative
1579 }
1580 },
1581
1582 // abstract
1583 findHighlightableChoices: function() {
1584 return this.results.find(".select2-result-selectable:not(.select2-disabled):not(.select2-selected)");
1585 },
1586
1587 // abstract
1588 moveHighlight: function (delta) {
1589 var choices = this.findHighlightableChoices(),
1590 index = this.highlight();
1591
1592 while (index > -1 && index < choices.length) {
1593 index += delta;
1594 var choice = $(choices[index]);
1595 if (choice.hasClass("select2-result-selectable") && !choice.hasClass("select2-disabled") && !choice.hasClass("select2-selected")) {
1596 this.highlight(index);
1597 break;
1598 }
1599 }
1600 },
1601
1602 // abstract
1603 highlight: function (index) {
1604 var choices = this.findHighlightableChoices(),
1605 choice,
1606 data;
1607
1608 if (arguments.length === 0) {
1609 return indexOf(choices.filter(".select2-highlighted")[0], choices.get());
1610 }
1611
1612 if (index >= choices.length) index = choices.length - 1;
1613 if (index < 0) index = 0;
1614
1615 this.removeHighlight();
1616
1617 choice = $(choices[index]);
1618 choice.addClass("select2-highlighted");
1619
1620 // ensure assistive technology can determine the active choice
1621 this.search.attr("aria-activedescendant", choice.find(".select2-result-label").attr("id"));
1622
1623 this.ensureHighlightVisible();
1624
1625 this.liveRegion.text(choice.text());
1626
1627 data = choice.data("select2-data");
1628 if (data) {
1629 this.opts.element.trigger({ type: "select2-highlight", val: this.id(data), choice: data });
1630 }
1631 },
1632
1633 removeHighlight: function() {
1634 this.results.find(".select2-highlighted").removeClass("select2-highlighted");
1635 },
1636
1637 touchMoved: function() {
1638 this._touchMoved = true;
1639 },
1640
1641 clearTouchMoved: function() {
1642 this._touchMoved = false;
1643 },
1644
1645 // abstract
1646 countSelectableResults: function() {
1647 return this.findHighlightableChoices().length;
1648 },
1649
1650 // abstract
1651 highlightUnderEvent: function (event) {
1652 var el = $(event.target).closest(".select2-result-selectable");
1653 if (el.length > 0 && !el.is(".select2-highlighted")) {
1654 var choices = this.findHighlightableChoices();
1655 this.highlight(choices.index(el));
1656 } else if (el.length == 0) {
1657 // if we are over an unselectable item remove all highlights
1658 this.removeHighlight();
1659 }
1660 },
1661
1662 // abstract
1663 loadMoreIfNeeded: function () {
1664 var results = this.results,
1665 more = results.find("li.select2-more-results"),
1666 below, // pixels the element is below the scroll fold, below==0 is when the element is starting to be visible
1667 page = this.resultsPage + 1,
1668 self=this,
1669 term=this.search.val(),
1670 context=this.context;
1671
1672 if (more.length === 0) return;
1673 below = more.offset().top - results.offset().top - results.height();
1674
1675 if (below <= this.opts.loadMorePadding) {
1676 more.addClass("select2-active");
1677 this.opts.query({
1678 element: this.opts.element,
1679 term: term,
1680 page: page,
1681 context: context,
1682 matcher: this.opts.matcher,
1683 callback: this.bind(function (data) {
1684
1685 // ignore a response if the select2 has been closed before it was received
1686 if (!self.opened()) return;
1687
1688
1689 self.opts.populateResults.call(this, results, data.results, {term: term, page: page, context:context});
1690 self.postprocessResults(data, false, false);
1691
1692 if (data.more===true) {
1693 more.detach().appendTo(results).html(self.opts.escapeMarkup(evaluate(self.opts.formatLoadMore, self.opts.element, page+1)));
1694 window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10);
1695 } else {
1696 more.remove();
1697 }
1698 self.positionDropdown();
1699 self.resultsPage = page;
1700 self.context = data.context;
1701 this.opts.element.trigger({ type: "select2-loaded", items: data });
1702 })});
1703 }
1704 },
1705
1706 /**
1707 * Default tokenizer function which does nothing
1708 */
1709 tokenize: function() {
1710
1711 },
1712
1713 /**
1714 * @param initial whether or not this is the call to this method right after the dropdown has been opened
1715 */
1716 // abstract
1717 updateResults: function (initial) {
1718 var search = this.search,
1719 results = this.results,
1720 opts = this.opts,
1721 data,
1722 self = this,
1723 input,
1724 term = search.val(),
1725 lastTerm = $.data(this.container, "select2-last-term"),
1726 // sequence number used to drop out-of-order responses
1727 queryNumber;
1728
1729 // prevent duplicate queries against the same term
1730 if (initial !== true && lastTerm && equal(term, lastTerm)) return;
1731
1732 $.data(this.container, "select2-last-term", term);
1733
1734 // if the search is currently hidden we do not alter the results
1735 if (initial !== true && (this.showSearchInput === false || !this.opened())) {
1736 return;
1737 }
1738
1739 function postRender() {
1740 search.removeClass("select2-active");
1741 self.positionDropdown();
1742 if (results.find('.select2-no-results,.select2-selection-limit,.select2-searching').length) {
1743 self.liveRegion.text(results.text());
1744 }
1745 else {
1746 self.liveRegion.text(self.opts.formatMatches(results.find('.select2-result-selectable:not(".select2-selected")').length));
1747 }
1748 }
1749
1750 function render(html) {
1751 results.html(html);
1752 postRender();
1753 }
1754
1755 queryNumber = ++this.queryCount;
1756
1757 var maxSelSize = this.getMaximumSelectionSize();
1758 if (maxSelSize >=1) {
1759 data = this.data();
1760 if ($.isArray(data) && data.length >= maxSelSize && checkFormatter(opts.formatSelectionTooBig, "formatSelectionTooBig")) {
1761 render("<li class='select2-selection-limit'>" + evaluate(opts.formatSelectionTooBig, opts.element, maxSelSize) + "</li>");
1762 return;
1763 }
1764 }
1765
1766 if (search.val().length < opts.minimumInputLength) {
1767 if (checkFormatter(opts.formatInputTooShort, "formatInputTooShort")) {
1768 render("<li class='select2-no-results'>" + evaluate(opts.formatInputTooShort, opts.element, search.val(), opts.minimumInputLength) + "</li>");
1769 } else {
1770 render("");
1771 }
1772 if (initial && this.showSearch) this.showSearch(true);
1773 return;
1774 }
1775
1776 if (opts.maximumInputLength && search.val().length > opts.maximumInputLength) {
1777 if (checkFormatter(opts.formatInputTooLong, "formatInputTooLong")) {
1778 render("<li class='select2-no-results'>" + evaluate(opts.formatInputTooLong, opts.element, search.val(), opts.maximumInputLength) + "</li>");
1779 } else {
1780 render("");
1781 }
1782 return;
1783 }
1784
1785 if (opts.formatSearching && this.findHighlightableChoices().length === 0) {
1786 render("<li class='select2-searching'>" + evaluate(opts.formatSearching, opts.element) + "</li>");
1787 }
1788
1789 search.addClass("select2-active");
1790
1791 this.removeHighlight();
1792
1793 // give the tokenizer a chance to pre-process the input
1794 input = this.tokenize();
1795 if (input != undefined && input != null) {
1796 search.val(input);
1797 }
1798
1799 this.resultsPage = 1;
1800
1801 opts.query({
1802 element: opts.element,
1803 term: search.val(),
1804 page: this.resultsPage,
1805 context: null,
1806 matcher: opts.matcher,
1807 callback: this.bind(function (data) {
1808 var def; // default choice
1809
1810 // ignore old responses
1811 if (queryNumber != this.queryCount) {
1812 return;
1813 }
1814
1815 // ignore a response if the select2 has been closed before it was received
1816 if (!this.opened()) {
1817 this.search.removeClass("select2-active");
1818 return;
1819 }
1820
1821 // handle ajax error
1822 if(data.hasError !== undefined && checkFormatter(opts.formatAjaxError, "formatAjaxError")) {
1823 render("<li class='select2-ajax-error'>" + evaluate(opts.formatAjaxError, opts.element, data.jqXHR, data.textStatus, data.errorThrown) + "</li>");
1824 return;
1825 }
1826
1827 // save context, if any
1828 this.context = (data.context===undefined) ? null : data.context;
1829 // create a default choice and prepend it to the list
1830 if (this.opts.createSearchChoice && search.val() !== "") {
1831 def = this.opts.createSearchChoice.call(self, search.val(), data.results);
1832 if (def !== undefined && def !== null && self.id(def) !== undefined && self.id(def) !== null) {
1833 if ($(data.results).filter(
1834 function () {
1835 return equal(self.id(this), self.id(def));
1836 }).length === 0) {
1837 this.opts.createSearchChoicePosition(data.results, def);
1838 }
1839 }
1840 }
1841
1842 if (data.results.length === 0 && checkFormatter(opts.formatNoMatches, "formatNoMatches")) {
1843 render("<li class='select2-no-results'>" + evaluate(opts.formatNoMatches, opts.element, search.val()) + "</li>");
1844 this.showSearch(search.val());
1845 return;
1846 }
1847
1848 results.empty();
1849 self.opts.populateResults.call(this, results, data.results, {term: search.val(), page: this.resultsPage, context:null});
1850
1851 if (data.more === true && checkFormatter(opts.formatLoadMore, "formatLoadMore")) {
1852 results.append("<li class='select2-more-results'>" + opts.escapeMarkup(evaluate(opts.formatLoadMore, opts.element, this.resultsPage)) + "</li>");
1853 window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10);
1854 }
1855
1856 this.postprocessResults(data, initial);
1857
1858 postRender();
1859
1860 this.opts.element.trigger({ type: "select2-loaded", items: data });
1861 })});
1862 },
1863
1864 // abstract
1865 cancel: function () {
1866 this.close();
1867 },
1868
1869 // abstract
1870 blur: function () {
1871 // if selectOnBlur == true, select the currently highlighted option
1872 if (this.opts.selectOnBlur)
1873 this.selectHighlighted({noFocus: true});
1874
1875 this.close();
1876 this.container.removeClass("select2-container-active");
1877 // synonymous to .is(':focus'), which is available in jquery >= 1.6
1878 if (this.search[0] === document.activeElement) { this.search.blur(); }
1879 this.clearSearch();
1880 this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
1881 },
1882
1883 // abstract
1884 focusSearch: function () {
1885 focus(this.search);
1886 },
1887
1888 // abstract
1889 selectHighlighted: function (options) {
1890 if (this._touchMoved) {
1891 this.clearTouchMoved();
1892 return;
1893 }
1894 var index=this.highlight(),
1895 highlighted=this.results.find(".select2-highlighted"),
1896 data = highlighted.closest('.select2-result').data("select2-data");
1897
1898 if (data) {
1899 this.highlight(index);
1900 this.onSelect(data, options);
1901 } else if (options && options.noFocus) {
1902 this.close();
1903 }
1904 },
1905
1906 // abstract
1907 getPlaceholder: function () {
1908 var placeholderOption;
1909 return this.opts.element.attr("placeholder") ||
1910 this.opts.element.attr("data-placeholder") || // jquery 1.4 compat
1911 this.opts.element.data("placeholder") ||
1912 this.opts.placeholder ||
1913 ((placeholderOption = this.getPlaceholderOption()) !== undefined ? placeholderOption.text() : undefined);
1914 },
1915
1916 // abstract
1917 getPlaceholderOption: function() {
1918 if (this.select) {
1919 var firstOption = this.select.children('option').first();
1920 if (this.opts.placeholderOption !== undefined ) {
1921 //Determine the placeholder option based on the specified placeholderOption setting
1922 return (this.opts.placeholderOption === "first" && firstOption) ||
1923 (typeof this.opts.placeholderOption === "function" && this.opts.placeholderOption(this.select));
1924 } else if ($.trim(firstOption.text()) === "" && firstOption.val() === "") {
1925 //No explicit placeholder option specified, use the first if it's blank
1926 return firstOption;
1927 }
1928 }
1929 },
1930
1931 /**
1932 * Get the desired width for the container element. This is
1933 * derived first from option `width` passed to select2, then
1934 * the inline 'style' on the original element, and finally
1935 * falls back to the jQuery calculated element width.
1936 */
1937 // abstract
1938 initContainerWidth: function () {
1939 function resolveContainerWidth() {
1940 var style, attrs, matches, i, l, attr;
1941
1942 if (this.opts.width === "off") {
1943 return null;
1944 } else if (this.opts.width === "element"){
1945 return this.opts.element.outerWidth(false) === 0 ? 'auto' : this.opts.element.outerWidth(false) + 'px';
1946 } else if (this.opts.width === "copy" || this.opts.width === "resolve") {
1947 // check if there is inline style on the element that contains width
1948 style = this.opts.element.attr('style');
1949 if (typeof(style) === "string") {
1950 attrs = style.split(';');
1951 for (i = 0, l = attrs.length; i < l; i = i + 1) {
1952 attr = attrs[i].replace(/\s/g, '');
1953 matches = attr.match(/^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i);
1954 if (matches !== null && matches.length >= 1)
1955 return matches[1];
1956 }
1957 }
1958
1959 if (this.opts.width === "resolve") {
1960 // next check if css('width') can resolve a width that is percent based, this is sometimes possible
1961 // when attached to input type=hidden or elements hidden via css
1962 style = this.opts.element.css('width');
1963 if (style.indexOf("%") > 0) return style;
1964
1965 // finally, fallback on the calculated width of the element
1966 return (this.opts.element.outerWidth(false) === 0 ? 'auto' : this.opts.element.outerWidth(false) + 'px');
1967 }
1968
1969 return null;
1970 } else if ($.isFunction(this.opts.width)) {
1971 return this.opts.width();
1972 } else {
1973 return this.opts.width;
1974 }
1975 };
1976
1977 var width = resolveContainerWidth.call(this);
1978 if (width !== null) {
1979 this.container.css("width", width);
1980 }
1981 }
1982 });
1983
1984 SingleSelect2 = clazz(AbstractSelect2, {
1985
1986 // single
1987
1988 createContainer: function () {
1989 var container = $(document.createElement("div")).attr({
1990 "class": "select2-container"
1991 }).html([
1992 "<a href='javascript:void(0)' class='select2-choice' tabindex='-1'>",
1993 " <span class='select2-chosen'>&#160;</span><abbr class='select2-search-choice-close'></abbr>",
1994 " <span class='select2-arrow' role='presentation'><b role='presentation'></b></span>",
1995 "</a>",
1996 "<label for='' class='select2-offscreen'></label>",
1997 "<input class='select2-focusser select2-offscreen' type='text' aria-haspopup='true' role='button' />",
1998 "<div class='select2-drop select2-display-none'>",
1999 " <div class='select2-search'>",
2000 " <label for='' class='select2-offscreen'></label>",
2001 " <input type='text' autocomplete='off' autocorrect='off' autocapitalize='off' spellcheck='false' class='select2-input' role='combobox' aria-expanded='true'",
2002 " aria-autocomplete='list' />",
2003 " </div>",
2004 " <ul class='select2-results' role='listbox'>",
2005 " </ul>",
2006 "</div>"].join(""));
2007 return container;
2008 },
2009
2010 // single
2011 enableInterface: function() {
2012 if (this.parent.enableInterface.apply(this, arguments)) {
2013 this.focusser.prop("disabled", !this.isInterfaceEnabled());
2014 }
2015 },
2016
2017 // single
2018 opening: function () {
2019 var el, range, len;
2020
2021 if (this.opts.minimumResultsForSearch >= 0) {
2022 this.showSearch(true);
2023 }
2024
2025 this.parent.opening.apply(this, arguments);
2026
2027 if (this.showSearchInput !== false) {
2028 // IE appends focusser.val() at the end of field :/ so we manually insert it at the beginning using a range
2029 // all other browsers handle this just fine
2030
2031 this.search.val(this.focusser.val());
2032 }
2033 if (this.opts.shouldFocusInput(this)) {
2034 this.search.focus();
2035 // move the cursor to the end after focussing, otherwise it will be at the beginning and
2036 // new text will appear *before* focusser.val()
2037 el = this.search.get(0);
2038 if (el.createTextRange) {
2039 range = el.createTextRange();
2040 range.collapse(false);
2041 range.select();
2042 } else if (el.setSelectionRange) {
2043 len = this.search.val().length;
2044 el.setSelectionRange(len, len);
2045 }
2046 }
2047
2048 this.prefillNextSearchTerm();
2049
2050 this.focusser.prop("disabled", true).val("");
2051 this.updateResults(true);
2052 this.opts.element.trigger($.Event("select2-open"));
2053 },
2054
2055 // single
2056 close: function () {
2057 if (!this.opened()) return;
2058 this.parent.close.apply(this, arguments);
2059
2060 this.focusser.prop("disabled", false);
2061
2062 if (this.opts.shouldFocusInput(this)) {
2063 this.focusser.focus();
2064 }
2065 },
2066
2067 // single
2068 focus: function () {
2069 if (this.opened()) {
2070 this.close();
2071 } else {
2072 this.focusser.prop("disabled", false);
2073 if (this.opts.shouldFocusInput(this)) {
2074 this.focusser.focus();
2075 }
2076 }
2077 },
2078
2079 // single
2080 isFocused: function () {
2081 return this.container.hasClass("select2-container-active");
2082 },
2083
2084 // single
2085 cancel: function () {
2086 this.parent.cancel.apply(this, arguments);
2087 this.focusser.prop("disabled", false);
2088
2089 if (this.opts.shouldFocusInput(this)) {
2090 this.focusser.focus();
2091 }
2092 },
2093
2094 // single
2095 destroy: function() {
2096 $("label[for='" + this.focusser.attr('id') + "']")
2097 .attr('for', this.opts.element.attr("id"));
2098 this.parent.destroy.apply(this, arguments);
2099
2100 cleanupJQueryElements.call(this,
2101 "selection",
2102 "focusser"
2103 );
2104 },
2105
2106 // single
2107 initContainer: function () {
2108
2109 var selection,
2110 container = this.container,
2111 dropdown = this.dropdown,
2112 idSuffix = nextUid(),
2113 elementLabel;
2114
2115 if (this.opts.minimumResultsForSearch < 0) {
2116 this.showSearch(false);
2117 } else {
2118 this.showSearch(true);
2119 }
2120
2121 this.selection = selection = container.find(".select2-choice");
2122
2123 this.focusser = container.find(".select2-focusser");
2124
2125 // add aria associations
2126 selection.find(".select2-chosen").attr("id", "select2-chosen-"+idSuffix);
2127 this.focusser.attr("aria-labelledby", "select2-chosen-"+idSuffix);
2128 this.results.attr("id", "select2-results-"+idSuffix);
2129 this.search.attr("aria-owns", "select2-results-"+idSuffix);
2130
2131 // rewrite labels from original element to focusser
2132 this.focusser.attr("id", "s2id_autogen"+idSuffix);
2133
2134 elementLabel = $("label[for='" + this.opts.element.attr("id") + "']");
2135 this.opts.element.focus(this.bind(function () { this.focus(); }));
2136
2137 this.focusser.prev()
2138 .text(elementLabel.text())
2139 .attr('for', this.focusser.attr('id'));
2140
2141 // Ensure the original element retains an accessible name
2142 var originalTitle = this.opts.element.attr("title");
2143 this.opts.element.attr("title", (originalTitle || elementLabel.text()));
2144
2145 this.focusser.attr("tabindex", this.elementTabIndex);
2146
2147 // write label for search field using the label from the focusser element
2148 this.search.attr("id", this.focusser.attr('id') + '_search');
2149
2150 this.search.prev()
2151 .text($("label[for='" + this.focusser.attr('id') + "']").text())
2152 .attr('for', this.search.attr('id'));
2153
2154 this.search.on("keydown", this.bind(function (e) {
2155 if (!this.isInterfaceEnabled()) return;
2156
2157 // filter 229 keyCodes (input method editor is processing key input)
2158 if (229 == e.keyCode) return;
2159
2160 if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) {
2161 // prevent the page from scrolling
2162 killEvent(e);
2163 return;
2164 }
2165
2166 switch (e.which) {
2167 case KEY.UP:
2168 case KEY.DOWN:
2169 this.moveHighlight((e.which === KEY.UP) ? -1 : 1);
2170 killEvent(e);
2171 return;
2172 case KEY.ENTER:
2173 this.selectHighlighted();
2174 killEvent(e);
2175 return;
2176 case KEY.TAB:
2177 this.selectHighlighted({noFocus: true});
2178 return;
2179 case KEY.ESC:
2180 this.cancel(e);
2181 killEvent(e);
2182 return;
2183 }
2184 }));
2185
2186 this.search.on("blur", this.bind(function(e) {
2187 // a workaround for chrome to keep the search field focussed when the scroll bar is used to scroll the dropdown.
2188 // without this the search field loses focus which is annoying
2189 if (document.activeElement === this.body.get(0)) {
2190 window.setTimeout(this.bind(function() {
2191 if (this.opened()) {
2192 this.search.focus();
2193 }
2194 }), 0);
2195 }
2196 }));
2197
2198 this.focusser.on("keydown", this.bind(function (e) {
2199 if (!this.isInterfaceEnabled()) return;
2200
2201 if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) {
2202 return;
2203 }
2204
2205 if (this.opts.openOnEnter === false && e.which === KEY.ENTER) {
2206 killEvent(e);
2207 return;
2208 }
2209
2210 if (e.which == KEY.DOWN || e.which == KEY.UP
2211 || (e.which == KEY.ENTER && this.opts.openOnEnter)) {
2212
2213 if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) return;
2214
2215 this.open();
2216 killEvent(e);
2217 return;
2218 }
2219
2220 if (e.which == KEY.DELETE || e.which == KEY.BACKSPACE) {
2221 if (this.opts.allowClear) {
2222 this.clear();
2223 }
2224 killEvent(e);
2225 return;
2226 }
2227 }));
2228
2229
2230 installKeyUpChangeEvent(this.focusser);
2231 this.focusser.on("keyup-change input", this.bind(function(e) {
2232 if (this.opts.minimumResultsForSearch >= 0) {
2233 e.stopPropagation();
2234 if (this.opened()) return;
2235 this.open();
2236 }
2237 }));
2238
2239 selection.on("mousedown touchstart", "abbr", this.bind(function (e) {
2240 if (!this.isInterfaceEnabled()) {
2241 return;
2242 }
2243
2244 this.clear();
2245 killEventImmediately(e);
2246 this.close();
2247
2248 if (this.selection) {
2249 this.selection.focus();
2250 }
2251 }));
2252
2253 selection.on("mousedown touchstart", this.bind(function (e) {
2254 // Prevent IE from generating a click event on the body
2255 reinsertElement(selection);
2256
2257 if (!this.container.hasClass("select2-container-active")) {
2258 this.opts.element.trigger($.Event("select2-focus"));
2259 }
2260
2261 if (this.opened()) {
2262 this.close();
2263 } else if (this.isInterfaceEnabled()) {
2264 this.open();
2265 }
2266
2267 killEvent(e);
2268 }));
2269
2270 dropdown.on("mousedown touchstart", this.bind(function() {
2271 if (this.opts.shouldFocusInput(this)) {
2272 this.search.focus();
2273 }
2274 }));
2275
2276 selection.on("focus", this.bind(function(e) {
2277 killEvent(e);
2278 }));
2279
2280 this.focusser.on("focus", this.bind(function(){
2281 if (!this.container.hasClass("select2-container-active")) {
2282 this.opts.element.trigger($.Event("select2-focus"));
2283 }
2284 this.container.addClass("select2-container-active");
2285 })).on("blur", this.bind(function() {
2286 if (!this.opened()) {
2287 this.container.removeClass("select2-container-active");
2288 this.opts.element.trigger($.Event("select2-blur"));
2289 }
2290 }));
2291 this.search.on("focus", this.bind(function(){
2292 if (!this.container.hasClass("select2-container-active")) {
2293 this.opts.element.trigger($.Event("select2-focus"));
2294 }
2295 this.container.addClass("select2-container-active");
2296 }));
2297
2298 this.initContainerWidth();
2299 this.opts.element.hide();
2300 this.setPlaceholder();
2301
2302 },
2303
2304 // single
2305 clear: function(triggerChange) {
2306 var data=this.selection.data("select2-data");
2307 if (data) { // guard against queued quick consecutive clicks
2308 var evt = $.Event("select2-clearing");
2309 this.opts.element.trigger(evt);
2310 if (evt.isDefaultPrevented()) {
2311 return;
2312 }
2313 var placeholderOption = this.getPlaceholderOption();
2314 this.opts.element.val(placeholderOption ? placeholderOption.val() : "");
2315 this.selection.find(".select2-chosen").empty();
2316 this.selection.removeData("select2-data");
2317 this.setPlaceholder();
2318
2319 if (triggerChange !== false){
2320 this.opts.element.trigger({ type: "select2-removed", val: this.id(data), choice: data });
2321 this.triggerChange({removed:data});
2322 }
2323 }
2324 },
2325
2326 /**
2327 * Sets selection based on source element's value
2328 */
2329 // single
2330 initSelection: function () {
2331 var selected;
2332 if (this.isPlaceholderOptionSelected()) {
2333 this.updateSelection(null);
2334 this.close();
2335 this.setPlaceholder();
2336 } else {
2337 var self = this;
2338 this.opts.initSelection.call(null, this.opts.element, function(selected){
2339 if (selected !== undefined && selected !== null) {
2340 self.updateSelection(selected);
2341 self.close();
2342 self.setPlaceholder();
2343 self.lastSearchTerm = self.search.val();
2344 }
2345 });
2346 }
2347 },
2348
2349 isPlaceholderOptionSelected: function() {
2350 var placeholderOption;
2351 if (this.getPlaceholder() === undefined) return false; // no placeholder specified so no option should be considered
2352 return ((placeholderOption = this.getPlaceholderOption()) !== undefined && placeholderOption.prop("selected"))
2353 || (this.opts.element.val() === "")
2354 || (this.opts.element.val() === undefined)
2355 || (this.opts.element.val() === null);
2356 },
2357
2358 // single
2359 prepareOpts: function () {
2360 var opts = this.parent.prepareOpts.apply(this, arguments),
2361 self=this;
2362
2363 if (opts.element.get(0).tagName.toLowerCase() === "select") {
2364 // install the selection initializer
2365 opts.initSelection = function (element, callback) {
2366 var selected = element.find("option").filter(function() { return this.selected && !this.disabled });
2367 // a single select box always has a value, no need to null check 'selected'
2368 callback(self.optionToData(selected));
2369 };
2370 } else if ("data" in opts) {
2371 // install default initSelection when applied to hidden input and data is local
2372 opts.initSelection = opts.initSelection || function (element, callback) {
2373 var id = element.val();
2374 //search in data by id, storing the actual matching item
2375 var match = null;
2376 opts.query({
2377 matcher: function(term, text, el){
2378 var is_match = equal(id, opts.id(el));
2379 if (is_match) {
2380 match = el;
2381 }
2382 return is_match;
2383 },
2384 callback: !$.isFunction(callback) ? $.noop : function() {
2385 callback(match);
2386 }
2387 });
2388 };
2389 }
2390
2391 return opts;
2392 },
2393
2394 // single
2395 getPlaceholder: function() {
2396 // if a placeholder is specified on a single select without a valid placeholder option ignore it
2397 if (this.select) {
2398 if (this.getPlaceholderOption() === undefined) {
2399 return undefined;
2400 }
2401 }
2402
2403 return this.parent.getPlaceholder.apply(this, arguments);
2404 },
2405
2406 // single
2407 setPlaceholder: function () {
2408 var placeholder = this.getPlaceholder();
2409
2410 if (this.isPlaceholderOptionSelected() && placeholder !== undefined) {
2411
2412 // check for a placeholder option if attached to a select
2413 if (this.select && this.getPlaceholderOption() === undefined) return;
2414
2415 this.selection.find(".select2-chosen").html(this.opts.escapeMarkup(placeholder));
2416
2417 this.selection.addClass("select2-default");
2418
2419 this.container.removeClass("select2-allowclear");
2420 }
2421 },
2422
2423 // single
2424 postprocessResults: function (data, initial, noHighlightUpdate) {
2425 var selected = 0, self = this, showSearchInput = true;
2426
2427 // find the selected element in the result list
2428
2429 this.findHighlightableChoices().each2(function (i, elm) {
2430 if (equal(self.id(elm.data("select2-data")), self.opts.element.val())) {
2431 selected = i;
2432 return false;
2433 }
2434 });
2435
2436 // and highlight it
2437 if (noHighlightUpdate !== false) {
2438 if (initial === true && selected >= 0) {
2439 this.highlight(selected);
2440 } else {
2441 this.highlight(0);
2442 }
2443 }
2444
2445 // hide the search box if this is the first we got the results and there are enough of them for search
2446
2447 if (initial === true) {
2448 var min = this.opts.minimumResultsForSearch;
2449 if (min >= 0) {
2450 this.showSearch(countResults(data.results) >= min);
2451 }
2452 }
2453 },
2454
2455 // single
2456 showSearch: function(showSearchInput) {
2457 if (this.showSearchInput === showSearchInput) return;
2458
2459 this.showSearchInput = showSearchInput;
2460
2461 this.dropdown.find(".select2-search").toggleClass("select2-search-hidden", !showSearchInput);
2462 this.dropdown.find(".select2-search").toggleClass("select2-offscreen", !showSearchInput);
2463 //add "select2-with-searchbox" to the container if search box is shown
2464 $(this.dropdown, this.container).toggleClass("select2-with-searchbox", showSearchInput);
2465 },
2466
2467 // single
2468 onSelect: function (data, options) {
2469
2470 if (!this.triggerSelect(data)) { return; }
2471
2472 var old = this.opts.element.val(),
2473 oldData = this.data();
2474
2475 this.opts.element.val(this.id(data));
2476 this.updateSelection(data);
2477
2478 this.opts.element.trigger({ type: "select2-selected", val: this.id(data), choice: data });
2479
2480 this.lastSearchTerm = this.search.val();
2481 this.close();
2482
2483 if ((!options || !options.noFocus) && this.opts.shouldFocusInput(this)) {
2484 this.focusser.focus();
2485 }
2486
2487 if (!equal(old, this.id(data))) {
2488 this.triggerChange({ added: data, removed: oldData });
2489 }
2490 },
2491
2492 // single
2493 updateSelection: function (data) {
2494
2495 var container=this.selection.find(".select2-chosen"), formatted, cssClass;
2496
2497 this.selection.data("select2-data", data);
2498
2499 container.empty();
2500 if (data !== null) {
2501 formatted=this.opts.formatSelection(data, container, this.opts.escapeMarkup);
2502 }
2503 if (formatted !== undefined) {
2504 container.append(formatted);
2505 }
2506 cssClass=this.opts.formatSelectionCssClass(data, container);
2507 if (cssClass !== undefined) {
2508 container.addClass(cssClass);
2509 }
2510
2511 this.selection.removeClass("select2-default");
2512
2513 if (this.opts.allowClear && this.getPlaceholder() !== undefined) {
2514 this.container.addClass("select2-allowclear");
2515 }
2516 },
2517
2518 // single
2519 val: function () {
2520 var val,
2521 triggerChange = false,
2522 data = null,
2523 self = this,
2524 oldData = this.data();
2525
2526 if (arguments.length === 0) {
2527 return this.opts.element.val();
2528 }
2529
2530 val = arguments[0];
2531
2532 if (arguments.length > 1) {
2533 triggerChange = arguments[1];
2534 }
2535
2536 if (this.select) {
2537 this.select
2538 .val(val)
2539 .find("option").filter(function() { return this.selected }).each2(function (i, elm) {
2540 data = self.optionToData(elm);
2541 return false;
2542 });
2543 this.updateSelection(data);
2544 this.setPlaceholder();
2545 if (triggerChange) {
2546 this.triggerChange({added: data, removed:oldData});
2547 }
2548 } else {
2549 // val is an id. !val is true for [undefined,null,'',0] - 0 is legal
2550 if (!val && val !== 0) {
2551 this.clear(triggerChange);
2552 return;
2553 }
2554 if (this.opts.initSelection === undefined) {
2555 throw new Error("cannot call val() if initSelection() is not defined");
2556 }
2557 this.opts.element.val(val);
2558 this.opts.initSelection(this.opts.element, function(data){
2559 self.opts.element.val(!data ? "" : self.id(data));
2560 self.updateSelection(data);
2561 self.setPlaceholder();
2562 if (triggerChange) {
2563 self.triggerChange({added: data, removed:oldData});
2564 }
2565 });
2566 }
2567 },
2568
2569 // single
2570 clearSearch: function () {
2571 this.search.val("");
2572 this.focusser.val("");
2573 },
2574
2575 // single
2576 data: function(value) {
2577 var data,
2578 triggerChange = false;
2579
2580 if (arguments.length === 0) {
2581 data = this.selection.data("select2-data");
2582 if (data == undefined) data = null;
2583 return data;
2584 } else {
2585 if (arguments.length > 1) {
2586 triggerChange = arguments[1];
2587 }
2588 if (!value) {
2589 this.clear(triggerChange);
2590 } else {
2591 data = this.data();
2592 this.opts.element.val(!value ? "" : this.id(value));
2593 this.updateSelection(value);
2594 if (triggerChange) {
2595 this.triggerChange({added: value, removed:data});
2596 }
2597 }
2598 }
2599 }
2600 });
2601
2602 MultiSelect2 = clazz(AbstractSelect2, {
2603
2604 // multi
2605 createContainer: function () {
2606 var container = $(document.createElement("div")).attr({
2607 "class": "select2-container select2-container-multi"
2608 }).html([
2609 "<ul class='select2-choices'>",
2610 " <li class='select2-search-field'>",
2611 " <label for='' class='select2-offscreen'></label>",
2612 " <input type='text' autocomplete='off' autocorrect='off' autocapitalize='off' spellcheck='false' class='select2-input'>",
2613 " </li>",
2614 "</ul>",
2615 "<div class='select2-drop select2-drop-multi select2-display-none'>",
2616 " <ul class='select2-results'>",
2617 " </ul>",
2618 "</div>"].join(""));
2619 return container;
2620 },
2621
2622 // multi
2623 prepareOpts: function () {
2624 var opts = this.parent.prepareOpts.apply(this, arguments),
2625 self=this;
2626
2627 // TODO validate placeholder is a string if specified
2628 if (opts.element.get(0).tagName.toLowerCase() === "select") {
2629 // install the selection initializer
2630 opts.initSelection = function (element, callback) {
2631
2632 var data = [];
2633
2634 element.find("option").filter(function() { return this.selected && !this.disabled }).each2(function (i, elm) {
2635 data.push(self.optionToData(elm));
2636 });
2637 callback(data);
2638 };
2639 } else if ("data" in opts) {
2640 // install default initSelection when applied to hidden input and data is local
2641 opts.initSelection = opts.initSelection || function (element, callback) {
2642 var ids = splitVal(element.val(), opts.separator, opts.transformVal);
2643 //search in data by array of ids, storing matching items in a list
2644 var matches = [];
2645 opts.query({
2646 matcher: function(term, text, el){
2647 var is_match = $.grep(ids, function(id) {
2648 return equal(id, opts.id(el));
2649 }).length;
2650 if (is_match) {
2651 matches.push(el);
2652 }
2653 return is_match;
2654 },
2655 callback: !$.isFunction(callback) ? $.noop : function() {
2656 // reorder matches based on the order they appear in the ids array because right now
2657 // they are in the order in which they appear in data array
2658 var ordered = [];
2659 for (var i = 0; i < ids.length; i++) {
2660 var id = ids[i];
2661 for (var j = 0; j < matches.length; j++) {
2662 var match = matches[j];
2663 if (equal(id, opts.id(match))) {
2664 ordered.push(match);
2665 matches.splice(j, 1);
2666 break;
2667 }
2668 }
2669 }
2670 callback(ordered);
2671 }
2672 });
2673 };
2674 }
2675
2676 return opts;
2677 },
2678
2679 // multi
2680 selectChoice: function (choice) {
2681
2682 var selected = this.container.find(".select2-search-choice-focus");
2683 if (selected.length && choice && choice[0] == selected[0]) {
2684
2685 } else {
2686 if (selected.length) {
2687 this.opts.element.trigger("choice-deselected", selected);
2688 }
2689 selected.removeClass("select2-search-choice-focus");
2690 if (choice && choice.length) {
2691 this.close();
2692 choice.addClass("select2-search-choice-focus");
2693 this.opts.element.trigger("choice-selected", choice);
2694 }
2695 }
2696 },
2697
2698 // multi
2699 destroy: function() {
2700 $("label[for='" + this.search.attr('id') + "']")
2701 .attr('for', this.opts.element.attr("id"));
2702 this.parent.destroy.apply(this, arguments);
2703
2704 cleanupJQueryElements.call(this,
2705 "searchContainer",
2706 "selection"
2707 );
2708 },
2709
2710 // multi
2711 initContainer: function () {
2712
2713 var selector = ".select2-choices", selection;
2714
2715 this.searchContainer = this.container.find(".select2-search-field");
2716 this.selection = selection = this.container.find(selector);
2717
2718 var _this = this;
2719 this.selection.on("click", ".select2-container:not(.select2-container-disabled) .select2-search-choice:not(.select2-locked)", function (e) {
2720 _this.search[0].focus();
2721 _this.selectChoice($(this));
2722 });
2723
2724 // rewrite labels from original element to focusser
2725 this.search.attr("id", "s2id_autogen"+nextUid());
2726
2727 this.search.prev()
2728 .text($("label[for='" + this.opts.element.attr("id") + "']").text())
2729 .attr('for', this.search.attr('id'));
2730 this.opts.element.focus(this.bind(function () { this.focus(); }));
2731
2732 this.search.on("input paste", this.bind(function() {
2733 if (this.search.attr('placeholder') && this.search.val().length == 0) return;
2734 if (!this.isInterfaceEnabled()) return;
2735 if (!this.opened()) {
2736 this.open();
2737 }
2738 }));
2739
2740 this.search.attr("tabindex", this.elementTabIndex);
2741
2742 this.keydowns = 0;
2743 this.search.on("keydown", this.bind(function (e) {
2744 if (!this.isInterfaceEnabled()) return;
2745
2746 ++this.keydowns;
2747 var selected = selection.find(".select2-search-choice-focus");
2748 var prev = selected.prev(".select2-search-choice:not(.select2-locked)");
2749 var next = selected.next(".select2-search-choice:not(.select2-locked)");
2750 var pos = getCursorInfo(this.search);
2751
2752 if (selected.length &&
2753 (e.which == KEY.LEFT || e.which == KEY.RIGHT || e.which == KEY.BACKSPACE || e.which == KEY.DELETE || e.which == KEY.ENTER)) {
2754 var selectedChoice = selected;
2755 if (e.which == KEY.LEFT && prev.length) {
2756 selectedChoice = prev;
2757 }
2758 else if (e.which == KEY.RIGHT) {
2759 selectedChoice = next.length ? next : null;
2760 }
2761 else if (e.which === KEY.BACKSPACE) {
2762 if (this.unselect(selected.first())) {
2763 this.search.width(10);
2764 selectedChoice = prev.length ? prev : next;
2765 }
2766 } else if (e.which == KEY.DELETE) {
2767 if (this.unselect(selected.first())) {
2768 this.search.width(10);
2769 selectedChoice = next.length ? next : null;
2770 }
2771 } else if (e.which == KEY.ENTER) {
2772 selectedChoice = null;
2773 }
2774
2775 this.selectChoice(selectedChoice);
2776 killEvent(e);
2777 if (!selectedChoice || !selectedChoice.length) {
2778 this.open();
2779 }
2780 return;
2781 } else if (((e.which === KEY.BACKSPACE && this.keydowns == 1)
2782 || e.which == KEY.LEFT) && (pos.offset == 0 && !pos.length)) {
2783
2784 this.selectChoice(selection.find(".select2-search-choice:not(.select2-locked)").last());
2785 killEvent(e);
2786 return;
2787 } else {
2788 this.selectChoice(null);
2789 }
2790
2791 if (this.opened()) {
2792 switch (e.which) {
2793 case KEY.UP:
2794 case KEY.DOWN:
2795 this.moveHighlight((e.which === KEY.UP) ? -1 : 1);
2796 killEvent(e);
2797 return;
2798 case KEY.ENTER:
2799 this.selectHighlighted();
2800 killEvent(e);
2801 return;
2802 case KEY.TAB:
2803 this.selectHighlighted({noFocus:true});
2804 this.close();
2805 return;
2806 case KEY.ESC:
2807 this.cancel(e);
2808 killEvent(e);
2809 return;
2810 }
2811 }
2812
2813 if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e)
2814 || e.which === KEY.BACKSPACE || e.which === KEY.ESC) {
2815 return;
2816 }
2817
2818 if (e.which === KEY.ENTER) {
2819 if (this.opts.openOnEnter === false) {
2820 return;
2821 } else if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) {
2822 return;
2823 }
2824 }
2825
2826 this.open();
2827
2828 if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) {
2829 // prevent the page from scrolling
2830 killEvent(e);
2831 }
2832
2833 if (e.which === KEY.ENTER) {
2834 // prevent form from being submitted
2835 killEvent(e);
2836 }
2837
2838 }));
2839
2840 this.search.on("keyup", this.bind(function (e) {
2841 this.keydowns = 0;
2842 this.resizeSearch();
2843 })
2844 );
2845
2846 this.search.on("blur", this.bind(function(e) {
2847 this.container.removeClass("select2-container-active");
2848 this.search.removeClass("select2-focused");
2849 this.selectChoice(null);
2850 if (!this.opened()) this.clearSearch();
2851 e.stopImmediatePropagation();
2852 this.opts.element.trigger($.Event("select2-blur"));
2853 }));
2854
2855 this.container.on("click", selector, this.bind(function (e) {
2856 if (!this.isInterfaceEnabled()) return;
2857 if ($(e.target).closest(".select2-search-choice").length > 0) {
2858 // clicked inside a select2 search choice, do not open
2859 return;
2860 }
2861 this.selectChoice(null);
2862 this.clearPlaceholder();
2863 if (!this.container.hasClass("select2-container-active")) {
2864 this.opts.element.trigger($.Event("select2-focus"));
2865 }
2866 this.open();
2867 this.focusSearch();
2868 e.preventDefault();
2869 }));
2870
2871 this.container.on("focus", selector, this.bind(function () {
2872 if (!this.isInterfaceEnabled()) return;
2873 if (!this.container.hasClass("select2-container-active")) {
2874 this.opts.element.trigger($.Event("select2-focus"));
2875 }
2876 this.container.addClass("select2-container-active");
2877 this.dropdown.addClass("select2-drop-active");
2878 this.clearPlaceholder();
2879 }));
2880
2881 this.initContainerWidth();
2882 this.opts.element.hide();
2883
2884 // set the placeholder if necessary
2885 this.clearSearch();
2886 },
2887
2888 // multi
2889 enableInterface: function() {
2890 if (this.parent.enableInterface.apply(this, arguments)) {
2891 this.search.prop("disabled", !this.isInterfaceEnabled());
2892 }
2893 },
2894
2895 // multi
2896 initSelection: function () {
2897 var data;
2898 if (this.opts.element.val() === "" && this.opts.element.text() === "") {
2899 this.updateSelection([]);
2900 this.close();
2901 // set the placeholder if necessary
2902 this.clearSearch();
2903 }
2904 if (this.select || this.opts.element.val() !== "") {
2905 var self = this;
2906 this.opts.initSelection.call(null, this.opts.element, function(data){
2907 if (data !== undefined && data !== null) {
2908 self.updateSelection(data);
2909 self.close();
2910 // set the placeholder if necessary
2911 self.clearSearch();
2912 }
2913 });
2914 }
2915 },
2916
2917 // multi
2918 clearSearch: function () {
2919 var placeholder = this.getPlaceholder(),
2920 maxWidth = this.getMaxSearchWidth();
2921
2922 if (placeholder !== undefined && this.getVal().length === 0 && this.search.hasClass("select2-focused") === false) {
2923 this.search.val(placeholder).addClass("select2-default");
2924 // stretch the search box to full width of the container so as much of the placeholder is visible as possible
2925 // 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
2926 this.search.width(maxWidth > 0 ? maxWidth : this.container.css("width"));
2927 } else {
2928 this.search.val("").width(10);
2929 }
2930 },
2931
2932 // multi
2933 clearPlaceholder: function () {
2934 if (this.search.hasClass("select2-default")) {
2935 this.search.val("").removeClass("select2-default");
2936 }
2937 },
2938
2939 // multi
2940 opening: function () {
2941 this.clearPlaceholder(); // should be done before super so placeholder is not used to search
2942 this.resizeSearch();
2943
2944 this.parent.opening.apply(this, arguments);
2945
2946 this.focusSearch();
2947
2948 this.prefillNextSearchTerm();
2949 this.updateResults(true);
2950
2951 if (this.opts.shouldFocusInput(this)) {
2952 this.search.focus();
2953 }
2954 this.opts.element.trigger($.Event("select2-open"));
2955 },
2956
2957 // multi
2958 close: function () {
2959 if (!this.opened()) return;
2960 this.parent.close.apply(this, arguments);
2961 },
2962
2963 // multi
2964 focus: function () {
2965 this.close();
2966 this.search.focus();
2967 },
2968
2969 // multi
2970 isFocused: function () {
2971 return this.search.hasClass("select2-focused");
2972 },
2973
2974 // multi
2975 updateSelection: function (data) {
2976 var ids = {}, filtered = [], self = this;
2977
2978 // filter out duplicates
2979 $(data).each(function () {
2980 if (!(self.id(this) in ids)) {
2981 ids[self.id(this)] = 0;
2982 filtered.push(this);
2983 }
2984 });
2985
2986 this.selection.find(".select2-search-choice").remove();
2987 this.addSelectedChoice(filtered);
2988 self.postprocessResults();
2989 },
2990
2991 // multi
2992 tokenize: function() {
2993 var input = this.search.val();
2994 input = this.opts.tokenizer.call(this, input, this.data(), this.bind(this.onSelect), this.opts);
2995 if (input != null && input != undefined) {
2996 this.search.val(input);
2997 if (input.length > 0) {
2998 this.open();
2999 }
3000 }
3001
3002 },
3003
3004 // multi
3005 onSelect: function (data, options) {
3006
3007 if (!this.triggerSelect(data) || data.text === "") { return; }
3008
3009 this.addSelectedChoice(data);
3010
3011 this.opts.element.trigger({ type: "selected", val: this.id(data), choice: data });
3012
3013 // keep track of the search's value before it gets cleared
3014 this.lastSearchTerm = this.search.val();
3015
3016 this.clearSearch();
3017 this.updateResults();
3018
3019 if (this.select || !this.opts.closeOnSelect) this.postprocessResults(data, false, this.opts.closeOnSelect===true);
3020
3021 if (this.opts.closeOnSelect) {
3022 this.close();
3023 this.search.width(10);
3024 } else {
3025 if (this.countSelectableResults()>0) {
3026 this.search.width(10);
3027 this.resizeSearch();
3028 if (this.getMaximumSelectionSize() > 0 && this.val().length >= this.getMaximumSelectionSize()) {
3029 // if we reached max selection size repaint the results so choices
3030 // are replaced with the max selection reached message
3031 this.updateResults(true);
3032 } else {
3033 // initializes search's value with nextSearchTerm and update search result
3034 if (this.prefillNextSearchTerm()) {
3035 this.updateResults();
3036 }
3037 }
3038 this.positionDropdown();
3039 } else {
3040 // if nothing left to select close
3041 this.close();
3042 this.search.width(10);
3043 }
3044 }
3045
3046 // since its not possible to select an element that has already been
3047 // added we do not need to check if this is a new element before firing change
3048 this.triggerChange({ added: data });
3049
3050 if (!options || !options.noFocus)
3051 this.focusSearch();
3052 },
3053
3054 // multi
3055 cancel: function () {
3056 this.close();
3057 this.focusSearch();
3058 },
3059
3060 addSelectedChoice: function (data) {
3061 var val = this.getVal(), self = this;
3062 $(data).each(function () {
3063 val.push(self.createChoice(this));
3064 });
3065 this.setVal(val);
3066 },
3067
3068 createChoice: function (data) {
3069 var enableChoice = !data.locked,
3070 enabledItem = $(
3071 "<li class='select2-search-choice'>" +
3072 " <div></div>" +
3073 " <a href='#' class='select2-search-choice-close' tabindex='-1'></a>" +
3074 "</li>"),
3075 disabledItem = $(
3076 "<li class='select2-search-choice select2-locked'>" +
3077 "<div></div>" +
3078 "</li>");
3079 var choice = enableChoice ? enabledItem : disabledItem,
3080 id = this.id(data),
3081 formatted,
3082 cssClass;
3083
3084 formatted=this.opts.formatSelection(data, choice.find("div"), this.opts.escapeMarkup);
3085 if (formatted != undefined) {
3086 choice.find("div").replaceWith($("<div></div>").html(formatted));
3087 }
3088 cssClass=this.opts.formatSelectionCssClass(data, choice.find("div"));
3089 if (cssClass != undefined) {
3090 choice.addClass(cssClass);
3091 }
3092
3093 if(enableChoice){
3094 choice.find(".select2-search-choice-close")
3095 .on("mousedown", killEvent)
3096 .on("click dblclick", this.bind(function (e) {
3097 if (!this.isInterfaceEnabled()) return;
3098
3099 this.unselect($(e.target));
3100 this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
3101 killEvent(e);
3102 this.close();
3103 this.focusSearch();
3104 })).on("focus", this.bind(function () {
3105 if (!this.isInterfaceEnabled()) return;
3106 this.container.addClass("select2-container-active");
3107 this.dropdown.addClass("select2-drop-active");
3108 }));
3109 }
3110
3111 choice.data("select2-data", data);
3112 choice.insertBefore(this.searchContainer);
3113
3114 return id;
3115 },
3116
3117 // multi
3118 unselect: function (selected) {
3119 var val = this.getVal(),
3120 data,
3121 index;
3122 selected = selected.closest(".select2-search-choice");
3123
3124 if (selected.length === 0) {
3125 throw "Invalid argument: " + selected + ". Must be .select2-search-choice";
3126 }
3127
3128 data = selected.data("select2-data");
3129
3130 if (!data) {
3131 // prevent a race condition when the 'x' is clicked really fast repeatedly the event can be queued
3132 // and invoked on an element already removed
3133 return;
3134 }
3135
3136 var evt = $.Event("select2-removing");
3137 evt.val = this.id(data);
3138 evt.choice = data;
3139 this.opts.element.trigger(evt);
3140
3141 if (evt.isDefaultPrevented()) {
3142 return false;
3143 }
3144
3145 while((index = indexOf(this.id(data), val)) >= 0) {
3146 val.splice(index, 1);
3147 this.setVal(val);
3148 if (this.select) this.postprocessResults();
3149 }
3150
3151 selected.remove();
3152
3153 this.opts.element.trigger({ type: "select2-removed", val: this.id(data), choice: data });
3154 this.triggerChange({ removed: data });
3155
3156 return true;
3157 },
3158
3159 // multi
3160 postprocessResults: function (data, initial, noHighlightUpdate) {
3161 var val = this.getVal(),
3162 choices = this.results.find(".select2-result"),
3163 compound = this.results.find(".select2-result-with-children"),
3164 self = this;
3165
3166 choices.each2(function (i, choice) {
3167 var id = self.id(choice.data("select2-data"));
3168 if (indexOf(id, val) >= 0) {
3169 choice.addClass("select2-selected");
3170 // mark all children of the selected parent as selected
3171 choice.find(".select2-result-selectable").addClass("select2-selected");
3172 }
3173 });
3174
3175 compound.each2(function(i, choice) {
3176 // hide an optgroup if it doesn't have any selectable children
3177 if (!choice.is('.select2-result-selectable')
3178 && choice.find(".select2-result-selectable:not(.select2-selected)").length === 0) {
3179 choice.addClass("select2-selected");
3180 }
3181 });
3182
3183 if (this.highlight() == -1 && noHighlightUpdate !== false && this.opts.closeOnSelect === true){
3184 self.highlight(0);
3185 }
3186
3187 //If all results are chosen render formatNoMatches
3188 if(!this.opts.createSearchChoice && !choices.filter('.select2-result:not(.select2-selected)').length > 0){
3189 if(!data || data && !data.more && this.results.find(".select2-no-results").length === 0) {
3190 if (checkFormatter(self.opts.formatNoMatches, "formatNoMatches")) {
3191 this.results.append("<li class='select2-no-results'>" + evaluate(self.opts.formatNoMatches, self.opts.element, self.search.val()) + "</li>");
3192 }
3193 }
3194 }
3195
3196 },
3197
3198 // multi
3199 getMaxSearchWidth: function() {
3200 return this.selection.width() - getSideBorderPadding(this.search);
3201 },
3202
3203 // multi
3204 resizeSearch: function () {
3205 var minimumWidth, left, maxWidth, containerLeft, searchWidth,
3206 sideBorderPadding = getSideBorderPadding(this.search);
3207
3208 minimumWidth = measureTextWidth(this.search) + 10;
3209
3210 left = this.search.offset().left;
3211
3212 maxWidth = this.selection.width();
3213 containerLeft = this.selection.offset().left;
3214
3215 searchWidth = maxWidth - (left - containerLeft) - sideBorderPadding;
3216
3217 if (searchWidth < minimumWidth) {
3218 searchWidth = maxWidth - sideBorderPadding;
3219 }
3220
3221 if (searchWidth < 40) {
3222 searchWidth = maxWidth - sideBorderPadding;
3223 }
3224
3225 if (searchWidth <= 0) {
3226 searchWidth = minimumWidth;
3227 }
3228
3229 this.search.width(Math.floor(searchWidth));
3230 },
3231
3232 // multi
3233 getVal: function () {
3234 var val;
3235 if (this.select) {
3236 val = this.select.val();
3237 return val === null ? [] : val;
3238 } else {
3239 val = this.opts.element.val();
3240 return splitVal(val, this.opts.separator, this.opts.transformVal);
3241 }
3242 },
3243
3244 // multi
3245 setVal: function (val) {
3246 if (this.select) {
3247 this.select.val(val);
3248 } else {
3249 var unique = [], valMap = {};
3250 // filter out duplicates
3251 $(val).each(function () {
3252 if (!(this in valMap)) {
3253 unique.push(this);
3254 valMap[this] = 0;
3255 }
3256 });
3257 this.opts.element.val(unique.length === 0 ? "" : unique.join(this.opts.separator));
3258 }
3259 },
3260
3261 // multi
3262 buildChangeDetails: function (old, current) {
3263 var current = current.slice(0),
3264 old = old.slice(0);
3265
3266 // remove intersection from each array
3267 for (var i = 0; i < current.length; i++) {
3268 for (var j = 0; j < old.length; j++) {
3269 if (equal(this.opts.id(current[i]), this.opts.id(old[j]))) {
3270 current.splice(i, 1);
3271 i--;
3272 old.splice(j, 1);
3273 break;
3274 }
3275 }
3276 }
3277
3278 return {added: current, removed: old};
3279 },
3280
3281
3282 // multi
3283 val: function (val, triggerChange) {
3284 var oldData, self=this;
3285
3286 if (arguments.length === 0) {
3287 return this.getVal();
3288 }
3289
3290 oldData=this.data();
3291 if (!oldData.length) oldData=[];
3292
3293 // val is an id. !val is true for [undefined,null,'',0] - 0 is legal
3294 if (!val && val !== 0) {
3295 this.opts.element.val("");
3296 this.updateSelection([]);
3297 this.clearSearch();
3298 if (triggerChange) {
3299 this.triggerChange({added: this.data(), removed: oldData});
3300 }
3301 return;
3302 }
3303
3304 // val is a list of ids
3305 this.setVal(val);
3306
3307 if (this.select) {
3308 this.opts.initSelection(this.select, this.bind(this.updateSelection));
3309 if (triggerChange) {
3310 this.triggerChange(this.buildChangeDetails(oldData, this.data()));
3311 }
3312 } else {
3313 if (this.opts.initSelection === undefined) {
3314 throw new Error("val() cannot be called if initSelection() is not defined");
3315 }
3316
3317 this.opts.initSelection(this.opts.element, function(data){
3318 var ids=$.map(data, self.id);
3319 self.setVal(ids);
3320 self.updateSelection(data);
3321 self.clearSearch();
3322 if (triggerChange) {
3323 self.triggerChange(self.buildChangeDetails(oldData, self.data()));
3324 }
3325 });
3326 }
3327 this.clearSearch();
3328 },
3329
3330 // multi
3331 onSortStart: function() {
3332 if (this.select) {
3333 throw new Error("Sorting of elements is not supported when attached to <select>. Attach to <input type='hidden'/> instead.");
3334 }
3335
3336 // collapse search field into 0 width so its container can be collapsed as well
3337 this.search.width(0);
3338 // hide the container
3339 this.searchContainer.hide();
3340 },
3341
3342 // multi
3343 onSortEnd:function() {
3344
3345 var val=[], self=this;
3346
3347 // show search and move it to the end of the list
3348 this.searchContainer.show();
3349 // make sure the search container is the last item in the list
3350 this.searchContainer.appendTo(this.searchContainer.parent());
3351 // since we collapsed the width in dragStarted, we resize it here
3352 this.resizeSearch();
3353
3354 // update selection
3355 this.selection.find(".select2-search-choice").each(function() {
3356 val.push(self.opts.id($(this).data("select2-data")));
3357 });
3358 this.setVal(val);
3359 this.triggerChange();
3360 },
3361
3362 // multi
3363 data: function(values, triggerChange) {
3364 var self=this, ids, old;
3365 if (arguments.length === 0) {
3366 return this.selection
3367 .children(".select2-search-choice")
3368 .map(function() { return $(this).data("select2-data"); })
3369 .get();
3370 } else {
3371 old = this.data();
3372 if (!values) { values = []; }
3373 ids = $.map(values, function(e) { return self.opts.id(e); });
3374 this.setVal(ids);
3375 this.updateSelection(values);
3376 this.clearSearch();
3377 if (triggerChange) {
3378 this.triggerChange(this.buildChangeDetails(old, this.data()));
3379 }
3380 }
3381 }
3382 });
3383
3384 $.fn.select2 = function () {
3385
3386 var args = Array.prototype.slice.call(arguments, 0),
3387 opts,
3388 select2,
3389 method, value, multiple,
3390 allowedMethods = ["val", "destroy", "opened", "open", "close", "focus", "isFocused", "container", "dropdown", "onSortStart", "onSortEnd", "enable", "disable", "readonly", "positionDropdown", "data", "search"],
3391 valueMethods = ["opened", "isFocused", "container", "dropdown"],
3392 propertyMethods = ["val", "data"],
3393 methodsMap = { search: "externalSearch" };
3394
3395 this.each(function () {
3396 if (args.length === 0 || typeof(args[0]) === "object") {
3397 opts = args.length === 0 ? {} : $.extend({}, args[0]);
3398 opts.element = $(this);
3399
3400 if (opts.element.get(0).tagName.toLowerCase() === "select") {
3401 multiple = opts.element.prop("multiple");
3402 } else {
3403 multiple = opts.multiple || false;
3404 if ("tags" in opts) {opts.multiple = multiple = true;}
3405 }
3406
3407 select2 = multiple ? new window.Select2["class"].multi() : new window.Select2["class"].single();
3408 select2.init(opts);
3409 } else if (typeof(args[0]) === "string") {
3410
3411 if (indexOf(args[0], allowedMethods) < 0) {
3412 throw "Unknown method: " + args[0];
3413 }
3414
3415 value = undefined;
3416 select2 = $(this).data("select2");
3417 if (select2 === undefined) return;
3418
3419 method=args[0];
3420
3421 if (method === "container") {
3422 value = select2.container;
3423 } else if (method === "dropdown") {
3424 value = select2.dropdown;
3425 } else {
3426 if (methodsMap[method]) method = methodsMap[method];
3427
3428 value = select2[method].apply(select2, args.slice(1));
3429 }
3430 if (indexOf(args[0], valueMethods) >= 0
3431 || (indexOf(args[0], propertyMethods) >= 0 && args.length == 1)) {
3432 return false; // abort the iteration, ready to return first matched value
3433 }
3434 } else {
3435 throw "Invalid arguments to select2 plugin: " + args;
3436 }
3437 });
3438 return (value === undefined) ? this : value;
3439 };
3440
3441 // plugin defaults, accessible to users
3442 $.fn.select2.defaults = {
3443 width: "copy",
3444 loadMorePadding: 0,
3445 closeOnSelect: true,
3446 openOnEnter: true,
3447 containerCss: {},
3448 dropdownCss: {},
3449 containerCssClass: "",
3450 dropdownCssClass: "",
3451 formatResult: function(result, container, query, escapeMarkup) {
3452 var markup=[];
3453 markMatch(this.text(result), query.term, markup, escapeMarkup);
3454 return markup.join("");
3455 },
3456 transformVal: function(val) {
3457 return $.trim(val);
3458 },
3459 formatSelection: function (data, container, escapeMarkup) {
3460 return data ? escapeMarkup(this.text(data)) : undefined;
3461 },
3462 sortResults: function (results, container, query) {
3463 return results;
3464 },
3465 formatResultCssClass: function(data) {return data.css;},
3466 formatSelectionCssClass: function(data, container) {return undefined;},
3467 minimumResultsForSearch: 0,
3468 minimumInputLength: 0,
3469 maximumInputLength: null,
3470 maximumSelectionSize: 0,
3471 id: function (e) { return e == undefined ? null : e.id; },
3472 text: function (e) {
3473 if (e && this.data && this.data.text) {
3474 if ($.isFunction(this.data.text)) {
3475 return this.data.text(e);
3476 } else {
3477 return e[this.data.text];
3478 }
3479 } else {
3480 return e.text;
3481 }
3482 },
3483 matcher: function(term, text) {
3484 return stripDiacritics(''+text).toUpperCase().indexOf(stripDiacritics(''+term).toUpperCase()) >= 0;
3485 },
3486 separator: ",",
3487 tokenSeparators: [],
3488 tokenizer: defaultTokenizer,
3489 escapeMarkup: defaultEscapeMarkup,
3490 blurOnChange: false,
3491 selectOnBlur: false,
3492 adaptContainerCssClass: function(c) { return c; },
3493 adaptDropdownCssClass: function(c) { return null; },
3494 nextSearchTerm: function(selectedObject, currentSearchTerm) { return undefined; },
3495 searchInputPlaceholder: '',
3496 createSearchChoicePosition: 'top',
3497 shouldFocusInput: function (instance) {
3498 // Attempt to detect touch devices
3499 var supportsTouchEvents = (('ontouchstart' in window) ||
3500 (navigator.msMaxTouchPoints > 0));
3501
3502 // Only devices which support touch events should be special cased
3503 if (!supportsTouchEvents) {
3504 return true;
3505 }
3506
3507 // Never focus the input if search is disabled
3508 if (instance.opts.minimumResultsForSearch < 0) {
3509 return false;
3510 }
3511
3512 return true;
3513 }
3514 };
3515
3516 $.fn.select2.locales = [];
3517
3518 $.fn.select2.locales['en'] = {
3519 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."; },
3520 formatNoMatches: function () { return "No matches found"; },
3521 formatAjaxError: function (jqXHR, textStatus, errorThrown) { return "Loading failed"; },
3522 formatInputTooShort: function (input, min) { var n = min - input.length; return "Please enter " + n + " or more character" + (n == 1 ? "" : "s"); },
3523 formatInputTooLong: function (input, max) { var n = input.length - max; return "Please delete " + n + " character" + (n == 1 ? "" : "s"); },
3524 formatSelectionTooBig: function (limit) { return "You can only select " + limit + " item" + (limit == 1 ? "" : "s"); },
3525 formatLoadMore: function (pageNumber) { return "Loading more results…"; },
3526 formatSearching: function () { return "Searching…"; }
3527 };
3528
3529 $.extend($.fn.select2.defaults, $.fn.select2.locales['en']);
3530
3531 $.fn.select2.ajaxDefaults = {
3532 transport: $.ajax,
3533 params: {
3534 type: "GET",
3535 cache: false,
3536 dataType: "json"
3537 }
3538 };
3539
3540 // exports
3541 window.Select2 = {
3542 query: {
3543 ajax: ajax,
3544 local: local,
3545 tags: tags
3546 }, util: {
3547 debounce: debounce,
3548 markMatch: markMatch,
3549 escapeMarkup: defaultEscapeMarkup,
3550 stripDiacritics: stripDiacritics
3551 }, "class": {
3552 "abstract": AbstractSelect2,
3553 "single": SingleSelect2,
3554 "multi": MultiSelect2
3555 }
3556 };
3557
3558 }(jQuery));
3559