select2-rails 2.1.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,5 @@
1
1
  module Select2
2
2
  module Rails
3
- VERSION = "2.1.0"
3
+ VERSION = "3.0.0"
4
4
  end
5
5
  end
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  Copyright 2012 Igor Vaynberg
3
-
4
- Version: 2.1 Timestamp: Tue Jun 12 19:50:25 PDT 2012
3
+
4
+ Version: 3.0 Timestamp: Tue Jul 31 21:09:16 PDT 2012
5
5
 
6
6
  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in
7
7
  compliance with the License. You may obtain a copy of the License in the LICENSE file, or at:
@@ -12,6 +12,26 @@
12
12
  distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and limitations under the License.
14
14
  */
15
+ (function ($) {
16
+ if(typeof $.fn.each2 == "undefined"){
17
+ $.fn.extend({
18
+ /*
19
+ * 4-10 times faster .each replacement
20
+ * use it carefully, as it overrides jQuery context of element on each iteration
21
+ */
22
+ each2 : function (c) {
23
+ var j = $([0]), i = -1, l = this.length;
24
+ while (
25
+ ++i < l
26
+ && (j.context = j[0] = this[i])
27
+ && c.call(j[0], i, j) !== false //"this"=DOM, i=index, j=jQuery object
28
+ );
29
+ return this;
30
+ }
31
+ });
32
+ }
33
+ })(jQuery);
34
+
15
35
  (function ($, undefined) {
16
36
  "use strict";
17
37
  /*global document, window, jQuery, console */
@@ -20,7 +40,7 @@
20
40
  return;
21
41
  }
22
42
 
23
- var KEY, AbstractSelect2, SingleSelect2, MultiSelect2;
43
+ var KEY, AbstractSelect2, SingleSelect2, MultiSelect2, nextUid, sizer;
24
44
 
25
45
  KEY = {
26
46
  TAB: 9,
@@ -59,6 +79,9 @@
59
79
  case KEY.ALT:
60
80
  return true;
61
81
  }
82
+
83
+ if (k.metaKey) return true;
84
+
62
85
  return false;
63
86
  },
64
87
  isFunctionKey: function (k) {
@@ -67,6 +90,16 @@
67
90
  }
68
91
  };
69
92
 
93
+ nextUid=(function() { var counter=1; return function() { return counter++; }; }());
94
+
95
+ function escapeMarkup(markup) {
96
+ if (markup && typeof(markup) === "string") {
97
+ return markup.replace("&", "&amp;");
98
+ } else {
99
+ return markup;
100
+ }
101
+ }
102
+
70
103
  function indexOf(value, array) {
71
104
  var i = 0, l = array.length, v;
72
105
 
@@ -90,7 +123,7 @@
90
123
  }
91
124
 
92
125
  /**
93
- * Compares equality of a and b taking into account that a and b may be strings, in which case localCompare is used
126
+ * Compares equality of a and b taking into account that a and b may be strings, in which case localeCompare is used
94
127
  * @param a
95
128
  * @param b
96
129
  */
@@ -122,28 +155,34 @@
122
155
  }
123
156
 
