app/assets/javascripts/bootstrap-editable.js in bootstrap-x-editable-rails-1.5.0 vs app/assets/javascripts/bootstrap-editable.js in bootstrap-x-editable-rails-1.5.1

- old
+ new

@@ -1,6 +1,6 @@ -/*! X-editable - v1.5.0 +/*! X-editable - v1.5.1 * In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery * http://github.com/vitalets/x-editable * Copyright (c) 2013 Vitaliy Potapov; Licensed MIT */ /** Form with single input element, two buttons and two states: normal/loading. @@ -183,11 +183,11 @@ $group.removeClass($.fn.editableform.errorGroupClass); $block.removeClass($.fn.editableform.errorBlockClass).empty().hide(); } else { //convert newline to <br> for more pretty error display if(msg) { - lines = msg.split("\n"); + lines = (''+msg).split('\n'); for (var i = 0; i < lines.length; i++) { lines[i] = $('<div>').text(lines[i]).html(); } msg = lines.join('<br>'); } @@ -198,15 +198,25 @@ submit: function(e) { e.stopPropagation(); e.preventDefault(); - var error, - newValue = this.input.input2value(); //get new value from input + //get new value from input + var newValue = this.input.input2value(); - //validation - if (error = this.validate(newValue)) { + //validation: if validate returns string or truthy value - means error + //if returns object like {newValue: '...'} => submitted value is reassigned to it + var error = this.validate(newValue); + if ($.type(error) === 'object' && error.newValue !== undefined) { + newValue = error.newValue; + this.input.value2input(newValue); + if(typeof error.msg === 'string') { + this.error(error.msg); + this.showForm(); + return; + } + } else if (error) { this.error(error); this.showForm(); return; } @@ -501,10 +511,12 @@ @default 'auto' **/ send: 'auto', /** Function for client-side validation. If returns string - means validation not passed and string showed as error. + Since 1.5.1 you can modify submitted value by returning object from `validate`: + `{newValue: '...'}` or `{newValue: '...', msg: '...'}` @property validate @type function @default null @example @@ -2013,11 +2025,13 @@ return result; /** This method collects values from several editable elements and submit them all to server. Internally it runs client-side validation for all fields and submits only in case of success. - See <a href="#newrecord">creating new records</a> for details. + See <a href="#newrecord">creating new records</a> for details. + Since 1.5.1 `submit` can be applied to single element to send data programmatically. In that case + `url`, `success` and `error` is taken from initial options and you can just call `$('#username').editable('submit')`. @method submit(options) @param {object} options @param {object} options.url url to submit data @param {object} options.data additional data to submit @@ -2027,35 +2041,80 @@ @returns {Object} jQuery object **/ case 'submit': //collects value, validate and submit to server for creating new record var config = arguments[1] || {}, $elems = this, - errors = this.editable('validate'), - values; + errors = this.editable('validate'); + // validation ok if($.isEmptyObject(errors)) { - values = this.editable('getValue'); - if(config.data) { - $.extend(values, config.data); + var ajaxOptions = {}; + + // for single element use url, success etc from options + if($elems.length === 1) { + var editable = $elems.data('editable'); + //standard params + var params = { + name: editable.options.name || '', + value: editable.input.value2submit(editable.value), + pk: (typeof editable.options.pk === 'function') ? + editable.options.pk.call(editable.options.scope) : + editable.options.pk + }; + + //additional params + if(typeof editable.options.params === 'function') { + params = editable.options.params.call(editable.options.scope, params); + } else { + //try parse json in single quotes (from data-params attribute) + editable.options.params = $.fn.editableutils.tryParseJson(editable.options.params, true); + $.extend(params, editable.options.params); + } + + ajaxOptions = { + url: editable.options.url, + data: params, + type: 'POST' + }; + + // use success / error from options + config.success = config.success || editable.options.success; + config.error = config.error || editable.options.error; + + // multiple elements + } else { + var values = this.editable('getValue'); + + ajaxOptions = { + url: config.url, + data: values, + type: 'POST' + }; } - - $.ajax($.extend({ - url: config.url, - data: values, - type: 'POST' - }, config.ajaxOptions)) - .success(function(response) { - //successful response 200 OK - if(typeof config.success === 'function') { + + // ajax success callabck (response 200 OK) + ajaxOptions.success = typeof config.success === 'function' ? function(response) { config.success.call($elems, response, config); - } - }) - .error(function(){ //ajax error - if(typeof config.error === 'function') { - config.error.apply($elems, arguments); - } - }); + } : $.noop; + + // ajax error callabck + ajaxOptions.error = typeof config.error === 'function' ? function() { + config.error.apply($elems, arguments); + } : $.noop; + + // extend ajaxOptions + if(config.ajaxOptions) { + $.extend(ajaxOptions, config.ajaxOptions); + } + + // extra data + if(config.data) { + $.extend(ajaxOptions.data, config.data); + } + + // perform ajax request + $.ajax(ajaxOptions); } else { //client-side validation error if(typeof config.error === 'function') { config.error.call($elems, errors); } } @@ -3573,11 +3632,15 @@ multiple: true } }); //remote source (simple) $('#country').editable({ - source: '/getCountries' + source: '/getCountries', + select2: { + placeholder: 'Select Country', + minimumInputLength: 1 + } }); //remote source (advanced) $('#country').editable({ select2: { placeholder: 'Select Country', @@ -3649,31 +3712,31 @@ //check format and convert x-editable format to select2 format (if needed) this.sourceData = this.convertSource(source); options.select2.data = this.sourceData; } } - + //overriding objects in config (as by default jQuery extend() is not recursive) 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); - + //store function returning ID of item //should be here as used inautotext for local source this.idFunc = this.options.select2.id; if (typeof(this.idFunc) !== "function") { var idKey = this.idFunc || 'id'; this.idFunc = function (e) { return e[idKey]; }; } - + //store function that renders text in select2 - this.formatSelection = this.options.select2.formatSelection; + this.formatSelection = this.options.select2.formatSelection; if (typeof(this.formatSelection) !== "function") { this.formatSelection = function (e) { return e.text; }; - } + } }; $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.abstractinput); $.extend(Constructor.prototype, { @@ -3691,81 +3754,87 @@ 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 + //trigger resize of editableform to re-position container in multi-valued mode if(this.isMultiple) { this.$input.on('change', function() { $(this).closest('form').parent().triggerHandler('resize'); - }); + }); } }, - + value2html: function(value, element) { var text = '', data, that = this; - + if(this.options.select2.tags) { //in tags mode just assign value data = value; - //data = $.fn.editableutils.itemsByValue(value, this.options.select2.tags, this.idFunc); + //data = $.fn.editableutils.itemsByValue(value, this.options.select2.tags, this.idFunc); } else if(this.sourceData) { data = $.fn.editableutils.itemsByValue(value, this.sourceData, this.idFunc); } else { //can not get list of possible values - //(e.g. autotext for select2 with ajax source) + //(e.g. autotext for select2 with ajax source) } - - //data may be array (when multiple values allowed) + + //data may be array (when multiple values allowed) if($.isArray(data)) { //collect selected data and show with separator text = []; $.each(data, function(k, v){ - text.push(v && typeof v === 'object' ? that.formatSelection(v) : v); - }); + text.push(v && typeof v === 'object' ? that.formatSelection(v) : v); + }); } else if(data) { - text = that.formatSelection(data); + text = that.formatSelection(data); } text = $.isArray(text) ? text.join(this.options.viewseparator) : text; //$(element).text(text); Constructor.superclass.value2html.call(this, text, element); - }, - + }, + html2value: function(html) { return this.options.select2.tags ? this.str2value(html, this.options.viewseparator) : null; - }, - + }, + value2input: function(value) { - //for local source use data directly from source (to allow autotext) - /* - if(!this.isRemote && !this.isMultiple) { - var items = $.fn.editableutils.itemsByValue(value, this.sourceData, this.idFunc); - if(items.length) { - this.$input.select2('data', items[0]); - return; - } - } - */ - + // if value array => join it anyway + if($.isArray(value)) { + value = value.join(this.getSeparator()); + } + //for remote source just set value, text is updated by initSelection if(!this.$input.data('select2')) { this.$input.val(value); this.$input.select2(this.options.select2); } else { //second argument needed to separate initial change from user's click (for autosubmit) this.$input.val(value).trigger('change', true); + + //Uncaught Error: cannot call val() if initSelection() is not defined + //this.$input.select2('val', value); } - - //if remote source AND no user's initSelection provided --> try to use element's text + + // if defined remote source AND no multiple mode AND no user's initSelection provided --> + // we should somehow get text for provided id. + // The solution is to use element's text as text for that id (exclude empty) if(this.isRemote && !this.isMultiple && !this.options.select2.initSelection) { + // customId and customText are methods to extract `id` and `text` from data object + // we can use this workaround only if user did not define these methods + // otherwise we cant construct data object var customId = this.options.select2.id, customText = this.options.select2.formatSelection; - if(!customId && !customText) { - var data = {id: value, text: $(this.options.scope).text()}; - this.$input.select2('data', data); + + if(!customId && !customText) { + var $el = $(this.options.scope); + if (!$el.data('editable').isEmpty) { + var data = {id: value, text: $el.text()}; + this.$input.select2('data', data); + } } } }, input2value: function() { @@ -3774,34 +3843,38 @@ str2value: function(str, separator) { if(typeof str !== 'string' || !this.isMultiple) { return str; } - - separator = separator || this.options.select2.separator || $.fn.select2.defaults.separator; - + + separator = separator || this.getSeparator(); + var val, i, l; - + if (str === null || str.length < 1) { return null; } val = str.split(separator); for (i = 0, l = val.length; i < l; i = i + 1) { val[i] = $.trim(val[i]); } - + return val; }, - + autosubmit: function() { this.$input.on('change', function(e, isInitial){ if(!isInitial) { $(this).closest('form').submit(); } }); }, - + + getSeparator: function() { + return this.options.select2.separator || $.fn.select2.defaults.separator; + }, + /* Converts source from x-editable format: {value: 1, text: "1"} to select2 format: {id: 1, text: "1"} */ convertSource: function(source) { @@ -3811,30 +3884,30 @@ source[i].id = source[i].value; delete source[i].value; } } } - return source; + return source; }, destroy: function() { if(this.$input.data('select2')) { this.$input.select2('destroy'); } - } + } - }); + }); Constructor.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { /** @property tpl @default <input type="hidden"> - **/ + **/ tpl:'<input type="hidden">', /** Configuration of select2. [Full list of options](http://ivaynberg.github.com/select2). - + @property select2 @type object @default null **/ select2: null, @@ -3842,42 +3915,42 @@ Placeholder attribute of select @property placeholder @type string @default null - **/ + **/ placeholder: null, /** Source data for select. It will be assigned to select2 `data` property and kept here just for convenience. Please note, that format is different from simple `select` input: use 'id' instead of 'value'. - E.g. `[{id: 1, text: "text1"}, {id: 2, text: "text2"}, ...]`. - + E.g. `[{id: 1, text: "text1"}, {id: 2, text: "text2"}, ...]`. + @property source @type array|string|function @default null **/ source: null, /** - Separator used to display tags. - + Separator used to display tags. + @property viewseparator @type string @default ', ' **/ viewseparator: ', ' }); - $.fn.editabletypes.select2 = Constructor; - + $.fn.editabletypes.select2 = Constructor; + }(window.jQuery)); /** -* Combodate - 1.0.4 +* Combodate - 1.0.5 * Dropdown date and time picker. * Converts text input into dropdowns to pick day, month, year, hour, minute and second. * Uses momentjs as datetime library http://momentjs.com. -* For internalization include corresponding file from https://github.com/timrwood/moment/tree/master/lang +* For i18n include corresponding file from https://github.com/timrwood/moment/tree/master/lang * * Confusion at noon and midnight - see http://en.wikipedia.org/wiki/12-hour_clock#Confusion_at_noon_and_midnight * In combodate: * 12:00 pm --> 12:00 (24-h format, midday) * 12:00 am --> 00:00 (24-h format, midnight, start of day) @@ -3920,20 +3993,26 @@ this.$widget = $('<span class="combodate"></span>').html(this.getTemplate()); this.initCombos(); //update original input on change - this.$widget.on('change', 'select', $.proxy(function(){ - this.$element.val(this.getValue()); + this.$widget.on('change', 'select', $.proxy(function(e) { + this.$element.val(this.getValue()).change(); + // update days count if month or year changes + if (this.options.smartDays) { + if ($(e.target).is('.month') || $(e.target).is('.year')) { + this.fillCombo('day'); + } + } }, this)); this.$widget.find('select').css('width', 'auto'); - //hide original input and insert widget + // hide original input and insert widget this.$element.hide().after(this.$widget); - //set initial value + // set initial value this.setValue(this.$element.val() || this.options.value); }, /* Replace tokens in template with <select> elements @@ -3966,26 +4045,45 @@ /* Initialize combos that presents in template */ initCombos: function() { - var that = this; - $.each(this.map, function(k, v) { - var $c = that.$widget.find('.'+k), f, items; - if($c.length) { - that['$'+k] = $c; //set properties like this.$day, this.$month etc. - f = 'fill' + k.charAt(0).toUpperCase() + k.slice(1); //define method name to fill items, e.g `fillDays` - items = that[f](); - that['$'+k].html(that.renderItems(items)); - } - }); + for (var k in this.map) { + var $c = this.$widget.find('.'+k); + // set properties like this.$day, this.$month etc. + this['$'+k] = $c.length ? $c : null; + // fill with items + this.fillCombo(k); + } }, - + /* + Fill combo with items + */ + fillCombo: function(k) { + var $combo = this['$'+k]; + if (!$combo) { + return; + } + + // define method name to fill items, e.g `fillDays` + var f = 'fill' + k.charAt(0).toUpperCase() + k.slice(1); + var items = this[f](); + var value = $combo.val(); + + $combo.empty(); + for(var i=0; i<items.length; i++) { + $combo.append('<option value="'+items[i][0]+'">'+items[i][1]+'</option>'); + } + + $combo.val(value); + }, + + /* Initialize items of combos. Handles `firstItem` option */ - initItems: function(key) { + fillCommon: function(key) { var values = [], relTime; if(this.options.firstItem === 'name') { //need both to support moment ver < 2 and >= 2 @@ -3996,42 +4094,44 @@ values.push(['', header]); } else if(this.options.firstItem === 'empty') { values.push(['', '']); } return values; - }, - - /* - render items to string of <option> tags - */ - renderItems: function(items) { - var str = []; - for(var i=0; i<items.length; i++) { - str.push('<option value="'+items[i][0]+'">'+items[i][1]+'</option>'); - } - return str.join("\n"); - }, + }, + /* fill day */ fillDay: function() { - var items = this.initItems('d'), name, i, - twoDigit = this.options.template.indexOf('DD') !== -1; - - for(i=1; i<=31; i++) { + var items = this.fillCommon('d'), name, i, + twoDigit = this.options.template.indexOf('DD') !== -1, + daysCount = 31; + + // detect days count (depends on month and year) + // originally https://github.com/vitalets/combodate/pull/7 + if (this.options.smartDays && this.$month && this.$year) { + var month = parseInt(this.$month.val(), 10); + var year = parseInt(this.$year.val(), 10); + + if (!isNaN(month) && !isNaN(year)) { + daysCount = moment([year, month]).daysInMonth(); + } + } + + for (i = 1; i <= daysCount; i++) { name = twoDigit ? this.leadZero(i) : i; items.push([i, name]); } return items; }, /* fill month */ fillMonth: function() { - var items = this.initItems('M'), name, i, + var items = this.fillCommon('M'), name, i, longNames = this.options.template.indexOf('MMMM') !== -1, shortNames = this.options.template.indexOf('MMM') !== -1, twoDigit = this.options.template.indexOf('MM') !== -1; for(i=0; i<=11; i++) { @@ -4060,20 +4160,20 @@ for(i=this.options.maxYear; i>=this.options.minYear; i--) { name = longNames ? i : (i+'').substring(2); items[this.options.yearDescending ? 'push' : 'unshift']([i, name]); } - items = this.initItems('y').concat(items); + items = this.fillCommon('y').concat(items); return items; }, /* fill hour */ fillHour: function() { - var items = this.initItems('h'), name, i, + var items = this.fillCommon('h'), name, i, h12 = this.options.template.indexOf('h') !== -1, h24 = this.options.template.indexOf('H') !== -1, twoDigit = this.options.template.toLowerCase().indexOf('hh') !== -1, min = h12 ? 1 : 0, max = h12 ? 12 : 23; @@ -4087,11 +4187,11 @@ /* fill minute */ fillMinute: function() { - var items = this.initItems('m'), name, i, + var items = this.fillCommon('m'), name, i, twoDigit = this.options.template.indexOf('mm') !== -1; for(i=0; i<=59; i+= this.options.minuteStep) { name = twoDigit ? this.leadZero(i) : i; items.push([i, name]); @@ -4101,11 +4201,11 @@ /* fill second */ fillSecond: function() { - var items = this.initItems('s'), name, i, + var items = this.fillCommon('s'), name, i, twoDigit = this.options.template.indexOf('ss') !== -1; for(i=0; i<=59; i+= this.options.secondStep) { name = twoDigit ? this.leadZero(i) : i; items.push([i, name]); @@ -4123,11 +4223,11 @@ ['am', ampmL ? 'am' : 'AM'], ['pm', ampmL ? 'pm' : 'PM'] ]; return items; }, - + /* Returns current date value from combos. If format not specified - `options.format` used. If format = `null` - Moment object returned. */ @@ -4186,67 +4286,72 @@ var dt = typeof value === 'string' ? moment(value, this.options.format) : moment(value), that = this, values = {}; - //function to find nearest value in select options - function getNearest($select, value) { - var delta = {}; - $select.children('option').each(function(i, opt){ - var optValue = $(opt).attr('value'), - distance; + //function to find nearest value in select options + function getNearest($select, value) { + var delta = {}; + $select.children('option').each(function(i, opt){ + var optValue = $(opt).attr('value'), + distance; - if(optValue === '') return; - distance = Math.abs(optValue - value); - if(typeof delta.distance === 'undefined' || distance < delta.distance) { - delta = {value: optValue, distance: distance}; - } - }); - return delta.value; - } + if(optValue === '') return; + distance = Math.abs(optValue - value); + if(typeof delta.distance === 'undefined' || distance < delta.distance) { + delta = {value: optValue, distance: distance}; + } + }); + return delta.value; + } if(dt.isValid()) { - //read values from date object - $.each(this.map, function(k, v) { - if(k === 'ampm') { - return; - } - values[k] = dt[v[1]](); - }); + //read values from date object + $.each(this.map, function(k, v) { + if(k === 'ampm') { + return; + } + values[k] = dt[v[1]](); + }); - if(this.$ampm) { - //12:00 pm --> 12:00 (24-h format, midday), 12:00 am --> 00:00 (24-h format, midnight, start of day) - if(values.hour >= 12) { - values.ampm = 'pm'; - if(values.hour > 12) { - values.hour -= 12; - } - } else { - values.ampm = 'am'; - if(values.hour === 0) { - values.hour = 12; - } - } - } + if(this.$ampm) { + //12:00 pm --> 12:00 (24-h format, midday), 12:00 am --> 00:00 (24-h format, midnight, start of day) + if(values.hour >= 12) { + values.ampm = 'pm'; + if(values.hour > 12) { + values.hour -= 12; + } + } else { + values.ampm = 'am'; + if(values.hour === 0) { + values.hour = 12; + } + } + } - $.each(values, function(k, v) { - //call val() for each existing combo, e.g. this.$hour.val() - if(that['$'+k]) { + $.each(values, function(k, v) { + //call val() for each existing combo, e.g. this.$hour.val() + if(that['$'+k]) { - if(k === 'minute' && that.options.minuteStep > 1 && that.options.roundTime) { - v = getNearest(that['$'+k], v); - } + if(k === 'minute' && that.options.minuteStep > 1 && that.options.roundTime) { + v = getNearest(that['$'+k], v); + } - if(k === 'second' && that.options.secondStep > 1 && that.options.roundTime) { - v = getNearest(that['$'+k], v); - } + if(k === 'second' && that.options.secondStep > 1 && that.options.roundTime) { + v = getNearest(that['$'+k], v); + } - that['$'+k].val(v); - } - }); + that['$'+k].val(v); + } + }); + + // update days count + if (this.options.smartDays) { + this.fillCombo('day'); + } - this.$element.val(dt.format(this.options.format)); + this.$element.val(dt.format(this.options.format)).change(); } }, /* highlight combos if date is invalid @@ -4317,11 +4422,12 @@ yearDescending: true, minuteStep: 5, secondStep: 1, firstItem: 'empty', //'name', 'empty', 'none' errorClass: null, - roundTime: true //whether to round minutes and seconds if step > 1 + roundTime: true, // whether to round minutes and seconds if step > 1 + smartDays: false // whether days in combo depend on selected month: 31, 30, 28 }; }(window.jQuery)); /** Combodate input - dropdown date and time picker. @@ -6195,10 +6301,10 @@ @default null **/ viewformat: null, /** Configuration of datepicker. - Full list of options: http://vitalets.github.com/bootstrap-datepicker + Full list of options: http://bootstrap-datepicker.readthedocs.org/en/latest/options.html @property datepicker @type object @default { weekStart: 0, \ No newline at end of file