locomotive_cms 2.1.4 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (143) hide show
  1. data/Gemfile +1 -5
  2. data/app/assets/images/locomotive/icons/flags/zh-CN.png +0 -0
  3. data/app/assets/javascripts/aloha/plugins/custom/inputcontrol/css/inputcontrol.css +3 -0
  4. data/app/assets/javascripts/aloha/plugins/custom/inputcontrol/lib/inputcontrol-plugin.js +94 -0
  5. data/app/assets/javascripts/aloha/plugins/custom/inputcontrol/package.json +1 -0
  6. data/app/assets/javascripts/locomotive.js +2 -1
  7. data/app/assets/javascripts/locomotive/models/content_type.js.coffee +6 -2
  8. data/app/assets/javascripts/locomotive/models/page.js.coffee +1 -1
  9. data/app/assets/javascripts/locomotive/models/site.js.coffee +1 -1
  10. data/app/assets/javascripts/locomotive/utils/aloha_settings.js.coffee +17 -7
  11. data/app/assets/javascripts/locomotive/utils/tinymce_settings.js.coffee +6 -0
  12. data/app/assets/javascripts/locomotive/views/application_view.js.coffee +13 -2
  13. data/app/assets/javascripts/locomotive/views/content_entries/_form_view.js.coffee +19 -2
  14. data/app/assets/javascripts/locomotive/views/content_entries/_popup_form_view.js.coffee +1 -1
  15. data/app/assets/javascripts/locomotive/views/content_entries/index_view.js.coffee +2 -2
  16. data/app/assets/javascripts/locomotive/views/content_types/_form_view.js.coffee +2 -1
  17. data/app/assets/javascripts/locomotive/views/content_types/custom_field_entry_view.js.coffee +3 -0
  18. data/app/assets/javascripts/locomotive/views/current_site/edit_view.js.coffee +2 -1
  19. data/app/assets/javascripts/locomotive/views/editable_elements/edit_all_view.js.coffee +3 -6
  20. data/app/assets/javascripts/locomotive/views/editable_elements/text_view.js.coffee +47 -0
  21. data/app/assets/javascripts/locomotive/views/inline_editor/toolbar_view.js.coffee +1 -1
  22. data/app/assets/javascripts/locomotive/views/pages/_form_view.js.coffee +2 -1
  23. data/app/assets/javascripts/locomotive/views/pages/list_view.js.coffee +2 -2
  24. data/app/assets/javascripts/locomotive/views/shared/fields/belongs_to_view.js.coffee +32 -0
  25. data/app/assets/javascripts/locomotive/views/shared/fields/has_many_view.js.coffee +2 -2
  26. data/app/assets/javascripts/locomotive/views/shared/fields/many_to_many_view.js.coffee +2 -2
  27. data/app/assets/javascripts/locomotive/views/shared/form_view.js.coffee +2 -0
  28. data/app/assets/javascripts/locomotive/views/snippets/_form_view.js.coffee +2 -1
  29. data/app/assets/javascripts/locomotive/views/theme_assets/_form_view.js.coffee +2 -1
  30. data/app/assets/javascripts/locomotive/views/theme_assets/index_view.js.coffee +0 -1
  31. data/app/assets/javascripts/tinymce/plugins/locomotive_media/langs/es.js +1 -0
  32. data/app/assets/javascripts/tinymce/plugins/locomotive_media/langs/et.js +1 -0
  33. data/app/assets/javascripts/tinymce/plugins/locomotive_media/langs/it.js +1 -0
  34. data/app/assets/javascripts/tinymce/plugins/locomotive_media/langs/ja.js +1 -0
  35. data/app/assets/javascripts/tinymce/plugins/locomotive_media/langs/nb.js +1 -0
  36. data/app/assets/javascripts/tinymce/plugins/locomotive_media/langs/nl.js +1 -0
  37. data/app/assets/javascripts/tinymce/plugins/locomotive_media/langs/no.js +1 -0
  38. data/app/assets/javascripts/tinymce/plugins/locomotive_media/langs/pl.js +1 -0
  39. data/app/assets/javascripts/tinymce/plugins/locomotive_media/langs/pt.js +1 -0
  40. data/app/assets/javascripts/tinymce/plugins/locomotive_media/langs/zh-cn.js +1 -0
  41. data/app/assets/stylesheets/locomotive.css +1 -0
  42. data/app/assets/stylesheets/locomotive/backoffice/codemirror_changes.css.scss +2 -0
  43. data/app/assets/stylesheets/locomotive/backoffice/formtastic_changes.css.scss +17 -3
  44. data/app/controllers/locomotive/content_entries_controller.rb +6 -2
  45. data/app/controllers/locomotive/public/content_entries_controller.rb +6 -0
  46. data/app/helpers/locomotive/base_helper.rb +8 -0
  47. data/app/helpers/locomotive/sites_helper.rb +6 -0
  48. data/app/models/locomotive/content_entry.rb +35 -5
  49. data/app/models/locomotive/content_type.rb +26 -8
  50. data/app/models/locomotive/editable_element.rb +15 -1
  51. data/app/models/locomotive/editable_file.rb +0 -2
  52. data/app/models/locomotive/editable_long_text.rb +3 -3
  53. data/app/models/locomotive/editable_short_text.rb +3 -64
  54. data/app/models/locomotive/editable_text.rb +84 -0
  55. data/app/models/locomotive/extensions/content_entry/csv.rb +7 -3
  56. data/app/models/locomotive/extensions/page/editable_elements.rb +3 -5
  57. data/app/models/locomotive/extensions/site/locales.rb +20 -0
  58. data/app/models/locomotive/extensions/site/timezone.rb +35 -0
  59. data/app/models/locomotive/site.rb +20 -16
  60. data/app/models/locomotive/theme_asset.rb +1 -1
  61. data/app/presenters/locomotive/content_entry_presenter.rb +1 -1
  62. data/app/presenters/locomotive/{editable_short_text_presenter.rb → editable_text_presenter.rb} +14 -2
  63. data/app/views/locomotive/current_site/_form.html.haml +1 -0
  64. data/app/views/locomotive/custom_fields/types/_belongs_to.html.haml +6 -5
  65. data/app/views/locomotive/pages/_editable_elements.html.haml +1 -1
  66. data/app/views/locomotive/shared/_head.html.haml +2 -0
  67. data/app/views/locomotive/shared/_main_app_head_before_backbone.html.haml +1 -0
  68. data/config/locales/admin_ui.de.yml +1 -0
  69. data/config/locales/admin_ui.en.yml +1 -0
  70. data/config/locales/admin_ui.et.yml +1 -0
  71. data/config/locales/admin_ui.fr.yml +1 -0
  72. data/config/locales/admin_ui.ja.yml +1 -0
  73. data/config/locales/admin_ui.nb.yml +2 -0
  74. data/config/locales/admin_ui.pl.yml +1 -0
  75. data/config/locales/admin_ui.pt-BR.yml +2 -1
  76. data/config/locales/admin_ui.ru.yml +24 -4
  77. data/config/locales/admin_ui.zh-CN.yml +347 -0
  78. data/config/locales/carrierwave.zh-CN.yml +4 -0
  79. data/config/locales/default.zh-CN.yml +116 -0
  80. data/config/locales/devise.nb.yml +1 -0
  81. data/config/locales/devise.zh-CN.yml +64 -0
  82. data/config/locales/flash.zh-CN.yml +115 -0
  83. data/config/locales/formtastic.en.yml +1 -0
  84. data/config/locales/formtastic.ru.yml +4 -1
  85. data/config/locales/formtastic.zh-CN.yml +112 -0
  86. data/features/backoffice/content_types/localized.feature +63 -0
  87. data/features/backoffice/pages.feature +3 -1
  88. data/features/backoffice/site.feature +7 -0
  89. data/features/public/contact_form.feature +11 -0
  90. data/features/public/content_entries.feature +13 -0
  91. data/features/public/pages.feature +24 -0
  92. data/features/step_definitions/page_steps.rb +6 -0
  93. data/features/step_definitions/web_steps.rb +24 -7
  94. data/lib/generators/locomotive/install/templates/locomotive.rb +2 -2
  95. data/lib/generators/locomotive/install/templates/mongoid.yml +23 -29
  96. data/lib/locomotive.rb +1 -4
  97. data/lib/locomotive/configuration.rb +3 -3
  98. data/lib/locomotive/dependencies.rb +1 -0
  99. data/lib/locomotive/engine.rb +2 -1
  100. data/lib/locomotive/liquid/drops/page.rb +1 -1
  101. data/lib/locomotive/liquid/filters/date.rb +3 -1
  102. data/lib/locomotive/liquid/filters/misc.rb +4 -0
  103. data/lib/locomotive/liquid/filters/text.rb +4 -0
  104. data/lib/locomotive/liquid/tags/editable.rb +1 -2
  105. data/lib/locomotive/liquid/tags/editable/text.rb +79 -0
  106. data/lib/locomotive/liquid/tags/inline_editor.rb +1 -1
  107. data/lib/locomotive/liquid/tags/link_to.rb +72 -13
  108. data/lib/locomotive/liquid/tags/with_scope.rb +3 -3
  109. data/lib/locomotive/middlewares.rb +0 -1
  110. data/lib/locomotive/mongoid/patches.rb +0 -16
  111. data/lib/locomotive/render.rb +1 -1
  112. data/lib/locomotive/version.rb +1 -1
  113. data/lib/tasks/locomotive.rake +15 -9
  114. data/mongodb/migrate/20130326201349_rename_entry_to_content_entry.rb +1 -1
  115. data/mongodb/migrate/20130621135025_create_editable_texts.rb +42 -0
  116. data/mongodb/migrate/20130627101548_localize_slugs_of_content_entries.rb +43 -0
  117. data/spec/dummy/config/initializers/locomotive.rb +1 -1
  118. data/spec/dummy/config/initializers/session_store.rb +1 -1
  119. data/spec/lib/locomotive/liquid/drops/page_spec.rb +1 -1
  120. data/spec/lib/locomotive/liquid/filters/misc_spec.rb +26 -0
  121. data/spec/lib/locomotive/liquid/tags/editable/text_spec.rb +85 -0
  122. data/spec/lib/locomotive/liquid/tags/link_to_spec.rb +111 -0
  123. data/spec/lib/locomotive/liquid/tags/with_scope_spec.rb +6 -0
  124. data/spec/models/locomotive/content_entry_spec.rb +27 -7
  125. data/spec/models/locomotive/{editable_short_text_spec.rb → editable_text_spec.rb} +53 -8
  126. data/spec/models/locomotive/extensions/page/editable_elements_spec.rb +6 -6
  127. data/spec/models/locomotive/site_spec.rb +52 -32
  128. data/vendor/assets/images/select2-spinner.gif +0 -0
  129. data/vendor/assets/images/select2.png +0 -0
  130. data/vendor/assets/images/select2x2.png +0 -0
  131. data/vendor/assets/javascripts/locomotive/liquid_mode.js +1 -1
  132. data/vendor/assets/javascripts/select2/select2.js +3054 -0
  133. data/vendor/assets/stylesheets/select2/select2.css.scss +652 -0
  134. metadata +77 -33
  135. data/app/assets/javascripts/locomotive/views/editable_elements/long_text_view.js.coffee +0 -36
  136. data/app/assets/javascripts/locomotive/views/editable_elements/short_text_view.js.coffee +0 -22
  137. data/app/presenters/locomotive/editable_long_text_presenter.rb +0 -5
  138. data/lib/locomotive/liquid/tags/editable/long_text.rb +0 -33
  139. data/lib/locomotive/liquid/tags/editable/short_text.rb +0 -41
  140. data/lib/locomotive/middlewares/fonts.rb +0 -42
  141. data/lib/locomotive/session_store.rb +0 -64
  142. data/spec/lib/locomotive/liquid/tags/editable/short_text_spec.rb +0 -46
  143. data/spec/models/locomotive/editable_long_text_spec.rb +0 -50
@@ -38,5 +38,5 @@ CodeMirror.defineMode("liquid", function(config, parserConfig) {
38
38
  }
39
39
  };
40
40
 
41
- return CodeMirror.overlayParser(CodeMirror.getMode(config, parserConfig.backdrop || "text/html"), liquidOverlay);
41
+ return CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || "text/html"), liquidOverlay);
42
42
  });