124
157
  function installKeyUpChangeEvent(element) {
158
+ var key="keyup-change-value";
125
159
  element.bind("keydown", function () {
126
- element.data("keyup-change-value", element.val());
160
+ if ($.data(element, key) === undefined) {
161
+ $.data(element, key, element.val());
162
+ }
127
163
  });
128
164
  element.bind("keyup", function () {
129
- if (element.val() !== element.data("keyup-change-value")) {
165
+ var val= $.data(element, key);
166
+ if (val !== undefined && element.val() !== val) {
167
+ $.removeData(element, key);
130
168
  element.trigger("keyup-change");
131
169
  }
132
170
  });
133
171
  }
134
172
 
173
+ $(document).delegate("*", "mousemove", function (e) {
174
+ $.data(document, "select2-lastpos", {x: e.pageX, y: e.pageY});
175
+ });
176
+
135
177
  /**
136
178
  * filters mouse events so an event is fired only if the mouse moved.
137
179
  *
138
180
  * filters out mouse events that occur when mouse is stationary but
139
181
  * the elements under the pointer are scrolled.
140
182
  */
141
- $(document).delegate("*", "mousemove", function (e) {
142
- $(document).data("select2-lastpos", {x: e.pageX, y: e.pageY});
143
- });
144
183
  function installFilteredMouseMove(element) {
145
- element.bind("mousemove", function (e) {
146
- var lastpos = $(document).data("select2-lastpos");
184
+ element.bind("mousemove", function (e) {
185
+ var lastpos = $.data(document, "select2-lastpos");
147
186
  if (lastpos === undefined || lastpos.x !== e.pageX || lastpos.y !== e.pageY) {
148
187
  $(e.target).trigger("mousemove-filtered", e);
149
188
  }
@@ -166,6 +205,20 @@
166
205
  };
167
206
  }
168
207
 
208
+ /**
209
+ * A simple implementation of a thunk
210
+ * @param formula function used to lazily initialize the thunk
211
+ * @return {Function}
212
+ */
213
+ function thunk(formula) {
214
+ var evaluated = false,
215
+ value;
216
+ return function() {
217
+ if (evaluated === false) { value = formula(); evaluated = true; }
218
+ return value;
219
+ };
220
+ };
221
+
169
222
  function installDebouncedScroll(threshold, element) {
170
223
  var notify = debounce(threshold, function (e) { element.trigger("scroll-debounced", e);});
171
224
  element.bind("scroll", function (e) {
@@ -179,25 +232,41 @@
179
232
  }
180
233
 
181
234
  function measureTextWidth(e) {
182
- var sizer, width;
183
- sizer = $("<div></div>").css({
184
- position: "absolute",
185
- left: "-1000px",
186
- top: "-1000px",
187
- display: "none",
188
- fontSize: e.css("fontSize"),
189
- fontFamily: e.css("fontFamily"),
190
- fontStyle: e.css("fontStyle"),
191
- fontWeight: e.css("fontWeight"),
192
- letterSpacing: e.css("letterSpacing"),
193
- textTransform: e.css("textTransform"),
194
- whiteSpace: "nowrap"
195
- });
235
+ if (!sizer){
236
+ var style = e[0].currentStyle || window.getComputedStyle(e[0], null);
237
+ sizer = $("<div></div>").css({
238
+ position: "absolute",
239
+ left: "-10000px",
240
+ top: "-10000px",
241
+ display: "none",
242
+ fontSize: style.fontSize,
243
+ fontFamily: style.fontFamily,
244
+ fontStyle: style.fontStyle,
245
+ fontWeight: style.fontWeight,
246
+ letterSpacing: style.letterSpacing,
247
+ textTransform: style.textTransform,
248
+ whiteSpace: "nowrap"
249
+ });
250
+ $("body").append(sizer);
251
+ }
196
252
  sizer.text(e.val());
197
- $("body").append(sizer);
198
- width = sizer.width();
199
- sizer.remove();
200
- return width;
253
+ return sizer.width();
254
+ }
255
+
256
+ function markMatch(text, term, markup) {
257
+ var match=text.toUpperCase().indexOf(term.toUpperCase()),
258
+ tl=term.length;
259
+
260
+ if (match<0) {
261
+ markup.push(text);
262
+ return;
263
+ }
264
+
265
+ markup.push(text.substring(0, match));
266
+ markup.push("<span class='select2-match'>");
267
+ markup.push(text.substring(match, match + tl));
268
+ markup.push("</span>");
269
+ markup.push(text.substring(match + tl, text.length));
201
270
  }
202
271
 
203
272
  /**
@@ -227,17 +296,18 @@
227
296
  requestSequence += 1; // increment the sequence
228
297
  var requestNumber = requestSequence, // this request's sequence number
229
298
  data = options.data, // ajax data function
230
- transport = options.transport || $.ajax;
299
+ transport = options.transport || $.ajax,
300
+ type = options.type || 'GET'; // set type of request (GET or POST)
231
301
 
232
302
  data = data.call(this, query.term, query.page, query.context);
233
303
 
234
- if( null !== handler){
235
- handler.abort();
236
- }
304
+ if( null !== handler) { handler.abort(); }
305
+
237
306
  handler = transport.call(null, {
238
307
  url: options.url,
239
308
  dataType: options.dataType,
240
309
  data: data,
310
+ type: type,
241
311
  success: function (data) {
242
312
  if (requestNumber < requestSequence) {
243
313
  return;
@@ -267,12 +337,16 @@
267
337
  */
268
338
  function local(options) {
269
339
  var data = options, // data elements
340
+ dataText,
270
341
  text = function (item) { return ""+item.text; }; // function used to retrieve the text portion of a data item that is matched against the search
271
342
 
272
343
  if (!$.isArray(data)) {
273
344
  text = data.text;
274
345
  // if text is not a function we assume it to be a key name
275
- if (!$.isFunction(text)) text = function (item) { return item[data.text]; };
346
+ if (!$.isFunction(text)) {
347
+ dataText = data.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
348
+ text = function (item) { return item[dataText]; };
349
+ }
276
350
  data = data.results;
277
351
  }
278
352
 
@@ -312,18 +386,53 @@
312
386
  };
313
387
  }
314
388
 
389
+ /**
390
+ * Checks if the formatter function should be used.
391
+ *
392
+ * Throws an error if it is not a function. Returns true if it should be used,
393
+ * false if no formatting should be performed.
394
+ *
395
+ * @param formatter
396
+ */
397
+ function checkFormatter(formatter, formatterName) {
398
+ if ($.isFunction(formatter)) return true;
399
+ if (!formatter) return fasle;
400
+ throw new Error("formatterName must be a function or a falsy value");
401
+ }
402
+
315
403
  /**
316
404
  * blurs any Select2 container that has focus when an element outside them was clicked or received focus
405
+ *
406
+ * also takes care of clicks on label tags that point to the source element
317
407
  */
318
408
  $(document).ready(function () {
319
- $(document).delegate("*", "mousedown focusin touchend", function (e) {
320
- var target = $(e.target).closest("div.select2-container").get(0);
321
- $(document).find("div.select2-container-active").each(function () {
322
- if (this !== target) $(this).data("select2").blur();
323
- });
409
+ $(document).delegate("*", "mousedown touchend", function (e) {
410
+ var target = $(e.target).closest("div.select2-container").get(0), attr;
411
+ if (target) {
412
+ $(document).find("div.select2-container-active").each(function () {
413
+ if (this !== target) $(this).data("select2").blur();
414
+ });
415
+ } else {
416
+ target = $(e.target).closest("div.select2-drop").get(0);
417
+ $(document).find("div.select2-drop-active").each(function () {
418
+ if (this !== target) $(this).data("select2").blur();
419
+ });
420
+ }
421
+
422
+ target=$(e.target);
423
+ attr = target.attr("for");
424
+ if ("LABEL" === e.target.tagName && attr && attr.length > 0) {
425
+ target = $("#"+attr);
426
+ target = target.data("select2");
427
+ if (target !== undefined) { target.focus(); e.preventDefault();}
428
+ }
324
429
  });
325
430
  });
326
431
 
432
+ function evaluate(val) {
433
+ return $.isFunction(val) ? val() : val;
434
+ }
435
+
327
436
  /**
328
437
  * Creates a new class
329
438
  *
@@ -341,6 +450,7 @@
341
450
 
342
451
  AbstractSelect2 = clazz(Object, {
343
452
 
453
+ // abstract
344
454
  bind: function (func) {
345
455
  var self = this;
346
456
  return function () {
@@ -348,6 +458,7 @@
348
458
  };
349
459
  },
350
460
 
461
+ // abstract
351
462
  init: function (opts) {
352
463
  var results, search, resultsSelector = ".select2-results";
353
464
 
@@ -365,10 +476,16 @@
365
476
  this.enabled=true;
366
477
  this.container = this.createContainer();
367
478
 
479
+ // cache the body so future lookups are cheap
480
+ this.body = thunk(function() { return opts.element.closest("body"); });
481
+
368
482
  if (opts.element.attr("class") !== undefined) {
369
483
  this.container.addClass(opts.element.attr("class"));
370
484
  }
371
485
 
486
+ this.container.css(evaluate(opts.containerCss));
487
+ this.container.addClass(evaluate(opts.containerCssClass));
488
+
372
489
  // swap container for the element
373
490
  this.opts.element
374
491
  .data("select2", this)
@@ -377,20 +494,27 @@
377
494
  this.container.data("select2", this);
378
495
 
379
496
  this.dropdown = this.container.find(".select2-drop");
497
+ this.dropdown.css(evaluate(opts.dropdownCss));
498
+ this.dropdown.addClass(evaluate(opts.dropdownCssClass));
499
+ this.dropdown.data("select2", this);
500
+
380
501
  this.results = results = this.container.find(resultsSelector);
381
- this.search = search = this.container.find("input[type=text]");
502
+ this.search = search = this.container.find("input.select2-input");
503
+
504
+ search.attr("tabIndex", this.opts.element.attr("tabIndex"));
382
505
 
383
506
  this.resultsPage = 0;
384
507
  this.context = null;
385
508
 
386
509
  // initialize the container
387
510
  this.initContainer();
511
+ this.initContainerWidth();
388
512
 
389
513
  installFilteredMouseMove(this.results);
390
- this.container.delegate(resultsSelector, "mousemove-filtered", this.bind(this.highlightUnderEvent));
514
+ this.dropdown.delegate(resultsSelector, "mousemove-filtered", this.bind(this.highlightUnderEvent));
391
515
 
392
516
  installDebouncedScroll(80, this.results);
393
- this.container.delegate(resultsSelector, "scroll-debounced", this.bind(this.loadMoreIfNeeded));
517
+ this.dropdown.delegate(resultsSelector, "scroll-debounced", this.bind(this.loadMoreIfNeeded));
394
518
 
395
519
  // if jquery.mousewheel plugin is installed we can prevent out-of-bounds scrolling of results via mousewheel
396
520
  if ($.fn.mousewheel) {
@@ -408,19 +532,24 @@
408
532
 
409
533
  installKeyUpChangeEvent(search);
410
534
  search.bind("keyup-change", this.bind(this.updateResults));
411
- search.bind("focus", function () { search.addClass("select2-focused");});
535
+ search.bind("focus", function () { search.addClass("select2-focused"); if (search.val() === " ") search.val(""); });
412
536
  search.bind("blur", function () { search.removeClass("select2-focused");});
413
537
 
414
- this.container.delegate(resultsSelector, "click", this.bind(function (e) {
415
- if ($(e.target).closest(".select2-result:not(.select2-disabled)").length > 0) {
538
+ this.dropdown.delegate(resultsSelector, "mouseup", this.bind(function (e) {
539
+ if ($(e.target).closest(".select2-result-selectable:not(.select2-disabled)").length > 0) {
416
540
  this.highlightUnderEvent(e);
417
541
  this.selectHighlighted(e);
418
542
  } else {
419
- killEvent(e);
420
543
  this.focusSearch();
421
544
  }
545
+ killEvent(e);
422
546
  }));
423
547
 
548
+ // trap all mouse events from leaving the dropdown. sometimes there may be a modal that is listening
549
+ // for mouse events outside of itself so it can close itself. since the dropdown is now outside the select2's
550
+ // dom it will trigger the popup close, which is not what we want
551
+ this.dropdown.bind("click mouseup mousedown", function (e) { e.stopPropagation(); });
552
+
424
553
  if ($.isFunction(this.opts.initSelection)) {
425
554
  // initialize selection based on the current value of the source element
426
555
  this.initSelection();
@@ -433,10 +562,12 @@
433
562
  if (opts.element.is(":disabled")) this.disable();
434
563
  },
435
564
 
565
+ // abstract
436
566
  destroy: function () {
437
567
  var select2 = this.opts.element.data("select2");
438
568
  if (select2 !== undefined) {
439
569
  select2.container.remove();
570
+ select2.dropdown.remove();
440
571
  select2.opts.element
441
572
  .removeData("select2")
442
573
  .unbind(".select2")
@@ -444,8 +575,9 @@
444
575
  }
445
576
  },
446
577
 
578
+ // abstract
447
579
  prepareOpts: function (opts) {
448
- var element, select, idKey;
580
+ var element, select, idKey, ajaxUrl;
449
581
 
450
582
  element = opts.element;
451
583
 
@@ -453,6 +585,9 @@
453
585
  this.select = select = opts.element;
454
586
  }
455
587
 
588
+ //Custom tags separator.
589
+ opts.separator = opts.separator || ",";
590
+
456
591
  if (select) {
457
592
  // these options are not allowed when attached to a select because they are picked up off the element itself
458
593
  $.each(["id", "multiple", "ajax", "query", "createSearchChoice", "initSelection", "data", "tags"], function () {
@@ -463,17 +598,50 @@
463
598
  }
464
599
 
465
600
  opts = $.extend({}, {
466
- formatResult: function (data) { return data.text; },
467
- formatSelection: function (data) { return data.text; },
468
- formatNoMatches: function () { return "No matches found"; },
469
- formatInputTooShort: function (input, min) { return "Please enter " + (min - input.length) + " more characters"; },
470
- minimumResultsForSearch: 0,
471
- minimumInputLength: 0,
472
- id: function (e) { return e.id; },
473
- matcher: function(term, text) {
474
- return text.toUpperCase().indexOf(term.toUpperCase()) >= 0;
601
+ populateResults: function(container, results, query) {
602
+ var populate, data, result, children, id=this.opts.id;
603
+
604
+ populate=function(results, container, depth) {
605
+
606
+ var i, l, result, selectable, compound, node, label, innerContainer, formatted;
607
+ for (i = 0, l = results.length; i < l; i = i + 1) {
608
+
609
+ result=results[i];
610
+ selectable=id(result) !== undefined;
611
+ compound=("children" in result) && result.children.length > 0;
612
+
613
+ node=$("<li></li>");
614
+ node.addClass("select2-results-dept-"+depth);
615
+ node.addClass("select2-result");
616
+ node.addClass(selectable ? "select2-result-selectable" : "select2-result-unselectable");
617
+ if (compound) { node.addClass("select2-result-with-children"); }
618
+
619
+ label=$("<div></div>");
620
+ label.addClass("select2-result-label");
621
+
622
+ formatted=opts.formatResult(result, label, query);
623
+ if (formatted!==undefined) {
624
+ label.html(escapeMarkup(formatted));
625
+ }
626
+
627
+ node.append(label);
628
+
629
+ if (compound) {
630
+
631
+ innerContainer=$("<ul></ul>");
632
+ innerContainer.addClass("select2-result-sub");
633
+ populate(result.children, innerContainer, depth+1);
634
+ node.append(innerContainer);
635
+ }
636
+
637
+ node.data("select2-data", result);
638
+ container.append(node);
639
+ }
640
+ };
641
+
642
+ populate(results, container, 0);
475
643
  }
476
- }, opts);
644
+ }, $.fn.select2.defaults, opts);
477
645
 
478
646
  if (typeof(opts.id) !== "function") {
479
647
  idKey = opts.id;
@@ -482,19 +650,37 @@
482
650
 
483
651
  if (select) {
484
652
  opts.query = this.bind(function (query) {
485
- var data = {results: [], more: false},
653
+ var data = { results: [], more: false },
486
654
  term = query.term,
487
- placeholder = this.getPlaceholder();
488
- element.find("option").each(function (i) {
489
- var e = $(this),
490
- text = e.text();
655
+ children, firstChild, process;
656
+
657
+ process=function(element, collection) {
658
+ var group;
659
+ if (element.is("option")) {
660
+ if (query.matcher(term, element.text(), element)) {
661
+ collection.push({id:element.attr("value"), text:element.text(), element: element.get()});
662
+ }
663
+ } else if (element.is("optgroup")) {
664
+ group={text:element.attr("label"), children:[], element: element.get()};
665
+ element.children().each2(function(i, elm) { process(elm, group.children); });
666
+ if (group.children.length>0) {
667
+ collection.push(group);
668
+ }
669
+ }
670
+ };
491
671
 
492
- if (i === 0 && placeholder !== undefined && text === "") return true;
672
+ children=element.children();
493
673
 
494
- if (query.matcher(term, text)) {
495
- data.results.push({id: e.attr("value"), text: text});
674
+ // ignore the placeholder option if there is one
675
+ if (this.getPlaceholder() !== undefined && children.length > 0) {
676
+ firstChild = children[0];
677
+ if ($(firstChild).text() === "") {
678
+ children=children.not(firstChild);
496
679
  }
497
- });
680
+ }
681
+
682
+ children.each2(function(i, elm) { process(elm, data.results); });
683
+
498
684
  query.callback(data);
499
685
  });
500
686
  // this is needed because inside val() we construct choices from options and there id is hardcoded
@@ -502,18 +688,26 @@
502
688
  } else {
503
689
  if (!("query" in opts)) {
504
690
  if ("ajax" in opts) {
691
+ ajaxUrl = opts.element.data("ajax-url");
692
+ if (ajaxUrl && ajaxUrl.length > 0) {
693
+ opts.ajax.url = ajaxUrl;
694
+ }
505
695
  opts.query = ajax(opts.ajax);
506
696
  } else if ("data" in opts) {
507
697
  opts.query = local(opts.data);
508
698
  } else if ("tags" in opts) {
509
699
  opts.query = tags(opts.tags);
510
700
  opts.createSearchChoice = function (term) { return {id: term, text: term}; };
511
- opts.initSelection = function (element) {
701
+ opts.initSelection = function (element, callback) {
512
702
  var data = [];
513
- $(splitVal(element.val(), ",")).each(function () {
514
- data.push({id: this, text: this});
703
+ $(splitVal(element.val(), opts.separator)).each(function () {
704
+ var id = this, text = this, tags=opts.tags;
705
+ if ($.isFunction(tags)) tags=tags();
706
+ $(tags).each(function() { if (equal(this.id, id)) { text = this.text; return false; } });
707
+ data.push({id: id, text: text});
515
708
  });
516
- return data;
709
+
710
+ callback(data);
517
711
  };
518
712
  }
519
713
  }
@@ -528,6 +722,7 @@
528
722
  /**
529
723
  * Monitor the original element for changes and update select2 accordingly
530
724
  */
725
+ // abstract
531
726
  monitorSource: function () {
532
727
  this.opts.element.bind("change.select2", this.bind(function (e) {
533
728
  if (this.opts.element.data("select2-change-triggered") !== true) {
@@ -539,14 +734,23 @@
539
734
  /**
540
735
  * Triggers the change event on the source element
541
736
  */
542
- triggerChange: function () {
543
- // Prevents recursive triggering
737
+ // abstract
738
+ triggerChange: function (details) {
739
+
740
+ details = details || {};
741
+ details= $.extend({}, details, { type: "change", val: this.val() });
742
+ // prevents recursive triggering
544
743
  this.opts.element.data("select2-change-triggered", true);
545
- this.opts.element.trigger("change");
744
+ this.opts.element.trigger(details);
546
745
  this.opts.element.data("select2-change-triggered", false);
746
+
747
+ // some validation frameworks ignore the change event and listen instead to keyup, click for selects
748
+ // so here we trigger the click event manually
749
+ this.opts.element.click();
547
750
  },
548
751
 
549
752
 
753
+ // abstract
550
754
  enable: function() {
551
755
  if (this.enabled) return;
552
756
 
@@ -554,6 +758,7 @@
554
758
  this.container.removeClass("select2-container-disabled");
555
759
  },
556
760
 
761
+ // abstract
557
762
  disable: function() {
558
763
  if (!this.enabled) return;
559
764
 
@@ -563,42 +768,156 @@
563
768
  this.container.addClass("select2-container-disabled");
564
769
  },
565
770
 
771
+ // abstract
566
772
  opened: function () {
567
773
  return this.container.hasClass("select2-dropdown-open");
568
774
  },
569
775
 
776
+ // abstract
777
+ positionDropdown: function() {
778
+ var offset = this.container.offset(),
779
+ height = this.container.outerHeight(),
780
+ width = this.container.outerWidth(),
781
+ dropHeight = this.dropdown.outerHeight(),
782
+ viewportBottom = $(window).scrollTop() + document.documentElement.clientHeight,
783
+ dropTop = offset.top + height,
784
+ enoughRoomBelow = dropTop + dropHeight <= viewportBottom,
785
+ enoughRoomAbove = (offset.top - dropHeight) >= this.body().scrollTop(),
786
+ aboveNow = this.dropdown.hasClass("select2-drop-above"),
787
+ above,
788
+ css;
789
+
790
+ // console.log("below/ droptop:", dropTop, "dropHeight", dropHeight, "sum", (dropTop+dropHeight)+" viewport bottom", viewportBottom, "enough?", enoughRoomBelow);
791
+ // console.log("above/ offset.top", offset.top, "dropHeight", dropHeight, "top", (offset.top-dropHeight), "scrollTop", this.body().scrollTop(), "enough?", enoughRoomAbove);
792
+
793
+ // always prefer the current above/below alignment, unless there is not enough room
794
+
795
+ if (aboveNow) {
796
+ above = true;
797
+ if (!enoughRoomAbove && enoughRoomBelow) above = false;
798
+ } else {
799
+ above = false;
800
+ if (!enoughRoomBelow && enoughRoomAbove) above = true;
801
+ }
802
+
803
+ if (above) {
804
+ dropTop = offset.top - dropHeight;
805
+ this.container.addClass("select2-drop-above");
806
+ this.dropdown.addClass("select2-drop-above");
807
+ }
808
+ else {
809
+ this.container.removeClass("select2-drop-above");
810
+ this.dropdown.removeClass("select2-drop-above");
811
+ }
812
+
813
+ css = {
814
+ top:dropTop,
815
+ left:offset.left,
816
+ width:width
817
+ };
818
+
819
+ this.dropdown.css(css);
820
+ },
821
+
822
+ // abstract
823
+ shouldOpen: function() {
824
+ var event;
825
+
826
+ if (this.opened()) return false;
827
+
828
+ event = jQuery.Event("open");
829
+ this.opts.element.trigger(event);
830
+ return !event.isDefaultPrevented();
831
+ },
832
+
833
+ // abstract
834
+ clearDropdownAlignmentPreference: function() {
835
+ // clear the classes used to figure out the preference of where the dropdown should be opened
836
+ this.container.removeClass("select2-drop-above");
837
+ this.dropdown.removeClass("select2-drop-above");
838
+ },
839
+
840
+ /**
841
+ * Opens the dropdown
842
+ *
843
+ * @return {Boolean} whether or not dropdown was opened. This method will return false if, for example,
844
+ * the dropdown is already open, or if the 'open' event listener on the element called preventDefault().
845
+ */
846
+ // abstract
570
847
  open: function () {
571
- if (this.opened()) return;
572
848
 
849
+ if (!this.shouldOpen()) return false;
850
+
851
+ window.setTimeout(this.bind(this.opening), 1);
852
+
853
+ return true;
854
+ },
855
+
856
+ /**
857
+ * Performs the opening of the dropdown
858
+ */
859
+ // abstract
860
+ opening: function() {
861
+ this.clearDropdownAlignmentPreference();
862
+
863
+ if (this.search.val() === " ") { this.search.val(""); }
864
+
865
+ this.dropdown.addClass("select2-drop-active");
573
866
  this.container.addClass("select2-dropdown-open").addClass("select2-container-active");
574
867
 
575
868
  this.updateResults(true);
869
+
870
+ if(this.dropdown[0] !== this.body().children().last()[0]) {
871
+ this.dropdown.detach().appendTo(this.body());
872
+ }
873
+
576
874
  this.dropdown.show();
577
875
  this.ensureHighlightVisible();
876
+
877
+ this.positionDropdown();
878
+
578
879
  this.focusSearch();
579
880
  },
580
881
 
882
+ // abstract
581
883
  close: function () {
582
884
  if (!this.opened()) return;
583
885
 
886
+ this.clearDropdownAlignmentPreference();
887
+
584
888
  this.dropdown.hide();
585
- this.container.removeClass("select2-dropdown-open");
889
+ this.container.removeClass("select2-dropdown-open").removeClass("select2-container-active");
586
890
  this.results.empty();
587
891
  this.clearSearch();
892
+
893
+ this.opts.element.trigger(jQuery.Event("close"));
588
894
  },
589
895
 
896
+ // abstract
590
897
  clearSearch: function () {
591
898
 
592
899
  },
593
900
 
901
+ // abstract
594
902
  ensureHighlightVisible: function () {
595
903
  var results = this.results, children, index, child, hb, rb, y, more;
596
904
 
597
- children = results.children(".select2-result");
598
905
  index = this.highlight();
599
906
 
600
907
  if (index < 0) return;
601
908
 
909
+ if (index == 0) {
910
+
911
+ // if the first element is highlighted scroll all the way to the top,
912
+ // that way any unselectable headers above it will also be scrolled
913
+ // into view
914
+
915
+ results.scrollTop(0);
916
+ return;
917
+ }
918
+
919
+ children = results.find(".select2-result-selectable");
920
+
602
921
  child = $(children[index]);
603
922
 
604
923
  hb = child.offset().top + child.outerHeight();
@@ -623,84 +942,89 @@
623
942
  }
624
943
  },
625
944
 
945
+ // abstract
626
946
  moveHighlight: function (delta) {
627
- var choices = this.results.children(".select2-result"),
947
+ var choices = this.results.find(".select2-result-selectable"),
628
948
  index = this.highlight();
629
949
 
630
950
  while (index > -1 && index < choices.length) {
631
951
  index += delta;
632
- if (!$(choices[index]).hasClass("select2-disabled")) {
952
+ var choice = $(choices[index]);
953
+ if (choice.hasClass("select2-result-selectable") && !choice.hasClass("select2-disabled")) {
633
954
  this.highlight(index);
634
955
  break;
635
956
  }
636
957
  }
637
958
  },
638
959
 
960
+ // abstract
639
961
  highlight: function (index) {
640
- var choices = this.results.children(".select2-result");
962
+ var choices = this.results.find(".select2-result-selectable").not(".select2-disabled");
641
963
 
642
964
  if (arguments.length === 0) {
643
965
  return indexOf(choices.filter(".select2-highlighted")[0], choices.get());
644
966
  }
645
967
 
646
- choices.removeClass("select2-highlighted");
647
-
648
968
  if (index >= choices.length) index = choices.length - 1;
649
969
  if (index < 0) index = 0;
650
970
 
971
+ choices.removeClass("select2-highlighted");
972
+
651
973
  $(choices[index]).addClass("select2-highlighted");
652
974
  this.ensureHighlightVisible();
653
975
 
654
- if (this.opened()) this.focusSearch();
655
976
  },
656
977
 
978
+ // abstract
979
+ countSelectableResults: function() {
980
+ return this.results.find(".select2-result-selectable").not(".select2-disabled").length;
981
+ },
982
+
983
+ // abstract
657
984
  highlightUnderEvent: function (event) {
658
- var el = $(event.target).closest(".select2-result");
659
- if (el.length > 0) {
660
- this.highlight(el.index());
985
+ var el = $(event.target).closest(".select2-result-selectable");
986
+ if (el.length > 0 && !el.is(".select2-highlighted")) {
987
+ var choices = this.results.find('.select2-result-selectable');
988
+ this.highlight(choices.index(el));
989
+ } else if (el.length == 0) {
990
+ // if we are over an unselectable item remove al highlights
991
+ this.results.find(".select2-highlighted").removeClass("select2-highlighted");
661
992
  }
662
993
  },
663
994
 
995
+ // abstract
664
996
  loadMoreIfNeeded: function () {
665
997
  var results = this.results,
666
998
  more = results.find("li.select2-more-results"),
667
999
  below, // pixels the element is below the scroll fold, below==0 is when the element is starting to be visible
668
1000
  offset = -1, // index of first element without data
669
- page = this.resultsPage + 1;
1001
+ page = this.resultsPage + 1,
1002
+ self=this,
1003
+ term=this.search.val(),
1004
+ context=this.context;
670
1005
 
671
1006
  if (more.length === 0) return;
672
-
673
1007
  below = more.offset().top - results.offset().top - results.height();
674
1008
 
675
1009
  if (below <= 0) {
676
1010
  more.addClass("select2-active");
677
1011
  this.opts.query({
678
- term: this.search.val(),
1012
+ term: term,
679
1013
  page: page,
680
- context: this.context,
1014
+ context: context,
681
1015
  matcher: this.opts.matcher,
682
1016
  callback: this.bind(function (data) {
683
- var parts = [], self = this;
684
- $(data.results).each(function () {
685
- parts.push("<li class='select2-result'>");
686
- parts.push(self.opts.formatResult(this));
687
- parts.push("</li>");
688
- });
689
- more.before(parts.join(""));
690
- results.find(".select2-result").each(function (i) {
691
- var e = $(this);
692
- if (e.data("select2-data") !== undefined) {
693
- offset = i;
694
- } else {
695
- e.data("select2-data", data.results[i - offset - 1]);
696
- }
697
- });
698
- if (data.more) {
699
- more.removeClass("select2-active");
1017
+
1018
+ self.opts.populateResults.call(this, results, data.results, {term: term, page: page, context:context});
1019
+
1020
+ if (data.more===true) {
1021
+ more.detach().appendTo(results.children(":last")).text(self.opts.formatLoadMore(page+1));
1022
+ window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10);
700
1023
  } else {
701
1024
  more.remove();
702
1025
  }
703
- this.resultsPage = page;
1026
+ self.positionDropdown();
1027
+ self.resultsPage = page;
704
1028
  })});
705
1029
  }
706
1030
  },
@@ -708,23 +1032,37 @@
708
1032
  /**
709
1033
  * @param initial whether or not this is the call to this method right after the dropdown has been opened
710
1034
  */
1035
+ // abstract
711
1036
  updateResults: function (initial) {
712
- var search = this.search, results = this.results, opts = this.opts, self=this;
1037
+ var search = this.search, results = this.results, opts = this.opts, data, self=this;
713
1038
 
714
1039
  // if the search is currently hidden we do not alter the results
715
- if (initial !== true && this.showSearchInput === false) {
1040
+ if (initial !== true && (this.showSearchInput === false || !this.opened())) {
716
1041
  return;
717
1042
  }
718
1043
 
719
1044
  search.addClass("select2-active");
720
1045
 
721
- function render(html) {
722
- results.html(html);
1046
+ function postRender() {
723
1047
  results.scrollTop(0);
724
1048
  search.removeClass("select2-active");
1049
+ self.positionDropdown();
725
1050
  }
726
1051
 
727
- if (search.val().length < opts.minimumInputLength) {
1052
+ function render(html) {
1053
+ results.html(escapeMarkup(html));
1054
+ postRender();
1055
+ }
1056
+
1057
+ if (opts.maximumSelectionSize >=1) {
1058
+ data = this.data();
1059
+ if ($.isArray(data) && data.length >= opts.maximumSelectionSize && checkFormatter(opts.formatSelectionTooBig, "formatSelectionTooBig")) {
1060
+ render("<li class='select2-selection-limit'>" + opts.formatSelectionTooBig(opts.maximumSelectionSize) + "</li>");
1061
+ return;
1062
+ }
1063
+ }
1064
+
1065
+ if (search.val().length < opts.minimumInputLength && checkFormatter(opts.formatInputTooShort, "formatInputTooShort")) {
728
1066
  render("<li class='select2-no-results'>" + opts.formatInputTooShort(search.val(), opts.minimumInputLength) + "</li>");
729
1067
  return;
730
1068
  }
@@ -736,8 +1074,7 @@
736
1074
  context: null,
737
1075
  matcher: opts.matcher,
738
1076
  callback: this.bind(function (data) {
739
- var parts = [], // html parts
740
- def; // default choice
1077
+ var def; // default choice
741
1078
 
742
1079
  // save context, if any
743
1080
  this.context = (data.context===undefined) ? null : data.context;
@@ -755,63 +1092,70 @@
755
1092
  }
756
1093
  }
757
1094
 
758
- if (data.results.length === 0) {
1095
+ if (data.results.length === 0 && checkFormatter(opts.formatNoMatches, "formatNoMatches")) {
759
1096
  render("<li class='select2-no-results'>" + opts.formatNoMatches(search.val()) + "</li>");
760
1097
  return;
761
1098
  }
762
1099
 
763
- $(data.results).each(function () {
764
- parts.push("<li class='select2-result'>");
765
- parts.push(opts.formatResult(this));
766
- parts.push("</li>");
767
- });
1100
+ results.empty();
1101
+ self.opts.populateResults.call(this, results, data.results, {term: search.val(), page: this.resultsPage, context:null});
768
1102
 
769
- if (data.more === true) {
770
- parts.push("<li class='select2-more-results'>Loading more results...</li>");
1103
+ if (data.more === true && checkFormatter(opts.formatLoadMore, "formatLoadMore")) {
1104
+ results.children().filter(":last").append("<li class='select2-more-results'>" + escapeMarkup(opts.formatLoadMore(this.resultsPage)) + "</li>");
1105
+ window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10);
771
1106
  }
772
1107
 
773
- render(parts.join(""));
774
- results.children(".select2-result").each(function (i) {
775
- var d = data.results[i];
776
- $(this).data("select2-data", d);
777
- });
778
1108
  this.postprocessResults(data, initial);
1109
+
1110
+ postRender();
779
1111
  })});
780
1112
  },
781
1113
 
1114
+ // abstract
782
1115
  cancel: function () {
783
1116
  this.close();
784
1117
  },
785
1118
 
1119
+ // abstract
786
1120
  blur: function () {
787
- /* we do this in a timeout so that current event processing can complete before this code is executed.
788
- this allows tab index to be preserved even if this code blurs the textfield */
789
- window.setTimeout(this.bind(function () {
790
- this.close();
791
- this.container.removeClass("select2-container-active");
792
- this.clearSearch();
793
- this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
794
- this.search.blur();
795
- }), 10);
1121
+ this.close();
1122
+ this.container.removeClass("select2-container-active");
1123
+ this.dropdown.removeClass("select2-drop-active");
1124
+ // synonymous to .is(':focus'), which is available in jquery >= 1.6
1125
+ if (this.search[0] === document.activeElement) { this.search.blur(); }
1126
+ this.clearSearch();
1127
+ this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
796
1128
  },
797
1129
 
1130
+ // abstract
798
1131
  focusSearch: function () {
799
1132
  /* we do this in a timeout so that current event processing can complete before this code is executed.
800
1133
  this makes sure the search field is focussed even if the current event would blur it */
801
1134
  window.setTimeout(this.bind(function () {
802
1135
  this.search.focus();
1136
+ // reset the value so IE places the cursor at the end of the input box
1137
+ this.search.val(this.search.val());
803
1138
  }), 10);
804
1139
  },
805
1140
 
1141
+ // abstract
806
1142
  selectHighlighted: function () {
807
- var data = this.results.find(".select2-highlighted:not(.select2-disabled)").data("select2-data");
1143
+ var index=this.highlight(),
1144
+ highlighted=this.results.find(".select2-highlighted").not(".select2-disabled"),
1145
+ data = highlighted.closest('.select2-result-selectable').data("select2-data");
808
1146
  if (data) {
1147
+ highlighted.addClass("select2-disabled");
1148
+ this.highlight(index);
809
1149
  this.onSelect(data);
810
1150
  }
811
1151
  },
812
1152
 
1153
+ // abstract
813
1154
  getPlaceholder: function () {
814
- return this.opts.element.attr("placeholder") || this.opts.element.data("placeholder") || this.opts.placeholder;
1155
+ return this.opts.element.attr("placeholder") ||
1156
+ this.opts.element.attr("data-placeholder") || // jquery 1.4 compat
1157
+ this.opts.element.data("placeholder") ||
1158
+ this.opts.placeholder;
815
1159
  },
816
1160
 
817
1161
  /**
@@ -819,178 +1163,296 @@
819
1163
  * derived first from option `width` passed to select2, then
820
1164
  * the inline 'style' on the original element, and finally
821
1165
  * falls back to the jQuery calculated element width.
822
- *
823
- * @returns The width string (with units) for the container.
824
1166
  */
825
- getContainerWidth: function () {
826
- var style, attrs, matches, i, l;
827
- if (this.opts.width !== undefined)
828
- return this.opts.width;
829
-
830
- style = this.opts.element.attr('style');
831
- if (style !== undefined) {
832
- attrs = style.split(';');
833
- for (i = 0, l = attrs.length; i < l; i = i + 1) {
834
- matches = attrs[i].replace(/\s/g, '')
835
- .match(/width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/);
836
- if (matches !== null && matches.length >= 1)
837
- return matches[1];
838
- }
1167
+ // abstract
1168
+ initContainerWidth: function () {
1169
+ function resolveContainerWidth() {
1170
+ var style, attrs, matches, i, l;
1171
+
1172
+ if (this.opts.width === "off") {
1173
+ return null;
1174
+ } else if (this.opts.width === "element"){
1175
+ return this.opts.element.outerWidth() === 0 ? 'auto' : this.opts.element.outerWidth() + 'px';
1176
+ } else if (this.opts.width === "copy" || this.opts.width === "resolve") {
1177
+ // check if there is inline style on the element that contains width
1178
+ style = this.opts.element.attr('style');
1179
+ if (style !== undefined) {
1180
+ attrs = style.split(';');
1181
+ for (i = 0, l = attrs.length; i < l; i = i + 1) {
1182
+ matches = attrs[i].replace(/\s/g, '')
1183
+ .match(/width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/);
1184
+ if (matches !== null && matches.length >= 1)
1185
+ return matches[1];
1186
+ }
1187
+ }
1188
+
1189
+ if (this.opts.width === "resolve") {
1190
+ // next check if css('width') can resolve a width that is percent based, this is sometimes possible
1191
+ // when attached to input type=hidden or elements hidden via css
1192
+ style = this.opts.element.css('width');
1193
+ if (style.indexOf("%") > 0) return style;
1194
+
1195
+ // finally, fallback on the calculated width of the element
1196
+ return (this.opts.element.outerWidth() === 0 ? 'auto' : this.opts.element.outerWidth() + 'px');
1197
+ }
1198
+
1199
+ return null;
1200
+ } else if ($.isFunction(this.opts.width)) {
1201
+ return this.opts.width();
1202
+ } else {
1203
+ return this.opts.width;
1204
+ }
1205
+ };
1206
+
1207
+ var width = resolveContainerWidth.call(this);
1208
+ if (width !== null) {
1209
+ this.container.attr("style", "width: "+width);
839
1210
  }
840
- return this.opts.element.width() + 'px';
841
1211
  }
842
1212
  });
843
1213
 
844
1214
  SingleSelect2 = clazz(AbstractSelect2, {
845
1215
 
846
- createContainer: function () {
847
- return $("<div></div>", {
848
- "class": "select2-container",
849
- "style": "width: " + this.getContainerWidth()
1216
+ // single
1217
+
1218
+ createContainer: function () {
1219
+ var container = $("<div></div>", {
1220
+ "class": "select2-container"
850
1221
  }).html([
851
1222
  " <a href='javascript:void(0)' class='select2-choice'>",
852
1223
  " <span></span><abbr class='select2-search-choice-close' style='display:none;'></abbr>",
853
1224
  " <div><b></b></div>" ,
854
1225
  "</a>",
855
- " <div class='select2-drop' style='display:none;'>" ,
1226
+ " <div class='select2-drop select2-offscreen'>" ,
856
1227
  " <div class='select2-search'>" ,
857
- " <input type='text' autocomplete='off'/>" ,
1228
+ " <input type='text' autocomplete='off' class='select2-input'/>" ,
858
1229
  " </div>" ,
859
1230
  " <ul class='select2-results'>" ,
860
1231
  " </ul>" ,
861
1232
  "</div>"].join(""));
1233
+ return container;
862
1234
  },
863
1235
 
864
- open: function () {
865
-
866
- if (this.opened()) return;
867
-
868
- this.parent.open.apply(this, arguments);
869
-
1236
+ // single
1237
+ opening: function () {
1238
+ this.search.show();
1239
+ this.parent.opening.apply(this, arguments);
1240
+ this.dropdown.removeClass("select2-offscreen");
870
1241
  },
871
1242
 
1243
+ // single
872
1244
  close: function () {
873
1245
  if (!this.opened()) return;
874
1246
  this.parent.close.apply(this, arguments);
1247
+ this.dropdown.removeAttr("style").addClass("select2-offscreen").insertAfter(this.selection).show();
875
1248
  },
876
1249
 
1250
+ // single
877
1251
  focus: function () {
878
1252
  this.close();
879
1253
  this.selection.focus();
880
1254
  },
881
1255
 
1256
+ // single
882
1257
  isFocused: function () {
883
- return this.selection.is(":focus");
1258
+ return this.selection[0] === document.activeElement;
884
1259
  },
885
1260
 
1261
+ // single
886
1262
  cancel: function () {
887
1263
  this.parent.cancel.apply(this, arguments);
888
1264
  this.selection.focus();
889
1265
  },
890
1266
 
1267
+ // single
891
1268
  initContainer: function () {
892
1269
 
893
- var selection, container = this.container, clickingInside = false,
894
- selector = ".select2-choice";
1270
+ var selection,
1271
+ container = this.container,
1272
+ dropdown = this.dropdown,
1273
+ clickingInside = false;
895
1274
 
896
- this.selection = selection = container.find(selector);
1275
+ this.selection = selection = container.find(".select2-choice");
897
1276
 
898
1277
  this.search.bind("keydown", this.bind(function (e) {
899
- switch (e.which) {
900
- case KEY.UP:
901
- case KEY.DOWN:
902
- this.moveHighlight((e.which === KEY.UP) ? -1 : 1);
903
- killEvent(e);
904
- return;
905
- case KEY.TAB:
906
- case KEY.ENTER:
907
- this.selectHighlighted();
1278
+ if (!this.enabled) return;
1279
+
1280
+ if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) {
1281
+ // prevent the page from scrolling
908
1282
  killEvent(e);
909
1283
  return;
910
- case KEY.ESC:
911
- this.cancel(e);
912
- e.preventDefault();
913
- return;
914
1284
  }
1285
+
1286
+ if (this.opened()) {
1287
+ switch (e.which) {
1288
+ case KEY.UP:
1289
+ case KEY.DOWN:
1290
+ this.moveHighlight((e.which === KEY.UP) ? -1 : 1);
1291
+ killEvent(e);
1292
+ return;
1293
+ case KEY.TAB:
1294
+ case KEY.ENTER:
1295
+ this.selectHighlighted();
1296
+ killEvent(e);
1297
+ return;
1298
+ case KEY.ESC:
1299
+ this.cancel(e);
1300
+ killEvent(e);
1301
+ return;
1302
+ }
1303
+ } else {
1304
+
1305
+ if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) {
1306
+ return;
1307
+ }
1308
+
1309
+ this.open();
1310
+
1311
+ if (e.which === KEY.ENTER) {
1312
+ // do not propagate the event otherwise we open, and propagate enter which closes
1313
+ return;
1314
+ }
1315
+ }
1316
+ }));
1317
+
1318
+ this.search.bind("focus", this.bind(function() {
1319
+ this.selection.attr("tabIndex", "-1");
1320
+ }));
1321
+ this.search.bind("blur", this.bind(function() {
1322
+ if (!this.opened()) this.container.removeClass("select2-container-active");
1323
+ window.setTimeout(this.bind(function() { this.selection.attr("tabIndex", this.opts.element.attr("tabIndex")); }), 10);
915
1324
  }));
916
1325
 
917
- container.delegate(selector, "click", this.bind(function (e) {
1326
+ selection.bind("mousedown", this.bind(function (e) {
918
1327
  clickingInside = true;
919
1328
 
920
1329
  if (this.opened()) {
921
1330
  this.close();
922
- selection.focus();
1331
+ this.selection.focus();
923
1332
  } else if (this.enabled) {
924
1333
  this.open();
925
1334
  }
926
- e.preventDefault();
1335
+ killEvent(e);
927
1336
 
928
1337
  clickingInside = false;
929
1338
  }));
930
- container.delegate(selector, "keydown", this.bind(function (e) {
931
- if (!this.enabled || e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) {
932
- return;
933
- }
934
- this.open();
935
- if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN || e.which === KEY.SPACE) {
1339
+
1340
+ dropdown.bind("mousedown", this.bind(function() { this.search.focus(); }));
1341
+
1342
+ selection.bind("focus", this.bind(function() {
1343
+ this.container.addClass("select2-container-active");
1344
+ // hide the search so the tab key does not focus on it
1345
+ this.search.attr("tabIndex", "-1");
1346
+ }));
1347
+
1348
+ selection.bind("blur", this.bind(function() {
1349
+ this.container.removeClass("select2-container-active");
1350
+ window.setTimeout(this.bind(function() { this.search.attr("tabIndex", this.opts.element.attr("tabIndex")); }), 10);
1351
+ }));
1352
+
1353
+ selection.bind("keydown", this.bind(function(e) {
1354
+ if (!this.enabled) return;
1355
+
1356
+ if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) {
936
1357
  // prevent the page from scrolling
937
1358
  killEvent(e);
1359
+ return;
1360
+ }
1361
+
1362
+ if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) {
1363
+ return;
938
1364
  }
1365
+
1366
+ this.open();
1367
+
939
1368
  if (e.which === KEY.ENTER) {
940
1369
  // do not propagate the event otherwise we open, and propagate enter which closes
941
1370
  killEvent(e);
1371
+ return;
942
1372
  }
943
- }));
944
- container.delegate(selector, "focus", function () { if (this.enabled) container.addClass("select2-container-active"); });
945
- container.delegate(selector, "blur", this.bind(function () {
946
- if (clickingInside) return;
947
- if (!this.opened()) this.blur();
1373
+
1374
+ // do not set the search input value for non-alpha-numeric keys
1375
+ // otherwise pressing down results in a '(' being set in the search field
1376
+ if (e.which < 48 ) { // '0' == 48
1377
+ killEvent(e);
1378
+ return;
1379
+ }
1380
+
1381
+ var keyWritten = String.fromCharCode(e.which).toLowerCase();
1382
+
1383
+ if (e.shiftKey) {
1384
+ keyWritten = keyWritten.toUpperCase();
1385
+ }
1386
+
1387
+ this.search.val(keyWritten);
1388
+
1389
+ // prevent event propagation so it doesnt replay on the now focussed search field and result in double key entry
1390
+ killEvent(e);
948
1391
  }));
949
1392
 
950
- selection.delegate("abbr", "click", this.bind(function (e) {
1393
+ selection.delegate("abbr", "mousedown", this.bind(function (e) {
951
1394
  if (!this.enabled) return;
952
- this.val("");
1395
+ this.clear();
953
1396
  killEvent(e);
954
1397
  this.close();
955
1398
  this.triggerChange();
1399
+ this.selection.focus();
956
1400
  }));
957
1401
 
958
1402
  this.setPlaceholder();
1403
+
1404
+ this.search.bind("focus", this.bind(function() {
1405
+ this.container.addClass("select2-container-active");
1406
+ }));
1407
+ },
1408
+
1409
+ // single
1410
+ clear: function() {
1411
+ this.opts.element.val("");
1412
+ this.selection.find("span").empty();
1413
+ this.selection.removeData("select2-data");
1414
+ this.setPlaceholder();
959
1415
  },
960
1416
 
961
1417
  /**
962
1418
  * Sets selection based on source element's value
963
1419
  */
1420
+ // single
964
1421
  initSelection: function () {
965
1422
  var selected;
966
1423
  if (this.opts.element.val() === "") {
967
- this.updateSelection({id: "", text: ""});
1424
+ this.close();
1425
+ this.setPlaceholder();
968
1426
  } else {
969
- selected = this.opts.initSelection.call(null, this.opts.element);
970
- if (selected !== undefined && selected !== null) {
971
- this.updateSelection(selected);
972
- }
1427
+ var self = this;
1428
+ this.opts.initSelection.call(null, this.opts.element, function(selected){
1429
+ if (selected !== undefined && selected !== null) {
1430
+ self.updateSelection(selected);
1431
+ self.close();
1432
+ self.setPlaceholder();
1433
+ }
1434
+ });
973
1435
  }
974
-
975
- this.close();
976
- this.setPlaceholder();
977
1436
  },
978
1437
 
1438
+ // single
979
1439
  prepareOpts: function () {
980
1440
  var opts = this.parent.prepareOpts.apply(this, arguments);
981
1441
 
982
1442
  if (opts.element.get(0).tagName.toLowerCase() === "select") {
983
- // install sthe selection initializer
984
- opts.initSelection = function (element) {
1443
+ // install the selection initializer
1444
+ opts.initSelection = function (element, callback) {
985
1445
  var selected = element.find(":selected");
986
1446
  // a single select box always has a value, no need to null check 'selected'
987
- return {id: selected.attr("value"), text: selected.text()};
1447
+ if ($.isFunction(callback))
1448
+ callback({id: selected.attr("value"), text: selected.text()});
988
1449
  };
989
1450
  }
990
1451
 
991
1452
  return opts;
992
1453
  },
993
1454
 
1455
+ // single
994
1456
  setPlaceholder: function () {
995
1457
  var placeholder = this.getPlaceholder();
996
1458
 
@@ -999,24 +1461,22 @@
999
1461
  // check for a first blank option if attached to a select
1000
1462
  if (this.select && this.select.find("option:first").text() !== "") return;
1001
1463
 
1002
- if (typeof(placeholder) === "object") {
1003
- this.updateSelection(placeholder);
1004
- } else {
1005
- this.selection.find("span").html(placeholder);
1006
- }
1464
+ this.selection.find("span").html(escapeMarkup(placeholder));
1465
+
1007
1466
  this.selection.addClass("select2-default");
1008
1467
 
1009
1468
  this.selection.find("abbr").hide();
1010
1469
  }
1011
1470
  },
1012
1471
 
1472
+ // single
1013
1473
  postprocessResults: function (data, initial) {
1014
1474
  var selected = 0, self = this, showSearchInput = true;
1015
1475
 
1016
1476
  // find the selected element in the result list
1017
1477
 
1018
- this.results.find(".select2-result").each(function (i) {
1019
- if (equal(self.id($(this).data("select2-data")), self.opts.element.val())) {
1478
+ this.results.find(".select2-result-selectable").each2(function (i, elm) {
1479
+ if (equal(self.id(elm.data("select2-data")), self.opts.element.val())) {
1020
1480
  selected = i;
1021
1481
  return false;
1022
1482
  }
@@ -1029,15 +1489,18 @@
1029
1489
  // hide the search box if this is the first we got the results and there are a few of them
1030
1490
 
1031
1491
  if (initial === true) {
1492
+ // TODO below we use data.results.length, but what we really need is something recursive to calc the length
1493
+ // TODO in case there are optgroups
1032
1494
  showSearchInput = this.showSearchInput = data.results.length >= this.opts.minimumResultsForSearch;
1033
- this.container.find(".select2-search")[showSearchInput ? "removeClass" : "addClass"]("select2-search-hidden");
1495
+ this.dropdown.find(".select2-search")[showSearchInput ? "removeClass" : "addClass"]("select2-search-hidden");
1034
1496
 
1035
1497
  //add "select2-with-searchbox" to the container if search box is shown
1036
- this.container[showSearchInput ? "addClass" : "removeClass"]("select2-with-searchbox");
1498
+ $(this.dropdown, this.container)[showSearchInput ? "addClass" : "removeClass"]("select2-with-searchbox");
1037
1499
  }
1038
1500
 
1039
1501
  },
1040
1502
 
1503
+ // single
1041
1504
  onSelect: function (data) {
1042
1505
  var old = this.opts.element.val();
1043
1506
 
@@ -1049,10 +1512,18 @@
1049
1512
  if (!equal(old, this.id(data))) { this.triggerChange(); }
1050
1513
  },
1051
1514
 
1515
+ // single
1052
1516
  updateSelection: function (data) {
1053
- this.selection
1054
- .find("span")
1055
- .html(this.opts.formatSelection(data));
1517
+
1518
+ var container=this.selection.find("span"), formatted;
1519
+
1520
+ this.selection.data("select2-data", data);
1521
+
1522
+ container.empty();
1523
+ formatted=this.opts.formatSelection(data, container);
1524
+ if (formatted !== undefined) {
1525
+ container.append(escapeMarkup(formatted));
1526
+ }
1056
1527
 
1057
1528
  this.selection.removeClass("select2-default");
1058
1529
 
@@ -1061,8 +1532,9 @@
1061
1532
  }
1062
1533
  },
1063
1534
 
1535
+ // single
1064
1536
  val: function () {
1065
- var val, data = null;
1537
+ var val, data = null, self = this;
1066
1538
 
1067
1539
  if (arguments.length === 0) {
1068
1540
  return this.opts.element.val();
@@ -1071,70 +1543,99 @@
1071
1543
  val = arguments[0];
1072
1544
 
1073
1545
  if (this.select) {
1074
- // val is an id
1075
1546
  this.select
1076
1547
  .val(val)
1077
- .find(":selected").each(function () {
1078
- data = {id: $(this).attr("value"), text: $(this).text()};
1548
+ .find(":selected").each2(function (i, elm) {
1549
+ data = {id: elm.attr("value"), text: elm.text()};
1079
1550
  return false;
1080
1551
  });
1081
1552
  this.updateSelection(data);
1553
+ this.setPlaceholder();
1082
1554
  } else {
1083
- // val is an object. !val is true for [undefined,null,'']
1084
- this.opts.element.val(!val ? "" : this.id(val));
1085
- this.updateSelection(val);
1555
+ if (this.opts.initSelection === undefined) {
1556
+ throw new Error("cannot call val() if initSelection() is not defined");
1557
+ }
1558
+ // val is an id. !val is true for [undefined,null,'']
1559
+ if (!val) {
1560
+ this.clear();
1561
+ return;
1562
+ }
1563
+ this.opts.initSelection(this.opts.element, function(data){
1564
+ self.opts.element.val(!data ? "" : self.id(data));
1565
+ self.updateSelection(data);
1566
+ self.setPlaceholder();
1567
+ });
1086
1568
  }
1087
- this.setPlaceholder();
1088
-
1089
1569
  },
1090
1570
 
1571
+ // single
1091
1572
  clearSearch: function () {
1092
1573
  this.search.val("");
1574
+ },
1575
+
1576
+ // single
1577
+ data: function(value) {
1578
+ var data;
1579
+
1580
+ if (arguments.length === 0) {
1581
+ data = this.selection.data("select2-data");
1582
+ if (data == undefined) data = null;
1583
+ return data;
1584
+ } else {
1585
+ if (!value || value === "") {
1586
+ this.clear();
1587
+ } else {
1588
+ this.opts.element.val(!value ? "" : this.id(value));
1589
+ this.updateSelection(value);
1590
+ }
1591
+ }
1093
1592
  }
1094
1593
  });
1095
1594
 
1096
1595
  MultiSelect2 = clazz(AbstractSelect2, {
1097
1596
 
1597
+ // multi
1098
1598
  createContainer: function () {
1099
- return $("<div></div>", {
1100
- "class": "select2-container select2-container-multi",
1101
- "style": "width: " + this.getContainerWidth()
1599
+ var container = $("<div></div>", {
1600
+ "class": "select2-container select2-container-multi"
1102
1601
  }).html([
1103
1602
  " <ul class='select2-choices'>",
1104
1603
  //"<li class='select2-search-choice'><span>California</span><a href="javascript:void(0)" class="select2-search-choice-close"></a></li>" ,
1105
1604
  " <li class='select2-search-field'>" ,
1106
- " <input type='text' autocomplete='off' style='width: 25px;'>" ,
1605
+ " <input type='text' autocomplete='off' style='width: 25px;' class='select2-input'>" ,
1107
1606
  " </li>" ,
1108
1607
  "</ul>" ,
1109
- "<div class='select2-drop' style='display:none;'>" ,
1608
+ "<div class='select2-drop select2-drop-multi' style='display:none;'>" ,
1110
1609
  " <ul class='select2-results'>" ,
1111
1610
  " </ul>" ,
1112
1611
  "</div>"].join(""));
1612
+ return container;
1113
1613
  },
1114
1614
 
1615
+ // multi
1115
1616
  prepareOpts: function () {
1116
1617
  var opts = this.parent.prepareOpts.apply(this, arguments);
1117
1618
 
1118
- opts = $.extend({}, {
1119
- closeOnSelect: true
1120
- }, opts);
1121
-
1122
1619
  // TODO validate placeholder is a string if specified
1123
1620
 
1124
1621
  if (opts.element.get(0).tagName.toLowerCase() === "select") {
1125
1622
  // install sthe selection initializer
1126
- opts.initSelection = function (element) {
1623
+ opts.initSelection = function (element,callback) {
1624
+
1127
1625
  var data = [];
1128
- element.find(":selected").each(function () {
1129
- data.push({id: $(this).attr("value"), text: $(this).text()});
1626
+ element.find(":selected").each2(function (i, elm) {
1627
+ data.push({id: elm.attr("value"), text: elm.text()});
1130
1628
  });
1131
- return data;
1629
+
1630
+ if ($.isFunction(callback))
1631
+ callback(data);
1132
1632
  };
1133
1633
  }
1134
1634
 
1135
1635
  return opts;
1136
1636
  },
1137
1637
 
1638
+ // multi
1138
1639
  initContainer: function () {
1139
1640
 
1140
1641
  var selector = ".select2-choices", selection;
@@ -1179,7 +1680,7 @@
1179
1680
  return;
1180
1681
  case KEY.ESC:
1181
1682
  this.cancel(e);
1182
- e.preventDefault();
1683
+ killEvent(e);
1183
1684
  return;
1184
1685
  }
1185
1686
  }
@@ -1198,8 +1699,13 @@
1198
1699
 
1199
1700
  this.search.bind("keyup", this.bind(this.resizeSearch));
1200
1701
 
1201
- this.container.delegate(selector, "click", this.bind(function (e) {
1702
+ this.search.bind("blur", this.bind(function() {
1703
+ this.container.removeClass("select2-container-active");
1704
+ }));
1705
+
1706
+ this.container.delegate(selector, "mousedown", this.bind(function (e) {
1202
1707
  if (!this.enabled) return;
1708
+ this.clearPlaceholder();
1203
1709
  this.open();
1204
1710
  this.focusSearch();
1205
1711
  e.preventDefault();
@@ -1208,6 +1714,7 @@
1208
1714
  this.container.delegate(selector, "focus", this.bind(function () {
1209
1715
  if (!this.enabled) return;
1210
1716
  this.container.addClass("select2-container-active");
1717
+ this.dropdown.addClass("select2-drop-active");
1211
1718
  this.clearPlaceholder();
1212
1719
  }));
1213
1720
 
@@ -1215,82 +1722,98 @@
1215
1722
  this.clearSearch();
1216
1723
  },
1217
1724
 
1725
+ // multi
1218
1726
  enable: function() {
1219
1727
  if (this.enabled) return;
1220
1728
 
1221
1729
  this.parent.enable.apply(this, arguments);
1222
1730
 
1223
- this.search.show();
1731
+ this.search.removeAttr("disabled");
1224
1732
  },
1225
1733
 
1734
+ // multi
1226
1735
  disable: function() {
1227
1736
  if (!this.enabled) return;
1228
1737
 
1229
1738
  this.parent.disable.apply(this, arguments);
1230
1739
 
1231
- this.search.hide();
1740
+ this.search.attr("disabled", true);
1232
1741
  },
1233
1742
 
1743
+ // multi
1234
1744
  initSelection: function () {
1235
1745
  var data;
1236
1746
  if (this.opts.element.val() === "") {
1237
1747
  this.updateSelection([]);
1748
+ this.close();
1749
+ // set the placeholder if necessary
1750
+ this.clearSearch();
1238
1751
  }
1239
1752
  if (this.select || this.opts.element.val() !== "") {
1240
- data = this.opts.initSelection.call(null, this.opts.element);
1241
- if (data !== undefined && data !== null) {
1242
- this.updateSelection(data);
1243
- }
1753
+ var self = this;
1754
+ this.opts.initSelection.call(null, this.opts.element, function(data){
1755
+ if (data !== undefined && data !== null) {
1756
+ self.updateSelection(data);
1757
+ self.close();
1758
+ // set the placeholder if necessary
1759
+ self.clearSearch();
1760
+ }
1761
+ });
1244
1762
  }
1245
-
1246
- this.close();
1247
-
1248
- // set the placeholder if necessary
1249
- this.clearSearch();
1250
1763
  },
1251
1764
 
1765
+ // multi
1252
1766
  clearSearch: function () {
1253
1767
  var placeholder = this.getPlaceholder();
1254
1768
 
1255
- if (placeholder !== undefined
1256
- && this.getVal().length === 0
1257
- && this.search.hasClass("select2-focused") === false) {
1258
-
1769
+ if (placeholder !== undefined && this.getVal().length === 0 && this.search.hasClass("select2-focused") === false) {
1259
1770
  this.search.val(placeholder).addClass("select2-default");
1260
1771
  // stretch the search box to full width of the container so as much of the placeholder is visible as possible
1261
- this.search.width(this.getContainerWidth());
1772
+ this.resizeSearch();
1262
1773
  } else {
1263
- this.search.val("").width(10);
1774
+ // we set this to " " instead of "" and later clear it on focus() because there is a firefox bug
1775
+ // that does not properly render the caret when the field starts out blank
1776
+ this.search.val(" ").width(10);
1264
1777
  }
1265
1778
  },
1266
1779
 
1780
+ // multi
1267
1781
  clearPlaceholder: function () {
1268
1782
  if (this.search.hasClass("select2-default")) {
1269
1783
  this.search.val("").removeClass("select2-default");
1784
+ } else {
1785
+ // work around for the space character we set to avoid firefox caret bug
1786
+ if (this.search.val() === " ") this.search.val("");
1270
1787
  }
1271
1788
  },
1272
1789
 
1273
- open: function () {
1274
- if (this.opened()) return;
1275
- this.parent.open.apply(this, arguments);
1276
- this.resizeSearch();
1790
+ // multi
1791
+ opening: function () {
1792
+ this.parent.opening.apply(this, arguments);
1793
+
1794
+ this.clearPlaceholder();
1795
+ this.resizeSearch();
1277
1796
  this.focusSearch();
1278
1797
  },
1279
1798
 
1799
+ // multi
1280
1800
  close: function () {
1281
1801
  if (!this.opened()) return;
1282
1802
  this.parent.close.apply(this, arguments);
1283
1803
  },
1284
1804
 
1805
+ // multi
1285
1806
  focus: function () {
1286
1807
  this.close();
1287
1808
  this.search.focus();
1288
1809
  },
1289
1810
 
1811
+ // multi
1290
1812
  isFocused: function () {
1291
1813
  return this.search.hasClass("select2-focused");
1292
1814
  },
1293
1815
 
1816
+ // multi
1294
1817
  updateSelection: function (data) {
1295
1818
  var ids = [], filtered = [], self = this;
1296
1819
 
@@ -1310,6 +1833,7 @@
1310
1833
  self.postprocessResults();
1311
1834
  },
1312
1835
 
1836
+ // multi
1313
1837
  onSelect: function (data) {
1314
1838
  this.addSelectedChoice(data);
1315
1839
  if (this.select) { this.postprocessResults(); }
@@ -1320,45 +1844,56 @@
1320
1844
  } else {
1321
1845
  this.search.width(10);
1322
1846
  this.resizeSearch();
1847
+
1848
+ if (this.countSelectableResults()>0) {
1849
+ this.positionDropdown();
1850
+ } else {
1851
+ // if nothing left to select close
1852
+ this.close();
1853
+ }
1323
1854
  }
1324
1855
 
1325
1856
  // since its not possible to select an element that has already been
1326
1857
  // added we do not need to check if this is a new element before firing change
1327
- this.triggerChange();
1858
+ this.triggerChange({ added: data });
1328
1859
 
1329
1860
  this.focusSearch();
1330
1861
  },
1331
1862
 
1863
+ // multi
1332
1864
  cancel: function () {
1333
1865
  this.close();
1334
1866
  this.focusSearch();
1335
1867
  },
1336
1868
 
1869
+ // multi
1337
1870
  addSelectedChoice: function (data) {
1338
- var choice,
1871
+ var choice=$(
1872
+ "<li class='select2-search-choice'>" +
1873
+ " <div></div>" +
1874
+ " <a href='javascript:void(0)' class='select2-search-choice-close' tabindex='-1'></a>" +
1875
+ "</li>"),
1339
1876
  id = this.id(data),
1340
- parts,
1341
- val = this.getVal();
1342
-
1343
- parts = ["<li class='select2-search-choice'>",
1344
- this.opts.formatSelection(data),
1345
- "<a href='javascript:void(0)' class='select2-search-choice-close' tabindex='-1'></a>",
1346
- "</li>"
1347
- ];
1877
+ val = this.getVal(),
1878
+ formatted;
1348
1879
 
1349
- choice = $(parts.join(""));
1350
- choice.find("a")
1880
+ formatted=this.opts.formatSelection(data, choice);
1881
+ choice.find("div").replaceWith("<div>"+escapeMarkup(formatted)+"</div>");
1882
+ choice.find(".select2-search-choice-close")
1351
1883
  .bind("click dblclick", this.bind(function (e) {
1352
1884
  if (!this.enabled) return;
1353
1885
 
1354
- this.unselect($(e.target));
1355
- this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
1886
+ $(e.target).closest(".select2-search-choice").fadeOut('fast').animate({width: "hide"}, 50, this.bind(function(){
1887
+ this.unselect($(e.target));
1888
+ this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
1889
+ this.close();
1890
+ this.focusSearch();
1891
+ })).dequeue();
1356
1892
  killEvent(e);
1357
- this.close();
1358
- this.focusSearch();
1359
1893
  })).bind("focus", this.bind(function () {
1360
1894
  if (!this.enabled) return;
1361
1895
  this.container.addClass("select2-container-active");
1896
+ this.dropdown.addClass("select2-drop-active");
1362
1897
  }));
1363
1898
 
1364
1899
  choice.data("select2-data", data);
@@ -1368,8 +1903,10 @@
1368
1903
  this.setVal(val);
1369
1904
  },
1370
1905
 
1906
+ // multi
1371
1907
  unselect: function (selected) {
1372
1908
  var val = this.getVal(),
1909
+ data,
1373
1910
  index;
1374
1911
 
1375
1912
  selected = selected.closest(".select2-search-choice");
@@ -1378,7 +1915,9 @@
1378
1915
  throw "Invalid argument: " + selected + ". Must be .select2-search-choice";
1379
1916
  }
1380
1917
 
1381
- index = indexOf(this.id(selected.data("select2-data")), val);
1918
+ data = selected.data("select2-data");
1919
+
1920
+ index = indexOf(this.id(data), val);
1382
1921
 
1383
1922
  if (index >= 0) {
1384
1923
  val.splice(index, 1);
@@ -1386,35 +1925,47 @@
1386
1925
  if (this.select) this.postprocessResults();
1387
1926
  }
1388
1927
  selected.remove();
1389
- this.triggerChange();
1928
+ this.triggerChange({ removed: data });
1390
1929
  },
1391
1930
 
1931
+ // multi
1392
1932
  postprocessResults: function () {
1393
1933
  var val = this.getVal(),
1394
- choices = this.results.find(".select2-result"),
1934
+ choices = this.results.find(".select2-result-selectable"),
1935
+ compound = this.results.find(".select2-result-with-children"),
1395
1936
  self = this;
1396
1937
 
1397
- choices.each(function () {
1398
- var choice = $(this), id = self.id(choice.data("select2-data"));
1938
+ choices.each2(function (i, choice) {
1939
+ var id = self.id(choice.data("select2-data"));
1399
1940
  if (indexOf(id, val) >= 0) {
1400
- choice.addClass("select2-disabled");
1941
+ choice.addClass("select2-disabled").removeClass("select2-result-selectable");
1401
1942
  } else {
1402
- choice.removeClass("select2-disabled");
1943
+ choice.removeClass("select2-disabled").addClass("select2-result-selectable");
1403
1944
  }
1404
1945
  });
1405
1946
 
1406
- choices.each(function (i) {
1407
- if (!$(this).hasClass("select2-disabled")) {
1408
- self.highlight(i);
1947
+ compound.each2(function(i, e) {
1948
+ if (e.find(".select2-result-selectable").length==0) {
1949
+ e.addClass("select2-disabled");
1950
+ } else {
1951
+ e.removeClass("select2-disabled");
1952
+ }
1953
+ });
1954
+
1955
+ choices.each2(function (i, choice) {
1956
+ if (!choice.hasClass("select2-disabled") && choice.hasClass("select2-result-selectable")) {
1957
+ self.highlight(0);
1409
1958
  return false;
1410
1959
  }
1411
1960
  });
1412
1961
 
1413
1962
  },
1414
1963
 
1964
+ // multi
1415
1965
  resizeSearch: function () {
1416
1966
 
1417
- var minimumWidth, left, maxWidth, containerLeft, searchWidth;
1967
+ var minimumWidth, left, maxWidth, containerLeft, searchWidth,
1968
+ sideBorderPadding = getSideBorderPadding(this.search);
1418
1969
 
1419
1970
  minimumWidth = measureTextWidth(this.search) + 10;
1420
1971
 
@@ -1423,18 +1974,19 @@
1423
1974
  maxWidth = this.selection.width();
1424
1975
  containerLeft = this.selection.offset().left;
1425
1976
 
1426
- searchWidth = maxWidth - (left - containerLeft) - getSideBorderPadding(this.search);
1977
+ searchWidth = maxWidth - (left - containerLeft) - sideBorderPadding;
1427
1978
 
1428
1979
  if (searchWidth < minimumWidth) {
1429
- searchWidth = maxWidth - getSideBorderPadding(this.search);
1980
+ searchWidth = maxWidth - sideBorderPadding;
1430
1981
  }
1431
1982
 
1432
1983
  if (searchWidth < 40) {
1433
- searchWidth = maxWidth - getSideBorderPadding(this.search);
1984
+ searchWidth = maxWidth - sideBorderPadding;
1434
1985
  }
1435
1986
  this.search.width(searchWidth);
1436
1987
  },
1437
1988
 
1989
+ // multi
1438
1990
  getVal: function () {
1439
1991
  var val;
1440
1992
  if (this.select) {
@@ -1442,10 +1994,11 @@
1442
1994
  return val === null ? [] : val;
1443
1995
  } else {
1444
1996
  val = this.opts.element.val();
1445
- return splitVal(val, ",");
1997
+ return splitVal(val, this.opts.separator);
1446
1998
  }
1447
1999
  },
1448
2000
 
2001
+ // multi
1449
2002
  setVal: function (val) {
1450
2003
  var unique;
1451
2004
  if (this.select) {
@@ -1456,10 +2009,11 @@
1456
2009
  $(val).each(function () {
1457
2010
  if (indexOf(this, unique) < 0) unique.push(this);
1458
2011
  });
1459
- this.opts.element.val(unique.length === 0 ? "" : unique.join(","));
2012
+ this.opts.element.val(unique.length === 0 ? "" : unique.join(this.opts.separator));
1460
2013
  }
1461
2014
  },
1462
2015
 
2016
+ // multi
1463
2017
  val: function () {
1464
2018
  var val, data = [], self=this;
1465
2019
 
@@ -1469,24 +2023,37 @@
1469
2023
 
1470
2024
  val = arguments[0];
1471
2025
 
2026
+ if (!val) {
2027
+ this.opts.element.val("");
2028
+ this.updateSelection([]);
2029
+ this.clearSearch();
2030
+ return;
2031
+ }
2032
+
2033
+ // val is a list of ids
2034
+ this.setVal(val);
2035
+
1472
2036
  if (this.select) {
1473
- // val is a list of ids
1474
- this.setVal(val);
1475
2037
  this.select.find(":selected").each(function () {
1476
2038
  data.push({id: $(this).attr("value"), text: $(this).text()});
1477
2039
  });
1478
2040
  this.updateSelection(data);
1479
2041
  } else {
1480
- val = (val === null) ? [] : val;
1481
- this.setVal(val);
1482
- // val is a list of objects
1483
- $(val).each(function () { data.push(self.id(this)); });
1484
- this.setVal(data);
1485
- this.updateSelection(val);
1486
- }
2042
+ if (this.opts.initSelection === undefined) {
2043
+ throw new Error("val() cannot be called if initSelection() is not defined")
2044
+ }
1487
2045
 
2046
+ this.opts.initSelection(this.opts.element, function(data){
2047
+ var ids=$(data).map(self.id);
2048
+ self.setVal(ids);
2049
+ self.updateSelection(data);
2050
+ self.clearSearch();
2051
+ });
2052
+ }
1488
2053
  this.clearSearch();
1489
2054
  },
2055
+
2056
+ // multi
1490
2057
  onSortStart: function() {
1491
2058
  if (this.select) {
1492
2059
  throw new Error("Sorting of elements is not supported when attached to <select>. Attach to <input type='hidden'/> instead.");
@@ -1497,6 +2064,8 @@
1497
2064
  // hide the container
1498
2065
  this.searchContainer.hide();
1499
2066
  },
2067
+
2068
+ // multi
1500
2069
  onSortEnd:function() {
1501
2070
 
1502
2071
  var val=[], self=this;
@@ -1505,7 +2074,7 @@
1505
2074
  this.searchContainer.show();
1506
2075
  // make sure the search container is the last item in the list
1507
2076
  this.searchContainer.appendTo(this.searchContainer.parent());
1508
- // since we collapsed the width in dragStarteed, we resize it here
2077
+ // since we collapsed the width in dragStarted, we resize it here
1509
2078
  this.resizeSearch();
1510
2079
 
1511
2080
  // update selection
@@ -1515,6 +2084,23 @@
1515
2084
  });
1516
2085
  this.setVal(val);
1517
2086
  this.triggerChange();
2087
+ },
2088
+
2089
+ // multi
2090
+ data: function(values) {
2091
+ var self=this, ids;
2092
+ if (arguments.length === 0) {
2093
+ return this.selection
2094
+ .find(".select2-search-choice")
2095
+ .map(function() { return $(this).data("select2-data"); })
2096
+ .get();
2097
+ } else {
2098
+ if (!values) { values = []; }
2099
+ ids = $.map(values, function(e) { return self.opts.id(e)});
2100
+ this.setVal(ids);
2101
+ this.updateSelection(values);
2102
+ this.clearSearch();
2103
+ }
1518
2104
  }
1519
2105
  });
1520
2106
 
@@ -1523,7 +2109,7 @@
1523
2109
  var args = Array.prototype.slice.call(arguments, 0),
1524
2110
  opts,
1525
2111
  select2,
1526
- value, multiple, allowedMethods = ["val", "destroy", "open", "close", "focus", "isFocused", "container", "onSortStart", "onSortEnd", "enable", "disable"];
2112
+ value, multiple, allowedMethods = ["val", "destroy", "open", "close", "focus", "isFocused", "container", "onSortStart", "onSortEnd", "enable", "disable", "positionDropdown", "data"];
1527
2113
 
1528
2114
  this.each(function () {
1529
2115
  if (args.length === 0 || typeof(args[0]) === "object") {
@@ -1561,6 +2147,35 @@
1561
2147
  return (value === undefined) ? this : value;
1562
2148
  };
1563
2149
 
2150
+ // plugin defaults, accessible to users
2151
+ $.fn.select2.defaults = {
2152
+ width: "copy",
2153
+ closeOnSelect: true,
2154
+ containerCss: {},
2155
+ dropdownCss: {},
2156
+ containerCssClass: "",
2157
+ dropdownCssClass: "",
2158
+ formatResult: function(result, container, query) {
2159
+ var markup=[];
2160
+ markMatch(result.text, query.term, markup);
2161
+ return markup.join("");
2162
+ },
2163
+ formatSelection: function (data, container) {
2164
+ return data.text;
2165
+ },
2166
+ formatNoMatches: function () { return "No matches found"; },
2167
+ formatInputTooShort: function (input, min) { return "Please enter " + (min - input.length) + " more characters"; },
2168
+ formatSelectionTooBig: function (limit) { return "You can only select " + limit + " items"; },
2169
+ formatLoadMore: function (pageNumber) { return "Loading more results..."; },
2170
+ minimumResultsForSearch: 0,
2171
+ minimumInputLength: 0,
2172
+ maximumSelectionSize: 0,
2173
+ id: function (e) { return e.id; },
2174
+ matcher: function(term, text) {
2175
+ return text.toUpperCase().indexOf(term.toUpperCase()) >= 0;
2176
+ }
2177
+ };
2178
+
1564
2179
  // exports
1565
2180
  window.Select2 = {
1566
2181
  query: {
@@ -1568,7 +2183,8 @@
1568
2183
  local: local,
1569
2184
  tags: tags
1570
2185
  }, util: {
1571
- debounce: debounce
2186
+ debounce: debounce,
2187
+ markMatch: markMatch
1572
2188
  }, "class": {
1573
2189
  "abstract": AbstractSelect2,
1574
2190
  "single": SingleSelect2,