diff --git a/README.md b/README.md
index 3b61cad5..b00a4f2b 100755
--- a/README.md
+++ b/README.md
@@ -27,6 +27,13 @@ Browser Compatibility
* Safari 3+
* Opera 10.6+
+Integrations
+------------
+
+* [Wicket-Select2](https://github.com/ivaynberg/wicket-select2) (Java / Apache Wicket)
+* [select2-rails](https://github.com/argerim/select2-rails) (Ruby on Rails)
+* [AngularUI](http://angular-ui.github.com/#directives-select2) ([AngularJS](angularjs.org))
+
Bug tracker
-----------
diff --git a/select2.css b/select2.css
index 244dc03e..3f061ac5 100755
--- a/select2.css
+++ b/select2.css
@@ -13,11 +13,11 @@ Version: @@ver@@ Timestamp: @@timestamp@@
.select2-container,
.select2-drop,
.select2-search,
-.select2-container .select2-search input{
- /*
+.select2-search input{
+ /*
Force border-box so that % widths fit the parent
container without overlap because of margin/padding.
-
+
More Info : http://www.quirksmode.org/css/box.html
*/
-moz-box-sizing: border-box; /* firefox */
@@ -72,7 +72,7 @@ Version: @@ver@@ Timestamp: @@timestamp@@
width: 12px;
height: 12px;
font-size: 1px;
- background: url(select2.png) right top no-repeat;
+ background: url('select2.png') right top no-repeat;
cursor: pointer;
text-decoration: none;
border:0;
@@ -83,7 +83,7 @@ Version: @@ver@@ Timestamp: @@timestamp@@
cursor: pointer;
}
-.select2-container .select2-drop {
+.select2-drop {
background: #fff;
border: 1px solid #aaa;
border-top: 0;
@@ -133,7 +133,7 @@ Version: @@ver@@ Timestamp: @@timestamp@@
height: 100%;
}
-.select2-container .select2-search {
+.select2-search {
display: inline-block;
white-space: nowrap;
z-index: 1010;
@@ -144,13 +144,13 @@ Version: @@ver@@ Timestamp: @@timestamp@@
padding-right: 4px;
}
-.select2-container .select2-search-hidden {
+.select2-search-hidden {
display: block;
position: absolute;
left: -10000px;
}
-.select2-container .select2-search input {
+.select2-search input {
background: #fff url('select2.png') no-repeat 100% -22px;
background: url('select2.png') no-repeat 100% -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee));
background: url('select2.png') no-repeat 100% -22px, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%);
@@ -172,10 +172,10 @@ Version: @@ver@@ Timestamp: @@timestamp@@
box-shadow: none;
border-radius: 0;
-moz-border-radius: 0;
- -webkit-border-radius: 0;
+ -webkit-border-radius: 0;
}
-.select2-container .select2-search input.select2-active {
+.select2-search input.select2-active {
background: #fff url('spinner.gif') no-repeat 100%;
background: url('spinner.gif') no-repeat 100%, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee));
background: url('spinner.gif') no-repeat 100%, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%);
@@ -228,7 +228,7 @@ Version: @@ver@@ Timestamp: @@timestamp@@
}
/* results */
-.select2-container .select2-results {
+.select2-results {
margin: 4px 4px 4px 0;
padding: 0 0 0 4px;
position: relative;
@@ -236,46 +236,65 @@ Version: @@ver@@ Timestamp: @@timestamp@@
overflow-y: auto;
max-height: 200px;
}
-.select2-container .select2-results li {
- line-height: 80%;
- padding: 7px 7px 8px;
- margin: 0;
+
+.select2-results ul.select2-result-sub {
+ margin: 0 0 0 0;
+}
+
+.select2-results ul.select2-result-sub > li .select2-result-label { padding-left: 20px }
+.select2-results ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 40px }
+.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 60px }
+.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 80px }
+.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 100px }
+.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 110px }
+.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 120px }
+
+.select2-results li {
list-style: none;
- cursor: pointer;
display: list-item;
}
-.select2-container .select2-results .select2-highlighted {
+.select2-results li.select2-result-with-children > .select2-result-label {
+ font-weight: bold;
+}
+
+.select2-results .select2-result-label {
+ padding: 3px 7px 4px;
+ margin: 0;
+ cursor: pointer;
+}
+
+.select2-results .select2-highlighted {
background: #3875d7;
color: #fff;
}
-.select2-container .select2-results li em {
+.select2-results li em {
background: #feffde;
font-style: normal;
}
-.select2-container .select2-results .select2-highlighted em {
+.select2-results .select2-highlighted em {
background: transparent;
}
-.select2-container .select2-results .select2-no-results {
+.select2-results .select2-no-results {
background: #f4f4f4;
display: list-item;
}
/*
disabled look for already selected choices in the results dropdown
-.select2-container .select2-results .select2-disabled.select2-highlighted {
+.select2-results .select2-disabled.select2-highlighted {
color: #666;
background: #f4f4f4;
display: list-item;
cursor: default;
}
-.select2-container .select2-results .select2-disabled {
+.select2-results .select2-disabled {
background: #f4f4f4;
display: list-item;
cursor: default;
}
*/
-.select2-container .select2-results .select2-disabled {
+.select2-results .select2-disabled {
display: none;
}
@@ -324,10 +343,6 @@ disabled look for already selected choices in the results dropdown
position: relative;
}
-.select2-container-multi .select2-drop {
- margin-top:0;
-}
-
.select2-container-multi .select2-choices {
min-height: 26px;
}
@@ -366,6 +381,9 @@ disabled look for already selected choices in the results dropdown
box-shadow : none;
}
+.select2-container-multi .select2-choices .select2-search-field input.select2-active {
+ background: #fff url('spinner.gif') no-repeat 100% !important;
+}
.select2-default {
color: #999 !important;
@@ -412,7 +430,7 @@ disabled look for already selected choices in the results dropdown
width: 12px;
height: 13px;
font-size: 1px;
- background: url(select2.png) right top no-repeat;
+ background: url('select2.png') right top no-repeat;
outline: none;
}
@@ -428,12 +446,6 @@ disabled look for already selected choices in the results dropdown
background-position: right -11px;
}
-
-.select2-container-multi .select2-results {
- margin: -1px 0 0;
- padding: 0;
-}
-
/* disabled styles */
.select2-container-multi.select2-container-disabled .select2-choices{
@@ -454,3 +466,7 @@ disabled look for already selected choices in the results dropdown
display: none;
}
/* end multiselect */
+
+.select2-match { text-decoration: underline; }
+
+.select2-offscreen { position: absolute; left: -1000px; }
\ No newline at end of file
diff --git a/select2.js b/select2.js
index 580b725e..fe2c7f73 100755
--- a/select2.js
+++ b/select2.js
@@ -1,6 +1,6 @@
/*
Copyright 2012 Igor Vaynberg
-
+
Version: @@ver@@ Timestamp: @@timestamp@@
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in
@@ -12,6 +12,26 @@
distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and limitations under the License.
*/
+ (function ($) {
+ if(typeof $.fn.each2 == "undefined"){
+ $.fn.extend({
+ /*
+ * 4-10 times faster .each replacement
+ * use it carefully, as it overrides jQuery context of element on each iteration
+ */
+ each2 : function (c) {
+ var j = $([0]), i = -1, l = this.length;
+ while (
+ ++i < l
+ && (j.context = j[0] = this[i])
+ && c.call(j[0], i, j) !== false //"this"=DOM, i=index, j=jQuery object
+ );
+ return this;
+ }
+ });
+ }
+})(jQuery);
+
(function ($, undefined) {
"use strict";
/*global document, window, jQuery, console */
@@ -20,7 +40,7 @@
return;
}
- var KEY, AbstractSelect2, SingleSelect2, MultiSelect2;
+ var KEY, AbstractSelect2, SingleSelect2, MultiSelect2, nextUid, sizer;
KEY = {
TAB: 9,
@@ -67,10 +87,12 @@
}
};
+ nextUid=(function() { var counter=1; return function() { return counter++; }; }());
+
function indexOf(value, array) {
var i = 0, l = array.length, v;
- if (typeof value == 'undefined') {
+ if (typeof value === "undefined") {
return -1;
}
@@ -90,7 +112,7 @@
}
/**
- * Compares equality of a and b taking into account that a and b may be strings, in which case localCompare is used
+ * Compares equality of a and b taking into account that a and b may be strings, in which case localeCompare is used
* @param a
* @param b
*/
@@ -132,17 +154,18 @@
});
}
+ $(document).delegate("*", "mousemove", function (e) {
+ $(document).data("select2-lastpos", {x: e.pageX, y: e.pageY});
+ });
+
/**
* filters mouse events so an event is fired only if the mouse moved.
*
* filters out mouse events that occur when mouse is stationary but
* the elements under the pointer are scrolled.
- */
- $(document).delegate("*", "mousemove", function (e) {
- $(document).data("select2-lastpos", {x: e.pageX, y: e.pageY});
- });
+ */
function installFilteredMouseMove(element) {
- element.bind("mousemove", function (e) {
+ element.bind("mousemove", function (e) {
var lastpos = $(document).data("select2-lastpos");
if (lastpos === undefined || lastpos.x !== e.pageX || lastpos.y !== e.pageY) {
$(e.target).trigger("mousemove-filtered", e);
@@ -178,26 +201,42 @@
event.stopPropagation();
}
- function measureTextWidth(e) {
- var sizer, width;
- sizer = $("
").css({
- position: "absolute",
- left: "-1000px",
- top: "-1000px",
- display: "none",
- fontSize: e.css("fontSize"),
- fontFamily: e.css("fontFamily"),
- fontStyle: e.css("fontStyle"),
- fontWeight: e.css("fontWeight"),
- letterSpacing: e.css("letterSpacing"),
- textTransform: e.css("textTransform"),
- whiteSpace: "nowrap"
- });
- sizer.text(e.val());
- $("body").append(sizer);
- width = sizer.width();
- sizer.remove();
- return width;
+ function measureTextWidth(e) {
+ if (!sizer){
+ var style = e[0].currentStyle || window.getComputedStyle(e[0], null);
+ sizer = $("
").css({
+ position: "absolute",
+ left: "-1000px",
+ top: "-1000px",
+ display: "none",
+ fontSize: style.fontSize,
+ fontFamily: style.fontFamily,
+ fontStyle: style.fontStyle,
+ fontWeight: style.fontWeight,
+ letterSpacing: style.letterSpacing,
+ textTransform: style.textTransform,
+ whiteSpace: "nowrap"
+ });
+ $("body").append(sizer);
+ }
+ sizer.text(e.val());
+ return sizer.width();
+ }
+
+ function markMatch(text, term, markup) {
+ var match=text.toUpperCase().indexOf(term.toUpperCase()),
+ tl=term.length;
+
+ if (match<0) {
+ markup.push(text);
+ return;
+ }
+
+ markup.push(text.substring(0, match));
+ markup.push("");
+ markup.push(text.substring(match, match + tl));
+ markup.push(" ");
+ markup.push(text.substring(match + tl, text.length));
}
/**
@@ -227,24 +266,24 @@
requestSequence += 1; // increment the sequence
var requestNumber = requestSequence, // this request's sequence number
data = options.data, // ajax data function
- transport = options.transport || $.ajax;
-
+ transport = options.transport || $.ajax,
+ type = options.type || 'GET'; // set type of request (GET or POST)
+
data = data.call(this, query.term, query.page, query.context);
- if( null !== handler){
- handler.abort();
- }
+ if( null !== handler) { handler.abort(); }
+
handler = transport.call(null, {
url: options.url,
dataType: options.dataType,
data: data,
+ type: type,
success: function (data) {
if (requestNumber < requestSequence) {
return;
}
// TODO 3.0 - replace query.page with query so users have access to term, page, etc.
var results = options.results(data, query.page);
- self.context = results.context;
query.callback(results);
}
});
@@ -268,12 +307,16 @@
*/
function local(options) {
var data = options, // data elements
+ dataText,
text = function (item) { return ""+item.text; }; // function used to retrieve the text portion of a data item that is matched against the search
if (!$.isArray(data)) {
text = data.text;
// if text is not a function we assume it to be a key name
- if (!$.isFunction(text)) text = function (item) { return item[data.text]; };
+ if (!$.isFunction(text)) {
+ dataText = data.text; // we need to store this in a separate variable because in the next step data gets reset and data.text is no longer available
+ text = function (item) { return item[dataText]; };
+ }
data = data.results;
}
@@ -319,12 +362,23 @@
$(document).ready(function () {
$(document).delegate("*", "mousedown focusin touchend", function (e) {
var target = $(e.target).closest("div.select2-container").get(0);
- $(document).find("div.select2-container-active").each(function () {
- if (this !== target) $(this).data("select2").blur();
- });
+ if (target) {
+ $(document).find("div.select2-container-active").each(function () {
+ if (this !== target) $(this).data("select2").blur();
+ });
+ } else {
+ target = $(e.target).closest("div.select2-drop").get(0);
+ $(document).find("div.select2-drop-active").each(function () {
+ if (this !== target) $(this).data("select2").blur();
+ });
+ }
});
});
+ function evaluate(val) {
+ return $.isFunction(val) ? val() : val;
+ }
+
/**
* Creates a new class
*
@@ -342,6 +396,7 @@
AbstractSelect2 = clazz(Object, {
+ // abstract
bind: function (func) {
var self = this;
return function () {
@@ -349,6 +404,7 @@
};
},
+ // abstract
init: function (opts) {
var results, search, resultsSelector = ".select2-results";
@@ -365,11 +421,15 @@
this.enabled=true;
this.container = this.createContainer();
+ this.body = opts.element.closest("body"); // cache for future access
if (opts.element.attr("class") !== undefined) {
this.container.addClass(opts.element.attr("class"));
}
+ this.container.css(evaluate(opts.containerCss));
+ this.container.addClass(evaluate(opts.containerCssClass));
+
// swap container for the element
this.opts.element
.data("select2", this)
@@ -378,8 +438,12 @@
this.container.data("select2", this);
this.dropdown = this.container.find(".select2-drop");
+ this.dropdown.css(evaluate(opts.dropdownCss));
+ this.dropdown.addClass(evaluate(opts.dropdownCssClass));
+ this.dropdown.data("select2", this);
+
this.results = results = this.container.find(resultsSelector);
- this.search = search = this.container.find("input[type=text]");
+ this.search = search = this.container.find("input.select2-input");
this.resultsPage = 0;
this.context = null;
@@ -388,10 +452,10 @@
this.initContainer();
installFilteredMouseMove(this.results);
- this.container.delegate(resultsSelector, "mousemove-filtered", this.bind(this.highlightUnderEvent));
+ this.dropdown.delegate(resultsSelector, "mousemove-filtered", this.bind(this.highlightUnderEvent));
installDebouncedScroll(80, this.results);
- this.container.delegate(resultsSelector, "scroll-debounced", this.bind(this.loadMoreIfNeeded));
+ this.dropdown.delegate(resultsSelector, "scroll-debounced", this.bind(this.loadMoreIfNeeded));
// if jquery.mousewheel plugin is installed we can prevent out-of-bounds scrolling of results via mousewheel
if ($.fn.mousewheel) {
@@ -412,7 +476,7 @@
search.bind("focus", function () { search.addClass("select2-focused");});
search.bind("blur", function () { search.removeClass("select2-focused");});
- this.container.delegate(resultsSelector, "click", this.bind(function (e) {
+ this.dropdown.delegate(resultsSelector, "click", this.bind(function (e) {
if ($(e.target).closest(".select2-result:not(.select2-disabled)").length > 0) {
this.highlightUnderEvent(e);
this.selectHighlighted(e);
@@ -434,20 +498,23 @@
if (opts.element.is(":disabled")) this.disable();
},
+ // abstract
destroy: function () {
var select2 = this.opts.element.data("select2");
if (select2 !== undefined) {
select2.container.remove();
+ select2.dropdown.remove();
select2.opts.element
.removeData("select2")
.unbind(".select2")
- .show();
+ .show();
}
},
+ // abstract
prepareOpts: function (opts) {
var element, select, idKey;
-
+
element = opts.element;
if (element.get(0).tagName.toLowerCase() === "select") {
@@ -464,10 +531,75 @@
}
opts = $.extend({}, {
- formatResult: function (data) { return data.text; },
- formatSelection: function (data) { return data.text; },
+ containerCss: {},
+ dropdownCss: {},
+ containerCssClass: "",
+ dropdownCssClass: "",
+ populateResults: function(container, results, query) {
+ var uidToData={}, populate, markup=[], uid, data, result, children, formatted;
+
+ populate=function(results, depth) {
+
+ var i, l, uid, result, selectable, compound;
+ for (i = 0, l = results.length; i < l; i = i + 1) {
+
+ result=results[i];
+ selectable=("id" in result); // TODO switch to id() function
+ compound=("children" in result) && result.children.length > 0;
+
+ markup.push("");
+ formatted=opts.formatResult(result, query, markup);
+ // for backwards compat with <3.0 versions
+ if (formatted!==undefined) {
+ markup.push(formatted);
+ }
+ markup.push("
");
+
+ if (compound) {
+ markup.push("");
+ populate(result.children, depth + 1);
+ markup.push(" ");
+ }
+
+ markup.push(" ");
+ }
+ };
+
+ populate(results, 0);
+
+ children=container.children();
+ if (children.length===0) {
+ container.html(markup.join(""));
+ } else {
+ $(children[children.length-1]).append(markup.join(""));
+ }
+
+ for (uid in uidToData) {
+ $("#select2-result-"+uid, container).data("select2-data", uidToData[uid]);
+ }
+
+ },
+ formatResult: function(result, query, markup) {
+ markMatch(result.text, query.term, markup);
+ },
+ formatSelection: function (data) {
+ return data.fullText || data.text;
+ },
formatNoMatches: function () { return "No matches found"; },
formatInputTooShort: function (input, min) { return "Please enter " + (min - input.length) + " more characters"; },
+ formatLoadMore: function (pageNumber) { return "Loading more results..."; },
minimumResultsForSearch: 0,
minimumInputLength: 0,
id: function (e) { return e.id; },
@@ -483,19 +615,37 @@
if (select) {
opts.query = this.bind(function (query) {
- var data = {results: [], more: false},
+ var data = { results: [], more: false },
term = query.term,
- placeholder = this.getPlaceholder();
- element.find("option").each(function (i) {
- var e = $(this),
- text = e.text();
+ children, firstChild, process;
- if (i === 0 && placeholder !== undefined && text === "") return true;
-
- if (query.matcher(term, text)) {
- data.results.push({id: e.attr("value"), text: text});
+ process=function(element, collection) {
+ var group;
+ if (element.is("option")) {
+ if (query.matcher(term, element.text())) {
+ collection.push({id:element.attr("value"), text:element.text()});
+ }
+ } else if (element.is("optgroup")) {
+ group={text:element.attr("label"), children:[]};
+ element.children().each2(function(i, elm) { process(elm, group.children); });
+ if (group.children.length>0) {
+ collection.push(group);
+ }
}
- });
+ };
+
+ children=element.children();
+
+ // ignore the placeholder option if there is one
+ if (this.getPlaceholder() !== undefined && children.length > 0) {
+ firstChild = children[0];
+ if ($(firstChild).text() === "") {
+ children=children.not(firstChild);
+ }
+ }
+
+ children.each2(function(i, elm) { process(elm, data.results); });
+
query.callback(data);
});
// this is needed because inside val() we construct choices from options and there id is hardcoded
@@ -531,6 +681,7 @@
/**
* Monitor the original element for changes and update select2 accordingly
*/
+ // abstract
monitorSource: function () {
this.opts.element.bind("change.select2", this.bind(function (e) {
if (this.opts.element.data("select2-change-triggered") !== true) {
@@ -542,14 +693,19 @@
/**
* Triggers the change event on the source element
*/
- triggerChange: function () {
- // Prevents recursive triggering
+ // abstract
+ triggerChange: function (details) {
+
+ details = details || {};
+ details= $.extend({}, details, { type: "change", val: this.val() });
+ // prevents recursive triggering
this.opts.element.data("select2-change-triggered", true);
- this.opts.element.trigger("change");
+ this.opts.element.trigger(details);
this.opts.element.data("select2-change-triggered", false);
},
+ // abstract
enable: function() {
if (this.enabled) return;
@@ -557,6 +713,7 @@
this.container.removeClass("select2-container-disabled");
},
+ // abstract
disable: function() {
if (!this.enabled) return;
@@ -566,14 +723,37 @@
this.container.addClass("select2-container-disabled");
},
+ // abstract
opened: function () {
return this.container.hasClass("select2-dropdown-open");
},
+ // abstract
+ positionDropdown: function() {
+ var offset = this.container.offset();
+ var height = this.container.outerHeight();
+ var width = this.container.outerWidth();
+ var css = {
+ top: offset.top + height,
+ left: offset.left,
+ width: width
+ }
+ this.dropdown.css(css);
+ },
+
+ // abstract
open: function () {
if (this.opened()) return;
this.container.addClass("select2-dropdown-open").addClass("select2-container-active");
+ if(this.dropdown[0] !== this.body.children().last()[0]) {
+ // ensure the dropdown is the last child of body, so the z-index is always respected correctly
+ this.dropdown.detach().appendTo(this.body);
+ }
+
+ this.dropdown.addClass("select2-drop-active");
+
+ this.positionDropdown();
this.updateResults(true);
this.dropdown.show();
@@ -581,6 +761,7 @@
this.focusSearch();
},
+ // abstract
close: function () {
if (!this.opened()) return;
@@ -590,17 +771,20 @@
this.clearSearch();
},
+ // abstract
clearSearch: function () {
},
+ // abstract
ensureHighlightVisible: function () {
var results = this.results, children, index, child, hb, rb, y, more;
-
- children = results.children(".select2-result");
+
index = this.highlight();
if (index < 0) return;
+
+ children = results.find(".select2-result");
child = $(children[index]);
@@ -626,8 +810,9 @@
}
},
+ // abstract
moveHighlight: function (delta) {
- var choices = this.results.children(".select2-result"),
+ var choices = this.results.find(".select2-result"),
index = this.highlight();
while (index > -1 && index < choices.length) {
@@ -639,71 +824,71 @@
}
},
+ // abstract
highlight: function (index) {
- var choices = this.results.children(".select2-result");
+ var choices = this.results.find(".select2-result .select2-result-label");
if (arguments.length === 0) {
return indexOf(choices.filter(".select2-highlighted")[0], choices.get());
}
- choices.removeClass("select2-highlighted");
-
if (index >= choices.length) index = choices.length - 1;
if (index < 0) index = 0;
+ if ($(choices[index]).parent().is('.select2-result-unselectable')) {
+ return;
+ }
+
+ choices.removeClass("select2-highlighted");
+
$(choices[index]).addClass("select2-highlighted");
this.ensureHighlightVisible();
- if (this.opened()) this.focusSearch();
+ //if (this.opened()) this.focusSearch();
},
+ // abstract
highlightUnderEvent: function (event) {
- var el = $(event.target).closest(".select2-result");
- if (el.length > 0) {
- this.highlight(el.index());
+ var el = $(event.target).closest(".select2-result");
+ if (el.length > 0 && !el.is(".select2-highlighted")) {
+ var choices = this.results.find('.select2-result');
+ this.highlight(choices.index(el));
}
},
+ // abstract
loadMoreIfNeeded: function () {
var results = this.results,
more = results.find("li.select2-more-results"),
below, // pixels the element is below the scroll fold, below==0 is when the element is starting to be visible
offset = -1, // index of first element without data
- page = this.resultsPage + 1;
+ page = this.resultsPage + 1,
+ self=this,
+ term=this.search.val(),
+ context=this.context;
if (more.length === 0) return;
-
below = more.offset().top - results.offset().top - results.height();
if (below <= 0) {
more.addClass("select2-active");
this.opts.query({
- term: this.search.val(),
+ term: term,
page: page,
- context: self.context,
- matcher: self.opts.matcher,
+ context: context,
+ matcher: this.opts.matcher,
callback: this.bind(function (data) {
- var parts = [], self = this;
- $(data.results).each(function () {
- parts.push("");
- parts.push(self.opts.formatResult(this));
- parts.push(" ");
- });
- more.before(parts.join(""));
- results.find(".select2-result").each(function (i) {
- var e = $(this);
- if (e.data("select2-data") !== undefined) {
- offset = i;
- } else {
- e.data("select2-data", data.results[i - offset - 1]);
- }
- });
- if (data.more) {
+
+ self.opts.populateResults(results, data.results, {term: term, page: page, context:context});
+
+ if (data.more===true) {
+ more.detach();
+ results.children().filter(":last").append(more);
more.removeClass("select2-active");
} else {
more.remove();
}
- this.resultsPage = page;
+ self.resultsPage = page;
})});
}
},
@@ -711,6 +896,7 @@
/**
* @param initial whether or not this is the call to this method right after the dropdown has been opened
*/
+ // abstract
updateResults: function (initial) {
var search = this.search, results = this.results, opts = this.opts, self=this;
@@ -721,12 +907,16 @@
search.addClass("select2-active");
- function render(html) {
- results.html(html);
+ function postRender() {
results.scrollTop(0);
search.removeClass("select2-active");
}
+ function render(html) {
+ results.html(html);
+ postRender();
+ }
+
if (search.val().length < opts.minimumInputLength) {
render("" + opts.formatInputTooShort(search.val(), opts.minimumInputLength) + " ");
return;
@@ -739,8 +929,10 @@
context: null,
matcher: opts.matcher,
callback: this.bind(function (data) {
- var parts = [], // html parts
- def; // default choice
+ var def; // default choice
+
+ // save context, if any
+ this.context = (data.context===undefined) ? null : data.context;
// create a default choice and prepend it to the list
if (this.opts.createSearchChoice && search.val() !== "") {
@@ -760,41 +952,34 @@
return;
}
- $(data.results).each(function () {
- parts.push("");
- parts.push(opts.formatResult(this));
- parts.push(" ");
- });
+ results.empty();
+ self.opts.populateResults(results, data.results, {term: search.val(), page: this.resultsPage, context:null});
+ postRender();
if (data.more === true) {
- parts.push("Loading more results... ");
+ results.children().filter(":last").append("" + opts.formatLoadMore(this.resultsPage) + " ");
}
- render(parts.join(""));
- results.children(".select2-result").each(function (i) {
- var d = data.results[i];
- $(this).data("select2-data", d);
- });
this.postprocessResults(data, initial);
})});
},
+ // abstract
cancel: function () {
this.close();
},
+ // abstract
blur: function () {
- /* we do this in a timeout so that current event processing can complete before this code is executed.
- this allows tab index to be preserved even if this code blurs the textfield */
- window.setTimeout(this.bind(function () {
- this.close();
- this.container.removeClass("select2-container-active");
- this.clearSearch();
- this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
- this.search.blur();
- }), 10);
+ this.close();
+ this.container.removeClass("select2-container-active");
+ this.dropdown.removeClass("select2-drop-active");
+ if (this.search.is(":focus")) { this.search.blur(); }
+ this.clearSearch();
+ this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
},
+ // abstract
focusSearch: function () {
/* we do this in a timeout so that current event processing can complete before this code is executed.
this makes sure the search field is focussed even if the current event would blur it */
@@ -803,13 +988,15 @@
}), 10);
},
+ // abstract
selectHighlighted: function () {
- var data = this.results.find(".select2-highlighted:not(.select2-disabled)").data("select2-data");
+ var data = this.results.find(".select2-highlighted").not(".select2-disabled").closest('.select2-result').not('.select2-result-unselectable').data("select2-data");
if (data) {
this.onSelect(data);
}
},
+ // abstract
getPlaceholder: function () {
return this.opts.element.attr("placeholder") || this.opts.element.data("placeholder") || this.opts.placeholder;
},
@@ -822,6 +1009,7 @@
*
* @returns The width string (with units) for the container.
*/
+ // abstract
getContainerWidth: function () {
var style, attrs, matches, i, l;
if (this.opts.width !== undefined)
@@ -843,24 +1031,26 @@
SingleSelect2 = clazz(AbstractSelect2, {
+ // single
createContainer: function () {
return $("
", {
"class": "select2-container",
"style": "width: " + this.getContainerWidth()
}).html([
- " ",
+ " ",
" ",
"
" ,
" ",
" "].join(""));
},
+ // single
open: function () {
if (this.opened()) return;
@@ -869,29 +1059,39 @@
},
+ // single
close: function () {
if (!this.opened()) return;
this.parent.close.apply(this, arguments);
},
+ // single
focus: function () {
this.close();
this.selection.focus();
},
+ // single
isFocused: function () {
return this.selection.is(":focus");
},
+ // single
cancel: function () {
this.parent.cancel.apply(this, arguments);
this.selection.focus();
},
+ // single
initContainer: function () {
- var selection, container = this.container, clickingInside = false,
- selector = ".select2-choice";
+ var selection,
+ container = this.container,
+ dropdown = this.dropdown,
+ containers = $([this.container.get(0), this.dropdown.get(0)]),
+ clickingInside = false,
+ selector = ".select2-choice",
+ focusser=container.find("input.select2-focusser");
this.selection = selection = container.find(selector);
@@ -909,12 +1109,12 @@
return;
case KEY.ESC:
this.cancel(e);
- e.preventDefault();
+ killEvent(e);
return;
}
}));
- container.delegate(selector, "click", this.bind(function (e) {
+ containers.delegate(selector, "click", this.bind(function (e) {
clickingInside = true;
if (this.opened()) {
@@ -927,7 +1127,7 @@
clickingInside = false;
}));
- container.delegate(selector, "keydown", this.bind(function (e) {
+ containers.delegate(selector, "keydown", this.bind(function (e) {
if (!this.enabled || e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) {
return;
}
@@ -941,10 +1141,11 @@
killEvent(e);
}
}));
- container.delegate(selector, "focus", function () { if (this.enabled) container.addClass("select2-container-active"); });
- container.delegate(selector, "blur", this.bind(function () {
+ containers.delegate(selector, "focus", function () { if (this.enabled) { containers.addClass("select2-container-active"); dropdown.addClass("select2-drop-active"); }});
+ containers.delegate(selector, "blur", this.bind(function (e) {
if (clickingInside) return;
- if (!this.opened()) this.blur();
+ if (e.target===focusser.get(0)) return; // ignore blurs from focusser
+ if (!this.opened()) { this.blur(); }
}));
selection.delegate("abbr", "click", this.bind(function (e) {
@@ -953,14 +1154,24 @@
killEvent(e);
this.close();
this.triggerChange();
+ selection.focus();
}));
this.setPlaceholder();
+
+ focusser.bind("focus", function() { selection.focus(); });
+ selection.bind("focus", this.bind(function() {
+ focusser.hide();
+ this.container.addClass("select2-container-active");
+ }));
+ selection.bind("blur", function() { focusser.show(); });
+ this.opts.element.bind("open", function() { focusser.hide(); });
},
/**
* Sets selection based on source element's value
*/
+ // single
initSelection: function () {
var selected;
if (this.opts.element.val() === "") {
@@ -979,6 +1190,7 @@
}
},
+ // single
prepareOpts: function () {
var opts = this.parent.prepareOpts.apply(this, arguments);
@@ -995,6 +1207,7 @@
return opts;
},
+ // single
setPlaceholder: function () {
var placeholder = this.getPlaceholder();
@@ -1014,13 +1227,14 @@
}
},
+ // single
postprocessResults: function (data, initial) {
var selected = 0, self = this, showSearchInput = true;
// find the selected element in the result list
- this.results.find(".select2-result").each(function (i) {
- if (equal(self.id($(this).data("select2-data")), self.opts.element.val())) {
+ this.results.find(".select2-result").each2(function (i, elm) {
+ if (equal(self.id(elm.data("select2-data")), self.opts.element.val())) {
selected = i;
return false;
}
@@ -1033,15 +1247,18 @@
// hide the search box if this is the first we got the results and there are a few of them
if (initial === true) {
+ // TODO below we use data.results.length, but what we really need is something recursive to calc the length
+ // TODO in case there are optgroups
showSearchInput = this.showSearchInput = data.results.length >= this.opts.minimumResultsForSearch;
- this.container.find(".select2-search")[showSearchInput ? "removeClass" : "addClass"]("select2-search-hidden");
+ this.dropdown.find(".select2-search")[showSearchInput ? "removeClass" : "addClass"]("select2-search-hidden");
//add "select2-with-searchbox" to the container if search box is shown
- this.container[showSearchInput ? "addClass" : "removeClass"]("select2-with-searchbox");
+ $(this.dropdown, this.container)[showSearchInput ? "addClass" : "removeClass"]("select2-with-searchbox");
}
},
+ // single
onSelect: function (data) {
var old = this.opts.element.val();
@@ -1053,6 +1270,7 @@
if (!equal(old, this.id(data))) { this.triggerChange(); }
},
+ // single
updateSelection: function (data) {
this.selection
.find("span")
@@ -1065,6 +1283,7 @@
}
},
+ // single
val: function () {
var val, data = null;
@@ -1078,8 +1297,8 @@
// val is an id
this.select
.val(val)
- .find(":selected").each(function () {
- data = {id: $(this).attr("value"), text: $(this).text()};
+ .find(":selected").each2(function (i, elm) {
+ data = {id: elm.attr("value"), text: elm.text()};
return false;
});
this.updateSelection(data);
@@ -1092,6 +1311,7 @@
},
+ // single
clearSearch: function () {
this.search.val("");
}
@@ -1099,6 +1319,7 @@
MultiSelect2 = clazz(AbstractSelect2, {
+ // multi
createContainer: function () {
return $("
", {
"class": "select2-container select2-container-multi",
@@ -1107,15 +1328,16 @@
" " ,
- "" ,
+ "
"].join(""));
},
+ // multi
prepareOpts: function () {
var opts = this.parent.prepareOpts.apply(this, arguments);
@@ -1128,9 +1350,10 @@
if (opts.element.get(0).tagName.toLowerCase() === "select") {
// install sthe selection initializer
opts.initSelection = function (element,callback) {
+
var data = [];
- element.find(":selected").each(function () {
- data.push({id: $(this).attr("value"), text: $(this).text()});
+ element.find(":selected").each2(function (i, elm) {
+ data.push({id: elm.attr("value"), text: elm.text()});
});
if ($.isFunction(callback))
@@ -1141,6 +1364,7 @@
return opts;
},
+ // multi
initContainer: function () {
var selector = ".select2-choices", selection;
@@ -1185,7 +1409,7 @@
return;
case KEY.ESC:
this.cancel(e);
- e.preventDefault();
+ killEvent(e);
return;
}
}
@@ -1214,6 +1438,7 @@
this.container.delegate(selector, "focus", this.bind(function () {
if (!this.enabled) return;
this.container.addClass("select2-container-active");
+ this.dropdown.addClass("select2-drop-active");
this.clearPlaceholder();
}));
@@ -1221,6 +1446,7 @@
this.clearSearch();
},
+ // multi
enable: function() {
if (this.enabled) return;
@@ -1229,6 +1455,7 @@
this.search.show();
},
+ // multi
disable: function() {
if (!this.enabled) return;
@@ -1237,6 +1464,7 @@
this.search.hide();
},
+ // multi
initSelection: function () {
var data;
if (this.opts.element.val() === "") {
@@ -1258,13 +1486,11 @@
}
},
+ // multi
clearSearch: function () {
var placeholder = this.getPlaceholder();
- if (placeholder !== undefined
- && this.getVal().length === 0
- && this.search.hasClass("select2-focused") === false) {
-
+ if (placeholder !== undefined && this.getVal().length === 0 && this.search.hasClass("select2-focused") === false) {
this.search.val(placeholder).addClass("select2-default");
// stretch the search box to full width of the container so as much of the placeholder is visible as possible
this.search.width(this.getContainerWidth());
@@ -1273,33 +1499,40 @@
}
},
+ // multi
clearPlaceholder: function () {
if (this.search.hasClass("select2-default")) {
this.search.val("").removeClass("select2-default");
}
},
+ // multi
open: function () {
if (this.opened()) return;
this.parent.open.apply(this, arguments);
- this.resizeSearch();
+ this.clearPlaceholder();
+ this.resizeSearch();
this.focusSearch();
},
+ // multi
close: function () {
if (!this.opened()) return;
this.parent.close.apply(this, arguments);
},
+ // multi
focus: function () {
this.close();
this.search.focus();
},
+ // multi
isFocused: function () {
return this.search.hasClass("select2-focused");
},
+ // multi
updateSelection: function (data) {
var ids = [], filtered = [], self = this;
@@ -1319,6 +1552,7 @@
self.postprocessResults();
},
+ // multi
onSelect: function (data) {
this.addSelectedChoice(data);
if (this.select) { this.postprocessResults(); }
@@ -1333,16 +1567,18 @@
// since its not possible to select an element that has already been
// added we do not need to check if this is a new element before firing change
- this.triggerChange();
+ this.triggerChange({ added: data });
this.focusSearch();
},
+ // multi
cancel: function () {
this.close();
this.focusSearch();
},
+ // multi
addSelectedChoice: function (data) {
var choice,
id = this.id(data),
@@ -1368,6 +1604,7 @@
})).bind("focus", this.bind(function () {
if (!this.enabled) return;
this.container.addClass("select2-container-active");
+ this.dropdown.addClass("select2-drop-active");
}));
choice.data("select2-data", data);
@@ -1377,8 +1614,10 @@
this.setVal(val);
},
+ // multi
unselect: function (selected) {
var val = this.getVal(),
+ data,
index;
selected = selected.closest(".select2-search-choice");
@@ -1387,7 +1626,9 @@
throw "Invalid argument: " + selected + ". Must be .select2-search-choice";
}
- index = indexOf(this.id(selected.data("select2-data")), val);
+ data = selected.data("select2-data");
+
+ index = indexOf(this.id(data), val);
if (index >= 0) {
val.splice(index, 1);
@@ -1395,16 +1636,17 @@
if (this.select) this.postprocessResults();
}
selected.remove();
- this.triggerChange();
+ this.triggerChange({ removed: data });
},
+ // multi
postprocessResults: function () {
var val = this.getVal(),
choices = this.results.find(".select2-result"),
self = this;
- choices.each(function () {
- var choice = $(this), id = self.id(choice.data("select2-data"));
+ choices.each2(function (i, choice) {
+ var id = self.id(choice.data("select2-data"));
if (indexOf(id, val) >= 0) {
choice.addClass("select2-disabled");
} else {
@@ -1412,8 +1654,8 @@
}
});
- choices.each(function (i) {
- if (!$(this).hasClass("select2-disabled")) {
+ choices.each2(function (i, choice) {
+ if (!choice.hasClass("select2-disabled")) {
self.highlight(i);
return false;
}
@@ -1421,9 +1663,11 @@
},
+ // multi
resizeSearch: function () {
- var minimumWidth, left, maxWidth, containerLeft, searchWidth;
+ var minimumWidth, left, maxWidth, containerLeft, searchWidth,
+ sideBorderPadding = getSideBorderPadding(this.search);
minimumWidth = measureTextWidth(this.search) + 10;
@@ -1432,18 +1676,19 @@
maxWidth = this.selection.width();
containerLeft = this.selection.offset().left;
- searchWidth = maxWidth - (left - containerLeft) - getSideBorderPadding(this.search);
+ searchWidth = maxWidth - (left - containerLeft) - sideBorderPadding;
if (searchWidth < minimumWidth) {
- searchWidth = maxWidth - getSideBorderPadding(this.search);
+ searchWidth = maxWidth - sideBorderPadding;
}
if (searchWidth < 40) {
- searchWidth = maxWidth - getSideBorderPadding(this.search);
+ searchWidth = maxWidth - sideBorderPadding;
}
this.search.width(searchWidth);
},
+ // multi
getVal: function () {
var val;
if (this.select) {
@@ -1455,6 +1700,7 @@
}
},
+ // multi
setVal: function (val) {
var unique;
if (this.select) {
@@ -1469,6 +1715,7 @@
}
},
+ // multi
val: function () {
var val, data = [], self=this;
@@ -1489,7 +1736,6 @@
val = (val === null) ? [] : val;
this.setVal(val);
// val is a list of objects
- st
$(val).each(function () { data.push(self.id(this)); });
this.setVal(data);
this.updateSelection(val);
@@ -1497,6 +1743,8 @@
this.clearSearch();
},
+
+ // multi
onSortStart: function() {
if (this.select) {
throw new Error("Sorting of elements is not supported when attached to
. Attach to instead.");
@@ -1507,6 +1755,8 @@
// hide the container
this.searchContainer.hide();
},
+
+ // multi
onSortEnd:function() {
var val=[], self=this;
@@ -1533,7 +1783,7 @@
var args = Array.prototype.slice.call(arguments, 0),
opts,
select2,
- value, multiple, allowedMethods = ["val", "destroy", "open", "close", "focus", "isFocused", "container", "onSortStart", "onSortEnd", "enable", "disable"];
+ value, multiple, allowedMethods = ["val", "destroy", "open", "close", "focus", "isFocused", "container", "onSortStart", "onSortEnd", "enable", "disable", "positionDropdown"];
this.each(function () {
if (args.length === 0 || typeof(args[0]) === "object") {
@@ -1578,7 +1828,8 @@
local: local,
tags: tags
}, util: {
- debounce: debounce
+ debounce: debounce,
+ markMatch: markMatch
}, "class": {
"abstract": AbstractSelect2,
"single": SingleSelect2,