vendor/assets/javascripts/editable/jqueryui-editable.js in x-editable-rails-1.0.1 vs vendor/assets/javascripts/editable/jqueryui-editable.js in x-editable-rails-1.0.2

- old
+ new

@@ -1,6 +1,6 @@ -/*! X-editable - v1.4.4 +/*! X-editable - v1.4.5 * In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery * http://github.com/vitalets/x-editable * Copyright (c) 2013 Vitaliy Potapov; Licensed MIT */ /** @@ -63,10 +63,14 @@ } //show loading state this.showLoading(); + //flag showing is form now saving value to server. + //It is needed to wait when closing form. + this.isSaving = false; + /** Fired when rendering starts @event rendering @param {Object} event event object **/ @@ -215,75 +219,87 @@ **/ this.$div.triggerHandler('nochange'); return; } + //convert value for submitting to server + var submitValue = this.input.value2submit(newValue); + + this.isSaving = true; + //sending data to server - $.when(this.save(newValue)) + $.when(this.save(submitValue)) .done($.proxy(function(response) { + this.isSaving = false; + //run success callback var res = typeof this.options.success === 'function' ? this.options.success.call(this.options.scope, response, newValue) : null; - + //if success callback returns false --> keep form open and do not activate input if(res === false) { this.error(false); this.showForm(false); return; - } - + } + //if success callback returns string --> keep form open, show error and activate input if(typeof res === 'string') { this.error(res); this.showForm(); return; - } - + } + //if success callback returns object like {newValue: <something>} --> use that value instead of submitted //it is usefull if you want to chnage value in url-function if(res && typeof res === 'object' && res.hasOwnProperty('newValue')) { newValue = res.newValue; - } + } //clear error message this.error(false); this.value = newValue; /** Fired when form is submitted @event save @param {Object} event event object @param {Object} params additional params - @param {mixed} params.newValue submitted value + @param {mixed} params.newValue raw new value + @param {mixed} params.submitValue submitted value as string @param {Object} params.response ajax response @example $('#form-div').on('save'), function(e, params){ if(params.newValue === 'username') {...} - }); - **/ - this.$div.triggerHandler('save', {newValue: newValue, response: response}); + }); + **/ + this.$div.triggerHandler('save', {newValue: newValue, submitValue: submitValue, response: response}); }, this)) .fail($.proxy(function(xhr) { + this.isSaving = false; + var msg; if(typeof this.options.error === 'function') { msg = this.options.error.call(this.options.scope, xhr, newValue); } else { msg = typeof xhr === 'string' ? xhr : xhr.responseText || xhr.statusText || 'Unknown error!'; } - + this.error(msg); this.showForm(); }, this)); }, - save: function(newValue) { - //convert value for submitting to server - var submitValue = this.input.value2submit(newValue); - + save: function(submitValue) { //try parse composite pk defined as json string in data-pk this.options.pk = $.fn.editableutils.tryParseJson(this.options.pk, true); var pk = (typeof this.options.pk === 'function') ? this.options.pk.call(this.options.scope) : this.options.pk, + /* + send on server in following cases: + 1. url is function + 2. url is string AND (pk defined OR send option = always) + */ send = !!(typeof this.options.url === 'function' || (this.options.url && ((this.options.send === 'always') || (this.options.send === 'auto' && pk !== null && pk !== undefined)))), params; if (send) { //send to server this.showLoading(); @@ -814,10 +830,31 @@ return input; } else { $.error('Unknown type: '+ type); return false; } + }, + + //see http://stackoverflow.com/questions/7264899/detect-css-transitions-using-javascript-and-without-modernizr + supportsTransitions: function () { + var b = document.body || document.documentElement, + s = b.style, + p = 'transition', + v = ['Moz', 'Webkit', 'Khtml', 'O', 'ms']; + + if(typeof s[p] === 'string') { + return true; + } + + // Tests for vendor specific prop + p = p.charAt(0).toUpperCase() + p.substr(1); + for(var i=0; i<v.length; i++) { + if(typeof s[v[i] + p] === 'string') { + return true; + } + } + return false; } }; }(window.jQuery)); @@ -854,10 +891,13 @@ //set scope of form callbacks to element this.formOptions.scope = this.$element[0]; this.initContainer(); + + //flag to hide container, when saving value will finish + this.delayedHide = false; //bind 'destroyed' listener to destroy container when element is removed from dom this.$element.on('destroyed', $.proxy(function(){ this.destroy(); }, this)); @@ -958,11 +998,18 @@ .editableform(this.formOptions) .on({ save: $.proxy(this.save, this), //click on submit button (value changed) nochange: $.proxy(function(){ this.hide('nochange'); }, this), //click on submit button (value NOT changed) cancel: $.proxy(function(){ this.hide('cancel'); }, this), //click on calcel button - show: $.proxy(this.setPosition, this), //re-position container every time form is shown (occurs each time after loading state) + show: $.proxy(function() { + if(this.delayedHide) { + this.hide(this.delayedHide.reason); + this.delayedHide = false; + } else { + this.setPosition(); + } + }, this), //re-position container every time form is shown (occurs each time after loading state) rendering: $.proxy(this.setPosition, this), //this allows to place container correctly when loading shown resize: $.proxy(this.setPosition, this), //this allows to re-position container when form size is changed rendered: $.proxy(function(){ /** Fired when container is shown and form is rendered (for select will wait for loading dropdown options). @@ -1002,15 +1049,15 @@ this.innerShow(); this.tip().addClass(this.containerClass); /* Currently, form is re-rendered on every show. - The main reason is that we dont know, what container will do with content when closed: - remove(), detach() or just hide(). + The main reason is that we dont know, what will container do with content when closed: + remove(), detach() or just hide() - it depends on container. Detaching form itself before hide and re-insert before show is good solution, - but visually it looks ugly, as container changes size before hide. + but visually it looks ugly --> container changes size before hide. */ //if form already exist - delete previous data if(this.$form) { //todo: destroy prev data! @@ -1039,14 +1086,22 @@ hide: function(reason) { if(!this.tip() || !this.tip().is(':visible') || !this.$element.hasClass('editable-open')) { return; } + //if form is saving value, schedule hide + if(this.$form.data('editableform').isSaving) { + this.delayedHide = {reason: reason}; + return; + } else { + this.delayedHide = false; + } + this.$element.removeClass('editable-open'); this.innerHide(); - - /** + + /** Fired when container was hidden. It occurs on both save or cancel. **Note:** Bootstrap popover has own `hidden` event that now cannot be separated from x-editable's one. The workaround is to check `arguments.length` that is always `2` for x-editable. @event hidden @@ -1056,24 +1111,24 @@ $('#username').on('hidden', function(e, reason) { if(reason === 'save' || reason === 'cancel') { //auto-open next editable $(this).closest('tr').next().find('.editable').editable('show'); } - }); - **/ + }); + **/ this.$element.triggerHandler('hidden', reason || 'manual'); }, - + /* internal show method. To be overwritten in child classes */ innerShow: function () { }, - + /* internal hide method. To be overwritten in child classes */ innerHide: function () { - - }, + + }, /** Toggles container visibility (show / hide) @method toggle() @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. @@ -1114,11 +1169,11 @@ } }); **/ this.$element.triggerHandler('save', params); - //hide must be after trigger, as saving value may require methods od plugin, applied to input + //hide must be after trigger, as saving value may require methods of plugin, applied to input this.hide('save'); }, /** Sets new option @@ -1274,11 +1329,11 @@ @since 1.1.1 **/ onblur: 'cancel', /** - Animation speed (inline mode) + Animation speed (inline mode only) @property anim @type string @default false **/ anim: false, @@ -1378,10 +1433,15 @@ if(this.options.selector) { this.initLive(); } else { this.init(); } + + //check for transition support + if(this.options.highlight && !$.fn.editableutils.supportsTransitions()) { + this.options.highlight = false; + } }; Editable.prototype = { constructor: Editable, init: function () { @@ -1422,29 +1482,37 @@ //attach handler activating editable. In disabled mode it just prevent default action (useful for links) if(this.options.toggle !== 'manual') { this.$element.addClass('editable-click'); this.$element.on(this.options.toggle + '.editable', $.proxy(function(e){ - //prevent following link - e.preventDefault(); + //prevent following link if editable enabled + if(!this.options.disabled) { + e.preventDefault(); + } //stop propagation not required because in document click handler it checks event target //e.stopPropagation(); if(this.options.toggle === 'mouseenter') { //for hover only show container - this.show(); + this.show(); } else { //when toggle='click' we should not close all other containers as they will be closed automatically in document click listener var closeAll = (this.options.toggle !== 'click'); this.toggle(closeAll); - } + } }, this)); } else { this.$element.attr('tabindex', -1); //do not stop focus on element when toggled manually } + //if display is function it's far more convinient to have autotext = always to render correctly on init + //see https://github.com/vitalets/x-editable-yii/issues/34 + if(typeof this.options.display === 'function') { + this.options.autotext = 'always'; + } + //check conditions for autotext: switch(this.options.autotext) { case 'always': doAutotext = true; break; @@ -1619,16 +1687,33 @@ //do not handle empty if we do not display anything if(this.options.display === false) { return; } - this.isEmpty = isEmpty !== undefined ? isEmpty : $.trim(this.$element.text()) === ''; + /* + isEmpty may be set directly as param of method. + It is required when we enable/disable field and can't rely on content + as node content is text: "Empty" that is not empty %) + */ + if(isEmpty !== undefined) { + this.isEmpty = isEmpty; + } else { + //detect empty + if($.trim(this.$element.html()) === '') { + this.isEmpty = true; + } else if($.trim(this.$element.text()) !== '') { + this.isEmpty = false; + } else { + //e.g. '<img>' + this.isEmpty = !this.$element.height() || !this.$element.width(); + } + } //emptytext shown only for enabled if(!this.options.disabled) { if (this.isEmpty) { - this.$element.text(this.options.emptytext); + this.$element.html(this.options.emptytext); if(this.options.emptyclass) { this.$element.addClass(this.options.emptyclass); } } else if(this.options.emptyclass) { this.$element.removeClass(this.options.emptyclass); @@ -1719,10 +1804,25 @@ } else { this.$element.addClass(this.options.unsavedclass); } } + //highlight when saving + if(this.options.highlight) { + var $e = this.$element, + $bgColor = $e.css('background-color'); + + $e.css('background-color', this.options.highlight); + setTimeout(function(){ + $e.css('background-color', $bgColor); + $e.addClass('editable-bg-transition'); + setTimeout(function(){ + $e.removeClass('editable-bg-transition'); + }, 1700); + }, 0); + } + //set new value this.setValue(params.newValue, false, params.response); /** Fired when new value was submitted. You can use <code>$(this).data('editable')</code> to access to editable instance @@ -1785,10 +1885,12 @@ this.disable(); if(this.container) { this.container.destroy(); } + + this.input.destroy(); if(this.options.toggle !== 'manual') { this.$element.removeClass('editable-click'); this.$element.off(this.options.toggle + '.editable'); } @@ -1842,32 +1944,41 @@ return result; /** Returns current values of editable elements. Note that it returns an **object** with name-value pairs, not a value itself. It allows to get data from several elements. - If value of some editable is `null` or `undefined` it is excluded from result object. + If value of some editable is `null` or `undefined` it is excluded from result object. + When param `isSingle` is set to **true** - it is supposed you have single element and will return value of editable instead of object. @method getValue() + @param {bool} isSingle whether to return just value of single element @returns {Object} object of element names and values @example $('#username, #fullname').editable('getValue'); - // possible result: + //result: { username: "superuser", fullname: "John" } + //isSingle = true + $('#username').editable('getValue', true); + //result "superuser" **/ case 'getValue': - this.each(function () { - var $this = $(this), data = $this.data(datakey); - if (data && data.value !== undefined && data.value !== null) { - result[data.options.name] = data.input.value2submit(data.value); - } - }); + if(arguments.length === 2 && arguments[1] === true) { //isSingle = true + result = this.eq(0).data(datakey).value; + } else { + this.each(function () { + var $this = $(this), data = $this.data(datakey); + if (data && data.value !== undefined && data.value !== null) { + result[data.options.name] = data.input.value2submit(data.value); + } + }); + } 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. @method submit(options) @@ -2087,11 +2198,20 @@ url: '/post', pk: 1 }); </script> **/ - selector: null + selector: null, + /** + Color used to highlight element after update. Implemented via CSS3 transition, works in modern browsers. + + @property highlight + @type string|boolean + @since 1.4.5 + @default #FFFF80 + **/ + highlight: '#FFFF80' }; }(window.jQuery)); /** @@ -2101,27 +2221,27 @@ @class abstractinput **/ (function ($) { "use strict"; - + //types $.fn.editabletypes = {}; - + var AbstractInput = function () { }; AbstractInput.prototype = { /** Initializes input - + @method init() **/ init: function(type, options, defaults) { this.type = type; this.options = $.extend({}, defaults, options); }, - + /* this method called before render to init $tpl that is inserted in DOM */ prerender: function() { this.$tpl = $(this.options.tpl); //whole tpl as jquery object @@ -2131,132 +2251,138 @@ }, /** Renders input from tpl. Can return jQuery deferred object. Can be overwritten in child objects - - @method render() - **/ + + @method render() + **/ render: function() { }, /** Sets element's html by value. - - @method value2html(value, element) + + @method value2html(value, element) @param {mixed} value @param {DOMElement} element - **/ + **/ value2html: function(value, element) { - $(element).text(value); + $(element).text($.trim(value)); }, - + /** Converts element's html to value - - @method html2value(html) + + @method html2value(html) @param {string} html @returns {mixed} - **/ + **/ html2value: function(html) { return $('<div>').html(html).text(); }, - + /** Converts value to string (for internal compare). For submitting to server used value2submit(). - + @method value2str(value) @param {mixed} value @returns {string} - **/ + **/ value2str: function(value) { return value; }, - + /** Converts string received from server into value. Usually from `data-value` attribute. - - @method str2value(str) + + @method str2value(str) @param {string} str @returns {mixed} - **/ + **/ str2value: function(str) { return str; }, /** Converts value for submitting to server. Result can be string or object. - + @method value2submit(value) @param {mixed} value @returns {mixed} - **/ + **/ value2submit: function(value) { return value; - }, - + }, + /** Sets value of input. - + @method value2input(value) @param {mixed} value - **/ + **/ value2input: function(value) { this.$input.val(value); }, - + /** Returns value of input. Value can be object (e.g. datepicker) - + @method input2value() - **/ + **/ input2value: function() { return this.$input.val(); }, /** Activates input. For text it sets focus. - + @method activate() - **/ + **/ activate: function() { if(this.$input.is(':visible')) { this.$input.focus(); } }, - + /** Creates input. - + @method clear() **/ clear: function() { this.$input.val(null); }, - + /** method to escape html. **/ escape: function(str) { return $('<div>').text(str).html(); }, /** attach handler to automatically submit form when value changed (useful when buttons not shown) - **/ + **/ autosubmit: function() { }, + /** + Additional actions when destroying element + **/ + destroy: function() { + }, + // -------- helper functions -------- setClass: function() { if(this.options.inputclass) { this.$input.addClass(this.options.inputclass); } }, - + setAttr: function(attr) { if (this.options[attr] !== undefined && this.options[attr] !== null) { this.$input.attr(attr, this.options[attr]); } }, @@ -2354,34 +2480,37 @@ }, // ------------- additional functions ------------ onSourceReady: function (success, error) { + //run source if it function + var source; + if ($.isFunction(this.options.source)) { + source = this.options.source.call(this.options.scope); + this.sourceData = null; + //note: if function returns the same source as URL - sourceData will be taken from cahce and no extra request performed + } else { + source = this.options.source; + } + //if allready loaded just call success - if($.isArray(this.sourceData)) { + if(this.options.sourceCache && $.isArray(this.sourceData)) { success.call(this); return; } - // try parse json in single quotes (for double quotes jquery does automatically) + //try parse json in single quotes (for double quotes jquery does automatically) try { - this.options.source = $.fn.editableutils.tryParseJson(this.options.source, false); + source = $.fn.editableutils.tryParseJson(source, false); } catch (e) { error.call(this); return; } - - var source = this.options.source; - - //run source if it function - if ($.isFunction(source)) { - source = source.call(this.options.scope); - } //loading from url if (typeof source === 'string') { - //try to get from cache + //try to get sourceData from cache if(this.options.sourceCache) { var cacheID = source, cache; if (!$(document).data(cacheID)) { @@ -3297,29 +3426,33 @@ }); $.fn.editabletypes.range = Range; }(window.jQuery)); /** Select2 input. Based on amazing work of Igor Vaynberg https://github.com/ivaynberg/select2. -Please see [original docs](http://ivaynberg.github.com/select2) for detailed description and options. -You should manually include select2 distributive: +Please see [original select2 docs](http://ivaynberg.github.com/select2) for detailed description and options. +Compatible **select2 version is 3.4.1**! +You should manually download and 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: + + <a href="#" data-type="select2" data-value="1">Text1</a> + @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({ source: [ {id: 'gb', text: 'Great Britain'}, @@ -3336,82 +3469,69 @@ (function ($) { "use strict"; var Constructor = function (options) { this.init('select2', options, Constructor.defaults); - + options.select2 = options.select2 || {}; + + this.sourceData = null; - var that = this, - mixin = { //mixin to select2 options - placeholder: options.placeholder - }; + //placeholder + if(options.placeholder) { + options.select2.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; - } + //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); + } - //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); - } - - // $(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 { + //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, 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); $.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) { this.$input.on('change', function() { $(this).closest('form').parent().triggerHandler('resize'); @@ -3419,24 +3539,20 @@ } }, 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 = []; $.each(data, function(k, v){ text.push(v && typeof v === 'object' ? v.text : v); @@ -3453,11 +3569,30 @@ html2value: function(html) { return this.options.select2.tags ? this.str2value(html, this.options.viewseparator) : null; }, 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() { return this.$input.select2('val'); }, @@ -3486,10 +3621,26 @@ this.$input.on('change', function(e, isInitial){ if(!isInitial) { $(this).closest('form').submit(); } }); + }, + + /* + Converts source from x-editable format: {value: 1, text: "1"} to + select2 format: {id: 1, text: "1"} + */ + convertSource: function(source) { + if($.isArray(source) && source.length && source[0].value !== undefined) { + for(var i = 0; i<source.length; i++) { + if(source[i].value !== undefined) { + source[i].id = source[i].value; + delete source[i].value; + } + } + } + return source; } }); Constructor.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { @@ -3537,16 +3688,26 @@ $.fn.editabletypes.select2 = Constructor; }(window.jQuery)); /** -* Combodate - 1.0.3 +* Combodate - 1.0.4 * 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 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) +* +* Differs from momentjs parse rules: +* 00:00 pm, 12:00 pm --> 12:00 (24-h format, day not change) +* 00:00 am, 12:00 am --> 00:00 (24-h format, day not change) +* +* * Author: Vitaliy Potapov * Project page: http://github.com/vitalets/combodate * Copyright (c) 2012 Vitaliy Potapov. Released under MIT License. **/ (function ($) { @@ -3692,13 +3853,14 @@ shortNames = this.options.template.indexOf('MMM') !== -1, twoDigit = this.options.template.indexOf('MM') !== -1; for(i=0; i<=11; i++) { if(longNames) { - name = moment().month(i).format('MMMM'); + //see https://github.com/timrwood/momentjs.com/pull/36 + name = moment().date(1).month(i).format('MMMM'); } else if(shortNames) { - name = moment().month(i).format('MMM'); + name = moment().date(1).month(i).format('MMM'); } else if(twoDigit) { name = this.leadZero(i+1); } else { name = i+1; } @@ -3730,13 +3892,14 @@ fillHour: function() { var items = this.initItems('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; - for(i=0; i<=max; i++) { + for(i=min; i<=max; i++) { name = twoDigit ? this.leadZero(i) : i; items.push([i, name]); } return items; }, @@ -3781,11 +3944,11 @@ ]; return items; }, /* - Returns current date value. + Returns current date value from combos. If format not specified - `options.format` used. If format = `null` - Moment object returned. */ getValue: function(format) { var dt, values = {}, @@ -3810,16 +3973,18 @@ //if at least one visible combo not selected - return empty string if(notSelected) { return ''; } - //convert hours if 12h format + //convert hours 12h --> 24h if(this.$ampm) { - values.hour = this.$ampm.val() === 'am' ? values.hour : values.hour+12; - if(values.hour === 24) { - values.hour = 0; - } + //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.hour = this.$ampm.val() === 'am' ? 0 : 12; + } else { + values.hour = this.$ampm.val() === 'am' ? values.hour : values.hour+12; + } } dt = moment([values.year, values.month, values.day, values.hour, values.minute, values.second]); //highlight invalid date @@ -3866,14 +4031,20 @@ } values[k] = dt[v[1]](); }); if(this.$ampm) { - if(values.hour > 12) { - values.hour -= 12; + //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'; + 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() \ No newline at end of file