2 Copyright 2012 Igor Vaynberg
4 Version: @@ver@@ Timestamp: @@timestamp@@
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:
9 http://www.apache.org/licenses/LICENSE-2.0
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.
16 if(typeof $.fn.each2 == "undefined"){
19 * 4-10 times faster .each replacement
20 * use it carefully, as it overrides jQuery context of element on each iteration
22 each2 : function (c) {
23 var j = $([0]), i = -1, l = this.length;
26 && (j.context = j[0] = this[i])
27 && c.call(j[0], i, j) !== false //"this"=DOM, i=index, j=jQuery object
35 (function ($, undefined) {
37 /*global document, window, jQuery, console */
39 if (window.Select2 !== undefined) {
43 var KEY, AbstractSelect2, SingleSelect2, MultiSelect2, nextUid, sizer;
63 isArrow: function (k) {
64 k = k.which ? k.which : k;
74 isControl: function (e) {
83 if (e.metaKey) return true;
87 isFunctionKey: function (k) {
88 k = k.which ? k.which : k;
89 return k >= 112 && k <= 123;
93 nextUid=(function() { var counter=1; return function() { return counter++; }; }());
95 function indexOf(value, array) {
96 var i = 0, l = array.length, v;
98 if (typeof value === "undefined") {
102 if (value.constructor === String) {
103 for (; i < l; i = i + 1) if (value.localeCompare(array[i]) === 0) return i;
105 for (; i < l; i = i + 1) {
107 if (v.constructor === String) {
108 if (v.localeCompare(value) === 0) return i;
110 if (v === value) return i;
118 * Compares equality of a and b taking into account that a and b may be strings, in which case localeCompare is used
122 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;
132 * Splits the string into an array of values, trimming each value. An empty array is returned for nulls or empty
137 function splitVal(string, separator) {
139 if (string === null || string.length < 1) return [];
140 val = string.split(separator);
141 for (i = 0, l = val.length; i < l; i = i + 1) val[i] = $.trim(val[i]);
145 function getSideBorderPadding(element) {
146 return element.outerWidth() - element.width();
149 function installKeyUpChangeEvent(element) {
150 var key="keyup-change-value";
151 element.bind("keydown", function () {
152 if ($.data(element, key) === undefined) {
153 $.data(element, key, element.val());
156 element.bind("keyup", function () {
157 var val= $.data(element, key);
158 if (val !== undefined && element.val() !== val) {
159 $.removeData(element, key);
160 element.trigger("keyup-change");
165 $(document).delegate("*", "mousemove", function (e) {
166 $.data(document, "select2-lastpos", {x: e.pageX, y: e.pageY});
170 * filters mouse events so an event is fired only if the mouse moved.
172 * filters out mouse events that occur when mouse is stationary but
173 * the elements under the pointer are scrolled.
175 function installFilteredMouseMove(element) {
176 element.bind("mousemove", function (e) {
177 var lastpos = $.data(document, "select2-lastpos");
178 if (lastpos === undefined || lastpos.x !== e.pageX || lastpos.y !== e.pageY) {
179 $(e.target).trigger("mousemove-filtered", e);
185 * Debounces a function. Returns a function that calls the original fn function only if no invocations have been made
186 * within the last quietMillis milliseconds.
188 * @param quietMillis number of milliseconds to wait before invoking fn
189 * @param fn function to be debounced
190 * @param ctx object to be used as this reference within fn
191 * @return debounced version of fn
193 function debounce(quietMillis, fn, ctx) {
194 ctx = ctx || undefined;
197 var args = arguments;
198 window.clearTimeout(timeout);
199 timeout = window.setTimeout(function() {
206 * A simple implementation of a thunk
207 * @param formula function used to lazily initialize the thunk
210 function thunk(formula) {
211 var evaluated = false,
214 if (evaluated === false) { value = formula(); evaluated = true; }
219 function installDebouncedScroll(threshold, element) {
220 var notify = debounce(threshold, function (e) { element.trigger("scroll-debounced", e);});
221 element.bind("scroll", function (e) {
222 if (indexOf(e.target, element.get()) >= 0) notify(e);
226 function killEvent(event) {
227 event.preventDefault();
228 event.stopPropagation();
231 function measureTextWidth(e) {
233 var style = e[0].currentStyle || window.getComputedStyle(e[0], null);
234 sizer = $("<div></div>").css({
235 position: "absolute",
239 fontSize: style.fontSize,
240 fontFamily: style.fontFamily,
241 fontStyle: style.fontStyle,
242 fontWeight: style.fontWeight,
243 letterSpacing: style.letterSpacing,
244 textTransform: style.textTransform,
247 $("body").append(sizer);
250 return sizer.width();
253 function markMatch(text, term, markup) {
254 var match=text.toUpperCase().indexOf(term.toUpperCase()),
262 markup.push(text.substring(0, match));
263 markup.push("<span class='select2-match'>");
264 markup.push(text.substring(match, match + tl));
265 markup.push("</span>");
266 markup.push(text.substring(match + tl, text.length));
270 * Produces an ajax-based query function
272 * @param options object containing configuration paramters
273 * @param options.transport function that will be used to execute the ajax request. must be compatible with parameters supported by $.ajax
274 * @param options.url url for the data
275 * @param options.data a function(searchTerm, pageNumber, context) that should return an object containing query string parameters for the above url.
276 * @param options.dataType request data type: ajax, jsonp, other datatatypes supported by jQuery's $.ajax function or the transport function if specified
277 * @param options.traditional a boolean flag that should be true if you wish to use the traditional style of param serialization for the ajax request
278 * @param options.quietMillis (optional) milliseconds to wait before making the ajaxRequest, helps debounce the ajax function if invoked too often
279 * @param options.results a function(remoteData, pageNumber) that converts data returned form the remote request to the format expected by Select2.
280 * The expected format is an object containing the following keys:
281 * results array of objects that will be used as choices
282 * more (optional) boolean indicating whether there are more results available
283 * Example: {results:[{id:1, text:'Red'},{id:2, text:'Blue'}], more:true}
285 function ajax(options) {
286 var timeout, // current scheduled but not yet executed request
287 requestSequence = 0, // sequence used to drop out-of-order responses
289 quietMillis = options.quietMillis || 100;
291 return function (query) {
292 window.clearTimeout(timeout);
293 timeout = window.setTimeout(function () {
294 requestSequence += 1; // increment the sequence
295 var requestNumber = requestSequence, // this request's sequence number
296 data = options.data, // ajax data function
297 transport = options.transport || $.ajax,
298 traditional = options.traditional || false,
299 type = options.type || 'GET'; // set type of request (GET or POST)
301 data = data.call(this, query.term, query.page, query.context);
303 if( null !== handler) { handler.abort(); }
305 handler = transport.call(null, {
307 dataType: options.dataType,
310 traditional: traditional,
311 success: function (data) {
312 if (requestNumber < requestSequence) {
315 // TODO 3.0 - replace query.page with query so users have access to term, page, etc.
316 var results = options.results(data, query.page);
317 query.callback(results);
325 * Produces a query function that works with a local array
327 * @param options object containing configuration parameters. The options parameter can either be an array or an
330 * If the array form is used it is assumed that it contains objects with 'id' and 'text' keys.
332 * If the object form is used ti is assumed that it contains 'data' and 'text' keys. The 'data' key should contain
333 * an array of objects that will be used as choices. These objects must contain at least an 'id' key. The 'text'
334 * key can either be a String in which case it is expected that each element in the 'data' array has a key with the
335 * value of 'text' which will be used to match choices. Alternatively, text can be a function(item) that can extract
338 function local(options) {
339 var data = options, // data elements
341 text = function (item) { return ""+item.text; }; // function used to retrieve the text portion of a data item that is matched against the search
343 if (!$.isArray(data)) {
345 // if text is not a function we assume it to be a key name
346 if (!$.isFunction(text)) {
347 dataText = data.text; // we need to store this in a separate variable because in the next step data gets reset and data.text is no longer available
348 text = function (item) { return item[dataText]; };
353 return function (query) {
354 var t = query.term, filtered = { results: [] }, process;
356 query.callback({results: data});
360 process = function(datum, collection) {
363 if (datum.children) {
365 for (attr in datum) {
366 if (datum.hasOwnProperty(attr)) group[attr]=datum[attr];
369 $(datum.children).each2(function(i, childDatum) { process(childDatum, group.children); });
370 if (group.children.length) {
371 collection.push(group);
374 if (query.matcher(t, text(datum))) {
375 collection.push(datum);
380 $(data).each2(function(i, datum) { process(datum, filtered.results); });
381 query.callback(filtered);
386 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)) {
393 // if not a function we assume it to be an array
395 return function (query) {
396 var t = query.term, filtered = {results: []};
397 $(data).each(function () {
398 var isObject = this.text !== undefined,
399 text = isObject ? this.text : this;
400 if (t === "" || query.matcher(t, text)) {
401 filtered.results.push(isObject ? this : {id: this, text: this});
404 query.callback(filtered);
409 * Checks if the formatter function should be used.
411 * Throws an error if it is not a function. Returns true if it should be used,
412 * false if no formatting should be performed.
416 function checkFormatter(formatter, formatterName) {
417 if ($.isFunction(formatter)) return true;
418 if (!formatter) return false;
419 throw new Error("formatterName must be a function or a falsy value");
422 function evaluate(val) {
423 return $.isFunction(val) ? val() : val;
426 function countResults(results) {
428 $.each(results, function(i, item) {
430 count += countResults(item.children);
439 * Default tokenizer. This function uses breaks the input on substring match of any string from the
440 * opts.tokenSeparators array and uses opts.createSearchChoice to create the choice object. Both of those
441 * two options have to be defined in order for the tokenizer to work.
443 * @param input text user has typed so far or pasted into the search field
444 * @param selection currently selected choices
445 * @param selectCallback function(choice) callback tho add the choice to selection
446 * @param opts select2's opts
447 * @return undefined/null to leave the current input unchanged, or a string to change the input to the returned value
449 function defaultTokenizer(input, selection, selectCallback, opts) {
450 var original = input, // store the original so we can compare and know if we need to tell the search to update its text
451 dupe = false, // check for whether a token we extracted represents a duplicate selected choice
453 index, // position at which the separator was found
454 i, l, // looping variables
455 separator; // the matched separator
457 if (!opts.createSearchChoice || !opts.tokenSeparators || opts.tokenSeparators.length < 1) return undefined;
462 for (i = 0, l = opts.tokenSeparators.length; i < l; i++) {
463 separator = opts.tokenSeparators[i];
464 index = input.indexOf(separator);
465 if (index >= 0) break;
468 if (index < 0) break; // did not find any token separator in the input string, bail
470 token = input.substring(0, index);
471 input = input.substring(index + separator.length);
473 if (token.length > 0) {
474 token = opts.createSearchChoice(token, selection);
475 if (token !== undefined && token !== null && opts.id(token) !== undefined && opts.id(token) !== null) {
477 for (i = 0, l = selection.length; i < l; i++) {
478 if (equal(opts.id(token), opts.id(selection[i]))) {
483 if (!dupe) selectCallback(token);
488 if (original.localeCompare(input) != 0) return input;
492 * blurs any Select2 container that has focus when an element outside them was clicked or received focus
494 * also takes care of clicks on label tags that point to the source element
496 $(document).ready(function () {
497 $(document).delegate("*", "mousedown touchend", function (e) {
498 var target = $(e.target).closest("div.select2-container").get(0), attr;
500 $(document).find("div.select2-container-active").each(function () {
501 if (this !== target) $(this).data("select2").blur();
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();
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();}
521 * Creates a new class
526 function clazz(SuperClass, methods) {
527 var constructor = function () {};
528 constructor.prototype = new SuperClass;
529 constructor.prototype.constructor = constructor;
530 constructor.prototype.parent = SuperClass.prototype;
531 constructor.prototype = $.extend(constructor.prototype, methods);
535 AbstractSelect2 = clazz(Object, {
538 bind: function (func) {
541 func.apply(self, arguments);
546 init: function (opts) {
547 var results, search, resultsSelector = ".select2-results";
550 this.opts = opts = this.prepareOpts(opts);
554 // destroy if called on an existing component
555 if (opts.element.data("select2") !== undefined &&
556 opts.element.data("select2") !== null) {
561 this.container = this.createContainer();
563 this.containerId="s2id_"+(opts.element.attr("id") || "autogen"+nextUid());
564 this.containerSelector="#"+this.containerId.replace(/([;&,\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '\\$1');
565 this.container.attr("id", this.containerId);
567 // cache the body so future lookups are cheap
568 this.body = thunk(function() { return opts.element.closest("body"); });
570 if (opts.element.attr("class") !== undefined) {
571 this.container.addClass(opts.element.attr("class").replace(/validate\[[\S ]+] ?/, ''));
574 this.container.css(evaluate(opts.containerCss));
575 this.container.addClass(evaluate(opts.containerCssClass));
577 // swap container for the element
579 .data("select2", this)
581 .before(this.container);
582 this.container.data("select2", this);
584 this.dropdown = this.container.find(".select2-drop");
585 this.dropdown.addClass(evaluate(opts.dropdownCssClass));
586 this.dropdown.data("select2", this);
588 this.results = results = this.container.find(resultsSelector);
589 this.search = search = this.container.find("input.select2-input");
591 search.attr("tabIndex", this.opts.element.attr("tabIndex"));
593 this.resultsPage = 0;
596 // initialize the container
597 this.initContainer();
598 this.initContainerWidth();
600 installFilteredMouseMove(this.results);
601 this.dropdown.delegate(resultsSelector, "mousemove-filtered", this.bind(this.highlightUnderEvent));
603 installDebouncedScroll(80, this.results);
604 this.dropdown.delegate(resultsSelector, "scroll-debounced", this.bind(this.loadMoreIfNeeded));
606 // if jquery.mousewheel plugin is installed we can prevent out-of-bounds scrolling of results via mousewheel
607 if ($.fn.mousewheel) {
608 results.mousewheel(function (e, delta, deltaX, deltaY) {
609 var top = results.scrollTop(), height;
610 if (deltaY > 0 && top - deltaY <= 0) {
611 results.scrollTop(0);
613 } else if (deltaY < 0 && results.get(0).scrollHeight - results.scrollTop() + deltaY <= results.height()) {
614 results.scrollTop(results.get(0).scrollHeight - results.height());
620 installKeyUpChangeEvent(search);
621 search.bind("keyup-change", this.bind(this.updateResults));
622 search.bind("focus", function () { search.addClass("select2-focused"); if (search.val() === " ") search.val(""); });
623 search.bind("blur", function () { search.removeClass("select2-focused");});
625 this.dropdown.delegate(resultsSelector, "mouseup", this.bind(function (e) {
626 if ($(e.target).closest(".select2-result-selectable:not(.select2-disabled)").length > 0) {
627 this.highlightUnderEvent(e);
628 this.selectHighlighted(e);
635 // trap all mouse events from leaving the dropdown. sometimes there may be a modal that is listening
636 // for mouse events outside of itself so it can close itself. since the dropdown is now outside the select2's
637 // dom it will trigger the popup close, which is not what we want
638 this.dropdown.bind("click mouseup mousedown", function (e) { e.stopPropagation(); });
640 if ($.isFunction(this.opts.initSelection)) {
641 // initialize selection based on the current value of the source element
642 this.initSelection();
644 // if the user has provided a function that can set selection based on the value of the source element
645 // we monitor the change event on the element and trigger it, allowing for two way synchronization
646 this.monitorSource();
649 if (opts.element.is(":disabled") || opts.element.is("[readonly='readonly']")) this.disable();
653 destroy: function () {
654 var select2 = this.opts.element.data("select2");
655 if (select2 !== undefined) {
656 select2.container.remove();
657 select2.dropdown.remove();
659 .removeData("select2")
666 prepareOpts: function (opts) {
667 var element, select, idKey, ajaxUrl;
669 element = opts.element;
671 if (element.get(0).tagName.toLowerCase() === "select") {
672 this.select = select = opts.element;
676 // these options are not allowed when attached to a select because they are picked up off the element itself
677 $.each(["id", "multiple", "ajax", "query", "createSearchChoice", "initSelection", "data", "tags"], function () {
679 throw new Error("Option '" + this + "' is not allowed for Select2 when attached to a <select> element.");
684 opts = $.extend({}, {
685 populateResults: function(container, results, query) {
686 var populate, data, result, children, id=this.opts.id, self=this;
688 populate=function(results, container, depth) {
690 var i, l, result, selectable, compound, node, label, innerContainer, formatted;
691 for (i = 0, l = results.length; i < l; i = i + 1) {
694 selectable=id(result) !== undefined;
695 compound=("children" in result) && result.children.length > 0;
698 node.addClass("select2-results-dept-"+depth);
699 node.addClass("select2-result");
700 node.addClass(selectable ? "select2-result-selectable" : "select2-result-unselectable");
701 if (compound) { node.addClass("select2-result-with-children"); }
702 node.addClass(self.opts.formatResultCssClass(result));
704 label=$("<div></div>");
705 label.addClass("select2-result-label");
707 formatted=opts.formatResult(result, label, query);
708 if (formatted!==undefined) {
709 label.html(self.opts.escapeMarkup(formatted));
716 innerContainer=$("<ul></ul>");
717 innerContainer.addClass("select2-result-sub");
718 populate(result.children, innerContainer, depth+1);
719 node.append(innerContainer);
722 node.data("select2-data", result);
723 container.append(node);
727 populate(results, container, 0);
729 }, $.fn.select2.defaults, opts);
731 if (typeof(opts.id) !== "function") {
733 opts.id = function (e) { return e[idKey]; };
737 opts.query = this.bind(function (query) {
738 var data = { results: [], more: false },
740 children, firstChild, process;
742 process=function(element, collection) {
744 if (element.is("option")) {
745 if (query.matcher(term, element.text(), element)) {
746 collection.push({id:element.attr("value"), text:element.text(), element: element.get(), css: element.attr("class")});
748 } else if (element.is("optgroup")) {
749 group={text:element.attr("label"), children:[], element: element.get(), css: element.attr("class")};
750 element.children().each2(function(i, elm) { process(elm, group.children); });
751 if (group.children.length>0) {
752 collection.push(group);
757 children=element.children();
759 // ignore the placeholder option if there is one
760 if (this.getPlaceholder() !== undefined && children.length > 0) {
761 firstChild = children[0];
762 if ($(firstChild).text() === "") {
763 children=children.not(firstChild);
767 children.each2(function(i, elm) { process(elm, data.results); });
769 query.callback(data);
771 // this is needed because inside val() we construct choices from options and there id is hardcoded
772 opts.id=function(e) { return e.id; };
773 opts.formatResultCssClass = function(data) { return data.css; }
775 if (!("query" in opts)) {
776 if ("ajax" in opts) {
777 ajaxUrl = opts.element.data("ajax-url");
778 if (ajaxUrl && ajaxUrl.length > 0) {
779 opts.ajax.url = ajaxUrl;
781 opts.query = ajax(opts.ajax);
782 } else if ("data" in opts) {
783 opts.query = local(opts.data);
784 } else if ("tags" in opts) {
785 opts.query = tags(opts.tags);
786 opts.createSearchChoice = function (term) { return {id: term, text: term}; };
787 opts.initSelection = function (element, callback) {
789 $(splitVal(element.val(), opts.separator)).each(function () {
790 var id = this, text = this, tags=opts.tags;
791 if ($.isFunction(tags)) tags=tags();
792 $(tags).each(function() { if (equal(this.id, id)) { text = this.text; return false; } });
793 data.push({id: id, text: text});
801 if (typeof(opts.query) !== "function") {
802 throw "query function not defined for Select2 " + opts.element.attr("id");
809 * Monitor the original element for changes and update select2 accordingly
812 monitorSource: function () {
813 this.opts.element.bind("change.select2", this.bind(function (e) {
814 if (this.opts.element.data("select2-change-triggered") !== true) {
815 this.initSelection();
821 * Triggers the change event on the source element
824 triggerChange: function (details) {
826 details = details || {};
827 details= $.extend({}, details, { type: "change", val: this.val() });
828 // prevents recursive triggering
829 this.opts.element.data("select2-change-triggered", true);
830 this.opts.element.trigger(details);
831 this.opts.element.data("select2-change-triggered", false);
833 // some validation frameworks ignore the change event and listen instead to keyup, click for selects
834 // so here we trigger the click event manually
835 this.opts.element.click();
837 // ValidationEngine ignorea the change event and listens instead to blur
838 // so here we trigger the blur event manually if so desired
839 if (this.opts.blurOnChange)
840 this.opts.element.blur();
846 if (this.enabled) return;
849 this.container.removeClass("select2-container-disabled");
853 disable: function() {
854 if (!this.enabled) return;
859 this.container.addClass("select2-container-disabled");
863 opened: function () {
864 return this.container.hasClass("select2-dropdown-open");
868 positionDropdown: function() {
869 var offset = this.container.offset(),
870 height = this.container.outerHeight(),
871 width = this.container.outerWidth(),
872 dropHeight = this.dropdown.outerHeight(),
873 viewportBottom = $(window).scrollTop() + document.documentElement.clientHeight,
874 dropTop = offset.top + height,
875 dropLeft = offset.left,
876 enoughRoomBelow = dropTop + dropHeight <= viewportBottom,
877 enoughRoomAbove = (offset.top - dropHeight) >= this.body().scrollTop(),
878 aboveNow = this.dropdown.hasClass("select2-drop-above"),
883 // console.log("below/ droptop:", dropTop, "dropHeight", dropHeight, "sum", (dropTop+dropHeight)+" viewport bottom", viewportBottom, "enough?", enoughRoomBelow);
884 // console.log("above/ offset.top", offset.top, "dropHeight", dropHeight, "top", (offset.top-dropHeight), "scrollTop", this.body().scrollTop(), "enough?", enoughRoomAbove);
886 // fix positioning when body has an offset and is not position: static
888 if (this.body().css('position') !== 'static') {
889 bodyOffset = this.body().offset();
890 dropTop -= bodyOffset.top;
891 dropLeft -= bodyOffset.left;
894 // always prefer the current above/below alignment, unless there is not enough room
898 if (!enoughRoomAbove && enoughRoomBelow) above = false;
901 if (!enoughRoomBelow && enoughRoomAbove) above = true;
905 dropTop = offset.top - dropHeight;
906 this.container.addClass("select2-drop-above");
907 this.dropdown.addClass("select2-drop-above");
910 this.container.removeClass("select2-drop-above");
911 this.dropdown.removeClass("select2-drop-above");
918 }, evaluate(this.opts.dropdownCss));
920 this.dropdown.css(css);
924 shouldOpen: function() {
927 if (this.opened()) return false;
929 event = jQuery.Event("open");
930 this.opts.element.trigger(event);
931 return !event.isDefaultPrevented();
935 clearDropdownAlignmentPreference: function() {
936 // clear the classes used to figure out the preference of where the dropdown should be opened
937 this.container.removeClass("select2-drop-above");
938 this.dropdown.removeClass("select2-drop-above");
944 * @return {Boolean} whether or not dropdown was opened. This method will return false if, for example,
945 * the dropdown is already open, or if the 'open' event listener on the element called preventDefault().
950 if (!this.shouldOpen()) return false;
952 window.setTimeout(this.bind(this.opening), 1);
958 * Performs the opening of the dropdown
961 opening: function() {
962 var cid = this.containerId, selector = this.containerSelector,
963 scroll = "scroll." + cid, resize = "resize." + cid;
965 this.container.parents().each(function() {
966 $(this).bind(scroll, function() {
967 var s2 = $(selector);
968 if (s2.length == 0) {
969 $(this).unbind(scroll);
975 $(window).bind(resize, function() {
976 var s2 = $(selector);
977 if (s2.length == 0) {
978 $(window).unbind(resize);
983 this.clearDropdownAlignmentPreference();
985 if (this.search.val() === " ") { this.search.val(""); }
987 this.container.addClass("select2-dropdown-open").addClass("select2-container-active");
989 this.updateResults(true);
991 if(this.dropdown[0] !== this.body().children().last()[0]) {
992 this.dropdown.detach().appendTo(this.body());
995 this.dropdown.show();
997 this.positionDropdown();
998 this.dropdown.addClass("select2-drop-active");
1000 this.ensureHighlightVisible();
1006 close: function () {
1007 if (!this.opened()) return;
1011 this.container.parents().each(function() {
1012 $(this).unbind("scroll." + self.containerId);
1014 $(window).unbind("resize." + this.containerId);
1016 this.clearDropdownAlignmentPreference();
1018 this.dropdown.hide();
1019 this.container.removeClass("select2-dropdown-open").removeClass("select2-container-active");
1020 this.results.empty();
1023 this.opts.element.trigger(jQuery.Event("close"));
1027 clearSearch: function () {
1032 ensureHighlightVisible: function () {
1033 var results = this.results, children, index, child, hb, rb, y, more;
1035 index = this.highlight();
1037 if (index < 0) return;
1041 // if the first element is highlighted scroll all the way to the top,
1042 // that way any unselectable headers above it will also be scrolled
1045 results.scrollTop(0);
1049 children = results.find(".select2-result-selectable");
1051 child = $(children[index]);
1053 hb = child.offset().top + child.outerHeight();
1055 // if this is the last child lets also make sure select2-more-results is visible
1056 if (index === children.length - 1) {
1057 more = results.find("li.select2-more-results");
1058 if (more.length > 0) {
1059 hb = more.offset().top + more.outerHeight();
1063 rb = results.offset().top + results.outerHeight();
1065 results.scrollTop(results.scrollTop() + (hb - rb));
1067 y = child.offset().top - results.offset().top;
1069 // make sure the top of the element is visible
1071 results.scrollTop(results.scrollTop() + y); // y is negative
1076 moveHighlight: function (delta) {
1077 var choices = this.results.find(".select2-result-selectable"),
1078 index = this.highlight();
1080 while (index > -1 && index < choices.length) {
1082 var choice = $(choices[index]);
1083 if (choice.hasClass("select2-result-selectable") && !choice.hasClass("select2-disabled")) {
1084 this.highlight(index);
1091 highlight: function (index) {
1092 var choices = this.results.find(".select2-result-selectable").not(".select2-disabled");
1094 if (arguments.length === 0) {
1095 return indexOf(choices.filter(".select2-highlighted")[0], choices.get());
1098 if (index >= choices.length) index = choices.length - 1;
1099 if (index < 0) index = 0;
1101 choices.removeClass("select2-highlighted");
1103 $(choices[index]).addClass("select2-highlighted");
1104 this.ensureHighlightVisible();
1109 countSelectableResults: function() {
1110 return this.results.find(".select2-result-selectable").not(".select2-disabled").length;
1114 highlightUnderEvent: function (event) {
1115 var el = $(event.target).closest(".select2-result-selectable");
1116 if (el.length > 0 && !el.is(".select2-highlighted")) {
1117 var choices = this.results.find('.select2-result-selectable');
1118 this.highlight(choices.index(el));
1119 } else if (el.length == 0) {
1120 // if we are over an unselectable item remove al highlights
1121 this.results.find(".select2-highlighted").removeClass("select2-highlighted");
1126 loadMoreIfNeeded: function () {
1127 var results = this.results,
1128 more = results.find("li.select2-more-results"),
1129 below, // pixels the element is below the scroll fold, below==0 is when the element is starting to be visible
1130 offset = -1, // index of first element without data
1131 page = this.resultsPage + 1,
1133 term=this.search.val(),
1134 context=this.context;
1136 if (more.length === 0) return;
1137 below = more.offset().top - results.offset().top - results.height();
1140 more.addClass("select2-active");
1145 matcher: this.opts.matcher,
1146 callback: this.bind(function (data) {
1148 // ignore a response if the select2 has been closed before it was received
1149 if (!self.opened()) return;
1152 self.opts.populateResults.call(this, results, data.results, {term: term, page: page, context:context});
1154 if (data.more===true) {
1155 more.detach().appendTo(results).text(self.opts.formatLoadMore(page+1));
1156 window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10);
1160 self.positionDropdown();
1161 self.resultsPage = page;
1167 * Default tokenizer function which does nothing
1169 tokenize: function() {
1174 * @param initial whether or not this is the call to this method right after the dropdown has been opened
1177 updateResults: function (initial) {
1178 var search = this.search, results = this.results, opts = this.opts, data, self=this, input;
1180 // if the search is currently hidden we do not alter the results
1181 if (initial !== true && (this.showSearchInput === false || !this.opened())) {
1185 search.addClass("select2-active");
1187 function postRender() {
1188 results.scrollTop(0);
1189 search.removeClass("select2-active");
1190 self.positionDropdown();
1193 function render(html) {
1194 results.html(self.opts.escapeMarkup(html));
1198 if (opts.maximumSelectionSize >=1) {
1200 if ($.isArray(data) && data.length >= opts.maximumSelectionSize && checkFormatter(opts.formatSelectionTooBig, "formatSelectionTooBig")) {
1201 render("<li class='select2-selection-limit'>" + opts.formatSelectionTooBig(opts.maximumSelectionSize) + "</li>");
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>");
1211 render("<li class='select2-searching'>" + opts.formatSearching() + "</li>");
1214 // give the tokenizer a chance to pre-process the input
1215 input = this.tokenize();
1216 if (input != undefined && input != null) {
1220 this.resultsPage = 1;
1223 page: this.resultsPage,
1225 matcher: opts.matcher,
1226 callback: this.bind(function (data) {
1227 var def; // default choice
1229 // ignore a response if the select2 has been closed before it was received
1230 if (!this.opened()) return;
1232 // save context, if any
1233 this.context = (data.context===undefined) ? null : data.context;
1235 // create a default choice and prepend it to the list
1236 if (this.opts.createSearchChoice && search.val() !== "") {
1237 def = this.opts.createSearchChoice.call(null, search.val(), data.results);
1238 if (def !== undefined && def !== null && self.id(def) !== undefined && self.id(def) !== null) {
1239 if ($(data.results).filter(
1241 return equal(self.id(this), self.id(def));
1243 data.results.unshift(def);
1248 if (data.results.length === 0 && checkFormatter(opts.formatNoMatches, "formatNoMatches")) {
1249 render("<li class='select2-no-results'>" + opts.formatNoMatches(search.val()) + "</li>");
1254 self.opts.populateResults.call(this, results, data.results, {term: search.val(), page: this.resultsPage, context:null});
1256 if (data.more === true && checkFormatter(opts.formatLoadMore, "formatLoadMore")) {
1257 results.append("<li class='select2-more-results'>" + self.opts.escapeMarkup(opts.formatLoadMore(this.resultsPage)) + "</li>");
1258 window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10);
1261 this.postprocessResults(data, initial);
1268 cancel: function () {
1275 this.container.removeClass("select2-container-active");
1276 this.dropdown.removeClass("select2-drop-active");
1277 // synonymous to .is(':focus'), which is available in jquery >= 1.6
1278 if (this.search[0] === document.activeElement) { this.search.blur(); }
1280 this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
1284 focusSearch: function () {
1285 // need to do it here as well as in timeout so it works in IE
1287 this.search.focus();
1289 /* we do this in a timeout so that current event processing can complete before this code is executed.
1290 this makes sure the search field is focussed even if the current event would blur it */
1291 window.setTimeout(this.bind(function () {
1292 // reset the value so IE places the cursor at the end of the input box
1294 this.search.focus();
1295 this.search.val(this.search.val());
1300 selectHighlighted: function () {
1301 var index=this.highlight(),
1302 highlighted=this.results.find(".select2-highlighted").not(".select2-disabled"),
1303 data = highlighted.closest('.select2-result-selectable').data("select2-data");
1305 highlighted.addClass("select2-disabled");
1306 this.highlight(index);
1307 this.onSelect(data);
1312 getPlaceholder: function () {
1313 return this.opts.element.attr("placeholder") ||
1314 this.opts.element.attr("data-placeholder") || // jquery 1.4 compat
1315 this.opts.element.data("placeholder") ||
1316 this.opts.placeholder;
1320 * Get the desired width for the container element. This is
1321 * derived first from option `width` passed to select2, then
1322 * the inline 'style' on the original element, and finally
1323 * falls back to the jQuery calculated element width.
1326 initContainerWidth: function () {
1327 function resolveContainerWidth() {
1328 var style, attrs, matches, i, l;
1330 if (this.opts.width === "off") {
1332 } else if (this.opts.width === "element"){
1333 return this.opts.element.outerWidth() === 0 ? 'auto' : this.opts.element.outerWidth() + 'px';
1334 } else if (this.opts.width === "copy" || this.opts.width === "resolve") {
1335 // check if there is inline style on the element that contains width
1336 style = this.opts.element.attr('style');
1337 if (style !== undefined) {
1338 attrs = style.split(';');
1339 for (i = 0, l = attrs.length; i < l; i = i + 1) {
1340 matches = attrs[i].replace(/\s/g, '')
1341 .match(/width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/);
1342 if (matches !== null && matches.length >= 1)
1347 if (this.opts.width === "resolve") {
1348 // next check if css('width') can resolve a width that is percent based, this is sometimes possible
1349 // when attached to input type=hidden or elements hidden via css
1350 style = this.opts.element.css('width');
1351 if (style.indexOf("%") > 0) return style;
1353 // finally, fallback on the calculated width of the element
1354 return (this.opts.element.outerWidth() === 0 ? 'auto' : this.opts.element.outerWidth() + 'px');
1358 } else if ($.isFunction(this.opts.width)) {
1359 return this.opts.width();
1361 return this.opts.width;
1365 var width = resolveContainerWidth.call(this);
1366 if (width !== null) {
1367 this.container.attr("style", "width: "+width);
1372 SingleSelect2 = clazz(AbstractSelect2, {
1376 createContainer: function () {
1377 var container = $("<div></div>", {
1378 "class": "select2-container"
1380 " <a href='#' onclick='return false;' class='select2-choice'>",
1381 " <span></span><abbr class='select2-search-choice-close' style='display:none;'></abbr>",
1382 " <div><b></b></div>" ,
1384 " <div class='select2-drop select2-offscreen'>" ,
1385 " <div class='select2-search'>" ,
1386 " <input type='text' autocomplete='off' class='select2-input'/>" ,
1388 " <ul class='select2-results'>" ,
1390 "</div>"].join(""));
1395 opening: function () {
1397 this.parent.opening.apply(this, arguments);
1398 this.dropdown.removeClass("select2-offscreen");
1402 close: function () {
1403 if (!this.opened()) return;
1404 this.parent.close.apply(this, arguments);
1405 this.dropdown.removeAttr("style").addClass("select2-offscreen").insertAfter(this.selection).show();
1409 focus: function () {
1411 this.selection.focus();
1415 isFocused: function () {
1416 return this.selection[0] === document.activeElement;
1420 cancel: function () {
1421 this.parent.cancel.apply(this, arguments);
1422 this.selection.focus();
1426 initContainer: function () {
1429 container = this.container,
1430 dropdown = this.dropdown,
1431 clickingInside = false;
1433 this.selection = selection = container.find(".select2-choice");
1435 this.search.bind("keydown", this.bind(function (e) {
1436 if (!this.enabled) return;
1438 if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) {
1439 // prevent the page from scrolling
1444 if (this.opened()) {
1448 this.moveHighlight((e.which === KEY.UP) ? -1 : 1);
1453 this.selectHighlighted();
1463 if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) {
1467 if (this.opts.openOnEnter === false && e.which === KEY.ENTER) {
1473 if (e.which === KEY.ENTER) {
1474 // do not propagate the event otherwise we open, and propagate enter which closes
1480 this.search.bind("focus", this.bind(function() {
1481 this.selection.attr("tabIndex", "-1");
1483 this.search.bind("blur", this.bind(function() {
1484 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);
1488 selection.bind("mousedown", this.bind(function (e) {
1489 clickingInside = true;
1491 if (this.opened()) {
1493 this.selection.focus();
1494 } else if (this.enabled) {
1498 clickingInside = false;
1501 dropdown.bind("mousedown", this.bind(function() { this.search.focus(); }));
1503 selection.bind("focus", this.bind(function() {
1504 this.container.addClass("select2-container-active");
1505 // hide the search so the tab key does not focus on it
1506 this.search.attr("tabIndex", "-1");
1509 selection.bind("blur", this.bind(function() {
1510 if (!this.opened()) {
1511 this.container.removeClass("select2-container-active");
1513 window.setTimeout(this.bind(function() { this.search.attr("tabIndex", this.opts.element.attr("tabIndex")); }), 10);
1516 selection.bind("keydown", this.bind(function(e) {
1517 if (!this.enabled) return;
1519 if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) {
1520 // prevent the page from scrolling
1525 if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e)
1526 || e.which === KEY.ESC) {
1530 if (this.opts.openOnEnter === false && e.which === KEY.ENTER) {
1534 if (e.which == KEY.DELETE) {
1535 if (this.opts.allowClear) {
1543 if (e.which === KEY.ENTER) {
1544 // do not propagate the event otherwise we open, and propagate enter which closes
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
1556 var keyWritten = String.fromCharCode(e.which).toLowerCase();
1559 keyWritten = keyWritten.toUpperCase();
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);
1566 // prevent event propagation so it doesnt replay on the now focussed search field and result in double key entry
1570 selection.delegate("abbr", "mousedown", this.bind(function (e) {
1571 if (!this.enabled) return;
1575 this.triggerChange();
1576 this.selection.focus();
1579 this.setPlaceholder();
1581 this.search.bind("focus", this.bind(function() {
1582 this.container.addClass("select2-container-active");
1588 this.opts.element.val("");
1589 this.selection.find("span").empty();
1590 this.selection.removeData("select2-data");
1591 this.setPlaceholder();
1595 * Sets selection based on source element's value
1598 initSelection: function () {
1600 if (this.opts.element.val() === "") {
1602 this.setPlaceholder();
1605 this.opts.initSelection.call(null, this.opts.element, function(selected){
1606 if (selected !== undefined && selected !== null) {
1607 self.updateSelection(selected);
1609 self.setPlaceholder();
1616 prepareOpts: function () {
1617 var opts = this.parent.prepareOpts.apply(this, arguments);
1619 if (opts.element.get(0).tagName.toLowerCase() === "select") {
1620 // install the selection initializer
1621 opts.initSelection = function (element, callback) {
1622 var selected = element.find(":selected");
1623 // a single select box always has a value, no need to null check 'selected'
1624 if ($.isFunction(callback))
1625 callback({id: selected.attr("value"), text: selected.text()});
1633 setPlaceholder: function () {
1634 var placeholder = this.getPlaceholder();
1636 if (this.opts.element.val() === "" && placeholder !== undefined) {
1638 // check for a first blank option if attached to a select
1639 if (this.select && this.select.find("option:first").text() !== "") return;
1641 this.selection.find("span").html(this.opts.escapeMarkup(placeholder));
1643 this.selection.addClass("select2-default");
1645 this.selection.find("abbr").hide();
1650 postprocessResults: function (data, initial) {
1651 var selected = 0, self = this, showSearchInput = true;
1653 // find the selected element in the result list
1655 this.results.find(".select2-result-selectable").each2(function (i, elm) {
1656 if (equal(self.id(elm.data("select2-data")), self.opts.element.val())) {
1664 this.highlight(selected);
1666 // hide the search box if this is the first we got the results and there are a few of them
1668 if (initial === true) {
1669 showSearchInput = this.showSearchInput = countResults(data.results) >= this.opts.minimumResultsForSearch;
1670 this.dropdown.find(".select2-search")[showSearchInput ? "removeClass" : "addClass"]("select2-search-hidden");
1672 //add "select2-with-searchbox" to the container if search box is shown
1673 $(this.dropdown, this.container)[showSearchInput ? "addClass" : "removeClass"]("select2-with-searchbox");
1679 onSelect: function (data) {
1680 var old = this.opts.element.val();
1682 this.opts.element.val(this.id(data));
1683 this.updateSelection(data);
1685 this.selection.focus();
1687 if (!equal(old, this.id(data))) { this.triggerChange(); }
1691 updateSelection: function (data) {
1693 var container=this.selection.find("span"), formatted;
1695 this.selection.data("select2-data", data);
1698 formatted=this.opts.formatSelection(data, container);
1699 if (formatted !== undefined) {
1700 container.append(this.opts.escapeMarkup(formatted));
1703 this.selection.removeClass("select2-default");
1705 if (this.opts.allowClear && this.getPlaceholder() !== undefined) {
1706 this.selection.find("abbr").show();
1712 var val, data = null, self = this;
1714 if (arguments.length === 0) {
1715 return this.opts.element.val();
1723 .find(":selected").each2(function (i, elm) {
1724 data = {id: elm.attr("value"), text: elm.text()};
1727 this.updateSelection(data);
1728 this.setPlaceholder();
1730 if (this.opts.initSelection === undefined) {
1731 throw new Error("cannot call val() if initSelection() is not defined");
1733 // val is an id. !val is true for [undefined,null,'']
1738 this.opts.element.val(val);
1739 this.opts.initSelection(this.opts.element, function(data){
1740 self.opts.element.val(!data ? "" : self.id(data));
1741 self.updateSelection(data);
1742 self.setPlaceholder();
1748 clearSearch: function () {
1749 this.search.val("");
1753 data: function(value) {
1756 if (arguments.length === 0) {
1757 data = this.selection.data("select2-data");
1758 if (data == undefined) data = null;
1761 if (!value || value === "") {
1764 this.opts.element.val(!value ? "" : this.id(value));
1765 this.updateSelection(value);
1771 MultiSelect2 = clazz(AbstractSelect2, {
1774 createContainer: function () {
1775 var container = $("<div></div>", {
1776 "class": "select2-container select2-container-multi"
1778 " <ul class='select2-choices'>",
1779 //"<li class='select2-search-choice'><span>California</span><a href="javascript:void(0)" class="select2-search-choice-close"></a></li>" ,
1780 " <li class='select2-search-field'>" ,
1781 " <input type='text' autocomplete='off' class='select2-input'>" ,
1784 "<div class='select2-drop select2-drop-multi' style='display:none;'>" ,
1785 " <ul class='select2-results'>" ,
1787 "</div>"].join(""));
1792 prepareOpts: function () {
1793 var opts = this.parent.prepareOpts.apply(this, arguments);
1795 // TODO validate placeholder is a string if specified
1797 if (opts.element.get(0).tagName.toLowerCase() === "select") {
1798 // install sthe selection initializer
1799 opts.initSelection = function (element,callback) {
1802 element.find(":selected").each2(function (i, elm) {
1803 data.push({id: elm.attr("value"), text: elm.text()});
1806 if ($.isFunction(callback))
1815 initContainer: function () {
1817 var selector = ".select2-choices", selection;
1819 this.searchContainer = this.container.find(".select2-search-field");
1820 this.selection = selection = this.container.find(selector);
1822 this.search.bind("keydown", this.bind(function (e) {
1823 if (!this.enabled) return;
1825 if (e.which === KEY.BACKSPACE && this.search.val() === "") {
1829 selected = selection.find(".select2-search-choice-focus");
1830 if (selected.length > 0) {
1831 this.unselect(selected.first());
1832 this.search.width(10);
1837 choices = selection.find(".select2-search-choice");
1838 if (choices.length > 0) {
1839 choices.last().addClass("select2-search-choice-focus");
1842 selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
1845 if (this.opened()) {
1849 this.moveHighlight((e.which === KEY.UP) ? -1 : 1);
1854 this.selectHighlighted();
1864 if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e)
1865 || e.which === KEY.BACKSPACE || e.which === KEY.ESC) {
1869 if (this.opts.openOnEnter === false && e.which === KEY.ENTER) {
1875 if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) {
1876 // prevent the page from scrolling
1881 this.search.bind("keyup", this.bind(this.resizeSearch));
1883 this.search.bind("blur", this.bind(function(e) {
1884 this.container.removeClass("select2-container-active");
1885 this.search.removeClass("select2-focused");
1887 e.stopImmediatePropagation();
1890 this.container.delegate(selector, "mousedown", this.bind(function (e) {
1891 if (!this.enabled) return;
1892 if ($(e.target).closest(".select2-search-choice").length > 0) {
1893 // clicked inside a select2 search choice, do not open
1896 this.clearPlaceholder();
1902 this.container.delegate(selector, "focus", this.bind(function () {
1903 if (!this.enabled) return;
1904 this.container.addClass("select2-container-active");
1905 this.dropdown.addClass("select2-drop-active");
1906 this.clearPlaceholder();
1909 // set the placeholder if necessary
1914 enable: function() {
1915 if (this.enabled) return;
1917 this.parent.enable.apply(this, arguments);
1919 this.search.removeAttr("disabled");
1923 disable: function() {
1924 if (!this.enabled) return;
1926 this.parent.disable.apply(this, arguments);
1928 this.search.attr("disabled", true);
1932 initSelection: function () {
1934 if (this.opts.element.val() === "") {
1935 this.updateSelection([]);
1937 // set the placeholder if necessary
1940 if (this.select || this.opts.element.val() !== "") {
1942 this.opts.initSelection.call(null, this.opts.element, function(data){
1943 if (data !== undefined && data !== null) {
1944 self.updateSelection(data);
1946 // set the placeholder if necessary
1954 clearSearch: function () {
1955 var placeholder = this.getPlaceholder();
1957 if (placeholder !== undefined && this.getVal().length === 0 && this.search.hasClass("select2-focused") === false) {
1958 this.search.val(placeholder).addClass("select2-default");
1959 // stretch the search box to full width of the container so as much of the placeholder is visible as possible
1960 this.resizeSearch();
1962 // we set this to " " instead of "" and later clear it on focus() because there is a firefox bug
1963 // that does not properly render the caret when the field starts out blank
1964 this.search.val(" ").width(10);
1969 clearPlaceholder: function () {
1970 if (this.search.hasClass("select2-default")) {
1971 this.search.val("").removeClass("select2-default");
1973 // work around for the space character we set to avoid firefox caret bug
1974 if (this.search.val() === " ") this.search.val("");
1979 opening: function () {
1980 this.parent.opening.apply(this, arguments);
1982 this.clearPlaceholder();
1983 this.resizeSearch();
1988 close: function () {
1989 if (!this.opened()) return;
1990 this.parent.close.apply(this, arguments);
1994 focus: function () {
1996 this.search.focus();
2000 isFocused: function () {
2001 return this.search.hasClass("select2-focused");
2005 updateSelection: function (data) {
2006 var ids = [], filtered = [], self = this;
2008 // filter out duplicates
2009 $(data).each(function () {
2010 if (indexOf(self.id(this), ids) < 0) {
2011 ids.push(self.id(this));
2012 filtered.push(this);
2017 this.selection.find(".select2-search-choice").remove();
2018 $(data).each(function () {
2019 self.addSelectedChoice(this);
2021 self.postprocessResults();
2024 tokenize: function() {
2025 var input = this.search.val();
2026 input = this.opts.tokenizer(input, this.data(), this.bind(this.onSelect), this.opts);
2027 if (input != null && input != undefined) {
2028 this.search.val(input);
2029 if (input.length > 0) {
2037 onSelect: function (data) {
2038 this.addSelectedChoice(data);
2039 if (this.select) { this.postprocessResults(); }
2041 if (this.opts.closeOnSelect) {
2043 this.search.width(10);
2045 if (this.countSelectableResults()>0) {
2046 this.search.width(10);
2047 this.resizeSearch();
2048 this.positionDropdown();
2050 // if nothing left to select close
2055 // since its not possible to select an element that has already been
2056 // added we do not need to check if this is a new element before firing change
2057 this.triggerChange({ added: data });
2063 cancel: function () {
2069 addSelectedChoice: function (data) {
2071 "<li class='select2-search-choice'>" +
2073 " <a href='#' onclick='return false;' class='select2-search-choice-close' tabindex='-1'></a>" +
2076 val = this.getVal(),
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;
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");
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");
2099 choice.data("select2-data", data);
2100 choice.insertBefore(this.searchContainer);
2107 unselect: function (selected) {
2108 var val = this.getVal(),
2112 selected = selected.closest(".select2-search-choice");
2114 if (selected.length === 0) {
2115 throw "Invalid argument: " + selected + ". Must be .select2-search-choice";
2118 data = selected.data("select2-data");
2120 index = indexOf(this.id(data), val);
2123 val.splice(index, 1);
2125 if (this.select) this.postprocessResults();
2128 this.triggerChange({ removed: data });
2132 postprocessResults: function () {
2133 var val = this.getVal(),
2134 choices = this.results.find(".select2-result-selectable"),
2135 compound = this.results.find(".select2-result-with-children"),
2138 choices.each2(function (i, choice) {
2139 var id = self.id(choice.data("select2-data"));
2140 if (indexOf(id, val) >= 0) {
2141 choice.addClass("select2-disabled").removeClass("select2-result-selectable");
2143 choice.removeClass("select2-disabled").addClass("select2-result-selectable");
2147 compound.each2(function(i, e) {
2148 if (e.find(".select2-result-selectable").length==0) {
2149 e.addClass("select2-disabled");
2151 e.removeClass("select2-disabled");
2155 choices.each2(function (i, choice) {
2156 if (!choice.hasClass("select2-disabled") && choice.hasClass("select2-result-selectable")) {
2165 resizeSearch: function () {
2167 var minimumWidth, left, maxWidth, containerLeft, searchWidth,
2168 sideBorderPadding = getSideBorderPadding(this.search);
2170 minimumWidth = measureTextWidth(this.search) + 10;
2172 left = this.search.offset().left;
2174 maxWidth = this.selection.width();
2175 containerLeft = this.selection.offset().left;
2177 searchWidth = maxWidth - (left - containerLeft) - sideBorderPadding;
2178 if (searchWidth < minimumWidth) {
2179 searchWidth = maxWidth - sideBorderPadding;
2182 if (searchWidth < 40) {
2183 searchWidth = maxWidth - sideBorderPadding;
2185 this.search.width(searchWidth);
2189 getVal: function () {
2192 val = this.select.val();
2193 return val === null ? [] : val;
2195 val = this.opts.element.val();
2196 return splitVal(val, this.opts.separator);
2201 setVal: function (val) {
2204 this.select.val(val);
2207 // filter out duplicates
2208 $(val).each(function () {
2209 if (indexOf(this, unique) < 0) unique.push(this);
2211 this.opts.element.val(unique.length === 0 ? "" : unique.join(this.opts.separator));
2217 var val, data = [], self=this;
2219 if (arguments.length === 0) {
2220 return this.getVal();
2226 this.opts.element.val("");
2227 this.updateSelection([]);
2232 // val is a list of ids
2236 this.select.find(":selected").each(function () {
2237 data.push({id: $(this).attr("value"), text: $(this).text()});
2239 this.updateSelection(data);
2241 if (this.opts.initSelection === undefined) {
2242 throw new Error("val() cannot be called if initSelection() is not defined")
2245 this.opts.initSelection(this.opts.element, function(data){
2246 var ids=$(data).map(self.id);
2248 self.updateSelection(data);
2256 onSortStart: function() {
2258 throw new Error("Sorting of elements is not supported when attached to <select>. Attach to <input type='hidden'/> instead.");
2261 // collapse search field into 0 width so its container can be collapsed as well
2262 this.search.width(0);
2263 // hide the container
2264 this.searchContainer.hide();
2268 onSortEnd:function() {
2270 var val=[], self=this;
2272 // show search and move it to the end of the list
2273 this.searchContainer.show();
2274 // make sure the search container is the last item in the list
2275 this.searchContainer.appendTo(this.searchContainer.parent());
2276 // since we collapsed the width in dragStarted, we resize it here
2277 this.resizeSearch();
2281 this.selection.find(".select2-search-choice").each(function() {
2282 val.push(self.opts.id($(this).data("select2-data")));
2285 this.triggerChange();
2289 data: function(values) {
2291 if (arguments.length === 0) {
2292 return this.selection
2293 .find(".select2-search-choice")
2294 .map(function() { return $(this).data("select2-data"); })
2297 if (!values) { values = []; }
2298 ids = $.map(values, function(e) { return self.opts.id(e)});
2300 this.updateSelection(values);
2306 $.fn.select2 = function () {
2308 var args = Array.prototype.slice.call(arguments, 0),
2311 value, multiple, allowedMethods = ["val", "destroy", "opened", "open", "close", "focus", "isFocused", "container", "onSortStart", "onSortEnd", "enable", "disable", "positionDropdown", "data"];
2313 this.each(function () {
2314 if (args.length === 0 || typeof(args[0]) === "object") {
2315 opts = args.length === 0 ? {} : $.extend({}, args[0]);
2316 opts.element = $(this);
2318 if (opts.element.get(0).tagName.toLowerCase() === "select") {
2319 multiple = opts.element.attr("multiple");
2321 multiple = opts.multiple || false;
2322 if ("tags" in opts) {opts.multiple = multiple = true;}
2325 select2 = multiple ? new MultiSelect2() : new SingleSelect2();
2327 } else if (typeof(args[0]) === "string") {
2329 if (indexOf(args[0], allowedMethods) < 0) {
2330 throw "Unknown method: " + args[0];
2334 select2 = $(this).data("select2");
2335 if (select2 === undefined) return;
2336 if (args[0] === "container") {
2337 value=select2.container;
2339 value = select2[args[0]].apply(select2, args.slice(1));
2341 if (value !== undefined) {return false;}
2343 throw "Invalid arguments to select2 plugin: " + args;
2346 return (value === undefined) ? this : value;
2349 // plugin defaults, accessible to users
2350 $.fn.select2.defaults = {
2352 closeOnSelect: true,
2356 containerCssClass: "",
2357 dropdownCssClass: "",
2358 formatResult: function(result, container, query) {
2360 markMatch(result.text, query.term, markup);
2361 return markup.join("");
2363 formatSelection: function (data, container) {
2366 formatResultCssClass: function(data) {return undefined;},
2367 formatNoMatches: function () { return "No matches found"; },
2368 formatInputTooShort: function (input, min) { return "Please enter " + (min - input.length) + " more characters"; },
2369 formatSelectionTooBig: function (limit) { return "You can only select " + limit + " item" + (limit == 1 ? "" : "s"); },
2370 formatLoadMore: function (pageNumber) { return "Loading more results..."; },
2371 formatSearching: function () { return "Searching..."; },
2372 minimumResultsForSearch: 0,
2373 minimumInputLength: 0,
2374 maximumSelectionSize: 0,
2375 id: function (e) { return e.id; },
2376 matcher: function(term, text) {
2377 return text.toUpperCase().indexOf(term.toUpperCase()) >= 0;
2380 tokenSeparators: [],
2381 tokenizer: defaultTokenizer,
2382 escapeMarkup: function (markup) {
2383 if (markup && typeof(markup) === "string") {
2384 return markup.replace(/&/g, "&");
2399 markMatch: markMatch
2401 "abstract": AbstractSelect2,
2402 "single": SingleSelect2,
2403 "multi": MultiSelect2