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