@@ -0,0 +1,3054 @@
1
+ /*
2
+ Copyright 2012 Igor Vaynberg
3
+
4
+ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013
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 Licesnse 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
+ $.fn.extend({
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 KEY, AbstractSelect2, SingleSelect2, MultiSelect2, nextUid, sizer,
50
+ lastMousePosition, $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
+ $document = $(document);
102
+
103
+ nextUid=(function() { var counter=1; return function() { return counter++; }; }());
104
+
105
+ function indexOf(value, array) {
106
+ var i = 0, l = array.length;
107
+ for (; i < l; i = i + 1) {
108
+ if (equal(value, array[i])) return i;
109
+ }
110
+ return -1;
111
+ }
112
+
113
+ function measureScrollbar () {
114
+ var $template = $( MEASURE_SCROLLBAR_TEMPLATE );
115
+ $template.appendTo('body');
116
+
117
+ var dim = {
118
+ width: $template.width() - $template[0].clientWidth,
119
+ height: $template.height() - $template[0].clientHeight
120
+ };
121
+ $template.remove();
122
+
123
+ return dim;
124
+ }
125
+
126
+ /**
127
+ * Compares equality of a and b
128
+ * @param a
129
+ * @param b
130
+ */
131
+ function equal(a, b) {
132
+ if (a === b) return true;
133
+ if (a === undefined || b === undefined) return false;
134
+ if (a === null || b === null) return false;
135
+ if (a.constructor === String) return a+'' === b+''; // IE requires a+'' instead of just a
136
+ if (b.constructor === String) return b+'' === a+''; // IE requires b+'' instead of just b
137
+ return false;
138
+ }
139
+
140
+ /**
141
+ * Splits the string into an array of values, trimming each value. An empty array is returned for nulls or empty
142
+ * strings
143
+ * @param string
144
+ * @param separator
145
+ */
146
+ function splitVal(string, separator) {
147
+ var val, i, l;
148
+ if (string === null || string.length < 1) return [];
149
+ val = string.split(separator);
150
+ for (i = 0, l = val.length; i < l; i = i + 1) val[i] = $.trim(val[i]);
151
+ return val;
152
+ }
153
+
154
+ function getSideBorderPadding(element) {
155
+ return element.outerWidth(false) - element.width();
156
+ }
157
+
158
+ function installKeyUpChangeEvent(element) {
159
+ var key="keyup-change-value";
160
+ element.on("keydown", function () {
161
+ if ($.data(element, key) === undefined) {
162
+ $.data(element, key, element.val());
163
+ }
164
+ });
165
+ element.on("keyup", function () {
166
+ var val= $.data(element, key);
167
+ if (val !== undefined && element.val() !== val) {
168
+ $.removeData(element, key);
169
+ element.trigger("keyup-change");
170
+ }
171
+ });
172
+ }
173
+
174
+ $document.on("mousemove", function (e) {
175
+ lastMousePosition = {x: e.pageX, y: e.pageY};
176
+ });
177
+
178
+ /**
179
+ * filters mouse events so an event is fired only if the mouse moved.
180
+ *
181
+ * filters out mouse events that occur when mouse is stationary but
182
+ * the elements under the pointer are scrolled.
183
+ */
184
+ function installFilteredMouseMove(element) {
185
+ element.on("mousemove", function (e) {
186
+ var lastpos = lastMousePosition;
187
+ if (lastpos === undefined || lastpos.x !== e.pageX || lastpos.y !== e.pageY) {
188
+ $(e.target).trigger("mousemove-filtered", e);
189
+ }
190
+ });
191
+ }
192
+
193
+ /**
194
+ * Debounces a function. Returns a function that calls the original fn function only if no invocations have been made
195
+ * within the last quietMillis milliseconds.
196
+ *
197
+ * @param quietMillis number of milliseconds to wait before invoking fn
198
+ * @param fn function to be debounced
199
+ * @param ctx object to be used as this reference within fn
200
+ * @return debounced version of fn
201
+ */
202
+ function debounce(quietMillis, fn, ctx) {
203
+ ctx = ctx || undefined;
204
+ var timeout;
205
+ return function () {
206
+ var args = arguments;
207
+ window.clearTimeout(timeout);
208
+ timeout = window.setTimeout(function() {
209
+ fn.apply(ctx, args);
210
+ }, quietMillis);
211
+ };
212
+ }
213
+
214
+ /**
215
+ * A simple implementation of a thunk
216
+ * @param formula function used to lazily initialize the thunk
217
+ * @return {Function}
218
+ */
219
+ function thunk(formula) {
220
+ var evaluated = false,
221
+ value;
222
+ return function() {
223
+ if (evaluated === false) { value = formula(); evaluated = true; }
224
+ return value;
225
+ };
226
+ };
227
+
228
+ function installDebouncedScroll(threshold, element) {
229
+ var notify = debounce(threshold, function (e) { element.trigger("scroll-debounced", e);});
230
+ element.on("scroll", function (e) {
231
+ if (indexOf(e.target, element.get()) >= 0) notify(e);
232
+ });
233
+ }
234
+
235
+ function focus($el) {
236
+ if ($el[0] === document.activeElement) return;
237
+
238
+ /* set the focus in a 0 timeout - that way the focus is set after the processing
239
+ of the current event has finished - which seems like the only reliable way
240
+ to set focus */
241
+ window.setTimeout(function() {
242
+ var el=$el[0], pos=$el.val().length, range;
243
+
244
+ $el.focus();
245
+
246
+ /* make sure el received focus so we do not error out when trying to manipulate the caret.
247
+ sometimes modals or others listeners may steal it after its set */
248
+ if ($el.is(":visible") && el === document.activeElement) {
249
+
250
+ /* after the focus is set move the caret to the end, necessary when we val()
251
+ just before setting focus */
252
+ if(el.setSelectionRange)
253
+ {
254
+ el.setSelectionRange(pos, pos);
255
+ }
256
+ else if (el.createTextRange) {
257
+ range = el.createTextRange();
258
+ range.collapse(false);
259
+ range.select();
260
+ }
261
+ }
262
+ }, 0);
263
+ }
264
+
265
+ function getCursorInfo(el) {
266
+ el = $(el)[0];
267
+ var offset = 0;
268
+ var length = 0;
269
+ if ('selectionStart' in el) {
270
+ offset = el.selectionStart;
271
+ length = el.selectionEnd - offset;
272
+ } else if ('selection' in document) {
273
+ el.focus();
274
+ var sel = document.selection.createRange();
275
+ length = document.selection.createRange().text.length;
276
+ sel.moveStart('character', -el.value.length);
277
+ offset = sel.text.length - length;
278
+ }
279
+ return { offset: offset, length: length };
280
+ }
281
+
282
+ function killEvent(event) {
283
+ event.preventDefault();
284
+ event.stopPropagation();
285
+ }
286
+ function killEventImmediately(event) {
287
+ event.preventDefault();
288
+ event.stopImmediatePropagation();
289
+ }
290
+
291
+ function measureTextWidth(e) {
292
+ if (!sizer){
293
+ var style = e[0].currentStyle || window.getComputedStyle(e[0], null);
294
+ sizer = $(document.createElement("div")).css({
295
+ position: "absolute",
296
+ left: "-10000px",
297
+ top: "-10000px",
298
+ display: "none",
299
+ fontSize: style.fontSize,
300
+ fontFamily: style.fontFamily,
301
+ fontStyle: style.fontStyle,
302
+ fontWeight: style.fontWeight,
303
+ letterSpacing: style.letterSpacing,
304
+ textTransform: style.textTransform,
305
+ whiteSpace: "nowrap"
306
+ });
307
+ sizer.attr("class","select2-sizer");
308
+ $("body").append(sizer);
309
+ }
310
+ sizer.text(e.val());
311
+ return sizer.width();
312
+ }
313
+
314
+ function syncCssClasses(dest, src, adapter) {
315
+ var classes, replacements = [], adapted;
316
+
317
+ classes = dest.attr("class");
318
+ if (classes) {
319
+ classes = '' + classes; // for IE which returns object
320
+ $(classes.split(" ")).each2(function() {
321
+ if (this.indexOf("select2-") === 0) {
322
+ replacements.push(this);
323
+ }
324
+ });
325
+ }
326
+ classes = src.attr("class");
327
+ if (classes) {
328
+ classes = '' + classes; // for IE which returns object
329
+ $(classes.split(" ")).each2(function() {
330
+ if (this.indexOf("select2-") !== 0) {
331
+ adapted = adapter(this);
332
+ if (adapted) {
333
+ replacements.push(this);
334
+ }
335
+ }
336
+ });
337
+ }
338
+ dest.attr("class", replacements.join(" "));
339
+ }
340
+
341
+
342
+ function markMatch(text, term, markup, escapeMarkup) {
343
+ var match=text.toUpperCase().indexOf(term.toUpperCase()),
344
+ tl=term.length;
345
+
346
+ if (match<0) {
347
+ markup.push(escapeMarkup(text));
348
+ return;
349
+ }
350
+
351
+ markup.push(escapeMarkup(text.substring(0, match)));
352
+ markup.push("<span class='select2-match'>");
353
+ markup.push(escapeMarkup(text.substring(match, match + tl)));
354
+ markup.push("</span>");
355
+ markup.push(escapeMarkup(text.substring(match + tl, text.length)));
356
+ }
357
+
358
+ /**
359
+ * Produces an ajax-based query function
360
+ *
361
+ * @param options object containing configuration paramters
362
+ * @param options.params parameter map for the transport ajax call, can contain such options as cache, jsonpCallback, etc. see $.ajax
363
+ * @param options.transport function that will be used to execute the ajax request. must be compatible with parameters supported by $.ajax
364
+ * @param options.url url for the data
365
+ * @param options.data a function(searchTerm, pageNumber, context) that should return an object containing query string parameters for the above url.
366
+ * @param options.dataType request data type: ajax, jsonp, other datatatypes supported by jQuery's $.ajax function or the transport function if specified
367
+ * @param options.quietMillis (optional) milliseconds to wait before making the ajaxRequest, helps debounce the ajax function if invoked too often
368
+ * @param options.results a function(remoteData, pageNumber) that converts data returned form the remote request to the format expected by Select2.
369
+ * The expected format is an object containing the following keys:
370
+ * results array of objects that will be used as choices
371
+ * more (optional) boolean indicating whether there are more results available
372
+ * Example: {results:[{id:1, text:'Red'},{id:2, text:'Blue'}], more:true}
373
+ */
374
+ function ajax(options) {
375
+ var timeout, // current scheduled but not yet executed request
376
+ requestSequence = 0, // sequence used to drop out-of-order responses
377
+ handler = null,
378
+ quietMillis = options.quietMillis || 100,
379
+ ajaxUrl = options.url,
380
+ self = this;
381
+
382
+ return function (query) {
383
+ window.clearTimeout(timeout);
384
+ timeout = window.setTimeout(function () {
385
+ requestSequence += 1; // increment the sequence
386
+ var requestNumber = requestSequence, // this request's sequence number
387
+ data = options.data, // ajax data function
388
+ url = ajaxUrl, // ajax url string or function
389
+ transport = options.transport || $.fn.select2.ajaxDefaults.transport,
390
+ // deprecated - to be removed in 4.0 - use params instead
391
+ deprecated = {
392
+ type: options.type || 'GET', // set type of request (GET or POST)
393
+ cache: options.cache || false,
394
+ jsonpCallback: options.jsonpCallback||undefined,
395
+ dataType: options.dataType||"json"
396
+ },
397
+ params = $.extend({}, $.fn.select2.ajaxDefaults.params, deprecated);
398
+
399
+ data = data ? data.call(self, query.term, query.page, query.context) : null;
400
+ url = (typeof url === 'function') ? url.call(self, query.term, query.page, query.context) : url;
401
+
402
+ if( null !== handler) { handler.abort(); }
403
+
404
+ if (options.params) {
405
+ if ($.isFunction(options.params)) {
406
+ $.extend(params, options.params.call(self));
407
+ } else {
408
+ $.extend(params, options.params);
409
+ }
410
+ }
411
+
412
+ $.extend(params, {
413
+ url: url,
414
+ dataType: options.dataType,
415
+ data: data,
416
+ success: function (data) {
417
+ if (requestNumber < requestSequence) {
418
+ return;
419
+ }
420
+ // TODO - replace query.page with query so users have access to term, page, etc.
421
+ var results = options.results(data, query.page);
422
+ query.callback(results);
423
+ }
424
+ });
425
+ handler = transport.call(self, params);
426
+ }, quietMillis);
427
+ };
428
+ }
429
+
430
+ /**
431
+ * Produces a query function that works with a local array
432
+ *
433
+ * @param options object containing configuration parameters. The options parameter can either be an array or an
434
+ * object.
435
+ *
436
+ * If the array form is used it is assumed that it contains objects with 'id' and 'text' keys.
437
+ *
438
+ * If the object form is used ti is assumed that it contains 'data' and 'text' keys. The 'data' key should contain
439
+ * an array of objects that will be used as choices. These objects must contain at least an 'id' key. The 'text'
440
+ * key can either be a String in which case it is expected that each element in the 'data' array has a key with the
441
+ * value of 'text' which will be used to match choices. Alternatively, text can be a function(item) that can extract
442
+ * the text.
443
+ */
444
+ function local(options) {
445
+ var data = options, // data elements
446
+ dataText,
447
+ tmp,
448
+ text = function (item) { return ""+item.text; }; // function used to retrieve the text portion of a data item that is matched against the search
449
+
450
+ if ($.isArray(data)) {
451
+ tmp = data;
452
+ data = { results: tmp };
453
+ }
454
+
455
+ if ($.isFunction(data) === false) {
456
+ tmp = data;
457
+ data = function() { return tmp; };
458
+ }
459
+
460
+ var dataItem = data();
461
+ if (dataItem.text) {
462
+ text = dataItem.text;
463
+ // if text is not a function we assume it to be a key name
464
+ if (!$.isFunction(text)) {
465
+ 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
466
+ text = function (item) { return item[dataText]; };
467
+ }
468
+ }
469
+
470
+ return function (query) {
471
+ var t = query.term, filtered = { results: [] }, process;
472
+ if (t === "") {
473
+ query.callback(data());
474
+ return;
475
+ }
476
+
477
+ process = function(datum, collection) {
478
+ var group, attr;
479
+ datum = datum[0];
480
+ if (datum.children) {
481
+ group = {};
482
+ for (attr in datum) {
483
+ if (datum.hasOwnProperty(attr)) group[attr]=datum[attr];
484
+ }
485
+ group.children=[];
486
+ $(datum.children).each2(function(i, childDatum) { process(childDatum, group.children); });
487
+ if (group.children.length || query.matcher(t, text(group), datum)) {
488
+ collection.push(group);
489
+ }
490
+ } else {
491
+ if (query.matcher(t, text(datum), datum)) {
492
+ collection.push(datum);
493
+ }
494
+ }
495
+ };
496
+
497
+ $(data().results).each2(function(i, datum) { process(datum, filtered.results); });
498
+ query.callback(filtered);
499
+ };
500
+ }
501
+
502
+ // TODO javadoc
503
+ function tags(data) {
504
+ var isFunc = $.isFunction(data);
505
+ return function (query) {
506
+ var t = query.term, filtered = {results: []};
507
+ $(isFunc ? data() : data).each(function () {
508
+ var isObject = this.text !== undefined,
509
+ text = isObject ? this.text : this;
510
+ if (t === "" || query.matcher(t, text)) {
511
+ filtered.results.push(isObject ? this : {id: this, text: this});
512
+ }
513
+ });
514
+ query.callback(filtered);
515
+ };
516
+ }
517
+
518
+ /**
519
+ * Checks if the formatter function should be used.
520
+ *
521
+ * Throws an error if it is not a function. Returns true if it should be used,
522
+ * false if no formatting should be performed.
523
+ *
524
+ * @param formatter
525
+ */
526
+ function checkFormatter(formatter, formatterName) {
527
+ if ($.isFunction(formatter)) return true;
528
+ if (!formatter) return false;
529
+ throw new Error("formatterName must be a function or a falsy value");
530
+ }
531
+
532
+ function evaluate(val) {
533
+ return $.isFunction(val) ? val() : val;
534
+ }
535
+
536
+ function countResults(results) {
537
+ var count = 0;
538
+ $.each(results, function(i, item) {
539
+ if (item.children) {
540
+ count += countResults(item.children);
541
+ } else {
542
+ count++;
543
+ }
544
+ });
545
+ return count;
546
+ }
547
+
548
+ /**
549
+ * Default tokenizer. This function uses breaks the input on substring match of any string from the
550
+ * opts.tokenSeparators array and uses opts.createSearchChoice to create the choice object. Both of those
551
+ * two options have to be defined in order for the tokenizer to work.
552
+ *
553
+ * @param input text user has typed so far or pasted into the search field
554
+ * @param selection currently selected choices
555
+ * @param selectCallback function(choice) callback tho add the choice to selection
556
+ * @param opts select2's opts
557
+ * @return undefined/null to leave the current input unchanged, or a string to change the input to the returned value
558
+ */
559
+ function defaultTokenizer(input, selection, selectCallback, opts) {
560
+ var original = input, // store the original so we can compare and know if we need to tell the search to update its text
561
+ dupe = false, // check for whether a token we extracted represents a duplicate selected choice
562
+ token, // token
563
+ index, // position at which the separator was found
564
+ i, l, // looping variables
565
+ separator; // the matched separator
566
+
567
+ if (!opts.createSearchChoice || !opts.tokenSeparators || opts.tokenSeparators.length < 1) return undefined;
568
+
569
+ while (true) {
570
+ index = -1;
571
+
572
+ for (i = 0, l = opts.tokenSeparators.length; i < l; i++) {
573
+ separator = opts.tokenSeparators[i];
574
+ index = input.indexOf(separator);
575
+ if (index >= 0) break;
576
+ }
577
+
578
+ if (index < 0) break; // did not find any token separator in the input string, bail
579
+
580
+ token = input.substring(0, index);
581
+ input = input.substring(index + separator.length);
582
+
583
+ if (token.length > 0) {
584
+ token = opts.createSearchChoice(token, selection);
585
+ if (token !== undefined && token !== null && opts.id(token) !== undefined && opts.id(token) !== null) {
586
+ dupe = false;
587
+ for (i = 0, l = selection.length; i < l; i++) {
588
+ if (equal(opts.id(token), opts.id(selection[i]))) {
589
+ dupe = true; break;
590
+ }
591
+ }
592
+
593
+ if (!dupe) selectCallback(token);
594
+ }
595
+ }
596
+ }
597
+
598
+ if (original!==input) return input;
599
+ }
600
+
601
+ /**
602
+ * Creates a new class
603
+ *
604
+ * @param superClass
605
+ * @param methods
606
+ */
607
+ function clazz(SuperClass, methods) {
608
+ var constructor = function () {};
609
+ constructor.prototype = new SuperClass;
610
+ constructor.prototype.constructor = constructor;
611
+ constructor.prototype.parent = SuperClass.prototype;
612
+ constructor.prototype = $.extend(constructor.prototype, methods);
613
+ return constructor;
614
+ }
615
+
616
+ AbstractSelect2 = clazz(Object, {
617
+
618
+ // abstract
619
+ bind: function (func) {
620
+ var self = this;
621
+ return function () {
622
+ func.apply(self, arguments);
623
+ };
624
+ },
625
+
626
+ // abstract
627
+ init: function (opts) {
628
+ var results, search, resultsSelector = ".select2-results", disabled, readonly;
629
+
630
+ // prepare options
631
+ this.opts = opts = this.prepareOpts(opts);
632
+
633
+ this.id=opts.id;
634
+
635
+ // destroy if called on an existing component
636
+ if (opts.element.data("select2") !== undefined &&
637
+ opts.element.data("select2") !== null) {
638
+ this.destroy();
639
+ }
640
+
641
+ this.container = this.createContainer();
642
+
643
+ this.containerId="s2id_"+(opts.element.attr("id") || "autogen"+nextUid());
644
+ this.containerSelector="#"+this.containerId.replace(/([;&,\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '\\$1');
645
+ this.container.attr("id", this.containerId);
646
+
647
+ // cache the body so future lookups are cheap
648
+ this.body = thunk(function() { return opts.element.closest("body"); });
649
+
650
+ syncCssClasses(this.container, this.opts.element, this.opts.adaptContainerCssClass);
651
+
652
+ this.container.css(evaluate(opts.containerCss));
653
+ this.container.addClass(evaluate(opts.containerCssClass));
654
+
655
+ this.elementTabIndex = this.opts.element.attr("tabindex");
656
+
657
+ // swap container for the element
658
+ this.opts.element
659
+ .data("select2", this)
660
+ .attr("tabindex", "-1")
661
+ .before(this.container);
662
+ this.container.data("select2", this);
663
+
664
+ this.dropdown = this.container.find(".select2-drop");
665
+ this.dropdown.addClass(evaluate(opts.dropdownCssClass));
666
+ this.dropdown.data("select2", this);
667
+
668
+ this.results = results = this.container.find(resultsSelector);
669
+ this.search = search = this.container.find("input.select2-input");
670
+
671
+ this.resultsPage = 0;
672
+ this.context = null;
673
+
674
+ // initialize the container
675
+ this.initContainer();
676
+
677
+ installFilteredMouseMove(this.results);
678
+ this.dropdown.on("mousemove-filtered touchstart touchmove touchend", resultsSelector, this.bind(this.highlightUnderEvent));
679
+
680
+ installDebouncedScroll(80, this.results);
681
+ this.dropdown.on("scroll-debounced", resultsSelector, this.bind(this.loadMoreIfNeeded));
682
+
683
+ // do not propagate change event from the search field out of the component
684
+ $(this.container).on("change", ".select2-input", function(e) {e.stopPropagation();});
685
+ $(this.dropdown).on("change", ".select2-input", function(e) {e.stopPropagation();});
686
+
687
+ // if jquery.mousewheel plugin is installed we can prevent out-of-bounds scrolling of results via mousewheel
688
+ if ($.fn.mousewheel) {
689
+ results.mousewheel(function (e, delta, deltaX, deltaY) {
690
+ var top = results.scrollTop(), height;
691
+ if (deltaY > 0 && top - deltaY <= 0) {
692
+ results.scrollTop(0);
693
+ killEvent(e);
694
+ } else if (deltaY < 0 && results.get(0).scrollHeight - results.scrollTop() + deltaY <= results.height()) {
695
+ results.scrollTop(results.get(0).scrollHeight - results.height());
696
+ killEvent(e);
697
+ }
698
+ });
699
+ }
700
+
701
+ installKeyUpChangeEvent(search);
702
+ search.on("keyup-change input paste", this.bind(this.updateResults));
703
+ search.on("focus", function () { search.addClass("select2-focused"); });
704
+ search.on("blur", function () { search.removeClass("select2-focused");});
705
+
706
+ this.dropdown.on("mouseup", resultsSelector, this.bind(function (e) {
707
+ if ($(e.target).closest(".select2-result-selectable").length > 0) {
708
+ this.highlightUnderEvent(e);
709
+ this.selectHighlighted(e);
710
+ }
711
+ }));
712
+
713
+ // trap all mouse events from leaving the dropdown. sometimes there may be a modal that is listening
714
+ // for mouse events outside of itself so it can close itself. since the dropdown is now outside the select2's
715
+ // dom it will trigger the popup close, which is not what we want
716
+ this.dropdown.on("click mouseup mousedown", function (e) { e.stopPropagation(); });
717
+
718
+ if ($.isFunction(this.opts.initSelection)) {
719
+ // initialize selection based on the current value of the source element
720
+ this.initSelection();
721
+
722
+ // if the user has provided a function that can set selection based on the value of the source element
723
+ // we monitor the change event on the element and trigger it, allowing for two way synchronization
724
+ this.monitorSource();
725
+ }
726
+
727
+ if (opts.maximumInputLength !== null) {
728
+ this.search.attr("maxlength", opts.maximumInputLength);
729
+ }
730
+
731
+ var disabled = opts.element.prop("disabled");
732
+ if (disabled === undefined) disabled = false;
733
+ this.enable(!disabled);
734
+
735
+ var readonly = opts.element.prop("readonly");
736
+ if (readonly === undefined) readonly = false;
737
+ this.readonly(readonly);
738
+
739
+ // Calculate size of scrollbar
740
+ scrollBarDimensions = scrollBarDimensions || measureScrollbar();
741
+
742
+ this.autofocus = opts.element.prop("autofocus")
743
+ opts.element.prop("autofocus", false);
744
+ if (this.autofocus) this.focus();
745
+ },
746
+
747
+ // abstract
748
+ destroy: function () {
749
+ var select2 = this.opts.element.data("select2");
750
+
751
+ if (this.propertyObserver) { delete this.propertyObserver; this.propertyObserver = null; }
752
+
753
+ if (select2 !== undefined) {
754
+
755
+ select2.container.remove();
756
+ select2.dropdown.remove();
757
+ select2.opts.element
758
+ .removeClass("select2-offscreen")
759
+ .removeData("select2")
760
+ .off(".select2")
761
+ .attr({"tabindex": this.elementTabIndex})
762
+ .prop("autofocus", this.autofocus||false)
763
+ .show();
764
+ }
765
+ },
766
+
767
+ // abstract
768
+ optionToData: function(element) {
769
+ if (element.is("option")) {
770
+ return {
771
+ id:element.prop("value"),
772
+ text:element.text(),
773
+ element: element.get(),
774
+ css: element.attr("class"),
775
+ disabled: element.prop("disabled"),
776
+ locked: equal(element.attr("locked"), "locked")
777
+ };
778
+ } else if (element.is("optgroup")) {
779
+ return {
780
+ text:element.attr("label"),
781
+ children:[],
782
+ element: element.get(),
783
+ css: element.attr("class")
784
+ };
785
+ }
786
+ },
787
+
788
+ // abstract
789
+ prepareOpts: function (opts) {
790
+ var element, select, idKey, ajaxUrl, self = this;
791
+
792
+ element = opts.element;
793
+
794
+ if (element.get(0).tagName.toLowerCase() === "select") {
795
+ this.select = select = opts.element;
796
+ }
797
+
798
+ if (select) {
799
+ // these options are not allowed when attached to a select because they are picked up off the element itself
800
+ $.each(["id", "multiple", "ajax", "query", "createSearchChoice", "initSelection", "data", "tags"], function () {
801
+ if (this in opts) {
802
+ throw new Error("Option '" + this + "' is not allowed for Select2 when attached to a <select> element.");
803
+ }
804
+ });
805
+ }
806
+
807
+ opts = $.extend({}, {
808
+ populateResults: function(container, results, query) {
809
+ var populate, data, result, children, id=this.opts.id;
810
+
811
+ populate=function(results, container, depth) {
812
+
813
+ var i, l, result, selectable, disabled, compound, node, label, innerContainer, formatted;
814
+
815
+ results = opts.sortResults(results, container, query);
816
+
817
+ for (i = 0, l = results.length; i < l; i = i + 1) {
818
+
819
+ result=results[i];
820
+
821
+ disabled = (result.disabled === true);
822
+ selectable = (!disabled) && (id(result) !== undefined);
823
+
824
+ compound=result.children && result.children.length > 0;
825
+
826
+ node=$("<li></li>");
827
+ node.addClass("select2-results-dept-"+depth);
828
+ node.addClass("select2-result");
829
+ node.addClass(selectable ? "select2-result-selectable" : "select2-result-unselectable");
830
+ if (disabled) { node.addClass("select2-disabled"); }
831
+ if (compound) { node.addClass("select2-result-with-children"); }
832
+ node.addClass(self.opts.formatResultCssClass(result));
833
+
834
+ label=$(document.createElement("div"));
835
+ label.addClass("select2-result-label");
836
+
837
+ formatted=opts.formatResult(result, label, query, self.opts.escapeMarkup);
838
+ if (formatted!==undefined) {
839
+ label.html(formatted);
840
+ }
841
+
842
+ node.append(label);
843
+
844
+ if (compound) {
845
+
846
+ innerContainer=$("<ul></ul>");
847
+ innerContainer.addClass("select2-result-sub");
848
+ populate(result.children, innerContainer, depth+1);
849
+ node.append(innerContainer);
850
+ }
851
+
852
+ node.data("select2-data", result);
853
+ container.append(node);
854
+ }
855
+ };
856
+
857
+ populate(results, container, 0);
858
+ }
859
+ }, $.fn.select2.defaults, opts);
860
+
861
+ if (typeof(opts.id) !== "function") {
862
+ idKey = opts.id;
863
+ opts.id = function (e) { return e[idKey]; };
864
+ }
865
+
866
+ if ($.isArray(opts.element.data("select2Tags"))) {
867
+ if ("tags" in opts) {
868
+ throw "tags specified as both an attribute 'data-select2-tags' and in options of Select2 " + opts.element.attr("id");
869
+ }
870
+ opts.tags=opts.element.data("select2Tags");
871
+ }
872
+
873
+ if (select) {
874
+ opts.query = this.bind(function (query) {
875
+ var data = { results: [], more: false },
876
+ term = query.term,
877
+ children, firstChild, process;
878
+
879
+ process=function(element, collection) {
880
+ var group;
881
+ if (element.is("option")) {
882
+ if (query.matcher(term, element.text(), element)) {
883
+ collection.push(self.optionToData(element));
884
+ }
885
+ } else if (element.is("optgroup")) {
886
+ group=self.optionToData(element);
887
+ element.children().each2(function(i, elm) { process(elm, group.children); });
888
+ if (group.children.length>0) {
889
+ collection.push(group);
890
+ }
891
+ }
892
+ };
893
+
894
+ children=element.children();
895
+
896
+ // ignore the placeholder option if there is one
897
+ if (this.getPlaceholder() !== undefined && children.length > 0) {
898
+ firstChild = children[0];
899
+ if ($(firstChild).text() === "") {
900
+ children=children.not(firstChild);
901
+ }
902
+ }
903
+
904
+ children.each2(function(i, elm) { process(elm, data.results); });
905
+
906
+ query.callback(data);
907
+ });
908
+ // this is needed because inside val() we construct choices from options and there id is hardcoded
909
+ opts.id=function(e) { return e.id; };
910
+ opts.formatResultCssClass = function(data) { return data.css; };
911
+ } else {
912
+ if (!("query" in opts)) {
913
+
914
+ if ("ajax" in opts) {
915
+ ajaxUrl = opts.element.data("ajax-url");
916
+ if (ajaxUrl && ajaxUrl.length > 0) {
917
+ opts.ajax.url = ajaxUrl;
918
+ }
919
+ opts.query = ajax.call(opts.element, opts.ajax);
920
+ } else if ("data" in opts) {
921
+ opts.query = local(opts.data);
922
+ } else if ("tags" in opts) {
923
+ opts.query = tags(opts.tags);
924
+ if (opts.createSearchChoice === undefined) {
925
+ opts.createSearchChoice = function (term) { return {id: term, text: term}; };
926
+ }
927
+ if (opts.initSelection === undefined) {
928
+ opts.initSelection = function (element, callback) {
929
+ var data = [];
930
+ $(splitVal(element.val(), opts.separator)).each(function () {
931
+ var id = this, text = this, tags=opts.tags;
932
+ if ($.isFunction(tags)) tags=tags();
933
+ $(tags).each(function() { if (equal(this.id, id)) { text = this.text; return false; } });
934
+ data.push({id: id, text: text});
935
+ });
936
+
937
+ callback(data);
938
+ };
939
+ }
940
+ }
941
+ }
942
+ }
943
+ if (typeof(opts.query) !== "function") {
944
+ throw "query function not defined for Select2 " + opts.element.attr("id");
945
+ }
946
+
947
+ return opts;
948
+ },
949
+
950
+ /**
951
+ * Monitor the original element for changes and update select2 accordingly
952
+ */
953
+ // abstract
954
+ monitorSource: function () {
955
+ var el = this.opts.element, sync;
956
+
957
+ el.on("change.select2", this.bind(function (e) {
958
+ if (this.opts.element.data("select2-change-triggered") !== true) {
959
+ this.initSelection();
960
+ }
961
+ }));
962
+
963
+ sync = this.bind(function () {
964
+
965
+ var enabled, readonly, self = this;
966
+
967
+ // sync enabled state
968
+
969
+ var disabled = el.prop("disabled");
970
+ if (disabled === undefined) disabled = false;
971
+ this.enable(!disabled);
972
+
973
+ var readonly = el.prop("readonly");
974
+ if (readonly === undefined) readonly = false;
975
+ this.readonly(readonly);
976
+
977
+ syncCssClasses(this.container, this.opts.element, this.opts.adaptContainerCssClass);
978
+ this.container.addClass(evaluate(this.opts.containerCssClass));
979
+
980
+ syncCssClasses(this.dropdown, this.opts.element, this.opts.adaptDropdownCssClass);
981
+ this.dropdown.addClass(evaluate(this.opts.dropdownCssClass));
982
+
983
+ });
984
+
985
+ // mozilla and IE
986
+ el.on("propertychange.select2 DOMAttrModified.select2", sync);
987
+
988
+
989
+ // hold onto a reference of the callback to work around a chromium bug
990
+ if (this.mutationCallback === undefined) {
991
+ this.mutationCallback = function (mutations) {
992
+ mutations.forEach(sync);
993
+ }
994
+ }
995
+
996
+ // safari and chrome
997
+ if (typeof WebKitMutationObserver !== "undefined") {
998
+ if (this.propertyObserver) { delete this.propertyObserver; this.propertyObserver = null; }
999
+ this.propertyObserver = new WebKitMutationObserver(this.mutationCallback);
1000
+ this.propertyObserver.observe(el.get(0), { attributes:true, subtree:false });
1001
+ }
1002
+ },
1003
+
1004
+ // abstract
1005
+ triggerSelect: function(data) {
1006
+ var evt = $.Event("select2-selecting", { val: this.id(data), object: data });
1007
+ this.opts.element.trigger(evt);
1008
+ return !evt.isDefaultPrevented();
1009
+ },
1010
+
1011
+ /**
1012
+ * Triggers the change event on the source element
1013
+ */
1014
+ // abstract
1015
+ triggerChange: function (details) {
1016
+
1017
+ details = details || {};
1018
+ details= $.extend({}, details, { type: "change", val: this.val() });
1019
+ // prevents recursive triggering
1020
+ this.opts.element.data("select2-change-triggered", true);
1021
+ this.opts.element.trigger(details);
1022
+ this.opts.element.data("select2-change-triggered", false);
1023
+
1024
+ // some validation frameworks ignore the change event and listen instead to keyup, click for selects
1025
+ // so here we trigger the click event manually
1026
+ this.opts.element.click();
1027
+
1028
+ // ValidationEngine ignorea the change event and listens instead to blur
1029
+ // so here we trigger the blur event manually if so desired
1030
+ if (this.opts.blurOnChange)
1031
+ this.opts.element.blur();
1032
+ },
1033
+
1034
+ //abstract
1035
+ isInterfaceEnabled: function()
1036
+ {
1037
+ return this.enabledInterface === true;
1038
+ },
1039
+
1040
+ // abstract
1041
+ enableInterface: function() {
1042
+ var enabled = this._enabled && !this._readonly,
1043
+ disabled = !enabled;
1044
+
1045
+ if (enabled === this.enabledInterface) return false;
1046
+
1047
+ this.container.toggleClass("select2-container-disabled", disabled);
1048
+ this.close();
1049
+ this.enabledInterface = enabled;
1050
+
1051
+ return true;
1052
+ },
1053
+
1054
+ // abstract
1055
+ enable: function(enabled) {
1056
+ if (enabled === undefined) enabled = true;
1057
+ if (this._enabled === enabled) return false;
1058
+ this._enabled = enabled;
1059
+
1060
+ this.opts.element.prop("disabled", !enabled);
1061
+ this.enableInterface();
1062
+ return true;
1063
+ },
1064
+
1065
+ // abstract
1066
+ readonly: function(enabled) {
1067
+ if (enabled === undefined) enabled = false;
1068
+ if (this._readonly === enabled) return false;
1069
+ this._readonly = enabled;
1070
+
1071
+ this.opts.element.prop("readonly", enabled);
1072
+ this.enableInterface();
1073
+ return true;
1074
+ },
1075
+
1076
+ // abstract
1077
+ opened: function () {
1078
+ return this.container.hasClass("select2-dropdown-open");
1079
+ },
1080
+
1081
+ // abstract
1082
+ positionDropdown: function() {
1083
+ var $dropdown = this.dropdown,
1084
+ offset = this.container.offset(),
1085
+ height = this.container.outerHeight(false),
1086
+ width = this.container.outerWidth(false),
1087
+ dropHeight = $dropdown.outerHeight(false),
1088
+ viewPortRight = $(window).scrollLeft() + $(window).width(),
1089
+ viewportBottom = $(window).scrollTop() + $(window).height(),
1090
+ dropTop = offset.top + height,
1091
+ dropLeft = offset.left,
1092
+ enoughRoomBelow = dropTop + dropHeight <= viewportBottom,
1093
+ enoughRoomAbove = (offset.top - dropHeight) >= this.body().scrollTop(),
1094
+ dropWidth = $dropdown.outerWidth(false),
1095
+ enoughRoomOnRight = dropLeft + dropWidth <= viewPortRight,
1096
+ aboveNow = $dropdown.hasClass("select2-drop-above"),
1097
+ bodyOffset,
1098
+ above,
1099
+ css,
1100
+ resultsListNode;
1101
+
1102
+ if (this.opts.dropdownAutoWidth) {
1103
+ resultsListNode = $('.select2-results', $dropdown)[0];
1104
+ $dropdown.addClass('select2-drop-auto-width');
1105
+ $dropdown.css('width', '');
1106
+ // Add scrollbar width to dropdown if vertical scrollbar is present
1107
+ dropWidth = $dropdown.outerWidth(false) + (resultsListNode.scrollHeight === resultsListNode.clientHeight ? 0 : scrollBarDimensions.width);
1108
+ dropWidth > width ? width = dropWidth : dropWidth = width;
1109
+ enoughRoomOnRight = dropLeft + dropWidth <= viewPortRight;
1110
+ }
1111
+ else {
1112
+ this.container.removeClass('select2-drop-auto-width');
1113
+ }
1114
+
1115
+ //console.log("below/ droptop:", dropTop, "dropHeight", dropHeight, "sum", (dropTop+dropHeight)+" viewport bottom", viewportBottom, "enough?", enoughRoomBelow);
1116
+ //console.log("above/ offset.top", offset.top, "dropHeight", dropHeight, "top", (offset.top-dropHeight), "scrollTop", this.body().scrollTop(), "enough?", enoughRoomAbove);
1117
+
1118
+ // fix positioning when body has an offset and is not position: static
1119
+
1120
+ if (this.body().css('position') !== 'static') {
1121
+ bodyOffset = this.body().offset();
1122
+ dropTop -= bodyOffset.top;
1123
+ dropLeft -= bodyOffset.left;
1124
+ }
1125
+
1126
+ // always prefer the current above/below alignment, unless there is not enough room
1127
+
1128
+ if (aboveNow) {
1129
+ above = true;
1130
+ if (!enoughRoomAbove && enoughRoomBelow) above = false;
1131
+ } else {
1132
+ above = false;
1133
+ if (!enoughRoomBelow && enoughRoomAbove) above = true;
1134
+ }
1135
+
1136
+ if (!enoughRoomOnRight) {
1137
+ dropLeft = offset.left + width - dropWidth;
1138
+ }
1139
+
1140
+ if (above) {
1141
+ dropTop = offset.top - dropHeight;
1142
+ this.container.addClass("select2-drop-above");
1143
+ $dropdown.addClass("select2-drop-above");
1144
+ }
1145
+ else {
1146
+ this.container.removeClass("select2-drop-above");
1147
+ $dropdown.removeClass("select2-drop-above");
1148
+ }
1149
+
1150
+ css = $.extend({
1151
+ top: dropTop,
1152
+ left: dropLeft,
1153
+ width: width
1154
+ }, evaluate(this.opts.dropdownCss));
1155
+
1156
+ $dropdown.css(css);
1157
+ },
1158
+
1159
+ // abstract
1160
+ shouldOpen: function() {
1161
+ var event;
1162
+
1163
+ if (this.opened()) return false;
1164
+
1165
+ if (this._enabled === false || this._readonly === true) return false;
1166
+
1167
+ event = $.Event("select2-opening");
1168
+ this.opts.element.trigger(event);
1169
+ return !event.isDefaultPrevented();
1170
+ },
1171
+
1172
+ // abstract
1173
+ clearDropdownAlignmentPreference: function() {
1174
+ // clear the classes used to figure out the preference of where the dropdown should be opened
1175
+ this.container.removeClass("select2-drop-above");
1176
+ this.dropdown.removeClass("select2-drop-above");
1177
+ },
1178
+
1179
+ /**
1180
+ * Opens the dropdown
1181
+ *
1182
+ * @return {Boolean} whether or not dropdown was opened. This method will return false if, for example,
1183
+ * the dropdown is already open, or if the 'open' event listener on the element called preventDefault().
1184
+ */
1185
+ // abstract
1186
+ open: function () {
1187
+
1188
+ if (!this.shouldOpen()) return false;
1189
+
1190
+ this.opening();
1191
+
1192
+ return true;
1193
+ },
1194
+
1195
+ /**
1196
+ * Performs the opening of the dropdown
1197
+ */
1198
+ // abstract
1199
+ opening: function() {
1200
+ var cid = this.containerId,
1201
+ scroll = "scroll." + cid,
1202
+ resize = "resize."+cid,
1203
+ orient = "orientationchange."+cid,
1204
+ mask;
1205
+
1206
+ this.container.addClass("select2-dropdown-open").addClass("select2-container-active");
1207
+
1208
+ this.clearDropdownAlignmentPreference();
1209
+
1210
+ if(this.dropdown[0] !== this.body().children().last()[0]) {
1211
+ this.dropdown.detach().appendTo(this.body());
1212
+ }
1213
+
1214
+ // create the dropdown mask if doesnt already exist
1215
+ mask = $("#select2-drop-mask");
1216
+ if (mask.length == 0) {
1217
+ mask = $(document.createElement("div"));
1218
+ mask.attr("id","select2-drop-mask").attr("class","select2-drop-mask");
1219
+ mask.hide();
1220
+ mask.appendTo(this.body());
1221
+ mask.on("mousedown touchstart", function (e) {
1222
+ var dropdown = $("#select2-drop"), self;
1223
+ if (dropdown.length > 0) {
1224
+ self=dropdown.data("select2");
1225
+ if (self.opts.selectOnBlur) {
1226
+ self.selectHighlighted({noFocus: true});
1227
+ }
1228
+ self.close();
1229
+ e.preventDefault();
1230
+ e.stopPropagation();
1231
+ }
1232
+ });
1233
+ }
1234
+
1235
+ // ensure the mask is always right before the dropdown
1236
+ if (this.dropdown.prev()[0] !== mask[0]) {
1237
+ this.dropdown.before(mask);
1238
+ }
1239
+
1240
+ // move the global id to the correct dropdown
1241
+ $("#select2-drop").removeAttr("id");
1242
+ this.dropdown.attr("id", "select2-drop");
1243
+
1244
+ // show the elements
1245
+ mask.css(_makeMaskCss());
1246
+ mask.show();
1247
+ this.dropdown.show();
1248
+ this.positionDropdown();
1249
+
1250
+ this.dropdown.addClass("select2-drop-active");
1251
+ this.ensureHighlightVisible();
1252
+
1253
+ // attach listeners to events that can change the position of the container and thus require
1254
+ // the position of the dropdown to be updated as well so it does not come unglued from the container
1255
+ var that = this;
1256
+ this.container.parents().add(window).each(function () {
1257
+ $(this).on(resize+" "+scroll+" "+orient, function (e) {
1258
+ $("#select2-drop-mask").css(_makeMaskCss());
1259
+ that.positionDropdown();
1260
+ });
1261
+ });
1262
+
1263
+ function _makeMaskCss() {
1264
+ return {
1265
+ width : Math.max(document.documentElement.scrollWidth, $(window).width()),
1266
+ height : Math.max(document.documentElement.scrollHeight, $(window).height())
1267
+ }
1268
+ }
1269
+ },
1270
+
1271
+ // abstract
1272
+ close: function () {
1273
+ if (!this.opened()) return;
1274
+
1275
+ var cid = this.containerId,
1276
+ scroll = "scroll." + cid,
1277
+ resize = "resize."+cid,
1278
+ orient = "orientationchange."+cid;
1279
+
1280
+ // unbind event listeners
1281
+ this.container.parents().add(window).each(function () { $(this).off(scroll).off(resize).off(orient); });
1282
+
1283
+ this.clearDropdownAlignmentPreference();
1284
+
1285
+ $("#select2-drop-mask").hide();
1286
+ this.dropdown.removeAttr("id"); // only the active dropdown has the select2-drop id
1287
+ this.dropdown.hide();
1288
+ this.container.removeClass("select2-dropdown-open");
1289
+ this.results.empty();
1290
+
1291
+
1292
+ this.clearSearch();
1293
+ this.search.removeClass("select2-active");
1294
+ this.opts.element.trigger($.Event("select2-close"));
1295
+ },
1296
+
1297
+ // abstract
1298
+ clearSearch: function () {
1299
+
1300
+ },
1301
+
1302
+ //abstract
1303
+ getMaximumSelectionSize: function() {
1304
+ return evaluate(this.opts.maximumSelectionSize);
1305
+ },
1306
+
1307
+ // abstract
1308
+ ensureHighlightVisible: function () {
1309
+ var results = this.results, children, index, child, hb, rb, y, more;
1310
+
1311
+ index = this.highlight();
1312
+
1313
+ if (index < 0) return;
1314
+
1315
+ if (index == 0) {
1316
+
1317
+ // if the first element is highlighted scroll all the way to the top,
1318
+ // that way any unselectable headers above it will also be scrolled
1319
+ // into view
1320
+
1321
+ results.scrollTop(0);
1322
+ return;
1323
+ }
1324
+
1325
+ children = this.findHighlightableChoices().find('.select2-result-label');
1326
+
1327
+ child = $(children[index]);
1328
+
1329
+ hb = child.offset().top + child.outerHeight(true);
1330
+
1331
+ // if this is the last child lets also make sure select2-more-results is visible
1332
+ if (index === children.length - 1) {
1333
+ more = results.find("li.select2-more-results");
1334
+ if (more.length > 0) {
1335
+ hb = more.offset().top + more.outerHeight(true);
1336
+ }
1337
+ }
1338
+
1339
+ rb = results.offset().top + results.outerHeight(true);
1340
+ if (hb > rb) {
1341
+ results.scrollTop(results.scrollTop() + (hb - rb));
1342
+ }
1343
+ y = child.offset().top - results.offset().top;
1344
+
1345
+ // make sure the top of the element is visible
1346
+ if (y < 0 && child.css('display') != 'none' ) {
1347
+ results.scrollTop(results.scrollTop() + y); // y is negative
1348
+ }
1349
+ },
1350
+
1351
+ // abstract
1352
+ findHighlightableChoices: function() {
1353
+ return this.results.find(".select2-result-selectable:not(.select2-selected):not(.select2-disabled)");
1354
+ },
1355
+
1356
+ // abstract
1357
+ moveHighlight: function (delta) {
1358
+ var choices = this.findHighlightableChoices(),
1359
+ index = this.highlight();
1360
+
1361
+ while (index > -1 && index < choices.length) {
1362
+ index += delta;
1363
+ var choice = $(choices[index]);
1364
+ if (choice.hasClass("select2-result-selectable") && !choice.hasClass("select2-disabled") && !choice.hasClass("select2-selected")) {
1365
+ this.highlight(index);
1366
+ break;
1367
+ }
1368
+ }
1369
+ },
1370
+
1371
+ // abstract
1372
+ highlight: function (index) {
1373
+ var choices = this.findHighlightableChoices(),
1374
+ choice,
1375
+ data;
1376
+
1377
+ if (arguments.length === 0) {
1378
+ return indexOf(choices.filter(".select2-highlighted")[0], choices.get());
1379
+ }
1380
+
1381
+ if (index >= choices.length) index = choices.length - 1;
1382
+ if (index < 0) index = 0;
1383
+
1384
+ this.results.find(".select2-highlighted").removeClass("select2-highlighted");
1385
+
1386
+ choice = $(choices[index]);
1387
+ choice.addClass("select2-highlighted");
1388
+
1389
+ this.ensureHighlightVisible();
1390
+
1391
+ data = choice.data("select2-data");
1392
+ if (data) {
1393
+ this.opts.element.trigger({ type: "select2-highlight", val: this.id(data), choice: data });
1394
+ }
1395
+ },
1396
+
1397
+ // abstract
1398
+ countSelectableResults: function() {
1399
+ return this.findHighlightableChoices().length;
1400
+ },
1401
+
1402
+ // abstract
1403
+ highlightUnderEvent: function (event) {
1404
+ var el = $(event.target).closest(".select2-result-selectable");
1405
+ if (el.length > 0 && !el.is(".select2-highlighted")) {
1406
+ var choices = this.findHighlightableChoices();
1407
+ this.highlight(choices.index(el));
1408
+ } else if (el.length == 0) {
1409
+ // if we are over an unselectable item remove al highlights
1410
+ this.results.find(".select2-highlighted").removeClass("select2-highlighted");
1411
+ }
1412
+ },
1413
+
1414
+ // abstract
1415
+ loadMoreIfNeeded: function () {
1416
+ var results = this.results,
1417
+ more = results.find("li.select2-more-results"),
1418
+ below, // pixels the element is below the scroll fold, below==0 is when the element is starting to be visible
1419
+ offset = -1, // index of first element without data
1420
+ page = this.resultsPage + 1,
1421
+ self=this,
1422
+ term=this.search.val(),
1423
+ context=this.context;
1424
+
1425
+ if (more.length === 0) return;
1426
+ below = more.offset().top - results.offset().top - results.height();
1427
+
1428
+ if (below <= this.opts.loadMorePadding) {
1429
+ more.addClass("select2-active");
1430
+ this.opts.query({
1431
+ element: this.opts.element,
1432
+ term: term,
1433
+ page: page,
1434
+ context: context,
1435
+ matcher: this.opts.matcher,
1436
+ callback: this.bind(function (data) {
1437
+
1438
+ // ignore a response if the select2 has been closed before it was received
1439
+ if (!self.opened()) return;
1440
+
1441
+
1442
+ self.opts.populateResults.call(this, results, data.results, {term: term, page: page, context:context});
1443
+ self.postprocessResults(data, false, false);
1444
+
1445
+ if (data.more===true) {
1446
+ more.detach().appendTo(results).text(self.opts.formatLoadMore(page+1));
1447
+ window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10);
1448
+ } else {
1449
+ more.remove();
1450
+ }
1451
+ self.positionDropdown();
1452
+ self.resultsPage = page;
1453
+ self.context = data.context;
1454
+ })});
1455
+ }
1456
+ },
1457
+
1458
+ /**
1459
+ * Default tokenizer function which does nothing
1460
+ */
1461
+ tokenize: function() {
1462
+
1463
+ },
1464
+
1465
+ /**
1466
+ * @param initial whether or not this is the call to this method right after the dropdown has been opened
1467
+ */
1468
+ // abstract
1469
+ updateResults: function (initial) {
1470
+ var search = this.search,
1471
+ results = this.results,
1472
+ opts = this.opts,
1473
+ data,
1474
+ self = this,
1475
+ input,
1476
+ term = search.val(),
1477
+ lastTerm=$.data(this.container, "select2-last-term");
1478
+
1479
+ // prevent duplicate queries against the same term
1480
+ if (initial !== true && lastTerm && equal(term, lastTerm)) return;
1481
+
1482
+ $.data(this.container, "select2-last-term", term);
1483
+
1484
+ // if the search is currently hidden we do not alter the results
1485
+ if (initial !== true && (this.showSearchInput === false || !this.opened())) {
1486
+ return;
1487
+ }
1488
+
1489
+ function postRender() {
1490
+ results.scrollTop(0);
1491
+ search.removeClass("select2-active");
1492
+ self.positionDropdown();
1493
+ }
1494
+
1495
+ function render(html) {
1496
+ results.html(html);
1497
+ postRender();
1498
+ }
1499
+
1500
+ var maxSelSize = this.getMaximumSelectionSize();
1501
+ if (maxSelSize >=1) {
1502
+ data = this.data();
1503
+ if ($.isArray(data) && data.length >= maxSelSize && checkFormatter(opts.formatSelectionTooBig, "formatSelectionTooBig")) {
1504
+ render("<li class='select2-selection-limit'>" + opts.formatSelectionTooBig(maxSelSize) + "</li>");
1505
+ return;
1506
+ }
1507
+ }
1508
+
1509
+ if (search.val().length < opts.minimumInputLength) {
1510
+ if (checkFormatter(opts.formatInputTooShort, "formatInputTooShort")) {
1511
+ render("<li class='select2-no-results'>" + opts.formatInputTooShort(search.val(), opts.minimumInputLength) + "</li>");
1512
+ } else {
1513
+ render("");
1514
+ }
1515
+ if (initial) this.showSearch(true);
1516
+ return;
1517
+ }
1518
+
1519
+ if (opts.maximumInputLength && search.val().length > opts.maximumInputLength) {
1520
+ if (checkFormatter(opts.formatInputTooLong, "formatInputTooLong")) {
1521
+ render("<li class='select2-no-results'>" + opts.formatInputTooLong(search.val(), opts.maximumInputLength) + "</li>");
1522
+ } else {
1523
+ render("");
1524
+ }
1525
+ return;
1526
+ }
1527
+
1528
+ if (opts.formatSearching && this.findHighlightableChoices().length === 0) {
1529
+ render("<li class='select2-searching'>" + opts.formatSearching() + "</li>");
1530
+ }
1531
+
1532
+ search.addClass("select2-active");
1533
+
1534
+ // give the tokenizer a chance to pre-process the input
1535
+ input = this.tokenize();
1536
+ if (input != undefined && input != null) {
1537
+ search.val(input);
1538
+ }
1539
+
1540
+ this.resultsPage = 1;
1541
+
1542
+ opts.query({
1543
+ element: opts.element,
1544
+ term: search.val(),
1545
+ page: this.resultsPage,
1546
+ context: null,
1547
+ matcher: opts.matcher,
1548
+ callback: this.bind(function (data) {
1549
+ var def; // default choice
1550
+
1551
+ // ignore a response if the select2 has been closed before it was received
1552
+ if (!this.opened()) {
1553
+ this.search.removeClass("select2-active");
1554
+ return;
1555
+ }
1556
+
1557
+ // save context, if any
1558
+ this.context = (data.context===undefined) ? null : data.context;
1559
+ // create a default choice and prepend it to the list
1560
+ if (this.opts.createSearchChoice && search.val() !== "") {
1561
+ def = this.opts.createSearchChoice.call(null, search.val(), data.results);
1562
+ if (def !== undefined && def !== null && self.id(def) !== undefined && self.id(def) !== null) {
1563
+ if ($(data.results).filter(
1564
+ function () {
1565
+ return equal(self.id(this), self.id(def));
1566
+ }).length === 0) {
1567
+ data.results.unshift(def);
1568
+ }
1569
+ }
1570
+ }
1571
+
1572
+ if (data.results.length === 0 && checkFormatter(opts.formatNoMatches, "formatNoMatches")) {
1573
+ render("<li class='select2-no-results'>" + opts.formatNoMatches(search.val()) + "</li>");
1574
+ return;
1575
+ }
1576
+
1577
+ results.empty();
1578
+ self.opts.populateResults.call(this, results, data.results, {term: search.val(), page: this.resultsPage, context:null});
1579
+
1580
+ if (data.more === true && checkFormatter(opts.formatLoadMore, "formatLoadMore")) {
1581
+ results.append("<li class='select2-more-results'>" + self.opts.escapeMarkup(opts.formatLoadMore(this.resultsPage)) + "</li>");
1582
+ window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10);
1583
+ }
1584
+
1585
+ this.postprocessResults(data, initial);
1586
+
1587
+ postRender();
1588
+
1589
+ this.opts.element.trigger({ type: "select2-loaded", data:data });
1590
+ })});
1591
+ },
1592
+
1593
+ // abstract
1594
+ cancel: function () {
1595
+ this.close();
1596
+ },
1597
+
1598
+ // abstract
1599
+ blur: function () {
1600
+ // if selectOnBlur == true, select the currently highlighted option
1601
+ if (this.opts.selectOnBlur)
1602
+ this.selectHighlighted({noFocus: true});
1603
+
1604
+ this.close();
1605
+ this.container.removeClass("select2-container-active");
1606
+ // synonymous to .is(':focus'), which is available in jquery >= 1.6
1607
+ if (this.search[0] === document.activeElement) { this.search.blur(); }
1608
+ this.clearSearch();
1609
+ this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
1610
+ },
1611
+
1612
+ // abstract
1613
+ focusSearch: function () {
1614
+ focus(this.search);
1615
+ },
1616
+
1617
+ // abstract
1618
+ selectHighlighted: function (options) {
1619
+ var index=this.highlight(),
1620
+ highlighted=this.results.find(".select2-highlighted"),
1621
+ data = highlighted.closest('.select2-result').data("select2-data");
1622
+
1623
+ if (data) {
1624
+ this.highlight(index);
1625
+ this.onSelect(data, options);
1626
+ }
1627
+ },
1628
+
1629
+ // abstract
1630
+ getPlaceholder: function () {
1631
+ return this.opts.element.attr("placeholder") ||
1632
+ this.opts.element.attr("data-placeholder") || // jquery 1.4 compat
1633
+ this.opts.element.data("placeholder") ||
1634
+ this.opts.placeholder;
1635
+ },
1636
+
1637
+ /**
1638
+ * Get the desired width for the container element. This is
1639
+ * derived first from option `width` passed to select2, then
1640
+ * the inline 'style' on the original element, and finally
1641
+ * falls back to the jQuery calculated element width.
1642
+ */
1643
+ // abstract
1644
+ initContainerWidth: function () {
1645
+ function resolveContainerWidth() {
1646
+ var style, attrs, matches, i, l;
1647
+
1648
+ if (this.opts.width === "off") {
1649
+ return null;
1650
+ } else if (this.opts.width === "element"){
1651
+ return this.opts.element.outerWidth(false) === 0 ? 'auto' : this.opts.element.outerWidth(false) + 'px';
1652
+ } else if (this.opts.width === "copy" || this.opts.width === "resolve") {
1653
+ // check if there is inline style on the element that contains width
1654
+ style = this.opts.element.attr('style');
1655
+ if (style !== undefined) {
1656
+ attrs = style.split(';');
1657
+ for (i = 0, l = attrs.length; i < l; i = i + 1) {
1658
+ matches = attrs[i].replace(/\s/g, '')
1659
+ .match(/width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i);
1660
+ if (matches !== null && matches.length >= 1)
1661
+ return matches[1];
1662
+ }
1663
+ }
1664
+
1665
+ // next check if css('width') can resolve a width that is percent based, this is sometimes possible
1666
+ // when attached to input type=hidden or elements hidden via css
1667
+ style = this.opts.element.css('width');
1668
+ if (style && style.length > 0) return style;
1669
+
1670
+ if (this.opts.width === "resolve") {
1671
+ // finally, fallback on the calculated width of the element
1672
+ return (this.opts.element.outerWidth(false) === 0 ? 'auto' : this.opts.element.outerWidth(false) + 'px');
1673
+ }
1674
+
1675
+ return null;
1676
+ } else if ($.isFunction(this.opts.width)) {
1677
+ return this.opts.width();
1678
+ } else {
1679
+ return this.opts.width;
1680
+ }
1681
+ };
1682
+
1683
+ var width = resolveContainerWidth.call(this);
1684
+ if (width !== null) {
1685
+ this.container.css("width", width);
1686
+ }
1687
+ }
1688
+ });
1689
+
1690
+ SingleSelect2 = clazz(AbstractSelect2, {
1691
+
1692
+ // single
1693
+
1694
+ createContainer: function () {
1695
+ var container = $(document.createElement("div")).attr({
1696
+ "class": "select2-container"
1697
+ }).html([
1698
+ "<a href='javascript:void(0)' onclick='return false;' class='select2-choice' tabindex='-1'>",
1699
+ " <span>&nbsp;</span><abbr class='select2-search-choice-close'></abbr>",
1700
+ " <div><b></b></div>" ,
1701
+ "</a>",
1702
+ "<input class='select2-focusser select2-offscreen' type='text'/>",
1703
+ "<div class='select2-drop select2-display-none'>" ,
1704
+ " <div class='select2-search'>" ,
1705
+ " <input type='text' autocomplete='off' autocorrect='off' autocapitilize='off' spellcheck='false' class='select2-input'/>" ,
1706
+ " </div>" ,
1707
+ " <ul class='select2-results'>" ,
1708
+ " </ul>" ,
1709
+ "</div>"].join(""));
1710
+ return container;
1711
+ },
1712
+
1713
+ // single
1714
+ enableInterface: function() {
1715
+ if (this.parent.enableInterface.apply(this, arguments)) {
1716
+ this.focusser.prop("disabled", !this.isInterfaceEnabled());
1717
+ }
1718
+ },
1719
+
1720
+ // single
1721
+ opening: function () {
1722
+ var el, range;
1723
+ this.parent.opening.apply(this, arguments);
1724
+ if (this.showSearchInput !== false) {
1725
+ // IE appends focusser.val() at the end of field :/ so we manually insert it at the beginning using a range
1726
+ // all other browsers handle this just fine
1727
+
1728
+ this.search.val(this.focusser.val());
1729
+ }
1730
+ this.search.focus();
1731
+ // in IE we have to move the cursor to the end after focussing, otherwise it will be at the beginning and
1732
+ // new text will appear *before* focusser.val()
1733
+ el = this.search.get(0);
1734
+ if (el.createTextRange) {
1735
+ range = el.createTextRange();
1736
+ range.collapse(false);
1737
+ range.select();
1738
+ }
1739
+
1740
+ this.focusser.prop("disabled", true).val("");
1741
+ this.updateResults(true);
1742
+ this.opts.element.trigger($.Event("select2-open"));
1743
+ },
1744
+
1745
+ // single
1746
+ close: function () {
1747
+ if (!this.opened()) return;
1748
+ this.parent.close.apply(this, arguments);
1749
+ this.focusser.removeAttr("disabled");
1750
+ this.focusser.focus();
1751
+ },
1752
+
1753
+ // single
1754
+ focus: function () {
1755
+ if (this.opened()) {
1756
+ this.close();
1757
+ } else {
1758
+ this.focusser.removeAttr("disabled");
1759
+ this.focusser.focus();
1760
+ }
1761
+ },
1762
+
1763
+ // single
1764
+ isFocused: function () {
1765
+ return this.container.hasClass("select2-container-active");
1766
+ },
1767
+
1768
+ // single
1769
+ cancel: function () {
1770
+ this.parent.cancel.apply(this, arguments);
1771
+ this.focusser.removeAttr("disabled");
1772
+ this.focusser.focus();
1773
+ },
1774
+
1775
+ // single
1776
+ initContainer: function () {
1777
+
1778
+ var selection,
1779
+ container = this.container,
1780
+ dropdown = this.dropdown;
1781
+
1782
+ this.showSearch(false);
1783
+
1784
+ this.selection = selection = container.find(".select2-choice");
1785
+
1786
+ this.focusser = container.find(".select2-focusser");
1787
+
1788
+ // rewrite labels from original element to focusser
1789
+ this.focusser.attr("id", "s2id_autogen"+nextUid());
1790
+
1791
+ $("label[for='" + this.opts.element.attr("id") + "']")
1792
+ .attr('for', this.focusser.attr('id'));
1793
+
1794
+ this.focusser.attr("tabindex", this.elementTabIndex);
1795
+
1796
+ this.search.on("keydown", this.bind(function (e) {
1797
+ if (!this.isInterfaceEnabled()) return;
1798
+
1799
+ if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) {
1800
+ // prevent the page from scrolling
1801
+ killEvent(e);
1802
+ return;
1803
+ }
1804
+
1805
+ switch (e.which) {
1806
+ case KEY.UP:
1807
+ case KEY.DOWN:
1808
+ this.moveHighlight((e.which === KEY.UP) ? -1 : 1);
1809
+ killEvent(e);
1810
+ return;
1811
+ case KEY.ENTER:
1812
+ this.selectHighlighted();
1813
+ killEvent(e);
1814
+ return;
1815
+ case KEY.TAB:
1816
+ this.selectHighlighted({noFocus: true});
1817
+ return;
1818
+ case KEY.ESC:
1819
+ this.cancel(e);
1820
+ killEvent(e);
1821
+ return;
1822
+ }
1823
+ }));
1824
+
1825
+ this.search.on("blur", this.bind(function(e) {
1826
+ // a workaround for chrome to keep the search field focussed when the scroll bar is used to scroll the dropdown.
1827
+ // without this the search field loses focus which is annoying
1828
+ if (document.activeElement === this.body().get(0)) {
1829
+ window.setTimeout(this.bind(function() {
1830
+ this.search.focus();
1831
+ }), 0);
1832
+ }
1833
+ }));
1834
+
1835
+ this.focusser.on("keydown", this.bind(function (e) {
1836
+ if (!this.isInterfaceEnabled()) return;
1837
+
1838
+ if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) {
1839
+ return;
1840
+ }
1841
+
1842
+ if (this.opts.openOnEnter === false && e.which === KEY.ENTER) {
1843
+ killEvent(e);
1844
+ return;
1845
+ }
1846
+
1847
+ if (e.which == KEY.DOWN || e.which == KEY.UP
1848
+ || (e.which == KEY.ENTER && this.opts.openOnEnter)) {
1849
+ this.open();
1850
+ killEvent(e);
1851
+ return;
1852
+ }
1853
+
1854
+ if (e.which == KEY.DELETE || e.which == KEY.BACKSPACE) {
1855
+ if (this.opts.allowClear) {
1856
+ this.clear();
1857
+ }
1858
+ killEvent(e);
1859
+ return;
1860
+ }
1861
+ }));
1862
+
1863
+
1864
+ installKeyUpChangeEvent(this.focusser);
1865
+ this.focusser.on("keyup-change input", this.bind(function(e) {
1866
+ e.stopPropagation();
1867
+ if (this.opened()) return;
1868
+ this.open();
1869
+ }));
1870
+
1871
+ selection.on("mousedown", "abbr", this.bind(function (e) {
1872
+ if (!this.isInterfaceEnabled()) return;
1873
+ this.clear();
1874
+ killEventImmediately(e);
1875
+ this.close();
1876
+ this.selection.focus();
1877
+ }));
1878
+
1879
+ selection.on("mousedown", this.bind(function (e) {
1880
+
1881
+ if (!this.container.hasClass("select2-container-active")) {
1882
+ this.opts.element.trigger($.Event("select2-focus"));
1883
+ }
1884
+
1885
+ if (this.opened()) {
1886
+ this.close();
1887
+ } else if (this.isInterfaceEnabled()) {
1888
+ this.open();
1889
+ }
1890
+
1891
+ killEvent(e);
1892
+ }));
1893
+
1894
+ dropdown.on("mousedown", this.bind(function() { this.search.focus(); }));
1895
+
1896
+ selection.on("focus", this.bind(function(e) {
1897
+ killEvent(e);
1898
+ }));
1899
+
1900
+ this.focusser.on("focus", this.bind(function(){
1901
+ if (!this.container.hasClass("select2-container-active")) {
1902
+ this.opts.element.trigger($.Event("select2-focus"));
1903
+ }
1904
+ this.container.addClass("select2-container-active");
1905
+ })).on("blur", this.bind(function() {
1906
+ if (!this.opened()) {
1907
+ this.container.removeClass("select2-container-active");
1908
+ this.opts.element.trigger($.Event("select2-blur"));
1909
+ }
1910
+ }));
1911
+ this.search.on("focus", this.bind(function(){
1912
+ if (!this.container.hasClass("select2-container-active")) {
1913
+ this.opts.element.trigger($.Event("select2-focus"));
1914
+ }
1915
+ this.container.addClass("select2-container-active");
1916
+ }));
1917
+
1918
+ this.initContainerWidth();
1919
+ this.opts.element.addClass("select2-offscreen");
1920
+ this.setPlaceholder();
1921
+
1922
+ },
1923
+
1924
+ // single
1925
+ clear: function(triggerChange) {
1926
+ var data=this.selection.data("select2-data");
1927
+ if (data) { // guard against queued quick consecutive clicks
1928
+ this.opts.element.val("");
1929
+ this.selection.find("span").empty();
1930
+ this.selection.removeData("select2-data");
1931
+ this.setPlaceholder();
1932
+
1933
+ if (triggerChange !== false){
1934
+ this.opts.element.trigger({ type: "select2-removed", val: this.id(data), choice: data });
1935
+ this.triggerChange({removed:data});
1936
+ }
1937
+ }
1938
+ },
1939
+
1940
+ /**
1941
+ * Sets selection based on source element's value
1942
+ */
1943
+ // single
1944
+ initSelection: function () {
1945
+ var selected;
1946
+ if (this.opts.element.val() === "" && this.opts.element.text() === "") {
1947
+ this.updateSelection([]);
1948
+ this.close();
1949
+ this.setPlaceholder();
1950
+ } else {
1951
+ var self = this;
1952
+ this.opts.initSelection.call(null, this.opts.element, function(selected){
1953
+ if (selected !== undefined && selected !== null) {
1954
+ self.updateSelection(selected);
1955
+ self.close();
1956
+ self.setPlaceholder();
1957
+ }
1958
+ });
1959
+ }
1960
+ },
1961
+
1962
+ // single
1963
+ prepareOpts: function () {
1964
+ var opts = this.parent.prepareOpts.apply(this, arguments),
1965
+ self=this;
1966
+
1967
+ if (opts.element.get(0).tagName.toLowerCase() === "select") {
1968
+ // install the selection initializer
1969
+ opts.initSelection = function (element, callback) {
1970
+ var selected = element.find(":selected");
1971
+ // a single select box always has a value, no need to null check 'selected'
1972
+ callback(self.optionToData(selected));
1973
+ };
1974
+ } else if ("data" in opts) {
1975
+ // install default initSelection when applied to hidden input and data is local
1976
+ opts.initSelection = opts.initSelection || function (element, callback) {
1977
+ var id = element.val();
1978
+ //search in data by id, storing the actual matching item
1979
+ var match = null;
1980
+ opts.query({
1981
+ matcher: function(term, text, el){
1982
+ var is_match = equal(id, opts.id(el));
1983
+ if (is_match) {
1984
+ match = el;
1985
+ }
1986
+ return is_match;
1987
+ },
1988
+ callback: !$.isFunction(callback) ? $.noop : function() {
1989
+ callback(match);
1990
+ }
1991
+ });
1992
+ };
1993
+ }
1994
+
1995
+ return opts;
1996
+ },
1997
+
1998
+ // single
1999
+ getPlaceholder: function() {
2000
+ // if a placeholder is specified on a single select without the first empty option ignore it
2001
+ if (this.select) {
2002
+ if (this.select.find("option").first().text() !== "") {
2003
+ return undefined;
2004
+ }
2005
+ }
2006
+
2007
+ return this.parent.getPlaceholder.apply(this, arguments);
2008
+ },
2009
+
2010
+ // single
2011
+ setPlaceholder: function () {
2012
+ var placeholder = this.getPlaceholder();
2013
+
2014
+ if (this.opts.element.val() === "" && placeholder !== undefined) {
2015
+
2016
+ // check for a first blank option if attached to a select
2017
+ if (this.select && this.select.find("option:first").text() !== "") return;
2018
+
2019
+ this.selection.find("span").html(this.opts.escapeMarkup(placeholder));
2020
+
2021
+ this.selection.addClass("select2-default");
2022
+
2023
+ this.container.removeClass("select2-allowclear");
2024
+ }
2025
+ },
2026
+
2027
+ // single
2028
+ postprocessResults: function (data, initial, noHighlightUpdate) {
2029
+ var selected = 0, self = this, showSearchInput = true;
2030
+
2031
+ // find the selected element in the result list
2032
+
2033
+ this.findHighlightableChoices().each2(function (i, elm) {
2034
+ if (equal(self.id(elm.data("select2-data")), self.opts.element.val())) {
2035
+ selected = i;
2036
+ return false;
2037
+ }
2038
+ });
2039
+
2040
+ // and highlight it
2041
+ if (noHighlightUpdate !== false) {
2042
+ this.highlight(selected);
2043
+ }
2044
+
2045
+ // show the search box if this is the first we got the results and there are enough of them for search
2046
+
2047
+ if (initial === true && this.showSearchInput === false) {
2048
+ var min=this.opts.minimumResultsForSearch;
2049
+ if (min>=0) {
2050
+ this.showSearch(countResults(data.results)>=min);
2051
+ }
2052
+ }
2053
+
2054
+ },
2055
+
2056
+ // single
2057
+ showSearch: function(showSearchInput) {
2058
+ this.showSearchInput = showSearchInput;
2059
+
2060
+ this.dropdown.find(".select2-search").toggleClass("select2-search-hidden", !showSearchInput);
2061
+ this.dropdown.find(".select2-search").toggleClass("select2-offscreen", !showSearchInput);
2062
+ //add "select2-with-searchbox" to the container if search box is shown
2063
+ $(this.dropdown, this.container).toggleClass("select2-with-searchbox", showSearchInput);
2064
+ },
2065
+
2066
+ // single
2067
+ onSelect: function (data, options) {
2068
+
2069
+ if (!this.triggerSelect(data)) { return; }
2070
+
2071
+ var old = this.opts.element.val(),
2072
+ oldData = this.data();
2073
+
2074
+ this.opts.element.val(this.id(data));
2075
+ this.updateSelection(data);
2076
+
2077
+ this.opts.element.trigger({ type: "select2-selected", val: this.id(data), choice: data });
2078
+
2079
+ this.close();
2080
+
2081
+ if (!options || !options.noFocus)
2082
+ this.selection.focus();
2083
+
2084
+ if (!equal(old, this.id(data))) { this.triggerChange({added:data,removed:oldData}); }
2085
+ },
2086
+
2087
+ // single
2088
+ updateSelection: function (data) {
2089
+
2090
+ var container=this.selection.find("span"), formatted;
2091
+
2092
+ this.selection.data("select2-data", data);
2093
+
2094
+ container.empty();
2095
+ formatted=this.opts.formatSelection(data, container);
2096
+ if (formatted !== undefined) {
2097
+ container.append(this.opts.escapeMarkup(formatted));
2098
+ }
2099
+
2100
+ this.selection.removeClass("select2-default");
2101
+
2102
+ if (this.opts.allowClear && this.getPlaceholder() !== undefined) {
2103
+ this.container.addClass("select2-allowclear");
2104
+ }
2105
+ },
2106
+
2107
+ // single
2108
+ val: function () {
2109
+ var val,
2110
+ triggerChange = false,
2111
+ data = null,
2112
+ self = this,
2113
+ oldData = this.data();
2114
+
2115
+ if (arguments.length === 0) {
2116
+ return this.opts.element.val();
2117
+ }
2118
+
2119
+ val = arguments[0];
2120
+
2121
+ if (arguments.length > 1) {
2122
+ triggerChange = arguments[1];
2123
+ }
2124
+
2125
+ if (this.select) {
2126
+ this.select
2127
+ .val(val)
2128
+ .find(":selected").each2(function (i, elm) {
2129
+ data = self.optionToData(elm);
2130
+ return false;
2131
+ });
2132
+ this.updateSelection(data);
2133
+ this.setPlaceholder();
2134
+ if (triggerChange) {
2135
+ this.triggerChange({added: data, removed:oldData});
2136
+ }
2137
+ } else {
2138
+ if (this.opts.initSelection === undefined) {
2139
+ throw new Error("cannot call val() if initSelection() is not defined");
2140
+ }
2141
+ // val is an id. !val is true for [undefined,null,'',0] - 0 is legal
2142
+ if (!val && val !== 0) {
2143
+ this.clear(triggerChange);
2144
+ return;
2145
+ }
2146
+ this.opts.element.val(val);
2147
+ this.opts.initSelection(this.opts.element, function(data){
2148
+ self.opts.element.val(!data ? "" : self.id(data));
2149
+ self.updateSelection(data);
2150
+ self.setPlaceholder();
2151
+ if (triggerChange) {
2152
+ self.triggerChange({added: data, removed:oldData});
2153
+ }
2154
+ });
2155
+ }
2156
+ },
2157
+
2158
+ // single
2159
+ clearSearch: function () {
2160
+ this.search.val("");
2161
+ this.focusser.val("");
2162
+ },
2163
+
2164
+ // single
2165
+ data: function(value, triggerChange) {
2166
+ var data;
2167
+
2168
+ if (arguments.length === 0) {
2169
+ data = this.selection.data("select2-data");
2170
+ if (data == undefined) data = null;
2171
+ return data;
2172
+ } else {
2173
+ if (!value || value === "") {
2174
+ this.clear(triggerChange);
2175
+ } else {
2176
+ data = this.data();
2177
+ this.opts.element.val(!value ? "" : this.id(value));
2178
+ this.updateSelection(value);
2179
+ if (triggerChange) {
2180
+ this.triggerChange({added: value, removed:data});
2181
+ }
2182
+ }
2183
+ }
2184
+ }
2185
+ });
2186
+
2187
+ MultiSelect2 = clazz(AbstractSelect2, {
2188
+
2189
+ // multi
2190
+ createContainer: function () {
2191
+ var container = $(document.createElement("div")).attr({
2192
+ "class": "select2-container select2-container-multi"
2193
+ }).html([
2194
+ " <ul class='select2-choices'>",
2195
+ //"<li class='select2-search-choice'><span>California</span><a href="javascript:void(0)" class="select2-search-choice-close"></a></li>" ,
2196
+ " <li class='select2-search-field'>" ,
2197
+ " <input type='text' autocomplete='off' autocorrect='off' autocapitilize='off' spellcheck='false' class='select2-input'>" ,
2198
+ " </li>" ,
2199
+ "</ul>" ,
2200
+ "<div class='select2-drop select2-drop-multi select2-display-none'>" ,
2201
+ " <ul class='select2-results'>" ,
2202
+ " </ul>" ,
2203
+ "</div>"].join(""));
2204
+ return container;
2205
+ },
2206
+
2207
+ // multi
2208
+ prepareOpts: function () {
2209
+ var opts = this.parent.prepareOpts.apply(this, arguments),
2210
+ self=this;
2211
+
2212
+ // TODO validate placeholder is a string if specified
2213
+
2214
+ if (opts.element.get(0).tagName.toLowerCase() === "select") {
2215
+ // install sthe selection initializer
2216
+ opts.initSelection = function (element, callback) {
2217
+
2218
+ var data = [];
2219
+
2220
+ element.find(":selected").each2(function (i, elm) {
2221
+ data.push(self.optionToData(elm));
2222
+ });
2223
+ callback(data);
2224
+ };
2225
+ } else if ("data" in opts) {
2226
+ // install default initSelection when applied to hidden input and data is local
2227
+ opts.initSelection = opts.initSelection || function (element, callback) {
2228
+ var ids = splitVal(element.val(), opts.separator);
2229
+ //search in data by array of ids, storing matching items in a list
2230
+ var matches = [];
2231
+ opts.query({
2232
+ matcher: function(term, text, el){
2233
+ var is_match = $.grep(ids, function(id) {
2234
+ return equal(id, opts.id(el));
2235
+ }).length;
2236
+ if (is_match) {
2237
+ matches.push(el);
2238
+ }
2239
+ return is_match;
2240
+ },
2241
+ callback: !$.isFunction(callback) ? $.noop : function() {
2242
+ // reorder matches based on the order they appear in the ids array because right now
2243
+ // they are in the order in which they appear in data array
2244
+ var ordered = [];
2245
+ for (var i = 0; i < ids.length; i++) {
2246
+ var id = ids[i];
2247
+ for (var j = 0; j < matches.length; j++) {
2248
+ var match = matches[j];
2249
+ if (equal(id, opts.id(match))) {
2250
+ ordered.push(match);
2251
+ matches.splice(j, 1);
2252
+ break;
2253
+ }
2254
+ }
2255
+ }
2256
+ callback(ordered);
2257
+ }
2258
+ });
2259
+ };
2260
+ }
2261
+
2262
+ return opts;
2263
+ },
2264
+
2265
+ selectChoice: function (choice) {
2266
+
2267
+ var selected = this.container.find(".select2-search-choice-focus");
2268
+ if (selected.length && choice && choice[0] == selected[0]) {
2269
+
2270
+ } else {
2271
+ if (selected.length) {
2272
+ this.opts.element.trigger("choice-deselected", selected);
2273
+ }
2274
+ selected.removeClass("select2-search-choice-focus");
2275
+ if (choice && choice.length) {
2276
+ this.close();
2277
+ choice.addClass("select2-search-choice-focus");
2278
+ this.opts.element.trigger("choice-selected", choice);
2279
+ }
2280
+ }
2281
+ },
2282
+
2283
+ // multi
2284
+ initContainer: function () {
2285
+
2286
+ var selector = ".select2-choices", selection;
2287
+
2288
+ this.searchContainer = this.container.find(".select2-search-field");
2289
+ this.selection = selection = this.container.find(selector);
2290
+
2291
+ var _this = this;
2292
+ this.selection.on("mousedown", ".select2-search-choice", function (e) {
2293
+ //killEvent(e);
2294
+ _this.search[0].focus();
2295
+ _this.selectChoice($(this));
2296
+ })
2297
+ //.sortable({
2298
+ // items: " > li",
2299
+ // tolerance: "pointer",
2300
+ // revert: 100
2301
+ //});
2302
+
2303
+ // rewrite labels from original element to focusser
2304
+ this.search.attr("id", "s2id_autogen"+nextUid());
2305
+ $("label[for='" + this.opts.element.attr("id") + "']")
2306
+ .attr('for', this.search.attr('id'));
2307
+
2308
+ this.search.on("input paste", this.bind(function() {
2309
+ if (!this.isInterfaceEnabled()) return;
2310
+ if (!this.opened()) {
2311
+ this.open();
2312
+ }
2313
+ }));
2314
+
2315
+ this.search.attr("tabindex", this.elementTabIndex);
2316
+
2317
+ this.keydowns = 0;
2318
+ this.search.on("keydown", this.bind(function (e) {
2319
+ if (!this.isInterfaceEnabled()) return;
2320
+
2321
+ ++this.keydowns;
2322
+ var selected = selection.find(".select2-search-choice-focus");
2323
+ var prev = selected.prev(".select2-search-choice:not(.select2-locked)");
2324
+ var next = selected.next(".select2-search-choice:not(.select2-locked)");
2325
+ var pos = getCursorInfo(this.search);
2326
+
2327
+ if (selected.length &&
2328
+ (e.which == KEY.LEFT || e.which == KEY.RIGHT || e.which == KEY.BACKSPACE || e.which == KEY.DELETE || e.which == KEY.ENTER)) {
2329
+ var selectedChoice = selected;
2330
+ if (e.which == KEY.LEFT && prev.length) {
2331
+ selectedChoice = prev;
2332
+ }
2333
+ else if (e.which == KEY.RIGHT) {
2334
+ selectedChoice = next.length ? next : null;
2335
+ }
2336
+ else if (e.which === KEY.BACKSPACE) {
2337
+ this.unselect(selected.first());
2338
+ this.search.width(10);
2339
+ selectedChoice = prev.length ? prev : next;
2340
+ } else if (e.which == KEY.DELETE) {
2341
+ this.unselect(selected.first());
2342
+ this.search.width(10);
2343
+ selectedChoice = next.length ? next : null;
2344
+ } else if (e.which == KEY.ENTER) {
2345
+ selectedChoice = null;
2346
+ }
2347
+
2348
+ this.selectChoice(selectedChoice);
2349
+ killEvent(e);
2350
+ if (!selectedChoice || !selectedChoice.length) {
2351
+ this.open();
2352
+ }
2353
+ return;
2354
+ } else if (((e.which === KEY.BACKSPACE && this.keydowns == 1)
2355
+ || e.which == KEY.LEFT) && (pos.offset == 0 && !pos.length)) {
2356
+
2357
+ this.selectChoice(selection.find(".select2-search-choice:not(.select2-locked)").last());
2358
+ killEvent(e);
2359
+ return;
2360
+ } else {
2361
+ this.selectChoice(null);
2362
+ }
2363
+
2364
+ if (this.opened()) {
2365
+ switch (e.which) {
2366
+ case KEY.UP:
2367
+ case KEY.DOWN:
2368
+ this.moveHighlight((e.which === KEY.UP) ? -1 : 1);
2369
+ killEvent(e);
2370
+ return;
2371
+ case KEY.ENTER:
2372
+ this.selectHighlighted();
2373
+ killEvent(e);
2374
+ return;
2375
+ case KEY.TAB:
2376
+ this.selectHighlighted({noFocus:true});
2377
+ return;
2378
+ case KEY.ESC:
2379
+ this.cancel(e);
2380
+ killEvent(e);
2381
+ return;
2382
+ }
2383
+ }
2384
+
2385
+ if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e)
2386
+ || e.which === KEY.BACKSPACE || e.which === KEY.ESC) {
2387
+ return;
2388
+ }
2389
+
2390
+ if (e.which === KEY.ENTER) {
2391
+ if (this.opts.openOnEnter === false) {
2392
+ return;
2393
+ } else if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) {
2394
+ return;
2395
+ }
2396
+ }
2397
+
2398
+ this.open();
2399
+
2400
+ if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) {
2401
+ // prevent the page from scrolling
2402
+ killEvent(e);
2403
+ }
2404
+
2405
+ if (e.which === KEY.ENTER) {
2406
+ // prevent form from being submitted
2407
+ killEvent(e);
2408
+ }
2409
+
2410
+ }));
2411
+
2412
+ this.search.on("keyup", this.bind(function (e) {
2413
+ this.keydowns = 0;
2414
+ this.resizeSearch();
2415
+ })
2416
+ );
2417
+
2418
+ this.search.on("blur", this.bind(function(e) {
2419
+ this.container.removeClass("select2-container-active");
2420
+ this.search.removeClass("select2-focused");
2421
+ this.selectChoice(null);
2422
+ if (!this.opened()) this.clearSearch();
2423
+ e.stopImmediatePropagation();
2424
+ this.opts.element.trigger($.Event("select2-blur"));
2425
+ }));
2426
+
2427
+ this.container.on("mousedown", selector, this.bind(function (e) {
2428
+ if (!this.isInterfaceEnabled()) return;
2429
+ if ($(e.target).closest(".select2-search-choice").length > 0) {
2430
+ // clicked inside a select2 search choice, do not open
2431
+ return;
2432
+ }
2433
+ this.selectChoice(null);
2434
+ this.clearPlaceholder();
2435
+ if (!this.container.hasClass("select2-container-active")) {
2436
+ this.opts.element.trigger($.Event("select2-focus"));
2437
+ }
2438
+ this.open();
2439
+ this.focusSearch();
2440
+ e.preventDefault();
2441
+ }));
2442
+
2443
+ this.container.on("focus", selector, this.bind(function () {
2444
+ if (!this.isInterfaceEnabled()) return;
2445
+ if (!this.container.hasClass("select2-container-active")) {
2446
+ this.opts.element.trigger($.Event("select2-focus"));
2447
+ }
2448
+ this.container.addClass("select2-container-active");
2449
+ this.dropdown.addClass("select2-drop-active");
2450
+ this.clearPlaceholder();
2451
+ }));
2452
+
2453
+ this.initContainerWidth();
2454
+ this.opts.element.addClass("select2-offscreen");
2455
+
2456
+ // set the placeholder if necessary
2457
+ this.clearSearch();
2458
+ },
2459
+
2460
+ // multi
2461
+ enableInterface: function() {
2462
+ if (this.parent.enableInterface.apply(this, arguments)) {
2463
+ this.search.prop("disabled", !this.isInterfaceEnabled());
2464
+ }
2465
+ },
2466
+
2467
+ // multi
2468
+ initSelection: function () {
2469
+ var data;
2470
+ if (this.opts.element.val() === "" && this.opts.element.text() === "") {
2471
+ this.updateSelection([]);
2472
+ this.close();
2473
+ // set the placeholder if necessary
2474
+ this.clearSearch();
2475
+ }
2476
+ if (this.select || this.opts.element.val() !== "") {
2477
+ var self = this;
2478
+ this.opts.initSelection.call(null, this.opts.element, function(data){
2479
+ if (data !== undefined && data !== null) {
2480
+ self.updateSelection(data);
2481
+ self.close();
2482
+ // set the placeholder if necessary
2483
+ self.clearSearch();
2484
+ }
2485
+ });
2486
+ }
2487
+ },
2488
+
2489
+ // multi
2490
+ clearSearch: function () {
2491
+ var placeholder = this.getPlaceholder(),
2492
+ maxWidth = this.getMaxSearchWidth();
2493
+
2494
+ if (placeholder !== undefined && this.getVal().length === 0 && this.search.hasClass("select2-focused") === false) {
2495
+ this.search.val(placeholder).addClass("select2-default");
2496
+ // stretch the search box to full width of the container so as much of the placeholder is visible as possible
2497
+ // 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
2498
+ this.search.width(maxWidth > 0 ? maxWidth : this.container.css("width"));
2499
+ } else {
2500
+ this.search.val("").width(10);
2501
+ }
2502
+ },
2503
+
2504
+ // multi
2505
+ clearPlaceholder: function () {
2506
+ if (this.search.hasClass("select2-default")) {
2507
+ this.search.val("").removeClass("select2-default");
2508
+ }
2509
+ },
2510
+
2511
+ // multi
2512
+ opening: function () {
2513
+ this.clearPlaceholder(); // should be done before super so placeholder is not used to search
2514
+ this.resizeSearch();
2515
+
2516
+ this.parent.opening.apply(this, arguments);
2517
+
2518
+ this.focusSearch();
2519
+
2520
+ this.updateResults(true);
2521
+ this.search.focus();
2522
+ this.opts.element.trigger($.Event("select2-open"));
2523
+ },
2524
+
2525
+ // multi
2526
+ close: function () {
2527
+ if (!this.opened()) return;
2528
+ this.parent.close.apply(this, arguments);
2529
+ },
2530
+
2531
+ // multi
2532
+ focus: function () {
2533
+ this.close();
2534
+ this.search.focus();
2535
+ //this.opts.element.triggerHandler("focus");
2536
+ },
2537
+
2538
+ // multi
2539
+ isFocused: function () {
2540
+ return this.search.hasClass("select2-focused");
2541
+ },
2542
+
2543
+ // multi
2544
+ updateSelection: function (data) {
2545
+ var ids = [], filtered = [], self = this;
2546
+
2547
+ // filter out duplicates
2548
+ $(data).each(function () {
2549
+ if (indexOf(self.id(this), ids) < 0) {
2550
+ ids.push(self.id(this));
2551
+ filtered.push(this);
2552
+ }
2553
+ });
2554
+ data = filtered;
2555
+
2556
+ this.selection.find(".select2-search-choice").remove();
2557
+ $(data).each(function () {
2558
+ self.addSelectedChoice(this);
2559
+ });
2560
+ self.postprocessResults();
2561
+ },
2562
+
2563
+ // multi
2564
+ tokenize: function() {
2565
+ var input = this.search.val();
2566
+ input = this.opts.tokenizer(input, this.data(), this.bind(this.onSelect), this.opts);
2567
+ if (input != null && input != undefined) {
2568
+ this.search.val(input);
2569
+ if (input.length > 0) {
2570
+ this.open();
2571
+ }
2572
+ }
2573
+
2574
+ },
2575
+
2576
+ // multi
2577
+ onSelect: function (data, options) {
2578
+
2579
+ if (!this.triggerSelect(data)) { return; }
2580
+
2581
+ this.addSelectedChoice(data);
2582
+
2583
+ this.opts.element.trigger({ type: "selected", val: this.id(data), choice: data });
2584
+
2585
+ if (this.select || !this.opts.closeOnSelect) this.postprocessResults();
2586
+
2587
+ if (this.opts.closeOnSelect) {
2588
+ this.close();
2589
+ this.search.width(10);
2590
+ } else {
2591
+ if (this.countSelectableResults()>0) {
2592
+ this.search.width(10);
2593
+ this.resizeSearch();
2594
+ if (this.getMaximumSelectionSize() > 0 && this.val().length >= this.getMaximumSelectionSize()) {
2595
+ // if we reached max selection size repaint the results so choices
2596
+ // are replaced with the max selection reached message
2597
+ this.updateResults(true);
2598
+ }
2599
+ this.positionDropdown();
2600
+ } else {
2601
+ // if nothing left to select close
2602
+ this.close();
2603
+ this.search.width(10);
2604
+ }
2605
+ }
2606
+
2607
+ // since its not possible to select an element that has already been
2608
+ // added we do not need to check if this is a new element before firing change
2609
+ this.triggerChange({ added: data });
2610
+
2611
+ if (!options || !options.noFocus)
2612
+ this.focusSearch();
2613
+ },
2614
+
2615
+ // multi
2616
+ cancel: function () {
2617
+ this.close();
2618
+ this.focusSearch();
2619
+ },
2620
+
2621
+ addSelectedChoice: function (data) {
2622
+ var enableChoice = !data.locked,
2623
+ enabledItem = $(
2624
+ "<li class='select2-search-choice'>" +
2625
+ " <div></div>" +
2626
+ " <a href='#' onclick='return false;' class='select2-search-choice-close' tabindex='-1'></a>" +
2627
+ "</li>"),
2628
+ disabledItem = $(
2629
+ "<li class='select2-search-choice select2-locked'>" +
2630
+ "<div></div>" +
2631
+ "</li>");
2632
+ var choice = enableChoice ? enabledItem : disabledItem,
2633
+ id = this.id(data),
2634
+ val = this.getVal(),
2635
+ formatted;
2636
+
2637
+ formatted=this.opts.formatSelection(data, choice.find("div"));
2638
+ if (formatted != undefined) {
2639
+ choice.find("div").replaceWith("<div title='"+this.opts.escapeMarkup(formatted)+"'>"+this.opts.escapeMarkup(formatted)+"</div>");
2640
+ }
2641
+
2642
+ if(enableChoice){
2643
+ choice.find(".select2-search-choice-close")
2644
+ .on("mousedown", killEvent)
2645
+ .on("click dblclick", this.bind(function (e) {
2646
+ if (!this.isInterfaceEnabled()) return;
2647
+
2648
+ $(e.target).closest(".select2-search-choice").fadeOut('fast', this.bind(function(){
2649
+ this.unselect($(e.target));
2650
+ this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
2651
+ this.close();
2652
+ this.focusSearch();
2653
+ })).dequeue();
2654
+ killEvent(e);
2655
+ })).on("focus", this.bind(function () {
2656
+ if (!this.isInterfaceEnabled()) return;
2657
+ this.container.addClass("select2-container-active");
2658
+ this.dropdown.addClass("select2-drop-active");
2659
+ }));
2660
+ }
2661
+
2662
+ choice.data("select2-data", data);
2663
+ choice.insertBefore(this.searchContainer);
2664
+
2665
+ val.push(id);
2666
+ this.setVal(val);
2667
+ },
2668
+
2669
+ // multi
2670
+ unselect: function (selected) {
2671
+ var val = this.getVal(),
2672
+ data,
2673
+ index;
2674
+
2675
+ selected = selected.closest(".select2-search-choice");
2676
+
2677
+ if (selected.length === 0) {
2678
+ throw "Invalid argument: " + selected + ". Must be .select2-search-choice";
2679
+ }
2680
+
2681
+ data = selected.data("select2-data");
2682
+
2683
+ if (!data) {
2684
+ // prevent a race condition when the 'x' is clicked really fast repeatedly the event can be queued
2685
+ // and invoked on an element already removed
2686
+ return;
2687
+ }
2688
+
2689
+ index = indexOf(this.id(data), val);
2690
+
2691
+ if (index >= 0) {
2692
+ val.splice(index, 1);
2693
+ this.setVal(val);
2694
+ if (this.select) this.postprocessResults();
2695
+ }
2696
+ selected.remove();
2697
+
2698
+ this.opts.element.trigger({ type: "removed", val: this.id(data), choice: data });
2699
+ this.triggerChange({ removed: data });
2700
+ },
2701
+
2702
+ // multi
2703
+ postprocessResults: function (data, initial, noHighlightUpdate) {
2704
+ var val = this.getVal(),
2705
+ choices = this.results.find(".select2-result"),
2706
+ compound = this.results.find(".select2-result-with-children"),
2707
+ self = this;
2708
+
2709
+ choices.each2(function (i, choice) {
2710
+ var id = self.id(choice.data("select2-data"));
2711
+ if (indexOf(id, val) >= 0) {
2712
+ choice.addClass("select2-selected");
2713
+ // mark all children of the selected parent as selected
2714
+ choice.find(".select2-result-selectable").addClass("select2-selected");
2715
+ }
2716
+ });
2717
+
2718
+ compound.each2(function(i, choice) {
2719
+ // hide an optgroup if it doesnt have any selectable children
2720
+ if (!choice.is('.select2-result-selectable')
2721
+ && choice.find(".select2-result-selectable:not(.select2-selected)").length === 0) {
2722
+ choice.addClass("select2-selected");
2723
+ }
2724
+ });
2725
+
2726
+ if (this.highlight() == -1 && noHighlightUpdate !== false){
2727
+ self.highlight(0);
2728
+ }
2729
+
2730
+ //If all results are chosen render formatNoMAtches
2731
+ if(!this.opts.createSearchChoice && !choices.filter('.select2-result:not(.select2-selected)').length > 0){
2732
+ this.results.append("<li class='select2-no-results'>" + self.opts.formatNoMatches(self.search.val()) + "</li>");
2733
+ }
2734
+
2735
+ },
2736
+
2737
+ // multi
2738
+ getMaxSearchWidth: function() {
2739
+ return this.selection.width() - getSideBorderPadding(this.search);
2740
+ },
2741
+
2742
+ // multi
2743
+ resizeSearch: function () {
2744
+ var minimumWidth, left, maxWidth, containerLeft, searchWidth,
2745
+ sideBorderPadding = getSideBorderPadding(this.search);
2746
+
2747
+ minimumWidth = measureTextWidth(this.search) + 10;
2748
+
2749
+ left = this.search.offset().left;
2750
+
2751
+ maxWidth = this.selection.width();
2752
+ containerLeft = this.selection.offset().left;
2753
+
2754
+ searchWidth = maxWidth - (left - containerLeft) - sideBorderPadding;
2755
+
2756
+ if (searchWidth < minimumWidth) {
2757
+ searchWidth = maxWidth - sideBorderPadding;
2758
+ }
2759
+
2760
+ if (searchWidth < 40) {
2761
+ searchWidth = maxWidth - sideBorderPadding;
2762
+ }
2763
+
2764
+ if (searchWidth <= 0) {
2765
+ searchWidth = minimumWidth;
2766
+ }
2767
+
2768
+ this.search.width(searchWidth);
2769
+ },
2770
+
2771
+ // multi
2772
+ getVal: function () {
2773
+ var val;
2774
+ if (this.select) {
2775
+ val = this.select.val();
2776
+ return val === null ? [] : val;
2777
+ } else {
2778
+ val = this.opts.element.val();
2779
+ return splitVal(val, this.opts.separator);
2780
+ }
2781
+ },
2782
+
2783
+ // multi
2784
+ setVal: function (val) {
2785
+ var unique;
2786
+ if (this.select) {
2787
+ this.select.val(val);
2788
+ } else {
2789
+ unique = [];
2790
+ // filter out duplicates
2791
+ $(val).each(function () {
2792
+ if (indexOf(this, unique) < 0) unique.push(this);
2793
+ });
2794
+ this.opts.element.val(unique.length === 0 ? "" : unique.join(this.opts.separator));
2795
+ }
2796
+ },
2797
+
2798
+ // multi
2799
+ buildChangeDetails: function (old, current) {
2800
+ var current = current.slice(0),
2801
+ old = old.slice(0);
2802
+
2803
+ // remove intersection from each array
2804
+ for (var i = 0; i < current.length; i++) {
2805
+ for (var j = 0; j < old.length; j++) {
2806
+ if (equal(this.opts.id(current[i]), this.opts.id(old[j]))) {
2807
+ current.splice(i, 1);
2808
+ i--;
2809
+ old.splice(j, 1);
2810
+ j--;
2811
+ }
2812
+ }
2813
+ }
2814
+
2815
+ return {added: current, removed: old};
2816
+ },
2817
+
2818
+
2819
+ // multi
2820
+ val: function (val, triggerChange) {
2821
+ var oldData, self=this, changeDetails;
2822
+
2823
+ if (arguments.length === 0) {
2824
+ return this.getVal();
2825
+ }
2826
+
2827
+ oldData=this.data();
2828
+ if (!oldData.length) oldData=[];
2829
+
2830
+ // val is an id. !val is true for [undefined,null,'',0] - 0 is legal
2831
+ if (!val && val !== 0) {
2832
+ this.opts.element.val("");
2833
+ this.updateSelection([]);
2834
+ this.clearSearch();
2835
+ if (triggerChange) {
2836
+ this.triggerChange({added: this.data(), removed: oldData});
2837
+ }
2838
+ return;
2839
+ }
2840
+
2841
+ // val is a list of ids
2842
+ this.setVal(val);
2843
+
2844
+ if (this.select) {
2845
+ this.opts.initSelection(this.select, this.bind(this.updateSelection));
2846
+ if (triggerChange) {
2847
+ this.triggerChange(this.buildChangeDetails(oldData, this.data()));
2848
+ }
2849
+ } else {
2850
+ if (this.opts.initSelection === undefined) {
2851
+ throw new Error("val() cannot be called if initSelection() is not defined");
2852
+ }
2853
+
2854
+ this.opts.initSelection(this.opts.element, function(data){
2855
+ var ids=$(data).map(self.id);
2856
+ self.setVal(ids);
2857
+ self.updateSelection(data);
2858
+ self.clearSearch();
2859
+ if (triggerChange) {
2860
+ self.triggerChange(this.buildChangeDetails(oldData, this.data()));
2861
+ }
2862
+ });
2863
+ }
2864
+ this.clearSearch();
2865
+ },
2866
+
2867
+ // multi
2868
+ onSortStart: function() {
2869
+ if (this.select) {
2870
+ throw new Error("Sorting of elements is not supported when attached to <select>. Attach to <input type='hidden'/> instead.");
2871
+ }
2872
+
2873
+ // collapse search field into 0 width so its container can be collapsed as well
2874
+ this.search.width(0);
2875
+ // hide the container
2876
+ this.searchContainer.hide();
2877
+ },
2878
+
2879
+ // multi
2880
+ onSortEnd:function() {
2881
+
2882
+ var val=[], self=this;
2883
+
2884
+ // show search and move it to the end of the list
2885
+ this.searchContainer.show();
2886
+ // make sure the search container is the last item in the list
2887
+ this.searchContainer.appendTo(this.searchContainer.parent());
2888
+ // since we collapsed the width in dragStarted, we resize it here
2889
+ this.resizeSearch();
2890
+
2891
+ // update selection
2892
+
2893
+ this.selection.find(".select2-search-choice").each(function() {
2894
+ val.push(self.opts.id($(this).data("select2-data")));
2895
+ });
2896
+ this.setVal(val);
2897
+ this.triggerChange();
2898
+ },
2899
+
2900
+ // multi
2901
+ data: function(values, triggerChange) {
2902
+ var self=this, ids, old;
2903
+ if (arguments.length === 0) {
2904
+ return this.selection
2905
+ .find(".select2-search-choice")
2906
+ .map(function() { return $(this).data("select2-data"); })
2907
+ .get();
2908
+ } else {
2909
+ old = this.data();
2910
+ if (!values) { values = []; }
2911
+ ids = $.map(values, function(e) { return self.opts.id(e); });
2912
+ this.setVal(ids);
2913
+ this.updateSelection(values);
2914
+ this.clearSearch();
2915
+ if (triggerChange) {
2916
+ this.triggerChange(this.buildChangeDetails(old, this.data()));
2917
+ }
2918
+ }
2919
+ }
2920
+ });
2921
+
2922
+ $.fn.select2 = function () {
2923
+
2924
+ var args = Array.prototype.slice.call(arguments, 0),
2925
+ opts,
2926
+ select2,
2927
+ value, multiple,
2928
+ allowedMethods = ["val", "destroy", "opened", "open", "close", "focus", "isFocused", "container", "onSortStart", "onSortEnd", "enable", "readonly", "positionDropdown", "data"],
2929
+ valueMethods = ["val", "opened", "isFocused", "container", "data"];
2930
+
2931
+ this.each(function () {
2932
+ if (args.length === 0 || typeof(args[0]) === "object") {
2933
+ opts = args.length === 0 ? {} : $.extend({}, args[0]);
2934
+ opts.element = $(this);
2935
+
2936
+ if (opts.element.get(0).tagName.toLowerCase() === "select") {
2937
+ multiple = opts.element.prop("multiple");
2938
+ } else {
2939
+ multiple = opts.multiple || false;
2940
+ if ("tags" in opts) {opts.multiple = multiple = true;}
2941
+ }
2942
+
2943
+ select2 = multiple ? new MultiSelect2() : new SingleSelect2();
2944
+ select2.init(opts);
2945
+ } else if (typeof(args[0]) === "string") {
2946
+
2947
+ if (indexOf(args[0], allowedMethods) < 0) {
2948
+ throw "Unknown method: " + args[0];
2949
+ }
2950
+
2951
+ value = undefined;
2952
+ select2 = $(this).data("select2");
2953
+ if (select2 === undefined) return;
2954
+ if (args[0] === "container") {
2955
+ value=select2.container;
2956
+ } else {
2957
+ value = select2[args[0]].apply(select2, args.slice(1));
2958
+ }
2959
+ if (indexOf(args[0], valueMethods) >= 0) {
2960
+ return false;
2961
+ }
2962
+ } else {
2963
+ throw "Invalid arguments to select2 plugin: " + args;
2964
+ }
2965
+ });
2966
+ return (value === undefined) ? this : value;
2967
+ };
2968
+
2969
+ // plugin defaults, accessible to users
2970
+ $.fn.select2.defaults = {
2971
+ width: "copy",
2972
+ loadMorePadding: 0,
2973
+ closeOnSelect: true,
2974
+ openOnEnter: true,
2975
+ containerCss: {},
2976
+ dropdownCss: {},
2977
+ containerCssClass: "",
2978
+ dropdownCssClass: "",
2979
+ formatResult: function(result, container, query, escapeMarkup) {
2980
+ var markup=[];
2981
+ markMatch(result.text, query.term, markup, escapeMarkup);
2982
+ return markup.join("");
2983
+ },
2984
+ formatSelection: function (data, container) {
2985
+ return data ? data.text : undefined;
2986
+ },
2987
+ sortResults: function (results, container, query) {
2988
+ return results;
2989
+ },
2990
+ formatResultCssClass: function(data) {return undefined;},
2991
+ formatNoMatches: function () { return "No matches found"; },
2992
+ formatInputTooShort: function (input, min) { var n = min - input.length; return "Please enter " + n + " more character" + (n == 1? "" : "s"); },
2993
+ formatInputTooLong: function (input, max) { var n = input.length - max; return "Please delete " + n + " character" + (n == 1? "" : "s"); },
2994
+ formatSelectionTooBig: function (limit) { return "You can only select " + limit + " item" + (limit == 1 ? "" : "s"); },
2995
+ formatLoadMore: function (pageNumber) { return "Loading more results..."; },
2996
+ formatSearching: function () { return "Searching..."; },
2997
+ minimumResultsForSearch: 0,
2998
+ minimumInputLength: 0,
2999
+ maximumInputLength: null,
3000
+ maximumSelectionSize: 0,
3001
+ id: function (e) { return e.id; },
3002
+ matcher: function(term, text) {
3003
+ return (''+text).toUpperCase().indexOf((''+term).toUpperCase()) >= 0;
3004
+ },
3005
+ separator: ",",
3006
+ tokenSeparators: [],
3007
+ tokenizer: defaultTokenizer,
3008
+ escapeMarkup: function (markup) {
3009
+ var replace_map = {
3010
+ '\\': '&#92;',
3011
+ '&': '&amp;',
3012
+ '<': '&lt;',
3013
+ '>': '&gt;',
3014
+ '"': '&quot;',
3015
+ "'": '&#39;',
3016
+ "/": '&#47;'
3017
+ };
3018
+
3019
+ return String(markup).replace(/[&<>"'\/\\]/g, function (match) {
3020
+ return replace_map[match];
3021
+ });
3022
+ },
3023
+ blurOnChange: false,
3024
+ selectOnBlur: false,
3025
+ adaptContainerCssClass: function(c) { return c; },
3026
+ adaptDropdownCssClass: function(c) { return null; }
3027
+ };
3028
+
3029
+ $.fn.select2.ajaxDefaults = {
3030
+ transport: $.ajax,
3031
+ params: {
3032
+ type: "GET",
3033
+ cache: false,
3034
+ dataType: "json"
3035
+ }
3036
+ };
3037
+
3038
+ // exports
3039
+ window.Select2 = {
3040
+ query: {
3041
+ ajax: ajax,
3042
+ local: local,
3043
+ tags: tags
3044
+ }, util: {
3045
+ debounce: debounce,
3046
+ markMatch: markMatch
3047
+ }, "class": {
3048
+ "abstract": AbstractSelect2,
3049
+ "single": SingleSelect2,
3050
+ "multi": MultiSelect2
3051
+ }
3052
+ };
3053
+
3054
+ }(jQuery));