select2-rails 3.2.1 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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