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(""); + } + + 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 @@ " " , - "