app/assets/javascripts/select2.js in select2-rails-3.5.5 vs app/assets/javascripts/select2.js in select2-rails-3.5.6
- old
+ new
@@ -1,9 +1,9 @@
/*
Copyright 2012 Igor Vaynberg
-Version: 3.4.6 Timestamp: Sat Mar 22 22:30:15 EDT 2014
+Version: 3.4.7 Timestamp: Wed Apr 30 19:28:03 EDT 2014
This software is licensed under the Apache License, Version 2.0 (the "Apache License") or the GNU
General Public License version 2 (the "GPL License"). You may choose either license to govern your
use of this software only upon the condition that you accept all of the terms of either the Apache
License or the GPL License.
@@ -112,20 +112,16 @@
placeholder.before(element);
placeholder.remove();
}
function stripDiacritics(str) {
- var ret, i, l, c;
-
- if (!str || str.length < 1) return str;
-
- ret = "";
- for (i = 0, l = str.length; i < l; i++) {
- c = str.charAt(i);
- ret += DIACRITICS[c] || c;
+ // Used 'uni range + named function' from http://jsperf.com/diacritics/18
+ function match(a) {
+ return DIACRITICS[a] || a;
}
- return ret;
+
+ return str.replace(/[^\u0000-\u007E]/g, match);
}
function indexOf(value, array) {
var i = 0, l = array.length;
for (; i < l; i = i + 1) {
@@ -236,24 +232,10 @@
fn.apply(ctx, args);
}, quietMillis);
};
}
- /**
- * A simple implementation of a thunk
- * @param formula function used to lazily initialize the thunk
- * @return {Function}
- */
- function thunk(formula) {
- var evaluated = false,
- value;
- return function() {
- if (evaluated === false) { value = formula(); evaluated = true; }
- return value;
- };
- };
-
function installDebouncedScroll(threshold, element) {
var notify = debounce(threshold, function (e) { element.trigger("scroll-debounced", e);});
element.on("scroll", function (e) {
if (indexOf(e.target, element.get()) >= 0) notify(e);
});
@@ -471,11 +453,11 @@
* @param options object containing configuration parameters. The options parameter can either be an array or an
* object.
*
* If the array form is used it is assumed that it contains objects with 'id' and 'text' keys.
*
- * If the object form is used ti is assumed that it contains 'data' and 'text' keys. The 'data' key should contain
+ * If the object form is used it is assumed that it contains 'data' and 'text' keys. The 'data' key should contain
* an array of objects that will be used as choices. These objects must contain at least an 'id' key. The 'text'
* key can either be a String in which case it is expected that each element in the 'data' array has a key with the
* value of 'text' which will be used to match choices. Alternatively, text can be a function(item) that can extract
* the text.
*/
@@ -540,18 +522,21 @@
// TODO javadoc
function tags(data) {
var isFunc = $.isFunction(data);
return function (query) {
var t = query.term, filtered = {results: []};
- $(isFunc ? data() : data).each(function () {
- var isObject = this.text !== undefined,
- text = isObject ? this.text : this;
- if (t === "" || query.matcher(t, text)) {
- filtered.results.push(isObject ? this : {id: this, text: this});
- }
- });
- query.callback(filtered);
+ var result = $(isFunc ? data(query) : data);
+ if ($.isArray(result)) {
+ $(isFunc ? data() : data).each(function () {
+ var isObject = this.text !== undefined,
+ text = isObject ? this.text : this;
+ if (t === "" || query.matcher(t, text)) {
+ filtered.results.push(isObject ? this : {id: this, text: this});
+ }
+ });
+ query.callback(filtered);
+ }
};
}
/**
* Checks if the formatter function should be used.
@@ -639,10 +624,19 @@
}
if (original!==input) return input;
}
+ function cleanupJQueryElements() {
+ var self = this;
+
+ Array.prototype.forEach.call(arguments, function (element) {
+ self[element].remove();
+ self[element] = null;
+ });
+ }
+
/**
* Creates a new class
*
* @param superClass
* @param methods
@@ -688,17 +682,20 @@
"aria-live": "polite"
})
.addClass("select2-hidden-accessible")
.appendTo(document.body);
- this.containerId="s2id_"+(opts.element.attr("id") || "autogen"+nextUid()).replace(/([;&,\-\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '\\$1');
- this.containerSelector="#"+this.containerId;
+ this.containerId="s2id_"+(opts.element.attr("id") || "autogen"+nextUid());
+ this.containerEventName= this.containerId
+ .replace(/([.])/g, '_')
+ .replace(/([;&,\-\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '\\$1');
this.container.attr("id", this.containerId);
- // cache the body so future lookups are cheap
- this.body = thunk(function() { return opts.element.closest("body"); });
+ this.container.attr("title", opts.element.attr("title"));
+ this.body = $("body");
+
syncCssClasses(this.container, this.opts.element, this.opts.adaptContainerCssClass);
this.container.attr("style", opts.element.attr("style"));
this.container.css(evaluate(opts.containerCss));
this.container.addClass(evaluate(opts.containerCssClass));
@@ -733,15 +730,28 @@
this.initContainer();
this.container.on("click", killEvent);
installFilteredMouseMove(this.results);
- this.dropdown.on("mousemove-filtered touchstart touchmove touchend", resultsSelector, this.bind(this.highlightUnderEvent));
- this.dropdown.on("touchend", resultsSelector, this.bind(this.selectHighlighted));
+
+ this.dropdown.on("mousemove-filtered", resultsSelector, this.bind(this.highlightUnderEvent));
+ this.dropdown.on("touchstart touchmove touchend", resultsSelector, this.bind(function (event) {
+ this._touchEvent = true;
+ this.highlightUnderEvent(event);
+ }));
this.dropdown.on("touchmove", resultsSelector, this.bind(this.touchMoved));
this.dropdown.on("touchstart touchend", resultsSelector, this.bind(this.clearTouchMoved));
+ // Waiting for a click event on touch devices to select option and hide dropdown
+ // otherwise click will be triggered on an underlying element
+ this.dropdown.on('click', this.bind(function (event) {
+ if (this._touchEvent) {
+ this._touchEvent = false;
+ this.selectHighlighted();
+ }
+ }));
+
installDebouncedScroll(80, this.results);
this.dropdown.on("scroll-debounced", resultsSelector, this.bind(this.loadMoreIfNeeded));
// do not propagate change event from the search field out of the component
$(this.container).on("change", ".select2-input", function(e) {e.stopPropagation();});
@@ -775,11 +785,11 @@
// trap all mouse events from leaving the dropdown. sometimes there may be a modal that is listening
// for mouse events outside of itself so it can close itself. since the dropdown is now outside the select2's
// dom it will trigger the popup close, which is not what we want
// focusin can cause focus wars between modals and select2 since the dropdown is outside the modal.
- this.dropdown.on("click mouseup mousedown focusin", function (e) { e.stopPropagation(); });
+ this.dropdown.on("click mouseup mousedown touchstart touchend focusin", function (e) { e.stopPropagation(); });
this.nextSearchTerm = undefined;
if ($.isFunction(this.opts.initSelection)) {
// initialize selection based on the current value of the source element
@@ -816,11 +826,14 @@
destroy: function () {
var element=this.opts.element, select2 = element.data("select2");
this.close();
- if (this.propertyObserver) { delete this.propertyObserver; this.propertyObserver = null; }
+ if (this.propertyObserver) {
+ this.propertyObserver.disconnect();
+ this.propertyObserver = null;
+ }
if (select2 !== undefined) {
select2.container.remove();
select2.liveRegion.remove();
select2.dropdown.remove();
@@ -834,10 +847,18 @@
} else {
element.removeAttr("tabindex");
}
element.show();
}
+
+ cleanupJQueryElements.call(this,
+ "container",
+ "liveRegion",
+ "dropdown",
+ "results",
+ "search"
+ );
},
// abstract
optionToData: function(element) {
if (element.is("option")) {
@@ -1066,25 +1087,24 @@
syncCssClasses(this.dropdown, this.opts.element, this.opts.adaptDropdownCssClass);
this.dropdown.addClass(evaluate(this.opts.dropdownCssClass));
});
- // IE8-10
- el.on("propertychange.select2", sync);
-
- // hold onto a reference of the callback to work around a chromium bug
- if (this.mutationCallback === undefined) {
- this.mutationCallback = function (mutations) {
- mutations.forEach(sync);
- }
+ // IE8-10 (IE9/10 won't fire propertyChange via attachEventListener)
+ if (el.length && el[0].attachEvent) {
+ el.each(function() {
+ this.attachEvent("onpropertychange", sync);
+ });
}
-
+
// safari, chrome, firefox, IE11
observer = window.MutationObserver || window.WebKitMutationObserver|| window.MozMutationObserver;
if (observer !== undefined) {
if (this.propertyObserver) { delete this.propertyObserver; this.propertyObserver = null; }
- this.propertyObserver = new observer(this.mutationCallback);
+ this.propertyObserver = new observer(function (mutations) {
+ mutations.forEach(sync);
+ });
this.propertyObserver.observe(el.get(0), { attributes:true, subtree:false });
}
},
// abstract
@@ -1219,31 +1239,35 @@
dropTop = offset.top + height;
dropLeft = offset.left;
dropWidth = $dropdown.outerWidth(false);
enoughRoomOnRight = dropLeft + dropWidth <= viewPortRight;
$dropdown.show();
+
+ // fix so the cursor does not move to the left within the search-textbox in IE
+ this.focusSearch();
}
if (this.opts.dropdownAutoWidth) {
resultsListNode = $('.select2-results', $dropdown)[0];
$dropdown.addClass('select2-drop-auto-width');
$dropdown.css('width', '');
// Add scrollbar width to dropdown if vertical scrollbar is present
dropWidth = $dropdown.outerWidth(false) + (resultsListNode.scrollHeight === resultsListNode.clientHeight ? 0 : scrollBarDimensions.width);
dropWidth > width ? width = dropWidth : dropWidth = width;
+ dropHeight = $dropdown.outerHeight(false);
enoughRoomOnRight = dropLeft + dropWidth <= viewPortRight;
}
else {
this.container.removeClass('select2-drop-auto-width');
}
//console.log("below/ droptop:", dropTop, "dropHeight", dropHeight, "sum", (dropTop+dropHeight)+" viewport bottom", viewportBottom, "enough?", enoughRoomBelow);
- //console.log("above/ offset.top", offset.top, "dropHeight", dropHeight, "top", (offset.top-dropHeight), "scrollTop", this.body().scrollTop(), "enough?", enoughRoomAbove);
+ //console.log("above/ offset.top", offset.top, "dropHeight", dropHeight, "top", (offset.top-dropHeight), "scrollTop", this.body.scrollTop(), "enough?", enoughRoomAbove);
// fix positioning when body has an offset and is not position: static
- if (this.body().css('position') !== 'static') {
- bodyOffset = this.body().offset();
+ if (this.body.css('position') !== 'static') {
+ bodyOffset = this.body.offset();
dropTop -= bodyOffset.top;
dropLeft -= bodyOffset.left;
}
if (!enoughRoomOnRight) {
@@ -1311,31 +1335,31 @@
/**
* Performs the opening of the dropdown
*/
// abstract
opening: function() {
- var cid = this.containerId,
+ var cid = this.containerEventName,
scroll = "scroll." + cid,
resize = "resize."+cid,
orient = "orientationchange."+cid,
mask;
this.container.addClass("select2-dropdown-open").addClass("select2-container-active");
this.clearDropdownAlignmentPreference();
- if(this.dropdown[0] !== this.body().children().last()[0]) {
- this.dropdown.detach().appendTo(this.body());
+ if(this.dropdown[0] !== this.body.children().last()[0]) {
+ this.dropdown.detach().appendTo(this.body);
}
// create the dropdown mask if doesn't already exist
mask = $("#select2-drop-mask");
if (mask.length == 0) {
mask = $(document.createElement("div"));
mask.attr("id","select2-drop-mask").attr("class","select2-drop-mask");
mask.hide();
- mask.appendTo(this.body());
+ mask.appendTo(this.body);
mask.on("mousedown touchstart click", function (e) {
// Prevent IE from generating a click event on the body
reinsertElement(mask);
var dropdown = $("#select2-drop"), self;
@@ -1372,22 +1396,22 @@
// attach listeners to events that can change the position of the container and thus require
// the position of the dropdown to be updated as well so it does not come unglued from the container
var that = this;
this.container.parents().add(window).each(function () {
$(this).on(resize+" "+scroll+" "+orient, function (e) {
- that.positionDropdown();
+ if (that.opened()) that.positionDropdown();
});
});
},
// abstract
close: function () {
if (!this.opened()) return;
- var cid = this.containerId,
+ var cid = this.containerEventName,
scroll = "scroll." + cid,
resize = "resize."+cid,
orient = "orientationchange."+cid;
// unbind event listeners
@@ -1804,11 +1828,11 @@
var firstOption = this.select.children('option').first();
if (this.opts.placeholderOption !== undefined ) {
//Determine the placeholder option based on the specified placeholderOption setting
return (this.opts.placeholderOption === "first" && firstOption) ||
(typeof this.opts.placeholderOption === "function" && this.opts.placeholderOption(this.select));
- } else if (firstOption.text() === "" && firstOption.val() === "") {
+ } else if ($.trim(firstOption.text()) === "" && firstOption.val() === "") {
//No explicit placeholder option specified, use the first if it's blank
return firstOption;
}
}
},
@@ -1873,11 +1897,11 @@
createContainer: function () {
var container = $(document.createElement("div")).attr({
"class": "select2-container"
}).html([
"<a href='javascript:void(0)' class='select2-choice' tabindex='-1'>",
- " <span class='select2-chosen'> </span><abbr class='select2-search-choice-close'></abbr>",
+ " <span class='select2-chosen'> </span><abbr class='select2-search-choice-close'></abbr>",
" <span class='select2-arrow' role='presentation'><b role='presentation'></b></span>",
"</a>",
"<label for='' class='select2-offscreen'></label>",
"<input class='select2-focusser select2-offscreen' type='text' aria-haspopup='true' role='button' />",
"<div class='select2-drop select2-display-none'>",
@@ -1913,21 +1937,23 @@
// IE appends focusser.val() at the end of field :/ so we manually insert it at the beginning using a range
// all other browsers handle this just fine
this.search.val(this.focusser.val());
}
- this.search.focus();
- // move the cursor to the end after focussing, otherwise it will be at the beginning and
- // new text will appear *before* focusser.val()
- el = this.search.get(0);
- if (el.createTextRange) {
- range = el.createTextRange();
- range.collapse(false);
- range.select();
- } else if (el.setSelectionRange) {
- len = this.search.val().length;
- el.setSelectionRange(len, len);
+ if (this.opts.shouldFocusInput(this)) {
+ this.search.focus();
+ // move the cursor to the end after focussing, otherwise it will be at the beginning and
+ // new text will appear *before* focusser.val()
+ el = this.search.get(0);
+ if (el.createTextRange) {
+ range = el.createTextRange();
+ range.collapse(false);
+ range.select();
+ } else if (el.setSelectionRange) {
+ len = this.search.val().length;
+ el.setSelectionRange(len, len);
+ }
}
// initializes search's value with nextSearchTerm (if defined by user)
// ignore nextSearchTerm if the dropdown is opened by the user pressing a letter
if(this.search.val() === "") {
@@ -1984,10 +2010,15 @@
// single
destroy: function() {
$("label[for='" + this.focusser.attr('id') + "']")
.attr('for', this.opts.element.attr("id"));
this.parent.destroy.apply(this, arguments);
+
+ cleanupJQueryElements.call(this,
+ "selection",
+ "focusser"
+ );
},
// single
initContainer: function () {
@@ -2065,11 +2096,11 @@
}));
this.search.on("blur", this.bind(function(e) {
// a workaround for chrome to keep the search field focussed when the scroll bar is used to scroll the dropdown.
// without this the search field loses focus which is annoying
- if (document.activeElement === this.body().get(0)) {
+ if (document.activeElement === this.body.get(0)) {
window.setTimeout(this.bind(function() {
if (this.opened()) {
this.search.focus();
}
}), 0);
@@ -2140,11 +2171,15 @@
}
killEvent(e);
}));
- dropdown.on("mousedown touchstart", this.bind(function() { this.search.focus(); }));
+ dropdown.on("mousedown touchstart", this.bind(function() {
+ if (this.opts.shouldFocusInput(this)) {
+ this.search.focus();
+ }
+ }));
selection.on("focus", this.bind(function(e) {
killEvent(e);
}));
@@ -2217,11 +2252,11 @@
}
},
isPlaceholderOptionSelected: function() {
var placeholderOption;
- if (!this.getPlaceholder()) return false; // no placeholder specified so no option should be considered
+ if (this.getPlaceholder() === undefined) return false; // no placeholder specified so no option should be considered
return ((placeholderOption = this.getPlaceholderOption()) !== undefined && placeholderOption.prop("selected"))
|| (this.opts.element.val() === "")
|| (this.opts.element.val() === undefined)
|| (this.opts.element.val() === null);
},
@@ -2570,10 +2605,15 @@
// multi
destroy: function() {
$("label[for='" + this.search.attr('id') + "']")
.attr('for', this.opts.element.attr("id"));
this.parent.destroy.apply(this, arguments);
+
+ cleanupJQueryElements.call(this,
+ "searchContainer",
+ "selection"
+ );
},
// multi
initContainer: function () {
@@ -2819,11 +2859,13 @@
this.search.select();
}
}
this.updateResults(true);
- this.search.focus();
+ if (this.opts.shouldFocusInput(this)) {
+ this.search.focus();
+ }
this.opts.element.trigger($.Event("select2-open"));
},
// multi
close: function () {
@@ -3296,11 +3338,11 @@
if (methodsMap[method]) method = methodsMap[method];
value = select2[method].apply(select2, args.slice(1));
}
if (indexOf(args[0], valueMethods) >= 0
- || (indexOf(args[0], propertyMethods) && args.length == 1)) {
+ || (indexOf(args[0], propertyMethods) >= 0 && args.length == 1)) {
return false; // abort the iteration, ready to return first matched value
}
} else {
throw "Invalid arguments to select2 plugin: " + args;
}
@@ -3356,9 +3398,18 @@
adaptDropdownCssClass: function(c) { return null; },
nextSearchTerm: function(selectedObject, currentSearchTerm) { return undefined; },
searchInputPlaceholder: '',
createSearchChoicePosition: 'top',
shouldFocusInput: function (instance) {
+ // Attempt to detect touch devices
+ var supportsTouchEvents = (('ontouchstart' in window) ||
+ (navigator.msMaxTouchPoints > 0));
+
+ // Only devices which support touch events should be special cased
+ if (!supportsTouchEvents) {
+ return true;
+ }
+
// Never focus the input if search is disabled
if (instance.opts.minimumResultsForSearch < 0) {
return false;
}