vendor/assets/javascripts/bootstrap-editable.js in bootstrap-x-editable-rails-1.4.3 vs vendor/assets/javascripts/bootstrap-editable.js in bootstrap-x-editable-rails-1.4.4

- old
+ new

@@ -1,6 +1,6 @@ -/*! X-editable - v1.4.3 +/*! X-editable - v1.4.4 * In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery * http://github.com/vitalets/x-editable * Copyright (c) 2013 Vitaliy Potapov; Licensed MIT */ /** @@ -11,11 +11,12 @@ @class editableform @uses text @uses textarea **/ (function ($) { - + "use strict"; + var EditableForm = function (div, options) { this.options = $.extend({}, $.fn.editableform.defaults, options); this.$div = $(div); //div, containing form. Not form tag. Not editable-element. if(!this.options.scope) { this.options.scope = this; @@ -35,11 +36,15 @@ }, initTemplate: function() { this.$form = $($.fn.editableform.template); }, initButtons: function() { - this.$form.find('.editable-buttons').append($.fn.editableform.buttons); + var $btn = this.$form.find('.editable-buttons'); + $btn.append($.fn.editableform.buttons); + if(this.options.showbuttons === 'bottom') { + $btn.addClass('editable-buttons-bottom'); + } }, /** Renders editableform @method render @@ -255,12 +260,19 @@ }); **/ this.$div.triggerHandler('save', {newValue: newValue, response: response}); }, this)) .fail($.proxy(function(xhr) { - this.error(typeof xhr === 'string' ? xhr : xhr.responseText || xhr.statusText || 'Unknown error!'); - this.showForm(); + 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 @@ -268,11 +280,11 @@ //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 = !!(typeof this.options.url === 'function' || (this.options.url && ((this.options.send === 'always') || (this.options.send === 'auto' && pk)))), + 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(); @@ -476,11 +488,11 @@ } **/ validate: null, /** Success callback. Called when value successfully sent on server and **response status = 200**. - Useful to work with json response. For example, if your backend response can be <code>{success: true}</code> + Usefull to work with json response. For example, if your backend response can be <code>{success: true}</code> or <code>{success: false, msg: "server error"}</code> you can check it inside this callback. If it returns **string** - means error occured and string is shown as error message. If it returns **object like** <code>{newValue: &lt;something&gt;}</code> - it overwrites value, submitted by user. Otherwise newValue simply rendered into element. @@ -492,10 +504,29 @@ if(!response.success) return response.msg; } **/ success: null, /** + Error callback. Called when request failed (response status != 200). + Usefull when you want to parse error response and display a custom message. + Must return **string** - the message to be displayed in the error block. + + @property error + @type function + @default null + @since 1.4.4 + @example + error: function(response, newValue) { + if(response.status === 500) { + return 'Service unavailable. Please try later.'; + } else { + return response.responseText; + } + } + **/ + error: null, + /** Additional options for submit ajax request. List of values: http://api.jquery.com/jQuery.ajax @property ajaxOptions @type object @@ -507,15 +538,15 @@ dataType: 'json' } **/ ajaxOptions: null, /** - Whether to show buttons or not. + Where to show buttons: left(true)|bottom|false Form without buttons is auto-submitted. @property showbuttons - @type boolean + @type boolean|string @default true @since 1.1.1 **/ showbuttons: true, /** @@ -535,11 +566,11 @@ @property savenochange @type boolean @default false @since 1.2.0 **/ - savenochange: false + savenochange: false }; /* Note: following params could redefined in engine: bootstrap or jqueryui: Classes 'control-group' and 'editable-error-block' must always present! @@ -562,14 +593,17 @@ $.fn.editableform.errorGroupClass = null; //error class attached to editable-error-block $.fn.editableform.errorBlockClass = 'editable-error'; }(window.jQuery)); + /** * EditableForm utilites */ (function ($) { + "use strict"; + //utils $.fn.editableutils = { /** * classic JS inheritance function */ @@ -759,10 +793,15 @@ //if type still `date` and not exist in types, replace with `combodate` that is base input if(type === 'date' && !$.fn.editabletypes.date) { type = 'combodate'; } } + + //`datetime` should be datetimefield in 'inline' mode + if(type === 'datetime' && options.mode === 'inline') { + type = 'datetimefield'; + } //change wysihtml5 to textarea for jquery UI and plain versions if(type === 'wysihtml5' && !$.fn.editabletypes[type]) { type = 'textarea'; } @@ -790,10 +829,11 @@ @class editableContainer @uses editableform **/ (function ($) { + "use strict"; var Popup = function (element, options) { this.init(element, options); }; @@ -803,10 +843,11 @@ //methods Popup.prototype = { containerName: null, //tbd in child class innerCss: null, //tbd in child class + containerClass: 'editable-container editable-popup', //css class applied to container element init: function(element, options) { this.$element = $(element); //since 1.4.1 container do not use data-* directly as they already merged into options. this.options = $.extend({}, $.fn.editableContainer.defaults, options); this.splitOptions(); @@ -835,14 +876,28 @@ //(mousedown could be better than click, it closes everything also on drag drop) $(document).on('click.editable', function(e) { var $target = $(e.target), i, exclude_classes = ['.editable-container', '.ui-datepicker-header', + '.datepicker', //in inline mode datepicker is rendered into body '.modal-backdrop', '.bootstrap-wysihtml5-insert-image-modal', - '.bootstrap-wysihtml5-insert-link-modal']; + '.bootstrap-wysihtml5-insert-link-modal' + ]; + //check if element is detached. It occurs when clicking in bootstrap datepicker + if (!$.contains(document.documentElement, e.target)) { + return; + } + + //for some reason FF 20 generates extra event (click) in select2 widget with e.target = document + //we need to filter it via construction below. See https://github.com/vitalets/x-editable/issues/199 + //Possibly related to http://stackoverflow.com/questions/10119793/why-does-firefox-react-differently-from-webkit-and-ie-to-click-event-on-selec + if($target.is(document)) { + return; + } + //if click inside one of exclude classes --> no nothing for(i=0; i<exclude_classes.length; i++) { if($target.is(exclude_classes[i]) || $target.parents(exclude_classes[i]).length) { return; } @@ -858,10 +913,15 @@ //split options on containerOptions and formOptions splitOptions: function() { this.containerOptions = {}; this.formOptions = {}; + + if(!$.fn[this.containerName]) { + throw new Error(this.containerName + ' not found. Have you included corresponding js file?'); + } + var cDef = $.fn[this.containerName].defaults; //keys defined in container defaults go to container, others go to form for(var k in this.options) { if(k in cDef) { this.containerOptions[k] = this.options[k]; @@ -882,10 +942,11 @@ /* returns container object */ container: function() { return this.$element.data(this.containerDataName || this.containerName); }, + /* call native method of underlying container, e.g. this.$element.popover('method') */ call: function() { this.$element[this.containerName].apply(this.$element, arguments); }, initContainer: function(){ @@ -902,21 +963,25 @@ show: $.proxy(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) + Fired when container is shown and form is rendered (for select will wait for loading dropdown options). + **Note:** Bootstrap popover has own `shown` 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 shown @param {Object} event event object @example - $('#username').on('shown', function() { - var editable = $(this).data('editable'); + $('#username').on('shown', function(e, editable) { editable.input.$input.val('overwriting value of input..'); }); **/ - this.$element.triggerHandler('shown'); + /* + TODO: added second param mainly to distinguish from bootstrap's shown event. It's a hotfix that will be solved in future versions via namespaced events. + */ + this.$element.triggerHandler('shown', this); }, this) }) .editableform('render'); }, @@ -933,11 +998,11 @@ this.closeOthers(this.$element[0]); } //show container itself this.innerShow(); - this.tip().addClass('editable-container'); + 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(). @@ -978,24 +1043,26 @@ this.$element.removeClass('editable-open'); this.innerHide(); /** - Fired when container was hidden. It occurs on both save or cancel. + 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 @param {object} event event object - @param {string} reason Reason caused hiding. Can be <code>save|cancel|onblur|nochange|undefined (=manual)</code> + @param {string} reason Reason caused hiding. Can be <code>save|cancel|onblur|nochange|manual</code> @example $('#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); + this.$element.triggerHandler('hidden', reason || 'manual'); }, /* internal show method. To be overwritten in child classes */ innerShow: function () { @@ -1244,20 +1311,22 @@ /** * Editable Inline * --------------------- */ (function ($) { + "use strict"; //copy prototype from EditableContainer //extend methods $.extend($.fn.editableContainer.Inline.prototype, $.fn.editableContainer.Popup.prototype, { containerName: 'editableform', innerCss: '.editable-inline', + containerClass: 'editable-container editable-inline', //css class applied to container element initContainer: function(){ //container is <span> element - this.$tip = $('<span></span>').addClass('editable-inline'); + this.$tip = $('<span></span>'); //convert anim to miliseconds (int) if(!this.options.anim) { this.options.anim = 0; } @@ -1298,10 +1367,11 @@ @class editable @uses editableContainer **/ (function ($) { + "use strict"; var Editable = function (element, options) { this.$element = $(element); //data-* has more priority over js options: because dynamically created elements may change data-* this.options = $.extend({}, $.fn.editable.defaults, options, $.fn.editableutils.getConfigData(this.$element)); @@ -1392,20 +1462,22 @@ this.disable(); } else { this.enable(); } /** - Fired when element was initialized by editable method. + Fired when element was initialized by `$().editable()` method. + Please note that you should setup `init` handler **before** applying `editable`. @event init @param {Object} event event object @param {Object} editable editable instance (as here it cannot accessed via data('editable')) @since 1.2.0 @example $('#username').on('init', function(e, editable) { alert('initialized ' + editable.options.name); }); + $('#username').editable(); **/ this.$element.triggerHandler('init', this); }, this)); }, @@ -1708,10 +1780,12 @@ /** Removes editable feature from element @method destroy() **/ destroy: function() { + this.disable(); + if(this.container) { this.container.destroy(); } if(this.options.toggle !== 'manual') { @@ -1719,12 +1793,11 @@ this.$element.off(this.options.toggle + '.editable'); } this.$element.off("save.internal"); - this.$element.removeClass('editable'); - this.$element.removeClass('editable-open'); + this.$element.removeClass('editable editable-open editable-disabled'); this.$element.removeData('editable'); } }; /* EDITABLE PLUGIN DEFINITION @@ -2027,11 +2100,12 @@ To create your own input you can inherit from this class. @class abstractinput **/ (function ($) { - + "use strict"; + //types $.fn.editabletypes = {}; var AbstractInput = function () { }; @@ -2210,11 +2284,14 @@ @default input-medium **/ inputclass: 'input-medium', //scope for external methods (e.g. source defined as function) //for internal use only - scope: null + scope: null, + + //need to re-declare showbuttons here to get it's value from common config (passed only options existing in defaults) + showbuttons: true }; $.extend($.fn.editabletypes, {abstractinput: AbstractInput}); }(window.jQuery)); @@ -2224,11 +2301,12 @@ @class list @extends abstractinput **/ (function ($) { - + "use strict"; + var List = function (options) { }; $.fn.editableutils.inherit(List, $.fn.editabletypes.abstractinput); @@ -2553,10 +2631,12 @@ }); }); </script> **/ (function ($) { + "use strict"; + var Text = function (options) { this.init('text', options, Text.defaults); }; $.fn.editableutils.inherit(Text, $.fn.editabletypes.abstractinput); @@ -2602,22 +2682,21 @@ this.$clear.click($.proxy(this.clear, this)); } }, postrender: function() { + /* + //now `clear` is positioned via css if(this.$clear) { //can position clear button only here, when form is shown and height can be calculated - var h = this.$input.outerHeight() || 20, +// var h = this.$input.outerHeight(true) || 20, + var h = this.$clear.parent().height(), delta = (h - this.$clear.height()) / 2; - - //workaround for plain-popup - if(delta < 3) { - delta = 3; - } - this.$clear.css({bottom: delta, right: delta}); - } + //this.$clear.css({bottom: delta, right: delta}); + } + */ }, //show / hide clear button toggleClear: function(e) { if(!this.$clear) { @@ -2688,11 +2767,12 @@ }); }); </script> **/ (function ($) { - + "use strict"; + var Textarea = function (options) { this.init('textarea', options, Textarea.defaults); }; $.fn.editableutils.inherit(Textarea, $.fn.editabletypes.abstractinput); @@ -2800,11 +2880,12 @@ }); }); </script> **/ (function ($) { - + "use strict"; + var Select = function (options) { this.init('select', options, Select.defaults); }; $.fn.editableutils.inherit(Select, $.fn.editabletypes.list); @@ -2889,11 +2970,12 @@ }); }); </script> **/ (function ($) { - + "use strict"; + var Checklist = function (options) { this.init('checklist', options, Checklist.defaults); }; $.fn.editableutils.inherit(Checklist, $.fn.editabletypes.list); @@ -2932,10 +3014,12 @@ if(typeof str === 'string' && str.length) { reg = new RegExp('\\s*'+$.trim(this.options.separator)+'\\s*'); value = str.split(reg); } else if($.isArray(str)) { value = str; + } else { + value = [str]; } return value; }, //set checked on required checkboxes @@ -3057,10 +3141,12 @@ /* Password */ (function ($) { + "use strict"; + var Password = function (options) { this.init('password', options, Password.defaults); }; $.fn.editableutils.inherit(Password, $.fn.editabletypes.text); $.extend(Password.prototype, { @@ -3086,10 +3172,12 @@ /* Email */ (function ($) { + "use strict"; + var Email = function (options) { this.init('email', options, Email.defaults); }; $.fn.editableutils.inherit(Email, $.fn.editabletypes.text); Email.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { @@ -3101,10 +3189,12 @@ /* Url */ (function ($) { + "use strict"; + var Url = function (options) { this.init('url', options, Url.defaults); }; $.fn.editableutils.inherit(Url, $.fn.editabletypes.text); Url.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { @@ -3116,10 +3206,12 @@ /* Tel */ (function ($) { + "use strict"; + var Tel = function (options) { this.init('tel', options, Tel.defaults); }; $.fn.editableutils.inherit(Tel, $.fn.editabletypes.text); Tel.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { @@ -3131,21 +3223,37 @@ /* Number */ (function ($) { + "use strict"; + var NumberInput = function (options) { this.init('number', options, NumberInput.defaults); }; $.fn.editableutils.inherit(NumberInput, $.fn.editabletypes.text); $.extend(NumberInput.prototype, { render: function () { NumberInput.superclass.render.call(this); this.setAttr('min'); this.setAttr('max'); this.setAttr('step'); - } + }, + postrender: function() { + if(this.$clear) { + //increase right ffset for up/down arrows + this.$clear.css({right: 24}); + /* + //can position clear button only here, when form is shown and height can be calculated + var h = this.$input.outerHeight(true) || 20, + delta = (h - this.$clear.height()) / 2; + + //add 12px to offset right for up/down arrows + this.$clear.css({top: delta, right: delta + 16}); + */ + } + } }); NumberInput.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { tpl: '<input type="number">', inputclass: 'input-mini', min: null, @@ -3158,10 +3266,12 @@ /* Range (inherit from number) */ (function ($) { + "use strict"; + var Range = function (options) { this.init('range', options, Range.defaults); }; $.fn.editableutils.inherit(Range, $.fn.editabletypes.number); $.extend(Range.prototype, { @@ -3193,10 +3303,14 @@ 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): + + <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. @class select2 @extends abstractinput @@ -3218,11 +3332,12 @@ }); }); </script> **/ (function ($) { - + "use strict"; + var Constructor = function (options) { this.init('select2', options, Constructor.defaults); options.select2 = options.select2 || {}; @@ -3420,12 +3535,13 @@ }); $.fn.editabletypes.select2 = Constructor; }(window.jQuery)); + /** -* Combodate - 1.0.2 +* Combodate - 1.0.3 * 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 * @@ -3528,11 +3644,11 @@ initItems: function(key) { var values = [], relTime; if(this.options.firstItem === 'name') { - //need both to suuport moment ver < 2 and >= 2 + //need both to support moment ver < 2 and >= 2 relTime = moment.relativeTime || moment.langData()._relativeTime; var header = typeof relTime[key] === 'function' ? relTime[key](1, true, key, false) : relTime[key]; //take last entry (see momentjs lang files structure) header = header.split(' ').reverse()[0]; values.push(['', header]); @@ -3593,17 +3709,20 @@ /* fill year */ fillYear: function() { - var items = this.initItems('y'), name, i, + var items = [], name, i, longNames = this.options.template.indexOf('YYYY') !== -1; - + for(i=this.options.maxYear; i>=this.options.minYear; i--) { name = longNames ? i : (i+'').substring(2); - items.push([i, name]); - } + items[this.options.yearDescending ? 'push' : 'unshift']([i, name]); + } + + items = this.initItems('y').concat(items); + return items; }, /* fill hour @@ -3721,10 +3840,26 @@ 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; + + 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; @@ -3740,11 +3875,21 @@ values.ampm = 'am'; } } $.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 === 'second' && that.options.secondStep > 1 && that.options.roundTime) { + v = getNearest(that['$'+k], v); + } + that['$'+k].val(v); } }); this.$element.val(dt.format(this.options.format)); @@ -3815,14 +3960,16 @@ template: 'D / MMM / YYYY H : mm', //initial value, can be `new Date()` value: null, minYear: 1970, maxYear: 2015, + yearDescending: true, minuteStep: 5, secondStep: 1, firstItem: 'empty', //'name', 'empty', 'none' - errorClass: null + errorClass: null, + roundTime: true //whether to round minutes and seconds if step > 1 }; }(window.jQuery)); /** Combodate input - dropdown date and time picker. @@ -3863,11 +4010,12 @@ **/ /*global moment*/ (function ($) { - + "use strict"; + var Constructor = function (options) { this.init('combodate', options, Constructor.defaults); //by default viewformat equals to format if(!this.options.viewformat) { @@ -4015,12 +4163,13 @@ /* Editableform based on Twitter Bootstrap */ (function ($) { + "use strict"; - $.extend($.fn.editableform.Constructor.prototype, { + $.extend($.fn.editableform.Constructor.prototype, { initTemplate: function() { this.$form = $($.fn.editableform.template); this.$form.find('.editable-error-block').addClass('help-block'); } }); @@ -4038,16 +4187,17 @@ * Editable Popover * --------------------- * requires bootstrap-popover.js */ (function ($) { + "use strict"; //extend methods $.extend($.fn.editableContainer.Popup.prototype, { containerName: 'popover', //for compatibility with bootstrap <= 2.2.1 (content inserted into <p> instead of directly .popover-content) - innerCss: $($.fn.popover.defaults.template).find('p').length ? '.popover-content p' : '.popover-content', + innerCss: $.fn.popover && $($.fn.popover.defaults.template).find('p').length ? '.popover-content p' : '.popover-content', initContainer: function(){ $.extend(this.containerOptions, { trigger: 'manual', selector: false, @@ -4172,11 +4322,12 @@ }); }); </script> **/ (function ($) { - + "use strict"; + var Date = function (options) { this.init('date', options, Date.defaults); this.initPicker(options, Date.defaults); }; @@ -4256,10 +4407,13 @@ }, clear: function() { this.$input.data('datepicker').date = null; this.$input.find('.active').removeClass('active'); + if(!this.options.showbuttons) { + this.$input.closest('form').submit(); + } }, autosubmit: function() { this.$input.on('mouseup', '.day', function(e){ if($(e.currentTarget).is('.old') || $(e.currentTarget).is('.new')) { @@ -4355,11 +4509,12 @@ @extends date @since 1.4.0 **/ (function ($) { - + "use strict"; + var DateField = function (options) { this.init('datefield', options, DateField.defaults); this.initPicker(options, DateField.defaults); }; @@ -4466,48 +4621,36 @@ this.language = this.language in dates ? this.language : "en"; this.isRTL = dates[this.language].rtl||false; this.format = DPGlobal.parseFormat(options.format||this.element.data('date-format')||dates[this.language].format||'mm/dd/yyyy'); this.isInline = false; this.isInput = this.element.is('input'); - this.component = this.element.is('.date') ? this.element.find('.add-on') : false; + this.component = this.element.is('.date') ? this.element.find('.add-on, .btn') : false; this.hasInput = this.component && this.element.find('input').length; if(this.component && this.component.length === 0) this.component = false; - this._attachEvents(); - this.forceParse = true; if ('forceParse' in options) { this.forceParse = options.forceParse; } else if ('dateForceParse' in this.element.data()) { this.forceParse = this.element.data('date-force-parse'); } + this.picker = $(DPGlobal.template); + this._buildEvents(); + this._attachEvents(); - this.picker = $(DPGlobal.template) - .appendTo(this.isInline ? this.element : 'body') - .on({ - click: $.proxy(this.click, this), - mousedown: $.proxy(this.mousedown, this) - }); - if(this.isInline) { - this.picker.addClass('datepicker-inline'); + this.picker.addClass('datepicker-inline').appendTo(this.element); } else { this.picker.addClass('datepicker-dropdown dropdown-menu'); } if (this.isRTL){ this.picker.addClass('datepicker-rtl'); this.picker.find('.prev i, .next i') .toggleClass('icon-arrow-left icon-arrow-right'); } - $(document).on('mousedown', function (e) { - // Clicked outside the datepicker, hide it - if ($(e.target).closest('.datepicker.datepicker-inline, .datepicker.datepicker-dropdown').length === 0) { - that.hide(); - } - }); this.autoclose = false; if ('autoclose' in options) { this.autoclose = options.autoclose; } else if ('dateAutoclose' in this.element.data()) { @@ -4563,20 +4706,25 @@ this.picker.find('tfoot th.today') .attr('colspan', function(i, val){ return parseInt(val) + 1; }); + this._allow_update = false; + this.weekStart = ((options.weekStart||this.element.data('date-weekstart')||dates[this.language].weekStart||0) % 7); this.weekEnd = ((this.weekStart + 6) % 7); this.startDate = -Infinity; this.endDate = Infinity; this.daysOfWeekDisabled = []; this.setStartDate(options.startDate||this.element.data('date-startdate')); this.setEndDate(options.endDate||this.element.data('date-enddate')); this.setDaysOfWeekDisabled(options.daysOfWeekDisabled||this.element.data('date-days-of-week-disabled')); this.fillDow(); this.fillMonths(); + + this._allow_update = true; + this.update(); this.showMode(); if(this.isInline) { this.show(); @@ -4585,12 +4733,26 @@ Datepicker.prototype = { constructor: Datepicker, _events: [], - _attachEvents: function(){ - this._detachEvents(); + _secondaryEvents: [], + _applyEvents: function(evs){ + for (var i=0, el, ev; i<evs.length; i++){ + el = evs[i][0]; + ev = evs[i][1]; + el.on(ev); + } + }, + _unapplyEvents: function(evs){ + for (var i=0, el, ev; i<evs.length; i++){ + el = evs[i][0]; + ev = evs[i][1]; + el.off(ev); + } + }, + _buildEvents: function(){ if (this.isInput) { // single input this._events = [ [this.element, { focus: $.proxy(this.show, this), keyup: $.proxy(this.update, this), @@ -4609,43 +4771,61 @@ [this.component, { click: $.proxy(this.show, this) }] ]; } - else if (this.element.is('div')) { // inline datepicker - this.isInline = true; - } + else if (this.element.is('div')) { // inline datepicker + this.isInline = true; + } else { this._events = [ [this.element, { click: $.proxy(this.show, this) }] ]; } - for (var i=0, el, ev; i<this._events.length; i++){ - el = this._events[i][0]; - ev = this._events[i][1]; - el.on(ev); - } + + this._secondaryEvents = [ + [this.picker, { + click: $.proxy(this.click, this) + }], + [$(window), { + resize: $.proxy(this.place, this) + }], + [$(document), { + mousedown: $.proxy(function (e) { + // Clicked outside the datepicker, hide it + if ($(e.target).closest('.datepicker.datepicker-inline, .datepicker.datepicker-dropdown').length === 0) { + this.hide(); + } + }, this) + }] + ]; }, + _attachEvents: function(){ + this._detachEvents(); + this._applyEvents(this._events); + }, _detachEvents: function(){ - for (var i=0, el, ev; i<this._events.length; i++){ - el = this._events[i][0]; - ev = this._events[i][1]; - el.off(ev); - } - this._events = []; + this._unapplyEvents(this._events); }, + _attachSecondaryEvents: function(){ + this._detachSecondaryEvents(); + this._applyEvents(this._secondaryEvents); + }, + _detachSecondaryEvents: function(){ + this._unapplyEvents(this._secondaryEvents); + }, show: function(e) { + if (!this.isInline) + this.picker.appendTo('body'); this.picker.show(); this.height = this.component ? this.component.outerHeight() : this.element.outerHeight(); - this.update(); this.place(); - $(window).on('resize', $.proxy(this.place, this)); - if (e ) { - e.stopPropagation(); + this._attachSecondaryEvents(); + if (e) { e.preventDefault(); } this.element.trigger({ type: 'show', date: this.date @@ -4653,17 +4833,14 @@ }, hide: function(e){ if(this.isInline) return; if (!this.picker.is(':visible')) return; - this.picker.hide(); - $(window).off('resize', this.place); + this.picker.hide().detach(); + this._detachSecondaryEvents(); this.viewMode = this.startViewMode; this.showMode(); - if (!this.isInput) { - $(document).off('mousedown', this.hide); - } if ( this.forceParse && ( this.isInput && this.element.val() || @@ -4676,13 +4853,18 @@ date: this.date }); }, remove: function() { + this.hide(); this._detachEvents(); + this._detachSecondaryEvents(); this.picker.remove(); delete this.element.data().datepicker; + if (!this.isInput) { + delete this.element.data().date; + } }, getDate: function() { var d = this.getUTCDate(); return new Date(d.getTime() + (d.getTimezoneOffset()*60000)); @@ -4752,20 +4934,23 @@ place: function(){ if(this.isInline) return; var zIndex = parseInt(this.element.parents().filter(function() { return $(this).css('z-index') != 'auto'; }).first().css('z-index'))+10; - var offset = this.component ? this.component.offset() : this.element.offset(); + var offset = this.component ? this.component.parent().offset() : this.element.offset(); var height = this.component ? this.component.outerHeight(true) : this.element.outerHeight(true); this.picker.css({ top: offset.top + height, left: offset.left, zIndex: zIndex }); }, + _allow_update: true, update: function(){ + if (!this._allow_update) return; + var date, fromArgs = false; if(arguments && arguments.length && (typeof arguments[0] === 'string' || arguments[0] instanceof Date)) { date = arguments[0]; fromArgs = true; } else { @@ -4916,10 +5101,12 @@ } yearCont.html(html); }, updateNavArrows: function() { + if (!this._allow_update) return; + var d = new Date(this.viewDate), year = d.getUTCFullYear(), month = d.getUTCMonth(); switch (this.viewMode) { case 0: @@ -4949,11 +5136,10 @@ break; } }, click: function(e) { - e.stopPropagation(); e.preventDefault(); var target = $(e.target).closest('span, td, th'); if (target.length == 1) { switch(target[0].nodeName.toLowerCase()) { case 'th': @@ -5440,10 +5626,304 @@ $.fn.datepicker.DPGlobal = DPGlobal; }( window.jQuery ); /** +Bootstrap-datetimepicker. +Based on [smalot bootstrap-datetimepicker plugin](https://github.com/smalot/bootstrap-datetimepicker). +Before usage you should manually include dependent js and css: + + <link href="css/datetimepicker.css" rel="stylesheet" type="text/css"></link> + <script src="js/bootstrap-datetimepicker.js"></script> + +For **i18n** you should include js file from here: https://github.com/smalot/bootstrap-datetimepicker/tree/master/js/locales +and set `language` option. + +@class datetime +@extends abstractinput +@final +@since 1.4.4 +@example +<a href="#" id="last_seen" data-type="datetime" data-pk="1" data-url="/post" title="Select date & time">15/03/2013 12:45</a> +<script> +$(function(){ + $('#last_seen').editable({ + format: 'yyyy-mm-dd hh:ii', + viewformat: 'dd/mm/yyyy hh:ii', + datetimepicker: { + weekStart: 1 + } + } + }); +}); +</script> +**/ +(function ($) { + "use strict"; + + var DateTime = function (options) { + this.init('datetime', options, DateTime.defaults); + this.initPicker(options, DateTime.defaults); + }; + + $.fn.editableutils.inherit(DateTime, $.fn.editabletypes.abstractinput); + + $.extend(DateTime.prototype, { + initPicker: function(options, defaults) { + //'format' is set directly from settings or data-* attributes + + //by default viewformat equals to format + if(!this.options.viewformat) { + this.options.viewformat = this.options.format; + } + + //overriding datetimepicker config (as by default jQuery extend() is not recursive) + //since 1.4 datetimepicker internally uses viewformat instead of format. Format is for submit only + this.options.datetimepicker = $.extend({}, defaults.datetimepicker, options.datetimepicker, { + format: this.options.viewformat + }); + + //language + this.options.datetimepicker.language = this.options.datetimepicker.language || 'en'; + + //store DPglobal + this.dpg = $.fn.datetimepicker.DPGlobal; + + //store parsed formats + this.parsedFormat = this.dpg.parseFormat(this.options.format, this.options.formatType); + this.parsedViewFormat = this.dpg.parseFormat(this.options.viewformat, this.options.formatType); + + // + this.options.datetimepicker.startView = this.options.startView; + this.options.datetimepicker.minView = this.options.minView; + this.options.datetimepicker.maxView = this.options.maxView; + }, + + render: function () { + this.$input.datetimepicker(this.options.datetimepicker); + + //"clear" link + if(this.options.clear) { + this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){ + e.preventDefault(); + e.stopPropagation(); + this.clear(); + }, this)); + + this.$tpl.parent().append($('<div class="editable-clear">').append(this.$clear)); + } + }, + + value2html: function(value, element) { + //formatDate works with UTCDate! + var text = value ? this.dpg.formatDate(this.toUTC(value), this.parsedViewFormat, this.options.datetimepicker.language, this.options.formatType) : ''; + if(element) { + DateTime.superclass.value2html(text, element); + } else { + return text; + } + }, + + html2value: function(html) { + //parseDate return utc date! + var value = html ? this.dpg.parseDate(html, this.parsedViewFormat, this.options.datetimepicker.language, this.options.formatType) : null; + return value ? this.fromUTC(value) : null; + }, + + value2str: function(value) { + //formatDate works with UTCDate! + return value ? this.dpg.formatDate(this.toUTC(value), this.parsedFormat, this.options.datetimepicker.language, this.options.formatType) : ''; + }, + + str2value: function(str) { + //parseDate return utc date! + var value = str ? this.dpg.parseDate(str, this.parsedFormat, this.options.datetimepicker.language, this.options.formatType) : null; + return value ? this.fromUTC(value) : null; + }, + + value2submit: function(value) { + return this.value2str(value); + }, + + value2input: function(value) { + if(value) { + this.$input.data('datetimepicker').setDate(value); + } + }, + + input2value: function() { + //date may be cleared, in that case getDate() triggers error + var dt = this.$input.data('datetimepicker'); + return dt.date ? dt.getDate() : null; + }, + + activate: function() { + }, + + clear: function() { + this.$input.data('datetimepicker').date = null; + this.$input.find('.active').removeClass('active'); + if(!this.options.showbuttons) { + this.$input.closest('form').submit(); + } + }, + + autosubmit: function() { + this.$input.on('mouseup', '.minute', function(e){ + var $form = $(this).closest('form'); + setTimeout(function() { + $form.submit(); + }, 200); + }); + }, + + //convert date from local to utc + toUTC: function(value) { + return value ? new Date(value.valueOf() - value.getTimezoneOffset() * 60000) : value; + }, + + //convert date from utc to local + fromUTC: function(value) { + return value ? new Date(value.valueOf() + value.getTimezoneOffset() * 60000) : value; + } + + }); + + DateTime.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { + /** + @property tpl + @default <div></div> + **/ + tpl:'<div class="editable-date well"></div>', + /** + @property inputclass + @default null + **/ + inputclass: null, + /** + Format used for sending value to server. Also applied when converting date from <code>data-value</code> attribute.<br> + Possible tokens are: <code>d, dd, m, mm, yy, yyyy, h, i</code> + + @property format + @type string + @default yyyy-mm-dd hh:ii + **/ + format:'yyyy-mm-dd hh:ii', + formatType:'standard', + /** + Format used for displaying date. Also applied when converting date from element's text on init. + If not specified equals to <code>format</code> + + @property viewformat + @type string + @default null + **/ + viewformat: null, + /** + Configuration of datetimepicker. + Full list of options: https://github.com/smalot/bootstrap-datetimepicker + + @property datetimepicker + @type object + @default { } + **/ + datetimepicker:{ + todayHighlight: false, + autoclose: false + }, + /** + Text shown as clear date button. + If <code>false</code> clear button will not be rendered. + + @property clear + @type boolean|string + @default 'x clear' + **/ + clear: '&times; clear' + }); + + $.fn.editabletypes.datetime = DateTime; + +}(window.jQuery)); +/** +Bootstrap datetimefield input - datetime input for inline mode. +Shows normal <input type="text"> and binds popup datetimepicker. +Automatically shown in inline mode. + +@class datetimefield +@extends datetime + +**/ +(function ($) { + "use strict"; + + var DateTimeField = function (options) { + this.init('datetimefield', options, DateTimeField.defaults); + this.initPicker(options, DateTimeField.defaults); + }; + + $.fn.editableutils.inherit(DateTimeField, $.fn.editabletypes.datetime); + + $.extend(DateTimeField.prototype, { + render: function () { + this.$input = this.$tpl.find('input'); + this.setClass(); + this.setAttr('placeholder'); + + this.$tpl.datetimepicker(this.options.datetimepicker); + + //need to disable original event handlers + this.$input.off('focus keydown'); + + //update value of datepicker + this.$input.keyup($.proxy(function(){ + this.$tpl.removeData('date'); + this.$tpl.datetimepicker('update'); + }, this)); + + }, + + value2input: function(value) { + this.$input.val(this.value2html(value)); + this.$tpl.datetimepicker('update'); + }, + + input2value: function() { + return this.html2value(this.$input.val()); + }, + + activate: function() { + $.fn.editabletypes.text.prototype.activate.call(this); + }, + + autosubmit: function() { + //reset autosubmit to empty + } + }); + + DateTimeField.defaults = $.extend({}, $.fn.editabletypes.datetime.defaults, { + /** + @property tpl + **/ + tpl:'<div class="input-append date"><input type="text"/><span class="add-on"><i class="icon-th"></i></span></div>', + /** + @property inputclass + @default 'input-medium' + **/ + inputclass: 'input-medium', + + /* datetimepicker config */ + datetimepicker:{ + todayHighlight: false, + autoclose: true + } + }); + + $.fn.editabletypes.datetimefield = DateTimeField; + +}(window.jQuery)); +/** Typeahead input (bootstrap only). Based on Twitter Bootstrap [typeahead](http://twitter.github.com/bootstrap/javascript.html#typeahead). Depending on `source` format typeahead operates in two modes: * **strings**: When `source` defined as array of strings, e.g. `['text1', 'text2', 'text3' ...]`. @@ -5471,10 +5951,11 @@ }); }); </script> **/ (function ($) { - + "use strict"; + var Constructor = function (options) { this.init('typeahead', options, Constructor.defaults); //overriding objects in config (as by default jQuery extend() is not recursive) this.options.typeahead = $.extend({}, Constructor.defaults.typeahead, { \ No newline at end of file