select2-rails 3.2.1 → 3.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -26,13 +26,13 @@ Add to your `app/assets/stylesheets/application.css`:
26
26
  *= require select2
27
27
 
28
28
  ## Version
29
- From `v2.1.0` on, `select2-rails`'s version will match the version of `Select2` it uses. Currently, `select2-rails` uses `Select2 v3.1`.
29
+ From `v2.1.0` on, `select2-rails`'s version will match the version of `Select2` it uses. Currently, `select2-rails` uses `Select2 v3.2`.
30
30
 
31
31
  The last number of the version is the patch version specific to the gem. For example, for a version of the form `2.x.y`, `2.x` is the release of `Select2` we should be compatible with, and y is the patch version specific to the gem (ie. to resolve any gem-specific issues that crop up).
32
32
 
33
33
  ## Contributions
34
34
 
35
- If you wont to contribute, please:
35
+ If you want to contribute, please:
36
36
 
37
37
  * Fork the project.
38
38
  * Make your feature addition or bug fix.
@@ -40,4 +40,4 @@ If you wont to contribute, please:
40
40
 
41
41
  ## Copyright
42
42
 
43
- Copyright (c) 2012 Rogerio Medeiros. See [LICENSE](https://github.com/argerim/select2-rails/blob/master/LICENSE) for details.
43
+ Copyright (c) 2012 Rogerio Medeiros. See [LICENSE](https://github.com/argerim/select2-rails/blob/master/LICENSE) for details.
@@ -1,5 +1,5 @@
1
1
  module Select2
2
2
  module Rails
3
- VERSION = "3.2.1"
3
+ VERSION = "3.3.0"
4
4
  end
5
5
  end
@@ -16,6 +16,7 @@ Gem::Specification.new do |s|
16
16
  s.require_paths = ["lib"]
17
17
 
18
18
  s.add_dependency "thor", "~> 0.14"
19
+ s.add_runtime_dependency "sass-rails", "~> 3.2"
19
20
  s.add_development_dependency "bundler", "~> 1.0"
20
21
  s.add_development_dependency "rails", "~> 3.0"
21
22
  s.add_development_dependency "httpclient", "~> 2.2"
File without changes
File without changes
@@ -1,17 +1,23 @@
1
1
  /*
2
- Copyright 2012 Igor Vaynberg
2
+ Copyright 2012 Igor Vaynberg
3
3
 
4
- Version: 3.2 Timestamp: Mon Sep 10 10:38:04 PDT 2012
4
+ Version: 3.3.0 Timestamp: Tue Feb 5 18:33:54 PST 2013
5
5
 
6
- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in
7
- compliance with the License. You may obtain a copy of the License in the LICENSE file, or at:
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.
8
10
 
9
- http://www.apache.org/licenses/LICENSE-2.0
11
+ You may obtain a copy of the Apache License and the GPL License at:
10
12
 
11
- Unless required by applicable law or agreed to in writing, software distributed under the License is
12
- distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- See the License for the specific language governing permissions and limitations under the License.
14
- */
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
+ */
15
21
  (function ($) {
16
22
  if(typeof $.fn.each2 == "undefined"){
17
23
  $.fn.extend({
@@ -40,7 +46,8 @@
40
46
  return;
41
47
  }
42
48
 
43
- var KEY, AbstractSelect2, SingleSelect2, MultiSelect2, nextUid, sizer;
49
+ var KEY, AbstractSelect2, SingleSelect2, MultiSelect2, nextUid, sizer,
50
+ lastMousePosition, $document;
44
51
 
45
52
  KEY = {
46
53
  TAB: 9,
@@ -90,42 +97,23 @@
90
97
  }
91
98
  };
92
99
 
100
+ $document = $(document);
101
+
93
102
  nextUid=(function() { var counter=1; return function() { return counter++; }; }());
94
103
 
95
104
  function indexOf(value, array) {
96
- var i = 0, l = array.length, v;
97
-
98
- if (typeof value === "undefined") {
99
- return -1;
100
- }
101
-
102
- if (value.constructor === String) {
103
- for (; i < l; i = i + 1) if (value.localeCompare(array[i]) === 0) return i;
104
- } else {
105
- for (; i < l; i = i + 1) {
106
- v = array[i];
107
- if (v.constructor === String) {
108
- if (v.localeCompare(value) === 0) return i;
109
- } else {
110
- if (v === value) return i;
111
- }
112
- }
113
- }
105
+ var i = 0, l = array.length;
106
+ for (; i < l; i = i + 1) if (value === array[i]) return i;
114
107
  return -1;
115
108
  }
116
109
 
117
110
  /**
118
- * Compares equality of a and b taking into account that a and b may be strings, in which case localeCompare is used
111
+ * Compares equality of a and b
119
112
  * @param a
120
113
  * @param b
121
114
  */
122
115
  function equal(a, b) {
123
- if (a === b) return true;
124
- if (a === undefined || b === undefined) return false;
125
- if (a === null || b === null) return false;
126
- if (a.constructor === String) return a.localeCompare(b) === 0;
127
- if (b.constructor === String) return b.localeCompare(a) === 0;
128
- return false;
116
+ return a===b;
129
117
  }
130
118
 
131
119
  /**
@@ -143,7 +131,7 @@
143
131
  }
144
132
 
145
133
  function getSideBorderPadding(element) {
146
- return element.outerWidth() - element.width();
134
+ return element.outerWidth(false) - element.width();
147
135
  }
148
136
 
149
137
  function installKeyUpChangeEvent(element) {
@@ -162,8 +150,8 @@
162
150
  });
163
151
  }
164
152
 
165
- $(document).delegate("body", "mousemove", function (e) {
166
- $.data(document, "select2-lastpos", {x: e.pageX, y: e.pageY});
153
+ $document.bind("mousemove", function (e) {
154
+ lastMousePosition = {x: e.pageX, y: e.pageY};
167
155
  });
168
156
 
169
157
  /**
@@ -174,7 +162,7 @@
174
162
  */
175
163
  function installFilteredMouseMove(element) {
176
164
  element.bind("mousemove", function (e) {
177
- var lastpos = $.data(document, "select2-lastpos");
165
+ var lastpos = lastMousePosition;
178
166
  if (lastpos === undefined || lastpos.x !== e.pageX || lastpos.y !== e.pageY) {
179
167
  $(e.target).trigger("mousemove-filtered", e);
180
168
  }
@@ -227,11 +215,15 @@
227
215
  event.preventDefault();
228
216
  event.stopPropagation();
229
217
  }
218
+ function killEventImmediately(event) {
219
+ event.preventDefault();
220
+ event.stopImmediatePropagation();
221
+ }
230
222
 
231
223
  function measureTextWidth(e) {
232
224
  if (!sizer){
233
225
  var style = e[0].currentStyle || window.getComputedStyle(e[0], null);
234
- sizer = $("<div></div>").css({
226
+ sizer = $(document.createElement("div")).css({
235
227
  position: "absolute",
236
228
  left: "-10000px",
237
229
  top: "-10000px",
@@ -244,26 +236,27 @@
244
236
  textTransform: style.textTransform,
245
237
  whiteSpace: "nowrap"
246
238
  });
239
+ sizer.attr("class","select2-sizer");
247
240
  $("body").append(sizer);
248
241
  }
249
242
  sizer.text(e.val());
250
243
  return sizer.width();
251
244
  }
252
245
 
253
- function markMatch(text, term, markup) {
246
+ function markMatch(text, term, markup, escapeMarkup) {
254
247
  var match=text.toUpperCase().indexOf(term.toUpperCase()),
255
248
  tl=term.length;
256
249
 
257
250
  if (match<0) {
258
- markup.push(text);
251
+ markup.push(escapeMarkup(text));
259
252
  return;
260
253
  }
261
254
 
262
- markup.push(text.substring(0, match));
255
+ markup.push(escapeMarkup(text.substring(0, match)));
263
256
  markup.push("<span class='select2-match'>");
264
- markup.push(text.substring(match, match + tl));
257
+ markup.push(escapeMarkup(text.substring(match, match + tl)));
265
258
  markup.push("</span>");
266
- markup.push(text.substring(match + tl, text.length));
259
+ markup.push(escapeMarkup(text.substring(match + tl, text.length)));
267
260
  }
268
261
 
269
262
  /**
@@ -294,16 +287,18 @@
294
287
  requestSequence += 1; // increment the sequence
295
288
  var requestNumber = requestSequence, // this request's sequence number
296
289
  data = options.data, // ajax data function
290
+ url = options.url, // ajax url string or function
297
291
  transport = options.transport || $.ajax,
298
292
  traditional = options.traditional || false,
299
293
  type = options.type || 'GET'; // set type of request (GET or POST)
300
294
 
301
- data = data.call(this, query.term, query.page, query.context);
295
+ data = data ? data.call(this, query.term, query.page, query.context) : null;
296
+ url = (typeof url === 'function') ? url.call(this, query.term, query.page, query.context) : url;
302
297
 
303
298
  if( null !== handler) { handler.abort(); }
304
299
 
305
300
  handler = transport.call(null, {
306
- url: options.url,
301
+ url: url,
307
302
  dataType: options.dataType,
308
303
  data: data,
309
304
  type: type,
@@ -367,11 +362,11 @@
367
362
  }
368
363
  group.children=[];
369
364
  $(datum.children).each2(function(i, childDatum) { process(childDatum, group.children); });
370
- if (group.children.length) {
365
+ if (group.children.length || query.matcher(t, text(group), datum)) {
371
366
  collection.push(group);
372
367
  }
373
368
  } else {
374
- if (query.matcher(t, text(datum))) {
369
+ if (query.matcher(t, text(datum), datum)) {
375
370
  collection.push(datum);
376
371
  }
377
372
  }
@@ -384,17 +379,10 @@
384
379
 
385
380
  // TODO javadoc
386
381
  function tags(data) {
387
- // TODO even for a function we should probably return a wrapper that does the same object/string check as
388
- // the function for arrays. otherwise only functions that return objects are supported.
389
- if ($.isFunction(data)) {
390
- return data;
391
- }
392
-
393
- // if not a function we assume it to be an array
394
-
382
+ var isFunc = $.isFunction(data);
395
383
  return function (query) {
396
384
  var t = query.term, filtered = {results: []};
397
- $(data).each(function () {
385
+ $(isFunc ? data() : data).each(function () {
398
386
  var isObject = this.text !== undefined,
399
387
  text = isObject ? this.text : this;
400
388
  if (t === "" || query.matcher(t, text)) {
@@ -485,38 +473,9 @@
485
473
  }
486
474
  }
487
475
 
488
- if (original.localeCompare(input) != 0) return input;
476
+ if (original!==input) return input;
489
477
  }
490
478
 
491
- /**
492
- * blurs any Select2 container that has focus when an element outside them was clicked or received focus
493
- *
494
- * also takes care of clicks on label tags that point to the source element
495
- */
496
- $(document).ready(function () {
497
- $(document).delegate("body", "mousedown touchend", function (e) {
498
- var target = $(e.target).closest("div.select2-container").get(0), attr;
499
- if (target) {
500
- $(document).find("div.select2-container-active").each(function () {
501
- if (this !== target) $(this).data("select2").blur();
502
- });
503
- } else {
504
- target = $(e.target).closest("div.select2-drop").get(0);
505
- $(document).find("div.select2-drop-active").each(function () {
506
- if (this !== target) $(this).data("select2").blur();
507
- });
508
- }
509
-
510
- target=$(e.target);
511
- attr = target.attr("for");
512
- if ("LABEL" === e.target.tagName && attr && attr.length > 0) {
513
- target = $("#"+attr);
514
- target = target.data("select2");
515
- if (target !== undefined) { target.focus(); e.preventDefault();}
516
- }
517
- });
518
- });
519
-
520
479
  /**
521
480
  * Creates a new class
522
481
  *
@@ -544,7 +503,7 @@
544
503
 
545
504
  // abstract
546
505
  init: function (opts) {
547
- var results, search, resultsSelector = ".select2-results";
506
+ var results, search, resultsSelector = ".select2-results", mask;
548
507
 
549
508
  // prepare options
550
509
  this.opts = opts = this.prepareOpts(opts);
@@ -567,6 +526,25 @@
567
526
  // cache the body so future lookups are cheap
568
527
  this.body = thunk(function() { return opts.element.closest("body"); });
569
528
 
529
+ // create the dropdown mask if doesnt already exist
530
+ mask = $("#select2-drop-mask");
531
+ if (mask.length == 0) {
532
+ mask = $(document.createElement("div"));
533
+ mask.attr("id","select2-drop-mask").attr("class","select2-drop-mask");
534
+ mask.hide();
535
+ mask.appendTo(this.body());
536
+ mask.bind("mousedown touchstart", function (e) {
537
+ var dropdown = $("#select2-drop"), self;
538
+ if (dropdown.length > 0) {
539
+ self=dropdown.data("select2");
540
+ if (self.opts.selectOnBlur) {
541
+ self.selectHighlighted({noFocus: true});
542
+ }
543
+ self.close();
544
+ }
545
+ });
546
+ }
547
+
570
548
  if (opts.element.attr("class") !== undefined) {
571
549
  this.container.addClass(opts.element.attr("class").replace(/validate\[[\S ]+] ?/, ''));
572
550
  }
@@ -574,10 +552,14 @@
574
552
  this.container.css(evaluate(opts.containerCss));
575
553
  this.container.addClass(evaluate(opts.containerCssClass));
576
554
 
555
+ this.elementTabIndex = this.opts.element.attr("tabIndex");
556
+
577
557
  // swap container for the element
578
558
  this.opts.element
579
559
  .data("select2", this)
580
- .hide()
560
+ .addClass("select2-offscreen")
561
+ .bind("focus.select2", function() { $(this).select2("focus")})
562
+ .attr("tabIndex", "-1")
581
563
  .before(this.container);
582
564
  this.container.data("select2", this);
583
565
 
@@ -588,7 +570,7 @@
588
570
  this.results = results = this.container.find(resultsSelector);
589
571
  this.search = search = this.container.find("input.select2-input");
590
572
 
591
- search.attr("tabIndex", this.opts.element.attr("tabIndex"));
573
+ search.attr("tabIndex", this.elementTabIndex);
592
574
 
593
575
  this.resultsPage = 0;
594
576
  this.context = null;
@@ -623,7 +605,7 @@
623
605
  search.bind("blur", function () { search.removeClass("select2-focused");});
624
606
 
625
607
  this.dropdown.delegate(resultsSelector, "mouseup", this.bind(function (e) {
626
- if ($(e.target).closest(".select2-result-selectable:not(.select2-disabled)").length > 0) {
608
+ if ($(e.target).closest(".select2-result-selectable").length > 0) {
627
609
  this.highlightUnderEvent(e);
628
610
  this.selectHighlighted(e);
629
611
  } else {
@@ -652,12 +634,17 @@
652
634
  // abstract
653
635
  destroy: function () {
654
636
  var select2 = this.opts.element.data("select2");
637
+
638
+ if (this.propertyObserver) { delete this.propertyObserver; this.propertyObserver = null; }
639
+
655
640
  if (select2 !== undefined) {
641
+
656
642
  select2.container.remove();
657
643
  select2.dropdown.remove();
658
644
  select2.opts.element
659
645
  .removeData("select2")
660
646
  .unbind(".select2")
647
+ .attr("tabIndex", this.elementTabIndex)
661
648
  .show();
662
649
  }
663
650
  },
@@ -687,26 +674,33 @@
687
674
 
688
675
  populate=function(results, container, depth) {
689
676
 
690
- var i, l, result, selectable, compound, node, label, innerContainer, formatted;
677
+ var i, l, result, selectable, disabled, compound, node, label, innerContainer, formatted;
678
+
679
+ results = opts.sortResults(results, container, query);
680
+
691
681
  for (i = 0, l = results.length; i < l; i = i + 1) {
692
682
 
693
683
  result=results[i];
694
- selectable=id(result) !== undefined;
684
+
685
+ disabled = (result.disabled === true);
686
+ selectable = (!disabled) && (id(result) !== undefined);
687
+
695
688
  compound=result.children && result.children.length > 0;
696
689
 
697
690
  node=$("<li></li>");
698
691
  node.addClass("select2-results-dept-"+depth);
699
692
  node.addClass("select2-result");
700
693
  node.addClass(selectable ? "select2-result-selectable" : "select2-result-unselectable");
694
+ if (disabled) { node.addClass("select2-disabled"); }
701
695
  if (compound) { node.addClass("select2-result-with-children"); }
702
696
  node.addClass(self.opts.formatResultCssClass(result));
703
697
 
704
- label=$("<div></div>");
698
+ label=$(document.createElement("div"));
705
699
  label.addClass("select2-result-label");
706
700
 
707
- formatted=opts.formatResult(result, label, query);
701
+ formatted=opts.formatResult(result, label, query, self.opts.escapeMarkup);
708
702
  if (formatted!==undefined) {
709
- label.html(self.opts.escapeMarkup(formatted));
703
+ label.html(formatted);
710
704
  }
711
705
 
712
706
  node.append(label);
@@ -733,6 +727,13 @@
733
727
  opts.id = function (e) { return e[idKey]; };
734
728
  }
735
729
 
730
+ if ($.isArray(opts.element.data("select2Tags"))) {
731
+ if ("tags" in opts) {
732
+ throw "tags specified as both an attribute 'data-select2-tags' and in options of Select2 " + opts.element.attr("id");
733
+ }
734
+ opts.tags=opts.element.attr("data-select2-tags");
735
+ }
736
+
736
737
  if (select) {
737
738
  opts.query = this.bind(function (query) {
738
739
  var data = { results: [], more: false },
@@ -743,7 +744,7 @@
743
744
  var group;
744
745
  if (element.is("option")) {
745
746
  if (query.matcher(term, element.text(), element)) {
746
- collection.push({id:element.attr("value"), text:element.text(), element: element.get(), css: element.attr("class")});
747
+ collection.push({id:element.attr("value"), text:element.text(), element: element.get(), css: element.attr("class"), disabled: equal(element.attr("disabled"), "disabled") });
747
748
  }
748
749
  } else if (element.is("optgroup")) {
749
750
  group={text:element.attr("label"), children:[], element: element.get(), css: element.attr("class")};
@@ -773,6 +774,7 @@
773
774
  opts.formatResultCssClass = function(data) { return data.css; }
774
775
  } else {
775
776
  if (!("query" in opts)) {
777
+
776
778
  if ("ajax" in opts) {
777
779
  ajaxUrl = opts.element.data("ajax-url");
778
780
  if (ajaxUrl && ajaxUrl.length > 0) {
@@ -783,7 +785,9 @@
783
785
  opts.query = local(opts.data);
784
786
  } else if ("tags" in opts) {
785
787
  opts.query = tags(opts.tags);
786
- opts.createSearchChoice = function (term) { return {id: term, text: term}; };
788
+ if (opts.createSearchChoice === undefined) {
789
+ opts.createSearchChoice = function (term) { return {id: term, text: term}; };
790
+ }
787
791
  opts.initSelection = function (element, callback) {
788
792
  var data = [];
789
793
  $(splitVal(element.val(), opts.separator)).each(function () {
@@ -810,11 +814,39 @@
810
814
  */
811
815
  // abstract
812
816
  monitorSource: function () {
813
- this.opts.element.bind("change.select2", this.bind(function (e) {
817
+ var el = this.opts.element, sync;
818
+
819
+ el.bind("change.select2", this.bind(function (e) {
814
820
  if (this.opts.element.data("select2-change-triggered") !== true) {
815
821
  this.initSelection();
816
822
  }
817
823
  }));
824
+
825
+ sync = this.bind(function () {
826
+ var enabled = this.opts.element.attr("disabled") !== "disabled";
827
+ var readonly = this.opts.element.attr("readonly") === "readonly";
828
+
829
+ enabled = enabled && !readonly;
830
+
831
+ if (this.enabled !== enabled) {
832
+ if (enabled) {
833
+ this.enable();
834
+ } else {
835
+ this.disable();
836
+ }
837
+ }
838
+ });
839
+
840
+ // mozilla and IE
841
+ el.bind("propertychange.select2 DOMAttrModified.select2", sync);
842
+ // safari and chrome
843
+ if (typeof WebKitMutationObserver !== "undefined") {
844
+ if (this.propertyObserver) { delete this.propertyObserver; this.propertyObserver = null; }
845
+ this.propertyObserver = new WebKitMutationObserver(function (mutations) {
846
+ mutations.forEach(sync);
847
+ });
848
+ this.propertyObserver.observe(el.get(0), { attributes:true, subtree:false });
849
+ }
818
850
  },
819
851
 
820
852
  /**
@@ -840,13 +872,13 @@
840
872
  this.opts.element.blur();
841
873
  },
842
874
 
843
-
844
875
  // abstract
845
876
  enable: function() {
846
877
  if (this.enabled) return;
847
878
 
848
879
  this.enabled=true;
849
880
  this.container.removeClass("select2-container-disabled");
881
+ this.opts.element.removeAttr("disabled");
850
882
  },
851
883
 
852
884
  // abstract
@@ -857,6 +889,7 @@
857
889
 
858
890
  this.enabled=false;
859
891
  this.container.addClass("select2-container-disabled");
892
+ this.opts.element.attr("disabled", "disabled");
860
893
  },
861
894
 
862
895
  // abstract
@@ -867,14 +900,17 @@
867
900
  // abstract
868
901
  positionDropdown: function() {
869
902
  var offset = this.container.offset(),
870
- height = this.container.outerHeight(),
871
- width = this.container.outerWidth(),
872
- dropHeight = this.dropdown.outerHeight(),
903
+ height = this.container.outerHeight(false),
904
+ width = this.container.outerWidth(false),
905
+ dropHeight = this.dropdown.outerHeight(false),
906
+ viewPortRight = $(window).scrollLeft() + document.documentElement.clientWidth,
873
907
  viewportBottom = $(window).scrollTop() + document.documentElement.clientHeight,
874
908
  dropTop = offset.top + height,
875
909
  dropLeft = offset.left,
876
910
  enoughRoomBelow = dropTop + dropHeight <= viewportBottom,
877
911
  enoughRoomAbove = (offset.top - dropHeight) >= this.body().scrollTop(),
912
+ dropWidth = this.dropdown.outerWidth(false),
913
+ enoughRoomOnRight = dropLeft + dropWidth <= viewPortRight,
878
914
  aboveNow = this.dropdown.hasClass("select2-drop-above"),
879
915
  bodyOffset,
880
916
  above,
@@ -901,6 +937,10 @@
901
937
  if (!enoughRoomBelow && enoughRoomAbove) above = true;
902
938
  }
903
939
 
940
+ if (!enoughRoomOnRight) {
941
+ dropLeft = offset.left + width - dropWidth;
942
+ }
943
+
904
944
  if (above) {
905
945
  dropTop = offset.top - dropHeight;
906
946
  this.container.addClass("select2-drop-above");
@@ -959,26 +999,11 @@
959
999
  */
960
1000
  // abstract
961
1001
  opening: function() {
962
- var cid = this.containerId, selector = this.containerSelector,
963
- scroll = "scroll." + cid, resize = "resize." + cid;
964
-
965
- this.container.parents().each(function() {
966
- $(this).bind(scroll, function() {
967
- var s2 = $(selector);
968
- if (s2.length == 0) {
969
- $(this).unbind(scroll);
970
- }
971
- s2.select2("close");
972
- });
973
- });
974
-
975
- $(window).bind(resize, function() {
976
- var s2 = $(selector);
977
- if (s2.length == 0) {
978
- $(window).unbind(resize);
979
- }
980
- s2.select2("close");
981
- });
1002
+ var cid = this.containerId,
1003
+ scroll = "scroll." + cid,
1004
+ resize = "resize."+cid,
1005
+ orient = "orientationchange."+cid,
1006
+ mask;
982
1007
 
983
1008
  this.clearDropdownAlignmentPreference();
984
1009
 
@@ -986,19 +1011,46 @@
986
1011
 
987
1012
  this.container.addClass("select2-dropdown-open").addClass("select2-container-active");
988
1013
 
989
- this.updateResults(true);
990
1014
 
991
1015
  if(this.dropdown[0] !== this.body().children().last()[0]) {
992
1016
  this.dropdown.detach().appendTo(this.body());
993
1017
  }
994
1018
 
995
- this.dropdown.show();
1019
+ this.updateResults(true);
1020
+
1021
+ mask = $("#select2-drop-mask");
996
1022
 
1023
+ // ensure the mask is always right before the dropdown
1024
+ if (this.dropdown.prev()[0] !== mask[0]) {
1025
+ this.dropdown.before(mask);
1026
+ }
1027
+
1028
+ // move the global id to the correct dropdown
1029
+ $("#select2-drop").removeAttr("id");
1030
+ this.dropdown.attr("id", "select2-drop");
1031
+
1032
+ // show the elements
1033
+ mask.css({
1034
+ width: document.documentElement.scrollWidth,
1035
+ height: document.documentElement.scrollHeight});
1036
+ mask.show();
1037
+ this.dropdown.show();
997
1038
  this.positionDropdown();
998
- this.dropdown.addClass("select2-drop-active");
999
1039
 
1040
+ this.dropdown.addClass("select2-drop-active");
1000
1041
  this.ensureHighlightVisible();
1001
1042
 
1043
+ // attach listeners to events that can change the position of the container and thus require
1044
+ // the position of the dropdown to be updated as well so it does not come unglued from the container
1045
+ this.container.parents().add(window).each(function () {
1046
+ $(this).bind(resize+" "+scroll+" "+orient, function (e) {
1047
+ $("#select2-drop-mask").css({
1048
+ width:document.documentElement.scrollWidth,
1049
+ height:document.documentElement.scrollHeight});
1050
+ $("#select2-drop").data("select2").positionDropdown();
1051
+ });
1052
+ });
1053
+
1002
1054
  this.focusSearch();
1003
1055
  },
1004
1056
 
@@ -1006,15 +1058,18 @@
1006
1058
  close: function () {
1007
1059
  if (!this.opened()) return;
1008
1060
 
1009
- var self = this;
1061
+ var cid = this.containerId,
1062
+ scroll = "scroll." + cid,
1063
+ resize = "resize."+cid,
1064
+ orient = "orientationchange."+cid;
1010
1065
 
1011
- this.container.parents().each(function() {
1012
- $(this).unbind("scroll." + self.containerId);
1013
- });
1014
- $(window).unbind("resize." + this.containerId);
1066
+ // unbind event listeners
1067
+ this.container.parents().add(window).each(function () { $(this).unbind(scroll).unbind(resize).unbind(orient); });
1015
1068
 
1016
1069
  this.clearDropdownAlignmentPreference();
1017
1070
 
1071
+ $("#select2-drop-mask").hide();
1072
+ this.dropdown.removeAttr("id"); // only the active dropdown has the select2-drop id
1018
1073
  this.dropdown.hide();
1019
1074
  this.container.removeClass("select2-dropdown-open").removeClass("select2-container-active");
1020
1075
  this.results.empty();
@@ -1046,41 +1101,47 @@
1046
1101
  return;
1047
1102
  }
1048
1103
 
1049
- children = results.find(".select2-result-selectable");
1104
+ children = this.findHighlightableChoices();
1050
1105
 
1051
1106
  child = $(children[index]);
1052
1107
 
1053
- hb = child.offset().top + child.outerHeight();
1108
+ hb = child.offset().top + child.outerHeight(true);
1054
1109
 
1055
1110
  // if this is the last child lets also make sure select2-more-results is visible
1056
1111
  if (index === children.length - 1) {
1057
1112
  more = results.find("li.select2-more-results");
1058
1113
  if (more.length > 0) {
1059
- hb = more.offset().top + more.outerHeight();
1114
+ hb = more.offset().top + more.outerHeight(true);
1060
1115
  }
1061
1116
  }
1062
1117
 
1063
- rb = results.offset().top + results.outerHeight();
1118
+ rb = results.offset().top + results.outerHeight(true);
1064
1119
  if (hb > rb) {
1065
1120
  results.scrollTop(results.scrollTop() + (hb - rb));
1066
1121
  }
1067
1122
  y = child.offset().top - results.offset().top;
1068
1123
 
1069
1124
  // make sure the top of the element is visible
1070
- if (y < 0) {
1125
+ if (y < 0 && child.css('display') != 'none' ) {
1071
1126
  results.scrollTop(results.scrollTop() + y); // y is negative
1072
1127
  }
1073
1128
  },
1074
1129
 
1130
+ // abstract
1131
+ findHighlightableChoices: function() {
1132
+ var h=this.results.find(".select2-result-selectable:not(.select2-selected):not(.select2-disabled)");
1133
+ return this.results.find(".select2-result-selectable:not(.select2-selected):not(.select2-disabled)");
1134
+ },
1135
+
1075
1136
  // abstract
1076
1137
  moveHighlight: function (delta) {
1077
- var choices = this.results.find(".select2-result-selectable"),
1138
+ var choices = this.findHighlightableChoices(),
1078
1139
  index = this.highlight();
1079
1140
 
1080
1141
  while (index > -1 && index < choices.length) {
1081
1142
  index += delta;
1082
1143
  var choice = $(choices[index]);
1083
- if (choice.hasClass("select2-result-selectable") && !choice.hasClass("select2-disabled")) {
1144
+ if (choice.hasClass("select2-result-selectable") && !choice.hasClass("select2-disabled") && !choice.hasClass("select2-selected")) {
1084
1145
  this.highlight(index);
1085
1146
  break;
1086
1147
  }
@@ -1089,7 +1150,7 @@
1089
1150
 
1090
1151
  // abstract
1091
1152
  highlight: function (index) {
1092
- var choices = this.results.find(".select2-result-selectable").not(".select2-disabled");
1153
+ var choices = this.findHighlightableChoices();
1093
1154
 
1094
1155
  if (arguments.length === 0) {
1095
1156
  return indexOf(choices.filter(".select2-highlighted")[0], choices.get());
@@ -1098,23 +1159,22 @@
1098
1159
  if (index >= choices.length) index = choices.length - 1;
1099
1160
  if (index < 0) index = 0;
1100
1161
 
1101
- choices.removeClass("select2-highlighted");
1162
+ this.results.find(".select2-highlighted").removeClass("select2-highlighted");
1102
1163
 
1103
1164
  $(choices[index]).addClass("select2-highlighted");
1104
1165
  this.ensureHighlightVisible();
1105
-
1106
1166
  },
1107
1167
 
1108
1168
  // abstract
1109
1169
  countSelectableResults: function() {
1110
- return this.results.find(".select2-result-selectable").not(".select2-disabled").length;
1170
+ return this.findHighlightableChoices().length;
1111
1171
  },
1112
1172
 
1113
1173
  // abstract
1114
1174
  highlightUnderEvent: function (event) {
1115
1175
  var el = $(event.target).closest(".select2-result-selectable");
1116
1176
  if (el.length > 0 && !el.is(".select2-highlighted")) {
1117
- var choices = this.results.find('.select2-result-selectable');
1177
+ var choices = this.findHighlightableChoices();
1118
1178
  this.highlight(choices.index(el));
1119
1179
  } else if (el.length == 0) {
1120
1180
  // if we are over an unselectable item remove al highlights
@@ -1136,7 +1196,7 @@
1136
1196
  if (more.length === 0) return;
1137
1197
  below = more.offset().top - results.offset().top - results.height();
1138
1198
 
1139
- if (below <= 0) {
1199
+ if (below <= this.opts.loadMorePadding) {
1140
1200
  more.addClass("select2-active");
1141
1201
  this.opts.query({
1142
1202
  term: term,
@@ -1191,26 +1251,40 @@
1191
1251
  }
1192
1252
 
1193
1253
  function render(html) {
1194
- results.html(self.opts.escapeMarkup(html));
1254
+ results.html(html);
1195
1255
  postRender();
1196
1256
  }
1197
1257
 
1198
- if (opts.maximumSelectionSize >=1) {
1258
+ var maxSelSize = $.isFunction(opts.maximumSelectionSize) ? opts.maximumSelectionSize() : opts.maximumSelectionSize;
1259
+ if (maxSelSize >=1) {
1199
1260
  data = this.data();
1200
- if ($.isArray(data) && data.length >= opts.maximumSelectionSize && checkFormatter(opts.formatSelectionTooBig, "formatSelectionTooBig")) {
1201
- render("<li class='select2-selection-limit'>" + opts.formatSelectionTooBig(opts.maximumSelectionSize) + "</li>");
1261
+ if ($.isArray(data) && data.length >= maxSelSize && checkFormatter(opts.formatSelectionTooBig, "formatSelectionTooBig")) {
1262
+ render("<li class='select2-selection-limit'>" + opts.formatSelectionTooBig(maxSelSize) + "</li>");
1202
1263
  return;
1203
1264
  }
1204
1265
  }
1205
1266
 
1206
- if (search.val().length < opts.minimumInputLength && checkFormatter(opts.formatInputTooShort, "formatInputTooShort")) {
1207
- render("<li class='select2-no-results'>" + opts.formatInputTooShort(search.val(), opts.minimumInputLength) + "</li>");
1267
+ if (search.val().length < opts.minimumInputLength) {
1268
+ if (checkFormatter(opts.formatInputTooShort, "formatInputTooShort")) {
1269
+ render("<li class='select2-no-results'>" + opts.formatInputTooShort(search.val(), opts.minimumInputLength) + "</li>");
1270
+ } else {
1271
+ render("");
1272
+ }
1208
1273
  return;
1209
1274
  }
1210
- else {
1275
+ else if (opts.formatSearching() && initial===true) {
1211
1276
  render("<li class='select2-searching'>" + opts.formatSearching() + "</li>");
1212
1277
  }
1213
1278
 
1279
+ if (opts.maximumInputLength && search.val().length > opts.maximumInputLength) {
1280
+ if (checkFormatter(opts.formatInputTooLong, "formatInputTooLong")) {
1281
+ render("<li class='select2-no-results'>" + opts.formatInputTooLong(search.val(), opts.maximumInputLength) + "</li>");
1282
+ } else {
1283
+ render("");
1284
+ }
1285
+ return;
1286
+ }
1287
+
1214
1288
  // give the tokenizer a chance to pre-process the input
1215
1289
  input = this.tokenize();
1216
1290
  if (input != undefined && input != null) {
@@ -1271,6 +1345,10 @@
1271
1345
 
1272
1346
  // abstract
1273
1347
  blur: function () {
1348
+ // if selectOnBlur == true, select the currently highlighted option
1349
+ if (this.opts.selectOnBlur)
1350
+ this.selectHighlighted({noFocus: true});
1351
+
1274
1352
  this.close();
1275
1353
  this.container.removeClass("select2-container-active");
1276
1354
  this.dropdown.removeClass("select2-drop-active");
@@ -1278,6 +1356,7 @@
1278
1356
  if (this.search[0] === document.activeElement) { this.search.blur(); }
1279
1357
  this.clearSearch();
1280
1358
  this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
1359
+ this.opts.element.triggerHandler("blur");
1281
1360
  },
1282
1361
 
1283
1362
  // abstract
@@ -1297,19 +1376,27 @@
1297
1376
  },
1298
1377
 
1299
1378
  // abstract
1300
- selectHighlighted: function () {
1379
+ selectHighlighted: function (options) {
1301
1380
  var index=this.highlight(),
1302
- highlighted=this.results.find(".select2-highlighted").not(".select2-disabled"),
1303
- data = highlighted.closest('.select2-result-selectable').data("select2-data");
1381
+ highlighted=this.results.find(".select2-highlighted"),
1382
+ data = highlighted.closest('.select2-result').data("select2-data");
1383
+
1304
1384
  if (data) {
1305
- highlighted.addClass("select2-disabled");
1306
1385
  this.highlight(index);
1307
- this.onSelect(data);
1386
+ this.onSelect(data, options);
1308
1387
  }
1309
1388
  },
1310
1389
 
1311
1390
  // abstract
1312
1391
  getPlaceholder: function () {
1392
+
1393
+ // if a placeholder is specified on a select without the first empty option ignore it
1394
+ if (this.select) {
1395
+ if (this.select.find("option").first().text() !== "") {
1396
+ return undefined;
1397
+ }
1398
+ }
1399
+
1313
1400
  return this.opts.element.attr("placeholder") ||
1314
1401
  this.opts.element.attr("data-placeholder") || // jquery 1.4 compat
1315
1402
  this.opts.element.data("placeholder") ||
@@ -1330,7 +1417,7 @@
1330
1417
  if (this.opts.width === "off") {
1331
1418
  return null;
1332
1419
  } else if (this.opts.width === "element"){
1333
- return this.opts.element.outerWidth() === 0 ? 'auto' : this.opts.element.outerWidth() + 'px';
1420
+ return this.opts.element.outerWidth(false) === 0 ? 'auto' : this.opts.element.outerWidth(false) + 'px';
1334
1421
  } else if (this.opts.width === "copy" || this.opts.width === "resolve") {
1335
1422
  // check if there is inline style on the element that contains width
1336
1423
  style = this.opts.element.attr('style');
@@ -1351,7 +1438,7 @@
1351
1438
  if (style.indexOf("%") > 0) return style;
1352
1439
 
1353
1440
  // finally, fallback on the calculated width of the element
1354
- return (this.opts.element.outerWidth() === 0 ? 'auto' : this.opts.element.outerWidth() + 'px');
1441
+ return (this.opts.element.outerWidth(false) === 0 ? 'auto' : this.opts.element.outerWidth(false) + 'px');
1355
1442
  }
1356
1443
 
1357
1444
  return null;
@@ -1374,10 +1461,10 @@
1374
1461
  // single
1375
1462
 
1376
1463
  createContainer: function () {
1377
- var container = $("<div></div>", {
1464
+ var container = $(document.createElement("div")).attr({
1378
1465
  "class": "select2-container"
1379
1466
  }).html([
1380
- " <a href='#' onclick='return false;' class='select2-choice'>",
1467
+ " <a href='javascript:void(0)' onclick='return false;' class='select2-choice'>",
1381
1468
  " <span></span><abbr class='select2-search-choice-close' style='display:none;'></abbr>",
1382
1469
  " <div><b></b></div>" ,
1383
1470
  "</a>",
@@ -1391,6 +1478,31 @@
1391
1478
  return container;
1392
1479
  },
1393
1480
 
1481
+ // single
1482
+ disable: function() {
1483
+ if (!this.enabled) return;
1484
+
1485
+ this.parent.disable.apply(this, arguments);
1486
+
1487
+ this.selection.attr("tabIndex", "-1");
1488
+ this.search.attr("tabIndex", "-1");
1489
+ },
1490
+
1491
+ // single
1492
+ enable: function() {
1493
+ if (this.enabled) return;
1494
+
1495
+ this.parent.enable.apply(this, arguments);
1496
+
1497
+ if (this.elementTabIndex) {
1498
+ this.selection.attr("tabIndex", this.elementTabIndex)
1499
+ } else {
1500
+ this.selection.removeAttr("tabIndex");
1501
+ }
1502
+
1503
+ this.search.removeAttr("tabIndex");
1504
+ },
1505
+
1394
1506
  // single
1395
1507
  opening: function () {
1396
1508
  this.search.show();
@@ -1409,6 +1521,7 @@
1409
1521
  focus: function () {
1410
1522
  this.close();
1411
1523
  this.selection.focus();
1524
+ this.opts.element.triggerHandler("focus");
1412
1525
  },
1413
1526
 
1414
1527
  // single
@@ -1482,7 +1595,24 @@
1482
1595
  }));
1483
1596
  this.search.bind("blur", this.bind(function() {
1484
1597
  if (!this.opened()) this.container.removeClass("select2-container-active");
1485
- window.setTimeout(this.bind(function() { this.selection.attr("tabIndex", this.opts.element.attr("tabIndex")); }), 10);
1598
+ window.setTimeout(this.bind(function() {
1599
+ // restore original tab index
1600
+ var ti=this.elementTabIndex || 0;
1601
+ if (ti) {
1602
+ this.selection.attr("tabIndex", ti);
1603
+ } else {
1604
+ this.selection.removeAttr("tabIndex");
1605
+ }
1606
+ }), 10);
1607
+ }));
1608
+
1609
+ selection.delegate("abbr", "mousedown", this.bind(function (e) {
1610
+ if (!this.enabled) return;
1611
+ this.clear();
1612
+ killEventImmediately(e);
1613
+ this.close();
1614
+ this.triggerChange();
1615
+ this.selection.focus();
1486
1616
  }));
1487
1617
 
1488
1618
  selection.bind("mousedown", this.bind(function (e) {
@@ -1501,6 +1631,8 @@
1501
1631
  dropdown.bind("mousedown", this.bind(function() { this.search.focus(); }));
1502
1632
 
1503
1633
  selection.bind("focus", this.bind(function() {
1634
+ if (!this.enabled) return;
1635
+
1504
1636
  this.container.addClass("select2-container-active");
1505
1637
  // hide the search so the tab key does not focus on it
1506
1638
  this.search.attr("tabIndex", "-1");
@@ -1510,70 +1642,34 @@
1510
1642
  if (!this.opened()) {
1511
1643
  this.container.removeClass("select2-container-active");
1512
1644
  }
1513
- window.setTimeout(this.bind(function() { this.search.attr("tabIndex", this.opts.element.attr("tabIndex")); }), 10);
1645
+ window.setTimeout(this.bind(function() { this.search.attr("tabIndex", this.elementTabIndex || 0); }), 10);
1514
1646
  }));
1515
1647
 
1516
1648
  selection.bind("keydown", this.bind(function(e) {
1517
1649
  if (!this.enabled) return;
1518
1650
 
1519
- if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) {
1520
- // prevent the page from scrolling
1651
+ if (e.which == KEY.DOWN || e.which == KEY.UP
1652
+ || (e.which == KEY.ENTER && this.opts.openOnEnter)) {
1653
+ this.open();
1521
1654
  killEvent(e);
1522
1655
  return;
1523
1656
  }
1524
1657
 
1525
- if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e)
1526
- || e.which === KEY.ESC) {
1527
- return;
1528
- }
1529
-
1530
- if (this.opts.openOnEnter === false && e.which === KEY.ENTER) {
1531
- return;
1532
- }
1533
-
1534
- if (e.which == KEY.DELETE) {
1658
+ if (e.which == KEY.DELETE || e.which == KEY.BACKSPACE) {
1535
1659
  if (this.opts.allowClear) {
1536
1660
  this.clear();
1537
1661
  }
1538
- return;
1539
- }
1540
-
1541
- this.open();
1542
-
1543
- if (e.which === KEY.ENTER) {
1544
- // do not propagate the event otherwise we open, and propagate enter which closes
1545
- killEvent(e);
1546
- return;
1547
- }
1548
-
1549
- // do not set the search input value for non-alpha-numeric keys
1550
- // otherwise pressing down results in a '(' being set in the search field
1551
- if (e.which < 48 ) { // '0' == 48
1552
1662
  killEvent(e);
1553
1663
  return;
1554
1664
  }
1555
-
1556
- var keyWritten = String.fromCharCode(e.which).toLowerCase();
1557
-
1558
- if (e.shiftKey) {
1559
- keyWritten = keyWritten.toUpperCase();
1560
- }
1561
-
1562
- // focus the field before calling val so the cursor ends up after the value instead of before
1563
- this.search.focus();
1564
- this.search.val(keyWritten);
1565
-
1566
- // prevent event propagation so it doesnt replay on the now focussed search field and result in double key entry
1567
- killEvent(e);
1568
1665
  }));
1569
-
1570
- selection.delegate("abbr", "mousedown", this.bind(function (e) {
1571
- if (!this.enabled) return;
1572
- this.clear();
1573
- killEvent(e);
1574
- this.close();
1575
- this.triggerChange();
1576
- this.selection.focus();
1666
+ selection.bind("keypress", this.bind(function(e) {
1667
+ if (e.which == KEY.DELETE || e.which == KEY.BACKSPACE || e.which == KEY.TAB || e.which == KEY.ENTER || e.which == 0) {
1668
+ return
1669
+ }
1670
+ var key = String.fromCharCode(e.which);
1671
+ this.search.val(key);
1672
+ this.open();
1577
1673
  }));
1578
1674
 
1579
1675
  this.setPlaceholder();
@@ -1597,7 +1693,7 @@
1597
1693
  // single
1598
1694
  initSelection: function () {
1599
1695
  var selected;
1600
- if (this.opts.element.val() === "") {
1696
+ if (this.opts.element.val() === "" && this.opts.element.text() === "") {
1601
1697
  this.close();
1602
1698
  this.setPlaceholder();
1603
1699
  } else {
@@ -1622,7 +1718,21 @@
1622
1718
  var selected = element.find(":selected");
1623
1719
  // a single select box always has a value, no need to null check 'selected'
1624
1720
  if ($.isFunction(callback))
1625
- callback({id: selected.attr("value"), text: selected.text()});
1721
+ callback({id: selected.attr("value"), text: selected.text(), element:selected});
1722
+ };
1723
+ } else if ("data" in opts) {
1724
+ // install default initSelection when applied to hidden input and data is local
1725
+ opts.initSelection = opts.initSelection || function (element, callback) {
1726
+ var id = element.val();
1727
+ //search in data by id
1728
+ opts.query({
1729
+ matcher: function(term, text, el){
1730
+ return equal(id, opts.id(el));
1731
+ },
1732
+ callback: !$.isFunction(callback) ? $.noop : function(filtered) {
1733
+ callback(filtered.results.length ? filtered.results[0] : null);
1734
+ }
1735
+ });
1626
1736
  };
1627
1737
  }
1628
1738
 
@@ -1652,7 +1762,7 @@
1652
1762
 
1653
1763
  // find the selected element in the result list
1654
1764
 
1655
- this.results.find(".select2-result-selectable").each2(function (i, elm) {
1765
+ this.findHighlightableChoices().each2(function (i, elm) {
1656
1766
  if (equal(self.id(elm.data("select2-data")), self.opts.element.val())) {
1657
1767
  selected = i;
1658
1768
  return false;
@@ -1676,13 +1786,15 @@
1676
1786
  },
1677
1787
 
1678
1788
  // single
1679
- onSelect: function (data) {
1789
+ onSelect: function (data, options) {
1680
1790
  var old = this.opts.element.val();
1681
1791
 
1682
1792
  this.opts.element.val(this.id(data));
1683
1793
  this.updateSelection(data);
1684
1794
  this.close();
1685
- this.selection.focus();
1795
+
1796
+ if (!options || !options.noFocus)
1797
+ this.selection.focus();
1686
1798
 
1687
1799
  if (!equal(old, this.id(data))) { this.triggerChange(); }
1688
1800
  },
@@ -1709,7 +1821,7 @@
1709
1821
 
1710
1822
  // single
1711
1823
  val: function () {
1712
- var val, data = null, self = this;
1824
+ var val, triggerChange = false, data = null, self = this;
1713
1825
 
1714
1826
  if (arguments.length === 0) {
1715
1827
  return this.opts.element.val();
@@ -1717,6 +1829,10 @@
1717
1829
 
1718
1830
  val = arguments[0];
1719
1831
 
1832
+ if (arguments.length > 1) {
1833
+ triggerChange = arguments[1];
1834
+ }
1835
+
1720
1836
  if (this.select) {
1721
1837
  this.select
1722
1838
  .val(val)
@@ -1726,13 +1842,19 @@
1726
1842
  });
1727
1843
  this.updateSelection(data);
1728
1844
  this.setPlaceholder();
1845
+ if (triggerChange) {
1846
+ this.triggerChange();
1847
+ }
1729
1848
  } else {
1730
1849
  if (this.opts.initSelection === undefined) {
1731
1850
  throw new Error("cannot call val() if initSelection() is not defined");
1732
1851
  }
1733
- // val is an id. !val is true for [undefined,null,'']
1734
- if (!val) {
1852
+ // val is an id. !val is true for [undefined,null,'',0] - 0 is legal
1853
+ if (!val && val !== 0) {
1735
1854
  this.clear();
1855
+ if (triggerChange) {
1856
+ this.triggerChange();
1857
+ }
1736
1858
  return;
1737
1859
  }
1738
1860
  this.opts.element.val(val);
@@ -1740,6 +1862,7 @@
1740
1862
  self.opts.element.val(!data ? "" : self.id(data));
1741
1863
  self.updateSelection(data);
1742
1864
  self.setPlaceholder();
1865
+ self.triggerChange();
1743
1866
  });
1744
1867
  }
1745
1868
  },
@@ -1772,7 +1895,7 @@
1772
1895
 
1773
1896
  // multi
1774
1897
  createContainer: function () {
1775
- var container = $("<div></div>", {
1898
+ var container = $(document.createElement("div")).attr({
1776
1899
  "class": "select2-container select2-container-multi"
1777
1900
  }).html([
1778
1901
  " <ul class='select2-choices'>",
@@ -1800,12 +1923,28 @@
1800
1923
 
1801
1924
  var data = [];
1802
1925
  element.find(":selected").each2(function (i, elm) {
1803
- data.push({id: elm.attr("value"), text: elm.text()});
1926
+ data.push({id: elm.attr("value"), text: elm.text(), element: elm});
1804
1927
  });
1805
1928
 
1806
1929
  if ($.isFunction(callback))
1807
1930
  callback(data);
1808
1931
  };
1932
+ } else if ("data" in opts) {
1933
+ // install default initSelection when applied to hidden input and data is local
1934
+ opts.initSelection = opts.initSelection || function (element, callback) {
1935
+ var ids = splitVal(element.val(), opts.separator);
1936
+ //search in data by array of ids
1937
+ opts.query({
1938
+ matcher: function(term, text, el){
1939
+ return $.grep(ids, function(id) {
1940
+ return equal(id, opts.id(el));
1941
+ }).length;
1942
+ },
1943
+ callback: !$.isFunction(callback) ? $.noop : function(filtered) {
1944
+ callback(filtered.results);
1945
+ }
1946
+ });
1947
+ };
1809
1948
  }
1810
1949
 
1811
1950
  return opts;
@@ -1834,7 +1973,7 @@
1834
1973
  return;
1835
1974
  }
1836
1975
 
1837
- choices = selection.find(".select2-search-choice");
1976
+ choices = selection.find(".select2-search-choice:not(.select2-locked)");
1838
1977
  if (choices.length > 0) {
1839
1978
  choices.last().addClass("select2-search-choice-focus");
1840
1979
  }
@@ -1883,7 +2022,7 @@
1883
2022
  this.search.bind("blur", this.bind(function(e) {
1884
2023
  this.container.removeClass("select2-container-active");
1885
2024
  this.search.removeClass("select2-focused");
1886
- this.clearSearch();
2025
+ if (!this.opened()) this.clearSearch();
1887
2026
  e.stopImmediatePropagation();
1888
2027
  }));
1889
2028
 
@@ -1931,7 +2070,7 @@
1931
2070
  // multi
1932
2071
  initSelection: function () {
1933
2072
  var data;
1934
- if (this.opts.element.val() === "") {
2073
+ if (this.opts.element.val() === "" && this.opts.element.text() === "") {
1935
2074
  this.updateSelection([]);
1936
2075
  this.close();
1937
2076
  // set the placeholder if necessary
@@ -1994,6 +2133,7 @@
1994
2133
  focus: function () {
1995
2134
  this.close();
1996
2135
  this.search.focus();
2136
+ this.opts.element.triggerHandler("focus");
1997
2137
  },
1998
2138
 
1999
2139
  // multi
@@ -2034,9 +2174,9 @@
2034
2174
  },
2035
2175
 
2036
2176
  // multi
2037
- onSelect: function (data) {
2177
+ onSelect: function (data, options) {
2038
2178
  this.addSelectedChoice(data);
2039
- if (this.select) { this.postprocessResults(); }
2179
+ if (this.select || !this.opts.closeOnSelect) this.postprocessResults();
2040
2180
 
2041
2181
  if (this.opts.closeOnSelect) {
2042
2182
  this.close();
@@ -2056,7 +2196,8 @@
2056
2196
  // added we do not need to check if this is a new element before firing change
2057
2197
  this.triggerChange({ added: data });
2058
2198
 
2059
- this.focusSearch();
2199
+ if (!options || !options.noFocus)
2200
+ this.focusSearch();
2060
2201
  },
2061
2202
 
2062
2203
  // multi
@@ -2065,36 +2206,46 @@
2065
2206
  this.focusSearch();
2066
2207
  },
2067
2208
 
2068
- // multi
2069
2209
  addSelectedChoice: function (data) {
2070
- var choice=$(
2210
+ var enableChoice = !data.locked,
2211
+ enabledItem = $(
2071
2212
  "<li class='select2-search-choice'>" +
2072
2213
  " <div></div>" +
2073
2214
  " <a href='#' onclick='return false;' class='select2-search-choice-close' tabindex='-1'></a>" +
2074
2215
  "</li>"),
2216
+ disabledItem = $(
2217
+ "<li class='select2-search-choice select2-locked'>" +
2218
+ "<div></div>" +
2219
+ "</li>");
2220
+ var choice = enableChoice ? enabledItem : disabledItem,
2075
2221
  id = this.id(data),
2076
2222
  val = this.getVal(),
2077
2223
  formatted;
2078
2224
 
2079
- formatted=this.opts.formatSelection(data, choice);
2080
- choice.find("div").replaceWith("<div>"+this.opts.escapeMarkup(formatted)+"</div>");
2081
- choice.find(".select2-search-choice-close")
2082
- .bind("mousedown", killEvent)
2083
- .bind("click dblclick", this.bind(function (e) {
2084
- if (!this.enabled) return;
2225
+ formatted=this.opts.formatSelection(data, choice.find("div"));
2226
+ if (formatted != undefined) {
2227
+ choice.find("div").replaceWith("<div>"+this.opts.escapeMarkup(formatted)+"</div>");
2228
+ }
2085
2229
 
2086
- $(e.target).closest(".select2-search-choice").fadeOut('fast', this.bind(function(){
2087
- this.unselect($(e.target));
2088
- this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
2089
- this.close();
2090
- this.focusSearch();
2091
- })).dequeue();
2092
- killEvent(e);
2093
- })).bind("focus", this.bind(function () {
2094
- if (!this.enabled) return;
2095
- this.container.addClass("select2-container-active");
2096
- this.dropdown.addClass("select2-drop-active");
2097
- }));
2230
+ if(enableChoice){
2231
+ choice.find(".select2-search-choice-close")
2232
+ .bind("mousedown", killEvent)
2233
+ .bind("click dblclick", this.bind(function (e) {
2234
+ if (!this.enabled) return;
2235
+
2236
+ $(e.target).closest(".select2-search-choice").fadeOut('fast', this.bind(function(){
2237
+ this.unselect($(e.target));
2238
+ this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
2239
+ this.close();
2240
+ this.focusSearch();
2241
+ })).dequeue();
2242
+ killEvent(e);
2243
+ })).bind("focus", this.bind(function () {
2244
+ if (!this.enabled) return;
2245
+ this.container.addClass("select2-container-active");
2246
+ this.dropdown.addClass("select2-drop-active");
2247
+ }));
2248
+ }
2098
2249
 
2099
2250
  choice.data("select2-data", data);
2100
2251
  choice.insertBefore(this.searchContainer);
@@ -2117,6 +2268,12 @@
2117
2268
 
2118
2269
  data = selected.data("select2-data");
2119
2270
 
2271
+ if (!data) {
2272
+ // prevent a race condition when the 'x' is clicked really fast repeatedly the event can be queued
2273
+ // and invoked on an element already removed
2274
+ return;
2275
+ }
2276
+
2120
2277
  index = indexOf(this.id(data), val);
2121
2278
 
2122
2279
  if (index >= 0) {
@@ -2131,33 +2288,30 @@
2131
2288
  // multi
2132
2289
  postprocessResults: function () {
2133
2290
  var val = this.getVal(),
2134
- choices = this.results.find(".select2-result-selectable"),
2291
+ choices = this.results.find(".select2-result"),
2135
2292
  compound = this.results.find(".select2-result-with-children"),
2136
2293
  self = this;
2137
2294
 
2138
2295
  choices.each2(function (i, choice) {
2139
2296
  var id = self.id(choice.data("select2-data"));
2140
2297
  if (indexOf(id, val) >= 0) {
2141
- choice.addClass("select2-disabled").removeClass("select2-result-selectable");
2142
- } else {
2143
- choice.removeClass("select2-disabled").addClass("select2-result-selectable");
2298
+ choice.addClass("select2-selected");
2299
+ // mark all children of the selected parent as selected
2300
+ choice.find(".select2-result-selectable").addClass("select2-selected");
2144
2301
  }
2145
2302
  });
2146
2303
 
2147
- compound.each2(function(i, e) {
2148
- if (e.find(".select2-result-selectable").length==0) {
2149
- e.addClass("select2-disabled");
2150
- } else {
2151
- e.removeClass("select2-disabled");
2304
+ compound.each2(function(i, choice) {
2305
+ // hide an optgroup if it doesnt have any selectable children
2306
+ if (!choice.is('.select2-result-selectable')
2307
+ && choice.find(".select2-result-selectable:not(.select2-selected)").length === 0) {
2308
+ choice.addClass("select2-selected");
2152
2309
  }
2153
2310
  });
2154
2311
 
2155
- choices.each2(function (i, choice) {
2156
- if (!choice.hasClass("select2-disabled") && choice.hasClass("select2-result-selectable")) {
2157
- self.highlight(0);
2158
- return false;
2159
- }
2160
- });
2312
+ if (this.highlight() == -1){
2313
+ self.highlight(0);
2314
+ }
2161
2315
 
2162
2316
  },
2163
2317
 
@@ -2182,6 +2336,11 @@
2182
2336
  if (searchWidth < 40) {
2183
2337
  searchWidth = maxWidth - sideBorderPadding;
2184
2338
  }
2339
+
2340
+ if (searchWidth <= 0) {
2341
+ searchWidth = minimumWidth
2342
+ }
2343
+
2185
2344
  this.search.width(searchWidth);
2186
2345
  },
2187
2346
 
@@ -2214,7 +2373,7 @@
2214
2373
 
2215
2374
  // multi
2216
2375
  val: function () {
2217
- var val, data = [], self=this;
2376
+ var val, triggerChange = false, data = [], self=this;
2218
2377
 
2219
2378
  if (arguments.length === 0) {
2220
2379
  return this.getVal();
@@ -2222,10 +2381,18 @@
2222
2381
 
2223
2382
  val = arguments[0];
2224
2383
 
2225
- if (!val) {
2384
+ if (arguments.length > 1) {
2385
+ triggerChange = arguments[1];
2386
+ }
2387
+
2388
+ // val is an id. !val is true for [undefined,null,'',0] - 0 is legal
2389
+ if (!val && val !== 0) {
2226
2390
  this.opts.element.val("");
2227
2391
  this.updateSelection([]);
2228
2392
  this.clearSearch();
2393
+ if (triggerChange) {
2394
+ this.triggerChange();
2395
+ }
2229
2396
  return;
2230
2397
  }
2231
2398
 
@@ -2237,6 +2404,9 @@
2237
2404
  data.push({id: $(this).attr("value"), text: $(this).text()});
2238
2405
  });
2239
2406
  this.updateSelection(data);
2407
+ if (triggerChange) {
2408
+ this.triggerChange();
2409
+ }
2240
2410
  } else {
2241
2411
  if (this.opts.initSelection === undefined) {
2242
2412
  throw new Error("val() cannot be called if initSelection() is not defined")
@@ -2247,6 +2417,9 @@
2247
2417
  self.setVal(ids);
2248
2418
  self.updateSelection(data);
2249
2419
  self.clearSearch();
2420
+ if (triggerChange) {
2421
+ self.triggerChange();
2422
+ }
2250
2423
  });
2251
2424
  }
2252
2425
  this.clearSearch();
@@ -2349,28 +2522,34 @@
2349
2522
  // plugin defaults, accessible to users
2350
2523
  $.fn.select2.defaults = {
2351
2524
  width: "copy",
2525
+ loadMorePadding: 0,
2352
2526
  closeOnSelect: true,
2353
2527
  openOnEnter: true,
2354
2528
  containerCss: {},
2355
2529
  dropdownCss: {},
2356
2530
  containerCssClass: "",
2357
2531
  dropdownCssClass: "",
2358
- formatResult: function(result, container, query) {
2532
+ formatResult: function(result, container, query, escapeMarkup) {
2359
2533
  var markup=[];
2360
- markMatch(result.text, query.term, markup);
2534
+ markMatch(result.text, query.term, markup, escapeMarkup);
2361
2535
  return markup.join("");
2362
2536
  },
2363
2537
  formatSelection: function (data, container) {
2364
2538
  return data ? data.text : undefined;
2365
2539
  },
2540
+ sortResults: function (results, container, query) {
2541
+ return results;
2542
+ },
2366
2543
  formatResultCssClass: function(data) {return undefined;},
2367
2544
  formatNoMatches: function () { return "No matches found"; },
2368
- formatInputTooShort: function (input, min) { return "Please enter " + (min - input.length) + " more characters"; },
2545
+ formatInputTooShort: function (input, min) { var n = min - input.length; return "Please enter " + n + " more character" + (n == 1? "" : "s"); },
2546
+ formatInputTooLong: function (input, max) { var n = input.length - max; return "Please enter " + n + " less character" + (n == 1? "" : "s"); },
2369
2547
  formatSelectionTooBig: function (limit) { return "You can only select " + limit + " item" + (limit == 1 ? "" : "s"); },
2370
2548
  formatLoadMore: function (pageNumber) { return "Loading more results..."; },
2371
2549
  formatSearching: function () { return "Searching..."; },
2372
2550
  minimumResultsForSearch: 0,
2373
2551
  minimumInputLength: 0,
2552
+ maximumInputLength: null,
2374
2553
  maximumSelectionSize: 0,
2375
2554
  id: function (e) { return e.id; },
2376
2555
  matcher: function(term, text) {
@@ -2380,12 +2559,22 @@
2380
2559
  tokenSeparators: [],
2381
2560
  tokenizer: defaultTokenizer,
2382
2561
  escapeMarkup: function (markup) {
2383
- if (markup && typeof(markup) === "string") {
2384
- return markup.replace(/&/g, "&amp;");
2385
- }
2386
- return markup;
2562
+ var replace_map = {
2563
+ '\\': '&#92;',
2564
+ '&': '&amp;',
2565
+ '<': '&lt;',
2566
+ '>': '&gt;',
2567
+ '"': '&quot;',
2568
+ "'": '&apos;',
2569
+ "/": '&#47;'
2570
+ };
2571
+
2572
+ return String(markup).replace(/[&<>"'/\\]/g, function (match) {
2573
+ return replace_map[match[0]];
2574
+ });
2387
2575
  },
2388
- blurOnChange: false
2576
+ blurOnChange: false,
2577
+ selectOnBlur: false
2389
2578
  };
2390
2579
 
2391
2580
  // exports