diff --git a/src/inputs/select2/lib/select2.js b/src/inputs/select2/lib/select2.js index 992ba8b..60f8086 100644 --- a/src/inputs/select2/lib/select2.js +++ b/src/inputs/select2/lib/select2.js @@ -1,7 +1,7 @@ -/* +/* Copyright 2012 Igor Vaynberg -Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013 +Version: @@ver@@ Timestamp: @@timestamp@@ 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 @@ -47,7 +47,7 @@ the specific language governing permissions and limitations under the Apache Lic } var KEY, AbstractSelect2, SingleSelect2, MultiSelect2, nextUid, sizer, - lastMousePosition, $document, scrollBarDimensions, + lastMousePosition={x:0,y:0}, $document, scrollBarDimensions, KEY = { TAB: 9, @@ -172,7 +172,8 @@ the specific language governing permissions and limitations under the Apache Lic } $document.on("mousemove", function (e) { - lastMousePosition = {x: e.pageX, y: e.pageY}; + lastMousePosition.x = e.pageX; + lastMousePosition.y = e.pageY; }); /** @@ -355,6 +356,22 @@ the specific language governing permissions and limitations under the Apache Lic markup.push(escapeMarkup(text.substring(match + tl, text.length))); } + function defaultEscapeMarkup(markup) { + var replace_map = { + '\\': '\', + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + "/": '/' + }; + + return String(markup).replace(/[&<>"'\/\\]/g, function (match) { + return replace_map[match]; + }); + } + /** * Produces an ajax-based query function * @@ -399,7 +416,7 @@ the specific language governing permissions and limitations under the Apache Lic data = data ? data.call(self, query.term, query.page, query.context) : null; url = (typeof url === 'function') ? url.call(self, query.term, query.page, query.context) : url; - if( null !== handler) { handler.abort(); } + if (handler) { handler.abort(); } if (options.params) { if ($.isFunction(options.params)) { @@ -526,7 +543,7 @@ the specific language governing permissions and limitations under the Apache Lic function checkFormatter(formatter, formatterName) { if ($.isFunction(formatter)) return true; if (!formatter) return false; - throw new Error("formatterName must be a function or a falsy value"); + throw new Error(formatterName +" must be a function or a falsy value"); } function evaluate(val) { @@ -746,21 +763,24 @@ the specific language governing permissions and limitations under the Apache Lic // abstract destroy: function () { - var select2 = this.opts.element.data("select2"); + var element=this.opts.element, select2 = element.data("select2"); if (this.propertyObserver) { delete this.propertyObserver; this.propertyObserver = null; } if (select2 !== undefined) { - select2.container.remove(); select2.dropdown.remove(); - select2.opts.element + element .removeClass("select2-offscreen") .removeData("select2") .off(".select2") - .attr({"tabindex": this.elementTabIndex}) - .prop("autofocus", this.autofocus||false) - .show(); + .prop("autofocus", this.autofocus || false); + if (this.elementTabIndex) { + element.attr({tabindex: this.elementTabIndex}); + } else { + element.removeAttr("tabindex"); + } + element.show(); } }, @@ -874,7 +894,7 @@ the specific language governing permissions and limitations under the Apache Lic opts.query = this.bind(function (query) { var data = { results: [], more: false }, term = query.term, - children, firstChild, process; + children, placeholderOption, process; process=function(element, collection) { var group; @@ -895,9 +915,9 @@ the specific language governing permissions and limitations under the Apache Lic // 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); + placeholderOption = this.getPlaceholderOption(); + if (placeholderOption) { + children=children.not(placeholderOption); } } @@ -1201,7 +1221,7 @@ the specific language governing permissions and limitations under the Apache Lic scroll = "scroll." + cid, resize = "resize."+cid, orient = "orientationchange."+cid, - mask; + mask, maskCss; this.container.addClass("select2-dropdown-open").addClass("select2-container-active"); @@ -1218,7 +1238,7 @@ the specific language governing permissions and limitations under the Apache Lic mask.attr("id","select2-drop-mask").attr("class","select2-drop-mask"); mask.hide(); mask.appendTo(this.body()); - mask.on("mousedown touchstart", function (e) { + mask.on("mousedown touchstart click", function (e) { var dropdown = $("#select2-drop"), self; if (dropdown.length > 0) { self=dropdown.data("select2"); @@ -1242,20 +1262,22 @@ the specific language governing permissions and limitations under the Apache Lic this.dropdown.attr("id", "select2-drop"); // show the elements - mask.css(_makeMaskCss()); - mask.show(); + maskCss=_makeMaskCss(); + + mask.css(maskCss).show(); + this.dropdown.show(); this.positionDropdown(); this.dropdown.addClass("select2-drop-active"); - this.ensureHighlightVisible(); // 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) { - $("#select2-drop-mask").css(_makeMaskCss()); + var maskCss=_makeMaskCss(); + $("#select2-drop-mask").css(maskCss); that.positionDropdown(); }); }); @@ -1294,6 +1316,16 @@ the specific language governing permissions and limitations under the Apache Lic this.opts.element.trigger($.Event("select2-close")); }, + /** + * Opens control, sets input value, and updates results. + */ + // abstract + externalSearch: function (term) { + this.open(); + this.search.val(term); + this.updateResults(false); + }, + // abstract clearSearch: function () { @@ -1487,7 +1519,6 @@ the specific language governing permissions and limitations under the Apache Lic } function postRender() { - results.scrollTop(0); search.removeClass("select2-active"); self.positionDropdown(); } @@ -1512,7 +1543,7 @@ the specific language governing permissions and limitations under the Apache Lic } else { render(""); } - if (initial) this.showSearch(true); + if (initial && this.showSearch) this.showSearch(true); return; } @@ -1586,7 +1617,7 @@ the specific language governing permissions and limitations under the Apache Lic postRender(); - this.opts.element.trigger({ type: "select2-loaded", data:data }); + this.opts.element.trigger({ type: "select2-loaded", items: data }); })}); }, @@ -1623,15 +1654,34 @@ the specific language governing permissions and limitations under the Apache Lic if (data) { this.highlight(index); this.onSelect(data, options); + } else if (options.noFocus) { + this.close(); } }, // abstract getPlaceholder: function () { + var placeholderOption; return this.opts.element.attr("placeholder") || this.opts.element.attr("data-placeholder") || // jquery 1.4 compat this.opts.element.data("placeholder") || - this.opts.placeholder; + this.opts.placeholder || + ((placeholderOption = this.getPlaceholderOption()) !== undefined ? placeholderOption.text() : undefined); + }, + + // abstract + getPlaceholderOption: function() { + if (this.select) { + var firstOption = this.select.children().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() === "") { + //No explicit placeholder option specified, use the first if it's blank + return firstOption; + } + } }, /** @@ -1662,12 +1712,12 @@ the specific language governing permissions and limitations under the Apache Lic } } - // next check if css('width') can resolve a width that is percent based, this is sometimes possible - // when attached to input type=hidden or elements hidden via css - style = this.opts.element.css('width'); - if (style && style.length > 0) return style; - if (this.opts.width === "resolve") { + // next check if css('width') can resolve a width that is percent based, this is sometimes possible + // when attached to input type=hidden or elements hidden via css + style = this.opts.element.css('width'); + if (style.indexOf("%") > 0) return style; + // finally, fallback on the calculated width of the element return (this.opts.element.outerWidth(false) === 0 ? 'auto' : this.opts.element.outerWidth(false) + 'px'); } @@ -1702,7 +1752,7 @@ the specific language governing permissions and limitations under the Apache Lic "<input class='select2-focusser select2-offscreen' type='text'/>", "<div class='select2-drop select2-display-none'>" , " <div class='select2-search'>" , - " <input type='text' autocomplete='off' autocorrect='off' autocapitilize='off' spellcheck='false' class='select2-input'/>" , + " <input type='text' autocomplete='off' autocorrect='off' autocapitalize='off' spellcheck='false' class='select2-input'/>" , " </div>" , " <ul class='select2-results'>" , " </ul>" , @@ -1719,8 +1769,14 @@ the specific language governing permissions and limitations under the Apache Lic // single opening: function () { - var el, range; + var el, range, len; + + if (this.opts.minimumResultsForSearch >= 0) { + this.showSearch(true); + } + this.parent.opening.apply(this, arguments); + if (this.showSearchInput !== false) { // 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 @@ -1728,13 +1784,16 @@ the specific language governing permissions and limitations under the Apache Lic this.search.val(this.focusser.val()); } this.search.focus(); - // in IE we have to move the cursor to the end after focussing, otherwise it will be at the beginning and + // 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); } this.focusser.prop("disabled", true).val(""); @@ -1779,7 +1838,11 @@ the specific language governing permissions and limitations under the Apache Lic container = this.container, dropdown = this.dropdown; - this.showSearch(false); + if (this.opts.minimumResultsForSearch < 0) { + this.showSearch(false); + } else { + this.showSearch(true); + } this.selection = selection = container.find(".select2-choice"); @@ -1846,6 +1909,9 @@ the specific language governing permissions and limitations under the Apache Lic if (e.which == KEY.DOWN || e.which == KEY.UP || (e.which == KEY.ENTER && this.opts.openOnEnter)) { + + if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) return; + this.open(); killEvent(e); return; @@ -1863,9 +1929,11 @@ the specific language governing permissions and limitations under the Apache Lic installKeyUpChangeEvent(this.focusser); this.focusser.on("keyup-change input", this.bind(function(e) { - e.stopPropagation(); - if (this.opened()) return; - this.open(); + if (this.opts.minimumResultsForSearch >= 0) { + e.stopPropagation(); + if (this.opened()) return; + this.open(); + } })); selection.on("mousedown", "abbr", this.bind(function (e) { @@ -1925,7 +1993,8 @@ the specific language governing permissions and limitations under the Apache Lic clear: function(triggerChange) { var data=this.selection.data("select2-data"); if (data) { // guard against queued quick consecutive clicks - this.opts.element.val(""); + var placeholderOption = this.getPlaceholderOption(); + this.opts.element.val(placeholderOption ? placeholderOption.val() : ""); this.selection.find("span").empty(); this.selection.removeData("select2-data"); this.setPlaceholder(); @@ -1943,7 +2012,7 @@ the specific language governing permissions and limitations under the Apache Lic // single initSelection: function () { var selected; - if (this.opts.element.val() === "" && this.opts.element.text() === "") { + if (this.isPlaceholderOptionSelected()) { this.updateSelection([]); this.close(); this.setPlaceholder(); @@ -1959,6 +2028,14 @@ the specific language governing permissions and limitations under the Apache Lic } }, + isPlaceholderOptionSelected: function() { + var placeholderOption; + return ((placeholderOption = this.getPlaceholderOption()) !== undefined && placeholderOption.is(':selected')) || + (this.opts.element.val() === "") || + (this.opts.element.val() === undefined) || + (this.opts.element.val() === null); + }, + // single prepareOpts: function () { var opts = this.parent.prepareOpts.apply(this, arguments), @@ -1997,9 +2074,9 @@ the specific language governing permissions and limitations under the Apache Lic // single getPlaceholder: function() { - // if a placeholder is specified on a single select without the first empty option ignore it + // if a placeholder is specified on a single select without a valid placeholder option ignore it if (this.select) { - if (this.select.find("option").first().text() !== "") { + if (this.getPlaceholderOption() === undefined) { return undefined; } } @@ -2011,10 +2088,10 @@ the specific language governing permissions and limitations under the Apache Lic setPlaceholder: function () { var placeholder = this.getPlaceholder(); - if (this.opts.element.val() === "" && placeholder !== undefined) { + if (this.isPlaceholderOptionSelected() && placeholder !== undefined) { - // check for a first blank option if attached to a select - if (this.select && this.select.find("option:first").text() !== "") return; + // check for a placeholder option if attached to a select + if (this.select && this.getPlaceholderOption() === undefined) return; this.selection.find("span").html(this.opts.escapeMarkup(placeholder)); @@ -2039,22 +2116,27 @@ the specific language governing permissions and limitations under the Apache Lic // and highlight it if (noHighlightUpdate !== false) { - this.highlight(selected); - } - - // show the search box if this is the first we got the results and there are enough of them for search - - if (initial === true && this.showSearchInput === false) { - var min=this.opts.minimumResultsForSearch; - if (min>=0) { - this.showSearch(countResults(data.results)>=min); + if (initial === true && selected >= 0) { + this.highlight(selected); + } else { + this.highlight(0); } } + // hide the search box if this is the first we got the results and there are enough of them for search + + if (initial === true) { + var min = this.opts.minimumResultsForSearch; + if (min >= 0) { + this.showSearch(countResults(data.results) >= min); + } + } }, // single showSearch: function(showSearchInput) { + if (this.showSearchInput === showSearchInput) return; + this.showSearchInput = showSearchInput; this.dropdown.find(".select2-search").toggleClass("select2-search-hidden", !showSearchInput); @@ -2087,7 +2169,7 @@ the specific language governing permissions and limitations under the Apache Lic // single updateSelection: function (data) { - var container=this.selection.find("span"), formatted; + var container=this.selection.find("span"), formatted, cssClass; this.selection.data("select2-data", data); @@ -2096,6 +2178,10 @@ the specific language governing permissions and limitations under the Apache Lic if (formatted !== undefined) { container.append(this.opts.escapeMarkup(formatted)); } + cssClass=this.opts.formatSelectionCssClass(data, container); + if (cssClass !== undefined) { + container.addClass(cssClass); + } this.selection.removeClass("select2-default"); @@ -2135,14 +2221,14 @@ the specific language governing permissions and limitations under the Apache Lic this.triggerChange({added: data, removed:oldData}); } } else { - if (this.opts.initSelection === undefined) { - throw new Error("cannot call val() if initSelection() is not defined"); - } // val is an id. !val is true for [undefined,null,'',0] - 0 is legal if (!val && val !== 0) { this.clear(triggerChange); return; } + if (this.opts.initSelection === undefined) { + throw new Error("cannot call val() if initSelection() is not defined"); + } this.opts.element.val(val); this.opts.initSelection(this.opts.element, function(data){ self.opts.element.val(!data ? "" : self.id(data)); @@ -2374,6 +2460,7 @@ the specific language governing permissions and limitations under the Apache Lic return; case KEY.TAB: this.selectHighlighted({noFocus:true}); + this.close(); return; case KEY.ESC: this.cancel(e); @@ -2424,7 +2511,7 @@ the specific language governing permissions and limitations under the Apache Lic this.opts.element.trigger($.Event("select2-blur")); })); - this.container.on("mousedown", selector, this.bind(function (e) { + this.container.on("click", selector, this.bind(function (e) { if (!this.isInterfaceEnabled()) return; if ($(e.target).closest(".select2-search-choice").length > 0) { // clicked inside a select2 search choice, do not open @@ -2632,11 +2719,16 @@ the specific language governing permissions and limitations under the Apache Lic var choice = enableChoice ? enabledItem : disabledItem, id = this.id(data), val = this.getVal(), - formatted; + formatted, + cssClass; formatted=this.opts.formatSelection(data, choice.find("div")); if (formatted != undefined) { - choice.find("div").replaceWith("<div title='"+this.opts.escapeMarkup(formatted)+"'>"+this.opts.escapeMarkup(formatted)+"</div>"); + choice.find("div").replaceWith("<div>"+this.opts.escapeMarkup(formatted)+"</div>"); + } + cssClass=this.opts.formatSelectionCssClass(data, choice.find("div")); + if (cssClass != undefined) { + choice.addClass(cssClass); } if(enableChoice){ @@ -2729,7 +2821,11 @@ the specific language governing permissions and limitations under the Apache Lic //If all results are chosen render formatNoMAtches if(!this.opts.createSearchChoice && !choices.filter('.select2-result:not(.select2-selected)').length > 0){ - this.results.append("<li class='select2-no-results'>" + self.opts.formatNoMatches(self.search.val()) + "</li>"); + if(!data || data && !data.more && this.results.find(".select2-no-results").length === 0) { + if (checkFormatter(self.opts.formatNoMatches, "formatNoMatches")) { + this.results.append("<li class='select2-no-results'>" + self.opts.formatNoMatches(self.search.val()) + "</li>"); + } + } } }, @@ -2852,7 +2948,7 @@ the specific language governing permissions and limitations under the Apache Lic } this.opts.initSelection(this.opts.element, function(data){ - var ids=$(data).map(self.id); + var ids=$.map(data, self.id); self.setVal(ids); self.updateSelection(data); self.clearSearch(); @@ -2924,9 +3020,10 @@ the specific language governing permissions and limitations under the Apache Lic var args = Array.prototype.slice.call(arguments, 0), opts, select2, - value, multiple, - allowedMethods = ["val", "destroy", "opened", "open", "close", "focus", "isFocused", "container", "onSortStart", "onSortEnd", "enable", "readonly", "positionDropdown", "data"], - valueMethods = ["val", "opened", "isFocused", "container", "data"]; + method, value, multiple, + allowedMethods = ["val", "destroy", "opened", "open", "close", "focus", "isFocused", "container", "dropdown", "onSortStart", "onSortEnd", "enable", "readonly", "positionDropdown", "data", "search"], + valueMethods = ["val", "opened", "isFocused", "container", "data"], + methodsMap = { search: "externalSearch" }; this.each(function () { if (args.length === 0 || typeof(args[0]) === "object") { @@ -2951,10 +3048,17 @@ the specific language governing permissions and limitations under the Apache Lic value = undefined; select2 = $(this).data("select2"); if (select2 === undefined) return; - if (args[0] === "container") { - value=select2.container; + + method=args[0]; + + if (method === "container") { + value = select2.container; + } else if (method === "dropdown") { + value = select2.dropdown; } else { - value = select2[args[0]].apply(select2, args.slice(1)); + if (methodsMap[method]) method = methodsMap[method]; + + value = select2[method].apply(select2, args.slice(1)); } if (indexOf(args[0], valueMethods) >= 0) { return false; @@ -2988,6 +3092,7 @@ the specific language governing permissions and limitations under the Apache Lic return results; }, formatResultCssClass: function(data) {return undefined;}, + formatSelectionCssClass: function(data, container) {return undefined;}, formatNoMatches: function () { return "No matches found"; }, formatInputTooShort: function (input, min) { var n = min - input.length; return "Please enter " + n + " more character" + (n == 1? "" : "s"); }, formatInputTooLong: function (input, max) { var n = input.length - max; return "Please delete " + n + " character" + (n == 1? "" : "s"); }, @@ -3005,21 +3110,7 @@ the specific language governing permissions and limitations under the Apache Lic separator: ",", tokenSeparators: [], tokenizer: defaultTokenizer, - escapeMarkup: function (markup) { - var replace_map = { - '\\': '\', - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - "/": '/' - }; - - return String(markup).replace(/[&<>"'\/\\]/g, function (match) { - return replace_map[match]; - }); - }, + escapeMarkup: defaultEscapeMarkup, blurOnChange: false, selectOnBlur: false, adaptContainerCssClass: function(c) { return c; }, @@ -3043,7 +3134,8 @@ the specific language governing permissions and limitations under the Apache Lic tags: tags }, util: { debounce: debounce, - markMatch: markMatch + markMatch: markMatch, + escapeMarkup: defaultEscapeMarkup }, "class": { "abstract": AbstractSelect2, "single": SingleSelect2, diff --git a/src/inputs/select2/select2.js b/src/inputs/select2/select2.js index f68b606..271d212 100644 --- a/src/inputs/select2/select2.js +++ b/src/inputs/select2/select2.js @@ -6,19 +6,19 @@ You should manually include select2 distributive: <link href="select2/select2.css" rel="stylesheet" type="text/css"></link> <script src="select2/select2.js"></script> -For make it **Bootstrap-styled** you can use css from [here](https://github.com/t0m/select2-bootstrap-css): +To make it **Bootstrap-styled** you can use css from [here](https://github.com/t0m/select2-bootstrap-css): <link href="select2-bootstrap.css" rel="stylesheet" type="text/css"></link> -**Note:** currently `ajax` source for select2 is not supported, as it's not possible to load it in closed select2 state. -The solution is to load source manually and assign statically. +**Note:** currently `autotext` feature does not work for select2 with `ajax` remote source. +You need initially put both `data-value` and element's text youself. @class select2 @extends abstractinput @since 1.4.1 @final @example -<a href="#" id="country" data-type="select2" data-pk="1" data-value="ru" data-url="/post" data-original-title="Select country"></a> +<a href="#" id="country" data-type="select2" data-pk="1" data-value="ru" data-url="/post" data-title="Select country"></a> <script> $(function(){ $('#country').editable({ @@ -39,56 +39,42 @@ $(function(){ var Constructor = function (options) { this.init('select2', options, Constructor.defaults); - + options.select2 = options.select2 || {}; - - var that = this, - mixin = { //mixin to select2 options - placeholder: options.placeholder - }; - - //detect whether it is multi-valued - this.isMultiple = options.select2.tags || options.select2.multiple; - - //if not `tags` mode, we need define initSelection to set data from source - if(!options.select2.tags) { - if(options.source) { - mixin.data = options.source; - } - //this function can be defaulted in seletc2. See https://github.com/ivaynberg/select2/issues/710 - mixin.initSelection = function (element, callback) { - //temp: try update results - /* - if(options.select2 && options.select2.ajax) { - console.log('attached'); - var original = $(element).data('select2').postprocessResults; - console.log(original); - $(element).data('select2').postprocessResults = function(data, initial) { - console.log('postprocess'); - // this.element.triggerHandler('loaded', [data]); - original.apply(this, arguments); - } + this.sourceData = null; + + //if not `tags` mode, use source + if(!options.select2.tags && options.source) { + var source = options.source; + //if source is function, call it (once!) + if ($.isFunction(options.source)) { + source = options.source.call(options.scope); + } - // $(element).on('loaded', function(){console.log('loaded');}); - $(element).data('select2').updateResults(true); + if (typeof source === 'string') { + options.select2.ajax = options.select2.ajax || {}; + //some default ajax params + if(!options.select2.ajax.data) { + options.select2.ajax.data = function(term) {return { query:term };}; } - */ - - var val = that.str2value(element.val()), - data = $.fn.editableutils.itemsByValue(val, mixin.data, 'id'); - - //for single-valued mode should not use array. Take first element instead. - if($.isArray(data) && data.length && !that.isMultiple) { - data = data[0]; + if(!options.select2.ajax.results) { + options.select2.ajax.results = function(data) { return {results:data };}; } - - callback(data); - }; - } + options.select2.ajax.url = source; + } else { + //todo: possible convert x-editable source to select2 source format + options.select2.data = source; + this.sourceData = source; + } + } //overriding objects in config (as by default jQuery extend() is not recursive) - this.options.select2 = $.extend({}, Constructor.defaults.select2, mixin, options.select2); + this.options.select2 = $.extend({}, Constructor.defaults.select2, options.select2); + + //detect whether it is multi-valued + this.isMultiple = this.options.select2.tags || this.options.select2.multiple; + this.isRemote = ('ajax' in this.options.select2); }; $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.abstractinput); @@ -96,21 +82,17 @@ $(function(){ $.extend(Constructor.prototype, { render: function() { this.setClass(); + //apply select2 this.$input.select2(this.options.select2); - //when data is loaded via ajax, we need to know when it's done - if('ajax' in this.options.select2) { - /* - console.log('attached'); - var original = this.$input.data('select2').postprocessResults; - this.$input.data('select2').postprocessResults = function(data, initial) { - this.element.triggerHandler('loaded', [data]); - original.apply(this, arguments); - } - */ + //when data is loaded via ajax, we need to know when it's done to populate listData + if(this.isRemote) { + //listen to loaded event to populate data + this.$input.on('select2-loaded', $.proxy(function(e) { + this.sourceData = e.items.results; + }, this)); } - //trigger resize of editableform to re-position container in multi-valued mode if(this.isMultiple) { @@ -122,20 +104,16 @@ $(function(){ value2html: function(value, element) { var text = '', data; - if(this.$input) { //called when submitting form and select2 already exists - data = this.$input.select2('data'); - } else { //on init (autotext) - //here select2 instance not created yet and data may be even not loaded. - //we can check data/tags property of select config and if exist lookup text - if(this.options.select2.tags) { - data = value; - } else if(this.options.select2.data) { - data = $.fn.editableutils.itemsByValue(value, this.options.select2.data, 'id'); - } else { - //if('ajax' in this.options.select2) { - } + + if(this.options.select2.tags) { //in tags mode just assign value + data = value; + } else if(this.sourceData) { + data = $.fn.editableutils.itemsByValue(value, this.sourceData, 'id'); + } else { + //can not get list of possible values (e.g. autotext for select2 with ajax source) } + //data may be array (when multiple values allowed) if($.isArray(data)) { //collect selected data and show with separator text = []; @@ -156,7 +134,26 @@ $(function(){ }, value2input: function(value) { - this.$input.val(value).trigger('change', true); //second argument needed to separate initial change from user's click (for autosubmit) + //for remote source .val() is not working, need to look in sourceData + if(this.isRemote) { + //todo: check value for array + var item, items; + //if sourceData loaded, use it to get text for display + if(this.sourceData) { + items = $.fn.editableutils.itemsByValue(value, this.sourceData, 'id'); + if(items.length) { + item = items[0]; + } + } + //if item not found by sourceData, use element text (e.g. for the first show) + if(!item) { + item = {id: value, text: $(this.options.scope).text()}; + } + //select2('data', ...) allows to set both id and text --> usefull for initial show when items are not loaded + this.$input.select2('data', item).trigger('change', true); //second argument needed to separate initial change from user's click (for autosubmit) + } else { + this.$input.val(value).trigger('change', true); //second argument needed to separate initial change from user's click (for autosubmit) + } }, input2value: function() { diff --git a/test/index.html b/test/index.html index dca946e..73f4a59 100644 --- a/test/index.html +++ b/test/index.html @@ -21,6 +21,7 @@ <div> <div id="qunit"></div> <div id="qunit-fixture"></div> + <div id="qunit-fixture1"></div> <div id="async-fixture" style="padding-left:300px"></div> </div> </body> diff --git a/test/mocks.js b/test/mocks.js index 1238ed5..2703cdd 100644 --- a/test/mocks.js +++ b/test/mocks.js @@ -36,8 +36,10 @@ $(function () { //groups as array window.groupsArr = []; + window.groupsArr2 = []; for(var i in groups) { groupsArr.push({value: i, text: groups[i]}); + groupsArr2.push({id: i, text: groups[i]}); } window.size = groupsArr.length; @@ -46,6 +48,16 @@ $(function () { url: 'groups.php', responseText: groups }); + + $.mockjax({ + url: 'groupsArr', + responseText: groupsArr + }); + + $.mockjax({ + url: 'groupsArr2', + responseText: groupsArr2 + }); $.mockjax({ url: 'groups-error.php', diff --git a/test/unit/select2.js b/test/unit/select2.js index 7065fe3..fcba99c 100644 --- a/test/unit/select2.js +++ b/test/unit/select2.js @@ -8,7 +8,7 @@ $(function () { } }); - asyncTest("data (single)", function () { + asyncTest("init-change-save (not multiple, local)", function () { var s = 2, text = 'text2', e = $('<a href="#" data-type="select2" data-name="select2" data-value="'+s+'"></a>').appendTo(fx).editable({ source: [{id: 1, text: 'text1'}, {id: s, text: text}, {id: 3, text: 'text3'}], @@ -49,7 +49,7 @@ $(function () { }, timeout); }); - asyncTest("data (multiple)", function () { + asyncTest("init-change-save (multiple, local)", function () { var s = '2,3', text = 'text2, text3', e = $('<a href="#" data-type="select2" data-name="select2" data-value="'+s+'"></a>').appendTo(fx).editable({ source: [{id: 1, text: 'text1'}, {id: 2, text: 'text2'}, {id: 3, text: 'text3'}], @@ -97,7 +97,7 @@ $(function () { }, timeout); }); - asyncTest("tags", function () { + asyncTest("tags (local)", function () { var s = 'text2,abc', text = 'text2, abc', e = $('<a href="#" data-type="select2" data-name="select2">'+text+'</a>').appendTo(fx).editable({ viewseparator: ', ', @@ -141,6 +141,84 @@ $(function () { start(); }, timeout); }); - + + test("setValue (local)", function () { + var e = $('<a href="#" data-type="select2" data-name="select2" data-value="1">test2</a>').appendTo('#qunit-fixture').editable({ + source: [{id: 1, text: 'text1'}, {id: 2, text: 'text2'}, {id: 3, text: 'text3'}] + }); + + //autotext + equal(e.data('editable').value, 1, 'initial value ok'); + + //setValue before open + e.editable('setValue', 2); + equal(e.data('editable').value, 2, 'value ok'); + equal(e.text(), 'text2', 'text ok'); + + //open + e.click(); + var p = tip(e); + p.find('.editable-cancel').click(); + + //setValue after hide + e.editable('setValue', 3); + equal(e.data('editable').value, 3, 'value ok'); + equal(e.text(), 'text3', 'text ok'); + }); + + asyncTest("init-change-save (not multiple, remote)", function () { + var s = 2, text = groups[s], + newVal = 0, newText = groups[newVal], + e = $('<a href="#" data-type="select2" data-name="select2" data-value="'+s+'">'+text+'</a>').appendTo(fx).editable({ + source: 'groupsArr2' + }); + + e.click(); + var p = tip(e); + + ok(p.is(':visible'), 'popover visible'); + equal(p.find('.select2-choice span').text(), text, 'selected text correct'); + + var $input = p.find('input[type="hidden"]'); + ok($input.length, 'input exists'); + ok($input.select2, 'select2 applied'); + equal($input.val(), e.data('editable').value, 'selected value correct'); + + //click to load items + p.find('.select2-choice').mousedown(); + + setTimeout(function() { + equal($('.select2-results li').length, groupsArr2.length, 'items loaded'); + equal($('.select2-results .select2-highlighted > .select2-result-label').text(), text, 'highlight ok'); + + //select new value (0) + $('.select2-results li').eq(newVal).mouseup(); + equal(p.find('.select2-choice span').text(), newText, 'new selected text ok'); + + //submit + p.find('form').submit(); + + setTimeout(function() { + ok(!p.is(':visible'), 'popover closed'); + equal(e.data('editable').value, newVal, 'new value ok'); + equal(e.text(), newText, 'new text ok'); + + //open again + e.click(); + p = tip(e); + equal(p.find('.select2-choice span').text(), newText, 'text ok on second open'); + equal($input.val(), newVal, 'selected value ok on second open'); + + //setValue in closed state + p.find('.editable-cancel').click(); + e.editable('setValue', 1); + equal(e.data('editable').value, 1, 'setValue: value ok'); + equal(e.text(), groups[1], 'setValue: text ok'); + + e.remove(); + start(); + }, timeout); + }, timeout); + }); }); \ No newline at end of file