vendor/assets/javascripts/bootstrap-editable.js in bootstrap-editable-rails-0.0.5 vs vendor/assets/javascripts/bootstrap-editable.js in bootstrap-editable-rails-0.0.6

- old
+ new

@@ -1,21 +1,21 @@ -/*! X-editable - v1.4.3 -* In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery -* http://github.com/vitalets/x-editable -* Copyright (c) 2013 Vitaliy Potapov; Licensed MIT */ - +/*! 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. Applied as jQuery method to DIV tag (not to form tag!). This is because form can be in loading state when spinner shown. Editableform is linked with one of input types, e.g. 'text', 'select' etc. @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; @@ -30,16 +30,23 @@ this.input = this.options.input; //set initial value //todo: may be add check: typeof str === 'string' ? this.value = this.input.str2value(this.options.value); + + //prerender: get input.$input + this.input.prerender(); }, 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 @@ -58,10 +65,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 **/ @@ -69,13 +80,12 @@ //init input this.initInput(); //append input to form - this.input.prerender(); this.$form.find('div.editable-input').append(this.input.$tpl); - + //append form to container this.$div.append(this.$form); //render input $.when(this.input.render()) @@ -96,11 +106,12 @@ this.$form.submit(function(e){ e.preventDefault(); }); } else { this.error(false); this.input.$input.removeAttr('disabled'); this.$form.find('.editable-submit').removeAttr('disabled'); - this.input.value2input(this.value); + var value = (this.value === null || this.value === undefined || this.value === '') ? this.options.defaultValue : this.value; + this.input.value2input(value); //attach submit handler this.$form.submit($.proxy(this.submit, this)); } /** @@ -172,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>'); } @@ -187,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; } @@ -210,69 +231,88 @@ **/ 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.error(typeof xhr === 'string' ? xhr : xhr.responseText || xhr.statusText || 'Unknown error!'); - this.showForm(); + 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 = !!(typeof this.options.url === 'function' || (this.options.url && ((this.options.send === 'always') || (this.options.send === 'auto' && 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(); @@ -452,20 +492,31 @@ @type string|object @default null **/ value: null, /** - Strategy for sending data on server. Can be <code>auto|always|never</code>. - When 'auto' data will be sent on server only if pk defined, otherwise new value will be stored in element. + Value that will be displayed in input if original field value is empty (`null|undefined|''`). + @property defaultValue + @type string|object + @default null + @since 1.4.6 + **/ + defaultValue: null, + /** + Strategy for sending data on server. Can be `auto|always|never`. + When 'auto' data will be sent on server **only if pk and url defined**, otherwise new value will be stored locally. + @property send @type string @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 @@ -476,11 +527,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 +543,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 +577,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 +605,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! @@ -561,15 +631,21 @@ //error class attached to control-group $.fn.editableform.errorGroupClass = null; //error class attached to editable-error-block $.fn.editableform.errorBlockClass = 'editable-error'; -}(window.jQuery)); + + //engine + $.fn.editableform.engine = 'jquery'; +}(window.jQuery)); + /** * EditableForm utilites */ (function ($) { + "use strict"; + //utils $.fn.editableutils = { /** * classic JS inheritance function */ @@ -701,11 +777,14 @@ itemsByValue: function(value, sourceData, valueProp) { if(!sourceData || value === null) { return []; } - valueProp = valueProp || 'value'; + if (typeof(valueProp) !== "function") { + var idKey = valueProp || 'value'; + valueProp = function (e) { return e[idKey]; }; + } var isValArray = $.isArray(value), result = [], that = this; @@ -713,15 +792,16 @@ if(o.children) { result = result.concat(that.itemsByValue(value, o.children, valueProp)); } else { /*jslint eqeq: true*/ if(isValArray) { - if($.grep(value, function(v){ return v == (o && typeof o === 'object' ? o[valueProp] : o); }).length) { + if($.grep(value, function(v){ return v == (o && typeof o === 'object' ? valueProp(o) : o); }).length) { result.push(o); } } else { - if(value == (o && typeof o === 'object' ? o[valueProp] : o)) { + var itemValue = (o && (typeof o === 'object')) ? valueProp(o) : o; + if(value == itemValue) { result.push(o); } } /*jslint eqeq: false*/ } @@ -759,10 +839,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'; } @@ -775,25 +860,47 @@ 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)); - + /** Attaches stand-alone container with editable-form to HTML element. Element is used only for positioning, value is not stored anywhere.<br> This method applied internally in <code>$().editable()</code>. You should subscribe on it's events (save / cancel) to get profit of it.<br> Final realization can be different: bootstrap-popover, jqueryui-tooltip, poshytip, inline-div. It depends on which js file you include.<br> Applied as jQuery method. @class editableContainer @uses editableform **/ (function ($) { + "use strict"; var Popup = function (element, options) { this.init(element, options); }; @@ -801,22 +908,29 @@ this.init(element, options); }; //methods Popup.prototype = { - containerName: null, //tbd in child class + containerName: null, //method to call container on element + containerDataName: null, //object name in element's .data() innerCss: null, //tbd in child class + containerClass: 'editable-container editable-popup', //css class applied to container element + defaults: {}, //container itself defaults + 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(); //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)); @@ -835,14 +949,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,14 +986,18 @@ //split options on containerOptions and formOptions splitOptions: function() { this.containerOptions = {}; this.formOptions = {}; - var cDef = $.fn[this.containerName].defaults; + + if(!$.fn[this.containerName]) { + throw new Error(this.containerName + ' not found. Have you included corresponding js file?'); + } + //keys defined in container defaults go to container, others go to form for(var k in this.options) { - if(k in cDef) { + if(k in this.defaults) { this.containerOptions[k] = this.options[k]; } else { this.formOptions[k] = this.options[k]; } } @@ -879,13 +1011,23 @@ return this.container() ? this.container().$tip : null; }, /* returns container object */ container: function() { - return this.$element.data(this.containerDataName || this.containerName); + var container; + //first, try get it by `containerDataName` + if(this.containerDataName) { + if(container = this.$element.data(this.containerDataName)) { + return container; + } + } + //second, try `containerName` + container = this.$element.data(this.containerName); + return container; }, + /* call native method of underlying container, e.g. this.$element.popover('method') */ call: function() { this.$element[this.containerName].apply(this.$element, arguments); }, initContainer: function(){ @@ -897,26 +1039,37 @@ .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) + 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.options.scope).data('editable')); }, this) }) .editableform('render'); }, @@ -933,19 +1086,19 @@ 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(). + 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! @@ -974,39 +1127,49 @@ 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. + /** + 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 () { }, - + /* 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. @@ -1047,11 +1210,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 @@ -1207,11 +1370,11 @@ @since 1.1.1 **/ onblur: 'cancel', /** - Animation speed (inline mode) + Animation speed (inline mode only) @property anim @type string @default false **/ anim: false, @@ -1238,26 +1401,28 @@ } } }; }(window.jQuery)); - + /** * 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; } @@ -1290,28 +1455,34 @@ this.tip().empty().remove(); } } }); -}(window.jQuery)); +}(window.jQuery)); /** Makes editable any HTML element on the page. Applied as jQuery method. @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)); 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 () { @@ -1348,33 +1519,46 @@ } //add 'editable' class to every editable element this.$element.addClass('editable'); + //specifically for "textarea" add class .editable-pre-wrapped to keep linebreaks + if(this.input.type === 'textarea') { + this.$element.addClass('editable-pre-wrapped'); + } + //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; @@ -1392,20 +1576,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)); }, @@ -1547,16 +1733,32 @@ //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 + //for some inputs we need more smart check + //e.g. wysihtml5 may have <br>, <p></p>, <img> + if(typeof(this.input.isEmpty) === 'function') { + this.isEmpty = this.input.isEmpty(this.$element); + } else { + this.isEmpty = $.trim(this.$element.html()) === ''; + } + } //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); @@ -1647,10 +1849,28 @@ } 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(){ + if(bgColor === 'transparent') { + bgColor = ''; + } + $e.css('background-color', bgColor); + $e.addClass('editable-bg-transition'); + setTimeout(function(){ + $e.removeClass('editable-bg-transition'); + }, 1700); + }, 10); + } + //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 @@ -1708,23 +1928,26 @@ /** Removes editable feature from element @method destroy() **/ destroy: function() { + 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'); } 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 @@ -1769,35 +1992,46 @@ 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. + 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 @@ -1807,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); } } @@ -1846,10 +2125,18 @@ return this.each(function () { var $this = $(this), data = $this.data(datakey), options = typeof option === 'object' && option; + //for delegated targets do not store `editable` object for element + //it's allows several different selectors. + //see: https://github.com/vitalets/x-editable/issues/312 + if(options && options.selector) { + data = new Editable(this, options); + return; + } + if (!data) { $this.data(datakey, (data = new Editable(this, options))); } if (typeof option === 'string') { //call method @@ -2014,40 +2301,50 @@ 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)); - + /** AbstractInput - base class for all editable inputs. It defines interface to be implemented by any input type. To create your own input you can inherit from this class. @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 @@ -2057,132 +2354,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)[this.options.escape ? 'text' : 'html']($.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() { + 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]); } }, @@ -2205,30 +2508,47 @@ /** CSS class automatically applied to input @property inputclass @type string - @default input-medium + @default null **/ - inputclass: 'input-medium', + inputclass: null, + + /** + If `true` - html will be escaped in content of element via $.text() method. + If `false` - html will not be escaped, $.html() used. + When you use own `display` function, this option obviosly has no effect. + + @property escape + @type boolean + @since 1.5.0 + @default true + **/ + escape: true, + //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)); - + /** List - abstract class for inputs that have source option loaded from js array or via ajax @class list @extends abstractinput **/ (function ($) { - + "use strict"; + var List = function (options) { }; $.fn.editableutils.inherit(List, $.fn.editabletypes.abstractinput); @@ -2276,34 +2596,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)) { @@ -2332,12 +2655,12 @@ cache.callbacks = []; cache.err_callbacks = []; } } - //loading sourceData from server - $.ajax({ + //ajaxOptions for source. Can be overwritten bt options.sourceOptions + var ajaxOptions = $.extend({ url: source, type: 'get', cache: false, dataType: 'json', success: $.proxy(function (data) { @@ -2368,11 +2691,15 @@ cache.loading = false; //run error callbacks for other fields $.each(cache.err_callbacks, function () { this.call(); }); } }, this) - }); + }, this.options.sourceOptions); + + //loading sourceData from server + $.ajax(ajaxOptions); + } else { //options as json/array this.sourceData = this.makeArray(source); if($.isArray(this.sourceData)) { this.doPrepend(); @@ -2528,17 +2855,27 @@ @property sourceCache @type boolean @default true @since 1.2.0 **/ - sourceCache: true + sourceCache: true, + /** + Additional ajax options to be used in $.ajax() when loading list from server. + Useful to send extra parameters (`data` key) or change request method (`type` key). + + @property sourceOptions + @type object|function + @default null + @since 1.5.0 + **/ + sourceOptions: null }); $.fn.editabletypes.list = List; }(window.jQuery)); - + /** Text input @class text @extends abstractinput @@ -2553,10 +2890,12 @@ }); }); </script> **/ (function ($) { + "use strict"; + var Text = function (options) { this.init('text', options, Text.defaults); }; $.fn.editableutils.inherit(Text, $.fn.editabletypes.abstractinput); @@ -2602,22 +2941,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) { @@ -2668,11 +3006,11 @@ }); $.fn.editabletypes.text = Text; }(window.jQuery)); - + /** Textarea input @class textarea @extends abstractinput @@ -2688,11 +3026,12 @@ }); }); </script> **/ (function ($) { - + "use strict"; + var Textarea = function (options) { this.init('textarea', options, Textarea.defaults); }; $.fn.editableutils.inherit(Textarea, $.fn.editabletypes.abstractinput); @@ -2708,23 +3047,25 @@ if (e.ctrlKey && e.which === 13) { $(this).closest('form').submit(); } }); }, - - value2html: function(value, element) { + + //using `white-space: pre-wrap` solves \n <--> BR conversion very elegant! + /* + value2html: function(value, element) { var html = '', lines; if(value) { lines = value.split("\n"); for (var i = 0; i < lines.length; i++) { lines[i] = $('<div>').text(lines[i]).html(); } html = lines.join('<br>'); } $(element).html(html); }, - + html2value: function(html) { if(!html) { return ''; } @@ -2739,11 +3080,11 @@ lines[i] = text; } return lines.join("\n"); }, - + */ activate: function() { $.fn.editabletypes.text.prototype.activate.call(this); } }); @@ -2777,19 +3118,19 @@ }); $.fn.editabletypes.textarea = Textarea; }(window.jQuery)); - + /** Select (dropdown) @class select @extends list @final @example -<a href="#" id="status" data-type="select" data-pk="1" data-url="/post" data-original-title="Select status"></a> +<a href="#" id="status" data-type="select" data-pk="1" data-url="/post" data-title="Select status"></a> <script> $(function(){ $('#status').editable({ value: 2, source: [ @@ -2800,11 +3141,12 @@ }); }); </script> **/ (function ($) { - + "use strict"; + var Select = function (options) { this.init('select', options, Select.defaults); }; $.fn.editableutils.inherit(Select, $.fn.editabletypes.list); @@ -2812,16 +3154,23 @@ $.extend(Select.prototype, { renderList: function() { this.$input.empty(); var fillItems = function($el, data) { + var attr; if($.isArray(data)) { for(var i=0; i<data.length; i++) { + attr = {}; if(data[i].children) { - $el.append(fillItems($('<optgroup>', {label: data[i].text}), data[i].children)); + attr.label = data[i].text; + $el.append(fillItems($('<optgroup>', attr), data[i].children)); } else { - $el.append($('<option>', {value: data[i].value}).text(data[i].text)); + attr.value = data[i].value; + if(data[i].disabled) { + attr.disabled = true; + } + $el.append($('<option>', attr).text(data[i].text)); } } } return $el; }; @@ -2844,11 +3193,12 @@ if(items.length) { text = items[0].text; } - $(element).text(text); + //$(element).text(text); + $.fn.editabletypes.abstractinput.prototype.value2html.call(this, text, element); }, autosubmit: function() { this.$input.off('keydown.editable').on('change.editable', function(){ $(this).closest('form').submit(); @@ -2865,20 +3215,20 @@ }); $.fn.editabletypes.select = Select; }(window.jQuery)); - + /** List of checkboxes. Internally value stored as javascript array of values. @class checklist @extends list @final @example -<a href="#" id="options" data-type="checklist" data-pk="1" data-url="/post" data-original-title="Select options"></a> +<a href="#" id="options" data-type="checklist" data-pk="1" data-url="/post" data-title="Select options"></a> <script> $(function(){ $('#options').editable({ value: [2, 3], source: [ @@ -2889,11 +3239,12 @@ }); }); </script> **/ (function ($) { - + "use strict"; + var Checklist = function (options) { this.init('checklist', options, Checklist.defaults); }; $.fn.editableutils.inherit(Checklist, $.fn.editabletypes.list); @@ -2932,10 +3283,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 @@ -2965,14 +3318,18 @@ }, //collect text of checked boxes value2htmlFinal: function(value, element) { var html = [], - checked = $.fn.editableutils.itemsByValue(value, this.sourceData); + checked = $.fn.editableutils.itemsByValue(value, this.sourceData), + escape = this.options.escape; if(checked.length) { - $.each(checked, function(i, v) { html.push($.fn.editableutils.escape(v.text)); }); + $.each(checked, function(i, v) { + var text = escape ? $.fn.editableutils.escape(v.text) : v.text; + html.push(text); + }); $(element).html(html.join('<br>')); } else { $(element).empty(); } }, @@ -3015,21 +3372,22 @@ }); $.fn.editabletypes.checklist = Checklist; }(window.jQuery)); - + /** HTML5 input types. Following types are supported: * password * email * url * tel * number * range +* time Learn more about html5 inputs: http://www.w3.org/wiki/HTML5_form_additions To check browser compatibility please see: https://developer.mozilla.org/en-US/docs/HTML/Element/Input @@ -3057,10 +3415,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 +3446,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 +3463,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 +3480,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 +3497,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 +3540,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, { @@ -3184,210 +3568,346 @@ Range.defaults = $.extend({}, $.fn.editabletypes.number.defaults, { tpl: '<input type="range"><output style="width: 30px; display: inline-block"></output>', inputclass: 'input-medium' }); $.fn.editabletypes.range = Range; -}(window.jQuery)); +}(window.jQuery)); + +/* +Time +*/ +(function ($) { + "use strict"; + + var Time = function (options) { + this.init('time', options, Time.defaults); + }; + //inherit from abstract, as inheritance from text gives selection error. + $.fn.editableutils.inherit(Time, $.fn.editabletypes.abstractinput); + $.extend(Time.prototype, { + render: function() { + this.setClass(); + } + }); + Time.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { + tpl: '<input type="time">' + }); + $.fn.editabletypes.time = Time; +}(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. + +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> -**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. +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 `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(){ + //local source $('#country').editable({ source: [ {id: 'gb', text: 'Great Britain'}, {id: 'us', text: 'United States'}, {id: 'ru', text: 'Russia'} ], select2: { multiple: true } }); + //remote source (simple) + $('#country').editable({ + source: '/getCountries', + select2: { + placeholder: 'Select Country', + minimumInputLength: 1 + } + }); + //remote source (advanced) + $('#country').editable({ + select2: { + placeholder: 'Select Country', + allowClear: true, + minimumInputLength: 3, + id: function (item) { + return item.CountryId; + }, + ajax: { + url: '/getCountries', + dataType: 'json', + data: function (term, page) { + return { query: term }; + }, + results: function (data, page) { + return { results: data }; + } + }, + formatResult: function (item) { + return item.CountryName; + }, + formatSelection: function (item) { + return item.CountryName; + }, + initSelection: function (element, callback) { + return $.get('/getCountryById', { query: element.val() }, function (data) { + callback(data); + }); + } + } + }); }); </script> **/ (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); + + //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; + if (typeof(this.formatSelection) !== "function") { + this.formatSelection = function (e) { return e.text; }; + } }; $.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); - } - */ + //can not apply select2 here as it calls initSelection + //over input that does not have correct value yet. + //apply select2 only in value2input + //this.$input.select2(this.options.select2); + + //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 + //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; - 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) { - } + 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); + } 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) } - + + //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); - }); + text.push(v && typeof v === 'object' ? that.formatSelection(v) : v); + }); } else if(data) { - text = data.text; + text = that.formatSelection(data); } text = $.isArray(text) ? text.join(this.options.viewseparator) : text; - $(element).text(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) { - this.$input.val(value).trigger('change', true); //second argument needed to separate initial change from user's click (for autosubmit) + // 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 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 $el = $(this.options.scope); + if (!$el.data('editable').isEmpty) { + var data = {id: value, text: $el.text()}; + this.$input.select2('data', data); + } + } + } }, input2value: function() { return this.$input.select2('val'); }, 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) { + 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; + }, - }); + 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, @@ -3395,42 +3915,53 @@ 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 + @type array|string|function @default null **/ source: null, /** - Separator used to display tags. - + Separator used to display tags. + @property viewseparator @type string @default ', ' **/ - viewseparator: ', ' + viewseparator: ', ' }); - $.fn.editabletypes.select2 = Constructor; - -}(window.jQuery)); + $.fn.editabletypes.select2 = Constructor; + +}(window.jQuery)); + /** -* Combodate - 1.0.2 +* 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 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 ($) { @@ -3462,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 @@ -3508,81 +4045,103 @@ /* 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 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]); } 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++) { 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; } @@ -3593,42 +4152,46 @@ /* 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.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; - for(i=0; i<=max; i++) { + for(i=min; i<=max; i++) { name = twoDigit ? this.leadZero(i) : i; items.push([i, name]); } return items; }, /* 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]); @@ -3638,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]); @@ -3660,13 +4223,13 @@ ['am', ampmL ? 'am' : 'AM'], ['pm', ampmL ? 'pm' : 'PM'] ]; 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 = {}, @@ -3691,16 +4254,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 @@ -3721,35 +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; + + 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) { - if(values.hour > 12) { - values.hour -= 12; - values.ampm = 'pm'; - } else { - values.ampm = 'am'; - } - } + 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) { - if(that['$'+k]) { - that['$'+k].val(v); - } - }); + $.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); + } + }); + + // 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 @@ -3815,17 +4417,20 @@ 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 + smartDays: false // whether days in combo depend on selected month: 31, 30, 28 }; -}(window.jQuery)); +}(window.jQuery)); /** Combodate input - dropdown date and time picker. Based on [combodate](http://vitalets.github.com/combodate) plugin (included). To use it you should manually include [momentjs](http://momentjs.com). <script src="js/moment.min.js"></script> @@ -3842,11 +4447,11 @@ @class combodate @extends abstractinput @final @since 1.4.0 @example -<a href="#" id="dob" data-type="combodate" data-pk="1" data-url="/post" data-value="1984-05-15" data-original-title="Select date"></a> +<a href="#" id="dob" data-type="combodate" data-pk="1" data-url="/post" data-value="1984-05-15" data-title="Select date"></a> <script> $(function(){ $('#dob').editable({ format: 'YYYY-MM-DD', viewformat: 'DD.MM.YYYY', @@ -3863,11 +4468,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) { @@ -3887,11 +4493,18 @@ $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.abstractinput); $.extend(Constructor.prototype, { render: function () { this.$input.combodate(this.options.combodate); + + if($.fn.editableform.engine === 'bs3') { + this.$input.siblings().find('select').addClass('form-control'); + } + if(this.options.inputclass) { + this.$input.siblings().find('select').addClass(this.options.inputclass); + } //"clear" link /* if(this.options.clear) { this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){ e.preventDefault(); @@ -3904,11 +4517,12 @@ */ }, value2html: function(value, element) { var text = value ? value.format(this.options.viewformat) : ''; - $(element).text(text); + //$(element).text(text); + Constructor.superclass.value2html.call(this, text, element); }, html2value: function(html) { return html ? moment(html, this.options.viewformat) : null; }, @@ -4010,51 +4624,96 @@ }); $.fn.editabletypes.combodate = Constructor; }(window.jQuery)); - + /* -Editableform based on Twitter Bootstrap +Editableform based on Twitter Bootstrap 3 */ (function ($) { + "use strict"; - $.extend($.fn.editableform.Constructor.prototype, { - initTemplate: function() { + //store parent methods + var pInitInput = $.fn.editableform.Constructor.prototype.initInput; + + $.extend($.fn.editableform.Constructor.prototype, { + initTemplate: function() { this.$form = $($.fn.editableform.template); + this.$form.find('.control-group').addClass('form-group'); this.$form.find('.editable-error-block').addClass('help-block'); - } + }, + initInput: function() { + pInitInput.apply(this); + + //for bs3 set default class `input-sm` to standard inputs + var emptyInputClass = this.input.options.inputclass === null || this.input.options.inputclass === false; + var defaultClass = 'input-sm'; + + //bs3 add `form-control` class to standard inputs + var stdtypes = 'text,select,textarea,password,email,url,tel,number,range,time,typeaheadjs'.split(','); + if(~$.inArray(this.input.type, stdtypes)) { + this.input.$input.addClass('form-control'); + if(emptyInputClass) { + this.input.options.inputclass = defaultClass; + this.input.$input.addClass(defaultClass); + } + } + + //apply bs3 size class also to buttons (to fit size of control) + var $btn = this.$form.find('.editable-buttons'); + var classes = emptyInputClass ? [defaultClass] : this.input.options.inputclass.split(' '); + for(var i=0; i<classes.length; i++) { + // `btn-sm` is default now + /* + if(classes[i].toLowerCase() === 'input-sm') { + $btn.find('button').addClass('btn-sm'); + } + */ + if(classes[i].toLowerCase() === 'input-lg') { + $btn.find('button').removeClass('btn-sm').addClass('btn-lg'); + } + } + } }); //buttons - $.fn.editableform.buttons = '<button type="submit" class="btn btn-primary editable-submit"><i class="icon-ok icon-white"></i></button>'+ - '<button type="button" class="btn editable-cancel"><i class="icon-remove"></i></button>'; + $.fn.editableform.buttons = + '<button type="submit" class="btn btn-primary btn-sm editable-submit">'+ + '<i class="glyphicon glyphicon-ok"></i>'+ + '</button>'+ + '<button type="button" class="btn btn-default btn-sm editable-cancel">'+ + '<i class="glyphicon glyphicon-remove"></i>'+ + '</button>'; //error classes - $.fn.editableform.errorGroupClass = 'error'; - $.fn.editableform.errorBlockClass = null; - -}(window.jQuery)); + $.fn.editableform.errorGroupClass = 'has-error'; + $.fn.editableform.errorBlockClass = null; + //engine + $.fn.editableform.engine = 'bs3'; +}(window.jQuery)); /** -* Editable Popover +* Editable Popover3 (for Bootstrap 3) * --------------------- * 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', + containerDataName: 'bs.popover', + innerCss: '.popover-content', + defaults: $.fn.popover.Constructor.DEFAULTS, initContainer: function(){ $.extend(this.containerOptions, { trigger: 'manual', selector: false, content: ' ', - template: $.fn.popover.defaults.template + template: this.defaults.template }); //as template property is used in inputs, hide it from popover var t; if(this.$element.data('template')) { @@ -4090,21 +4749,26 @@ }, /** * move popover to new position. This function mainly copied from bootstrap-popover. */ - /*jshint laxcomma: true*/ + /*jshint laxcomma: true, eqeqeq: false*/ setPosition: function () { - - (function() { + + (function() { + /* var $tip = this.tip() , inside , pos , actualWidth , actualHeight , placement - , tp; + , tp + , tpt + , tpb + , tpl + , tpr; placement = typeof this.options.placement === 'function' ? this.options.placement.call(this, $tip[0], this.$element[0]) : this.options.placement; @@ -4120,313 +4784,142 @@ pos = this.getPosition(inside); actualWidth = $tip[0].offsetWidth; actualHeight = $tip[0].offsetHeight; - switch (inside ? placement.split(' ')[1] : placement) { + placement = inside ? placement.split(' ')[1] : placement; + + tpb = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2}; + tpt = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2}; + tpl = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth}; + tpr = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width}; + + switch (placement) { case 'bottom': - tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2}; + if ((tpb.top + actualHeight) > ($(window).scrollTop() + $(window).height())) { + if (tpt.top > $(window).scrollTop()) { + placement = 'top'; + } else if ((tpr.left + actualWidth) < ($(window).scrollLeft() + $(window).width())) { + placement = 'right'; + } else if (tpl.left > $(window).scrollLeft()) { + placement = 'left'; + } else { + placement = 'right'; + } + } break; case 'top': - tp = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2}; + if (tpt.top < $(window).scrollTop()) { + if ((tpb.top + actualHeight) < ($(window).scrollTop() + $(window).height())) { + placement = 'bottom'; + } else if ((tpr.left + actualWidth) < ($(window).scrollLeft() + $(window).width())) { + placement = 'right'; + } else if (tpl.left > $(window).scrollLeft()) { + placement = 'left'; + } else { + placement = 'right'; + } + } break; case 'left': - tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth}; + if (tpl.left < $(window).scrollLeft()) { + if ((tpr.left + actualWidth) < ($(window).scrollLeft() + $(window).width())) { + placement = 'right'; + } else if (tpt.top > $(window).scrollTop()) { + placement = 'top'; + } else if (tpt.top > $(window).scrollTop()) { + placement = 'bottom'; + } else { + placement = 'right'; + } + } break; case 'right': - tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width}; + if ((tpr.left + actualWidth) > ($(window).scrollLeft() + $(window).width())) { + if (tpl.left > $(window).scrollLeft()) { + placement = 'left'; + } else if (tpt.top > $(window).scrollTop()) { + placement = 'top'; + } else if (tpt.top > $(window).scrollTop()) { + placement = 'bottom'; + } + } break; } + switch (placement) { + case 'bottom': + tp = tpb; + break; + case 'top': + tp = tpt; + break; + case 'left': + tp = tpl; + break; + case 'right': + tp = tpr; + break; + } + $tip .offset(tp) .addClass(placement) .addClass('in'); - - }).call(this.container()); - /*jshint laxcomma: false*/ - } - }); + */ + + + var $tip = this.tip(); + + var placement = typeof this.options.placement == 'function' ? + this.options.placement.call(this, $tip[0], this.$element[0]) : + this.options.placement; -}(window.jQuery)); -/** -Bootstrap-datepicker. -Description and examples: https://github.com/eternicode/bootstrap-datepicker. -For **i18n** you should include js file from here: https://github.com/eternicode/bootstrap-datepicker/tree/master/js/locales -and set `language` option. -Since 1.4.0 date has different appearance in **popup** and **inline** modes. - -@class date -@extends abstractinput -@final -@example -<a href="#" id="dob" data-type="date" data-pk="1" data-url="/post" data-original-title="Select date">15/05/1984</a> -<script> -$(function(){ - $('#dob').editable({ - format: 'yyyy-mm-dd', - viewformat: 'dd/mm/yyyy', - datepicker: { - weekStart: 1 - } - } - }); -}); -</script> -**/ -(function ($) { - - var Date = function (options) { - this.init('date', options, Date.defaults); - this.initPicker(options, Date.defaults); - }; - - $.fn.editableutils.inherit(Date, $.fn.editabletypes.abstractinput); - - $.extend(Date.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; + var autoToken = /\s?auto?\s?/i; + var autoPlace = autoToken.test(placement); + if (autoPlace) { + placement = placement.replace(autoToken, '') || 'top'; } - //overriding datepicker config (as by default jQuery extend() is not recursive) - //since 1.4 datepicker internally uses viewformat instead of format. Format is for submit only - this.options.datepicker = $.extend({}, defaults.datepicker, options.datepicker, { - format: this.options.viewformat - }); - //language - this.options.datepicker.language = this.options.datepicker.language || 'en'; + var pos = this.getPosition(); + var actualWidth = $tip[0].offsetWidth; + var actualHeight = $tip[0].offsetHeight; - //store DPglobal - this.dpg = $.fn.datepicker.DPGlobal; + if (autoPlace) { + var $parent = this.$element.parent(); - //store parsed formats - this.parsedFormat = this.dpg.parseFormat(this.options.format); - this.parsedViewFormat = this.dpg.parseFormat(this.options.viewformat); - }, - - render: function () { - this.$input.datepicker(this.options.datepicker); - - //"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) { - var text = value ? this.dpg.formatDate(value, this.parsedViewFormat, this.options.datepicker.language) : ''; - Date.superclass.value2html(text, element); - }, + var orgPlacement = placement; + var docScroll = document.documentElement.scrollTop || document.body.scrollTop; + var parentWidth = this.options.container == 'body' ? window.innerWidth : $parent.outerWidth(); + var parentHeight = this.options.container == 'body' ? window.innerHeight : $parent.outerHeight(); + var parentLeft = this.options.container == 'body' ? 0 : $parent.offset().left; - html2value: function(html) { - return html ? this.dpg.parseDate(html, this.parsedViewFormat, this.options.datepicker.language) : null; - }, - - value2str: function(value) { - return value ? this.dpg.formatDate(value, this.parsedFormat, this.options.datepicker.language) : ''; - }, - - str2value: function(str) { - return str ? this.dpg.parseDate(str, this.parsedFormat, this.options.datepicker.language) : null; - }, - - value2submit: function(value) { - return this.value2str(value); - }, + placement = placement == 'bottom' && pos.top + pos.height + actualHeight - docScroll > parentHeight ? 'top' : + placement == 'top' && pos.top - docScroll - actualHeight < 0 ? 'bottom' : + placement == 'right' && pos.right + actualWidth > parentWidth ? 'left' : + placement == 'left' && pos.left - actualWidth < parentLeft ? 'right' : + placement; - value2input: function(value) { - this.$input.datepicker('update', value); - }, - - input2value: function() { - return this.$input.data('datepicker').date; - }, - - activate: function() { - }, - - clear: function() { - this.$input.data('datepicker').date = null; - this.$input.find('.active').removeClass('active'); - }, - - autosubmit: function() { - this.$input.on('mouseup', '.day', function(e){ - if($(e.currentTarget).is('.old') || $(e.currentTarget).is('.new')) { - return; - } - var $form = $(this).closest('form'); - setTimeout(function() { - $form.submit(); - }, 200); - }); - //changedate is not suitable as it triggered when showing datepicker. see #149 - /* - this.$input.on('changeDate', function(e){ - var $form = $(this).closest('form'); - setTimeout(function() { - $form.submit(); - }, 200); - }); - */ - } + $tip + .removeClass(orgPlacement) + .addClass(placement); + } - }); - - Date.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</code> - - @property format - @type string - @default yyyy-mm-dd - **/ - format:'yyyy-mm-dd', - /** - 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 datepicker. - Full list of options: http://vitalets.github.com/bootstrap-datepicker - - @property datepicker - @type object - @default { - weekStart: 0, - startView: 0, - minViewMode: 0, - autoclose: false - } - **/ - datepicker:{ - weekStart: 0, - startView: 0, - minViewMode: 0, - 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.date = Date; + var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight); -}(window.jQuery)); - -/** -Bootstrap datefield input - modification for inline mode. -Shows normal <input type="text"> and binds popup datepicker. -Automatically shown in inline mode. - -@class datefield -@extends date - -@since 1.4.0 -**/ -(function ($) { - - var DateField = function (options) { - this.init('datefield', options, DateField.defaults); - this.initPicker(options, DateField.defaults); - }; - - $.fn.editableutils.inherit(DateField, $.fn.editabletypes.date); - - $.extend(DateField.prototype, { - render: function () { - this.$input = this.$tpl.find('input'); - this.setClass(); - this.setAttr('placeholder'); - - this.$tpl.datepicker(this.options.datepicker); - - //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.datepicker('update'); - }, this)); - - }, - - value2input: function(value) { - this.$input.val(value ? this.dpg.formatDate(value, this.parsedViewFormat, this.options.datepicker.language) : ''); - this.$tpl.datepicker('update'); - }, - - input2value: function() { - return this.html2value(this.$input.val()); - }, - - activate: function() { - $.fn.editabletypes.text.prototype.activate.call(this); - }, - - autosubmit: function() { - //reset autosubmit to empty - } + this.applyPlacement(calculatedOffset, placement); + + + }).call(this.container()); + /*jshint laxcomma: false, eqeqeq: true*/ + } }); - - DateField.defaults = $.extend({}, $.fn.editabletypes.date.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-small' - **/ - inputclass: 'input-small', - - /* datepicker config */ - datepicker: { - weekStart: 0, - startView: 0, - minViewMode: 0, - autoclose: true - } - }); - - $.fn.editabletypes.datefield = DateField; -}(window.jQuery)); +}(window.jQuery)); + /* ========================================================= * bootstrap-datepicker.js * http://www.eyecon.ro/bootstrap-datepicker * ========================================================= * Copyright 2012 Stefan Petre @@ -4443,11 +4936,11 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ========================================================= */ -!function( $ ) { +(function( $ ) { function UTCDate(){ return new Date(Date.UTC.apply(Date, arguments)); } function UTCToday(){ @@ -4458,125 +4951,56 @@ // Picker object var Datepicker = function(element, options) { var that = this; + this._process_options(options); + this.element = $(element); - this.language = options.language||this.element.data('date-language')||"en"; - this.language = this.language in dates ? this.language : this.language.split('-')[0]; //Check if "de-DE" style date is available, if not language should fallback to 2 letter code eg "de" - 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.picker = $(DPGlobal.template); + this._buildEvents(); 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) - .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){ + + if (this.o.rtl){ 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()) { - this.autoclose = this.element.data('date-autoclose'); - } - this.keyboardNavigation = true; - if ('keyboardNavigation' in options) { - this.keyboardNavigation = options.keyboardNavigation; - } else if ('dateKeyboardNavigation' in this.element.data()) { - this.keyboardNavigation = this.element.data('date-keyboard-navigation'); - } + this.viewMode = this.o.startView; - this.viewMode = this.startViewMode = 0; - switch(options.startView || this.element.data('date-start-view')){ - case 2: - case 'decade': - this.viewMode = this.startViewMode = 2; - break; - case 1: - case 'year': - this.viewMode = this.startViewMode = 1; - break; - } - - this.minViewMode = options.minViewMode||this.element.data('date-min-view-mode')||0; - if (typeof this.minViewMode === 'string') { - switch (this.minViewMode) { - case 'months': - this.minViewMode = 1; - break; - case 'years': - this.minViewMode = 2; - break; - default: - this.minViewMode = 0; - break; - } - } - - this.viewMode = this.startViewMode = Math.max(this.startViewMode, this.minViewMode); - - this.todayBtn = (options.todayBtn||this.element.data('date-today-btn')||false); - this.todayHighlight = (options.todayHighlight||this.element.data('date-today-highlight')||false); - - this.calendarWeeks = false; - if ('calendarWeeks' in options) { - this.calendarWeeks = options.calendarWeeks; - } else if ('dateCalendarWeeks' in this.element.data()) { - this.calendarWeeks = this.element.data('date-calendar-weeks'); - } - if (this.calendarWeeks) + if (this.o.calendarWeeks) this.picker.find('tfoot th.today') .attr('colspan', function(i, val){ return parseInt(val) + 1; }); - 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._allow_update = false; + + this.setStartDate(this.o.startDate); + this.setEndDate(this.o.endDate); + this.setDaysOfWeekDisabled(this.o.daysOfWeekDisabled); + this.fillDow(); this.fillMonths(); + + this._allow_update = true; + this.update(); this.showMode(); if(this.isInline) { this.show(); @@ -4584,13 +5008,89 @@ }; Datepicker.prototype = { constructor: Datepicker, + _process_options: function(opts){ + // Store raw options for reference + this._o = $.extend({}, this._o, opts); + // Processed options + var o = this.o = $.extend({}, this._o); + + // Check if "de-DE" style date is available, if not language should + // fallback to 2 letter code eg "de" + var lang = o.language; + if (!dates[lang]) { + lang = lang.split('-')[0]; + if (!dates[lang]) + lang = defaults.language; + } + o.language = lang; + + switch(o.startView){ + case 2: + case 'decade': + o.startView = 2; + break; + case 1: + case 'year': + o.startView = 1; + break; + default: + o.startView = 0; + } + + switch (o.minViewMode) { + case 1: + case 'months': + o.minViewMode = 1; + break; + case 2: + case 'years': + o.minViewMode = 2; + break; + default: + o.minViewMode = 0; + } + + o.startView = Math.max(o.startView, o.minViewMode); + + o.weekStart %= 7; + o.weekEnd = ((o.weekStart + 6) % 7); + + var format = DPGlobal.parseFormat(o.format) + if (o.startDate !== -Infinity) { + o.startDate = DPGlobal.parseDate(o.startDate, format, o.language); + } + if (o.endDate !== Infinity) { + o.endDate = DPGlobal.parseDate(o.endDate, format, o.language); + } + + o.daysOfWeekDisabled = o.daysOfWeekDisabled||[]; + if (!$.isArray(o.daysOfWeekDisabled)) + o.daysOfWeekDisabled = o.daysOfWeekDisabled.split(/[,\s]*/); + o.daysOfWeekDisabled = $.map(o.daysOfWeekDisabled, function (d) { + return parseInt(d, 10); + }); + }, _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,80 +5109,112 @@ [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 (!( + this.element.is(e.target) || + this.element.find(e.target).size() || + this.picker.is(e.target) || + this.picker.find(e.target).size() + )) { + 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); + }, + _trigger: function(event, altdate){ + var date = altdate || this.date, + local_date = new Date(date.getTime() + (date.getTimezoneOffset()*60000)); + this.element.trigger({ + type: event, + date: local_date, + format: $.proxy(function(altformat){ + var format = altformat || this.o.format; + return DPGlobal.formatDate(date, format, this.o.language); + }, this) + }); + }, + 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 - }); + this._trigger('show'); }, hide: function(e){ if(this.isInline) return; if (!this.picker.is(':visible')) return; - this.picker.hide(); - $(window).off('resize', this.place); - this.viewMode = this.startViewMode; + this.picker.hide().detach(); + this._detachSecondaryEvents(); + this.viewMode = this.o.startView; this.showMode(); - if (!this.isInput) { - $(document).off('mousedown', this.hide); - } if ( - this.forceParse && + this.o.forceParse && ( this.isInput && this.element.val() || this.hasInput && this.element.find('input').val() ) ) this.setValue(); - this.element.trigger({ - type: 'hide', - date: this.date - }); + this._trigger('hide'); }, 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)); @@ -4705,180 +5237,216 @@ var formatted = this.getFormattedDate(); if (!this.isInput) { if (this.component){ this.element.find('input').val(formatted); } - this.element.data('date', formatted); } else { this.element.val(formatted); } }, getFormattedDate: function(format) { if (format === undefined) - format = this.format; - return DPGlobal.formatDate(this.date, format, this.language); + format = this.o.format; + return DPGlobal.formatDate(this.date, format, this.o.language); }, setStartDate: function(startDate){ - this.startDate = startDate||-Infinity; - if (this.startDate !== -Infinity) { - this.startDate = DPGlobal.parseDate(this.startDate, this.format, this.language); - } + this._process_options({startDate: startDate}); this.update(); this.updateNavArrows(); }, setEndDate: function(endDate){ - this.endDate = endDate||Infinity; - if (this.endDate !== Infinity) { - this.endDate = DPGlobal.parseDate(this.endDate, this.format, this.language); - } + this._process_options({endDate: endDate}); this.update(); this.updateNavArrows(); }, setDaysOfWeekDisabled: function(daysOfWeekDisabled){ - this.daysOfWeekDisabled = daysOfWeekDisabled||[]; - if (!$.isArray(this.daysOfWeekDisabled)) { - this.daysOfWeekDisabled = this.daysOfWeekDisabled.split(/,\s*/); - } - this.daysOfWeekDisabled = $.map(this.daysOfWeekDisabled, function (d) { - return parseInt(d, 10); - }); + this._process_options({daysOfWeekDisabled: daysOfWeekDisabled}); this.update(); this.updateNavArrows(); }, 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 { date = this.isInput ? this.element.val() : this.element.data('date') || this.element.find('input').val(); + delete this.element.data().date; } - this.date = DPGlobal.parseDate(date, this.format, this.language); + this.date = DPGlobal.parseDate(date, this.o.format, this.o.language); if(fromArgs) this.setValue(); - if (this.date < this.startDate) { - this.viewDate = new Date(this.startDate); - } else if (this.date > this.endDate) { - this.viewDate = new Date(this.endDate); + if (this.date < this.o.startDate) { + this.viewDate = new Date(this.o.startDate); + } else if (this.date > this.o.endDate) { + this.viewDate = new Date(this.o.endDate); } else { this.viewDate = new Date(this.date); } this.fill(); }, fillDow: function(){ - var dowCnt = this.weekStart, + var dowCnt = this.o.weekStart, html = '<tr>'; - if(this.calendarWeeks){ + if(this.o.calendarWeeks){ var cell = '<th class="cw">&nbsp;</th>'; html += cell; this.picker.find('.datepicker-days thead tr:first-child').prepend(cell); } - while (dowCnt < this.weekStart + 7) { - html += '<th class="dow">'+dates[this.language].daysMin[(dowCnt++)%7]+'</th>'; + while (dowCnt < this.o.weekStart + 7) { + html += '<th class="dow">'+dates[this.o.language].daysMin[(dowCnt++)%7]+'</th>'; } html += '</tr>'; this.picker.find('.datepicker-days thead').append(html); }, fillMonths: function(){ var html = '', i = 0; while (i < 12) { - html += '<span class="month">'+dates[this.language].monthsShort[i++]+'</span>'; + html += '<span class="month">'+dates[this.o.language].monthsShort[i++]+'</span>'; } this.picker.find('.datepicker-months td').html(html); }, + setRange: function(range){ + if (!range || !range.length) + delete this.range; + else + this.range = $.map(range, function(d){ return d.valueOf(); }); + this.fill(); + }, + + getClassNames: function(date){ + var cls = [], + year = this.viewDate.getUTCFullYear(), + month = this.viewDate.getUTCMonth(), + currentDate = this.date.valueOf(), + today = new Date(); + if (date.getUTCFullYear() < year || (date.getUTCFullYear() == year && date.getUTCMonth() < month)) { + cls.push('old'); + } else if (date.getUTCFullYear() > year || (date.getUTCFullYear() == year && date.getUTCMonth() > month)) { + cls.push('new'); + } + // Compare internal UTC date with local today, not UTC today + if (this.o.todayHighlight && + date.getUTCFullYear() == today.getFullYear() && + date.getUTCMonth() == today.getMonth() && + date.getUTCDate() == today.getDate()) { + cls.push('today'); + } + if (currentDate && date.valueOf() == currentDate) { + cls.push('active'); + } + if (date.valueOf() < this.o.startDate || date.valueOf() > this.o.endDate || + $.inArray(date.getUTCDay(), this.o.daysOfWeekDisabled) !== -1) { + cls.push('disabled'); + } + if (this.range){ + if (date > this.range[0] && date < this.range[this.range.length-1]){ + cls.push('range'); + } + if ($.inArray(date.valueOf(), this.range) != -1){ + cls.push('selected'); + } + } + return cls; + }, + fill: function() { var d = new Date(this.viewDate), year = d.getUTCFullYear(), month = d.getUTCMonth(), - startYear = this.startDate !== -Infinity ? this.startDate.getUTCFullYear() : -Infinity, - startMonth = this.startDate !== -Infinity ? this.startDate.getUTCMonth() : -Infinity, - endYear = this.endDate !== Infinity ? this.endDate.getUTCFullYear() : Infinity, - endMonth = this.endDate !== Infinity ? this.endDate.getUTCMonth() : Infinity, + startYear = this.o.startDate !== -Infinity ? this.o.startDate.getUTCFullYear() : -Infinity, + startMonth = this.o.startDate !== -Infinity ? this.o.startDate.getUTCMonth() : -Infinity, + endYear = this.o.endDate !== Infinity ? this.o.endDate.getUTCFullYear() : Infinity, + endMonth = this.o.endDate !== Infinity ? this.o.endDate.getUTCMonth() : Infinity, currentDate = this.date && this.date.valueOf(), - today = new Date(); - this.picker.find('.datepicker-days thead th.switch') - .text(dates[this.language].months[month]+' '+year); + tooltip; + this.picker.find('.datepicker-days thead th.datepicker-switch') + .text(dates[this.o.language].months[month]+' '+year); this.picker.find('tfoot th.today') - .text(dates[this.language].today) - .toggle(this.todayBtn !== false); + .text(dates[this.o.language].today) + .toggle(this.o.todayBtn !== false); + this.picker.find('tfoot th.clear') + .text(dates[this.o.language].clear) + .toggle(this.o.clearBtn !== false); this.updateNavArrows(); this.fillMonths(); var prevMonth = UTCDate(year, month-1, 28,0,0,0,0), day = DPGlobal.getDaysInMonth(prevMonth.getUTCFullYear(), prevMonth.getUTCMonth()); prevMonth.setUTCDate(day); - prevMonth.setUTCDate(day - (prevMonth.getUTCDay() - this.weekStart + 7)%7); + prevMonth.setUTCDate(day - (prevMonth.getUTCDay() - this.o.weekStart + 7)%7); var nextMonth = new Date(prevMonth); nextMonth.setUTCDate(nextMonth.getUTCDate() + 42); nextMonth = nextMonth.valueOf(); var html = []; var clsName; while(prevMonth.valueOf() < nextMonth) { - if (prevMonth.getUTCDay() == this.weekStart) { + if (prevMonth.getUTCDay() == this.o.weekStart) { html.push('<tr>'); - if(this.calendarWeeks){ + if(this.o.calendarWeeks){ // ISO 8601: First week contains first thursday. // ISO also states week starts on Monday, but we can be more abstract here. var // Start of current week: based on weekstart/current date - ws = new Date(+prevMonth + (this.weekStart - prevMonth.getUTCDay() - 7) % 7 * 864e5), + ws = new Date(+prevMonth + (this.o.weekStart - prevMonth.getUTCDay() - 7) % 7 * 864e5), // Thursday of this week th = new Date(+ws + (7 + 4 - ws.getUTCDay()) % 7 * 864e5), // First Thursday of year, year from thursday yth = new Date(+(yth = UTCDate(th.getUTCFullYear(), 0, 1)) + (7 + 4 - yth.getUTCDay())%7*864e5), // Calendar week: ms between thursdays, div ms per day, div 7 days calWeek = (th - yth) / 864e5 / 7 + 1; html.push('<td class="cw">'+ calWeek +'</td>'); } } - clsName = ''; - if (prevMonth.getUTCFullYear() < year || (prevMonth.getUTCFullYear() == year && prevMonth.getUTCMonth() < month)) { - clsName += ' old'; - } else if (prevMonth.getUTCFullYear() > year || (prevMonth.getUTCFullYear() == year && prevMonth.getUTCMonth() > month)) { - clsName += ' new'; - } - // Compare internal UTC date with local today, not UTC today - if (this.todayHighlight && - prevMonth.getUTCFullYear() == today.getFullYear() && - prevMonth.getUTCMonth() == today.getMonth() && - prevMonth.getUTCDate() == today.getDate()) { - clsName += ' today'; - } - if (currentDate && prevMonth.valueOf() == currentDate) { - clsName += ' active'; - } - if (prevMonth.valueOf() < this.startDate || prevMonth.valueOf() > this.endDate || - $.inArray(prevMonth.getUTCDay(), this.daysOfWeekDisabled) !== -1) { - clsName += ' disabled'; - } - html.push('<td class="day'+clsName+'">'+prevMonth.getUTCDate() + '</td>'); - if (prevMonth.getUTCDay() == this.weekEnd) { + clsName = this.getClassNames(prevMonth); + clsName.push('day'); + + var before = this.o.beforeShowDay(prevMonth); + if (before === undefined) + before = {}; + else if (typeof(before) === 'boolean') + before = {enabled: before}; + else if (typeof(before) === 'string') + before = {classes: before}; + if (before.enabled === false) + clsName.push('disabled'); + if (before.classes) + clsName = clsName.concat(before.classes.split(/\s+/)); + if (before.tooltip) + tooltip = before.tooltip; + + clsName = $.unique(clsName); + html.push('<td class="'+clsName.join(' ')+'"' + (tooltip ? ' title="'+tooltip+'"' : '') + '>'+prevMonth.getUTCDate() + '</td>'); + if (prevMonth.getUTCDay() == this.o.weekEnd) { html.push('</tr>'); } prevMonth.setUTCDate(prevMonth.getUTCDate()+1); } this.picker.find('.datepicker-days tbody').empty().append(html.join('')); @@ -4909,58 +5477,59 @@ .text(year + '-' + (year + 9)) .end() .find('td'); year -= 1; for (var i = -1; i < 11; i++) { - html += '<span class="year'+(i == -1 || i == 10 ? ' old' : '')+(currentYear == year ? ' active' : '')+(year < startYear || year > endYear ? ' disabled' : '')+'">'+year+'</span>'; + html += '<span class="year'+(i == -1 ? ' old' : i == 10 ? ' new' : '')+(currentYear == year ? ' active' : '')+(year < startYear || year > endYear ? ' disabled' : '')+'">'+year+'</span>'; year += 1; } 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: - if (this.startDate !== -Infinity && year <= this.startDate.getUTCFullYear() && month <= this.startDate.getUTCMonth()) { + if (this.o.startDate !== -Infinity && year <= this.o.startDate.getUTCFullYear() && month <= this.o.startDate.getUTCMonth()) { this.picker.find('.prev').css({visibility: 'hidden'}); } else { this.picker.find('.prev').css({visibility: 'visible'}); } - if (this.endDate !== Infinity && year >= this.endDate.getUTCFullYear() && month >= this.endDate.getUTCMonth()) { + if (this.o.endDate !== Infinity && year >= this.o.endDate.getUTCFullYear() && month >= this.o.endDate.getUTCMonth()) { this.picker.find('.next').css({visibility: 'hidden'}); } else { this.picker.find('.next').css({visibility: 'visible'}); } break; case 1: case 2: - if (this.startDate !== -Infinity && year <= this.startDate.getUTCFullYear()) { + if (this.o.startDate !== -Infinity && year <= this.o.startDate.getUTCFullYear()) { this.picker.find('.prev').css({visibility: 'hidden'}); } else { this.picker.find('.prev').css({visibility: 'visible'}); } - if (this.endDate !== Infinity && year >= this.endDate.getUTCFullYear()) { + if (this.o.endDate !== Infinity && year >= this.o.endDate.getUTCFullYear()) { this.picker.find('.next').css({visibility: 'hidden'}); } else { this.picker.find('.next').css({visibility: 'visible'}); } 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': switch(target[0].className) { - case 'switch': + case 'datepicker-switch': this.showMode(1); break; case 'prev': case 'next': var dir = DPGlobal.modes[this.viewMode].navStep * (target[0].className == 'prev' ? -1 : 1); @@ -4978,40 +5547,47 @@ case 'today': var date = new Date(); date = UTCDate(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0); this.showMode(-2); - var which = this.todayBtn == 'linked' ? null : 'view'; + var which = this.o.todayBtn == 'linked' ? null : 'view'; this._setDate(date, which); break; + case 'clear': + var element; + if (this.isInput) + element = this.element; + else if (this.component) + element = this.element.find('input'); + if (element) + element.val("").change(); + this._trigger('changeDate'); + this.update(); + if (this.o.autoclose) + this.hide(); + break; } break; case 'span': if (!target.is('.disabled')) { this.viewDate.setUTCDate(1); if (target.is('.month')) { var day = 1; var month = target.parent().find('span').index(target); var year = this.viewDate.getUTCFullYear(); this.viewDate.setUTCMonth(month); - this.element.trigger({ - type: 'changeMonth', - date: this.viewDate - }); - if ( this.minViewMode == 1 ) { + this._trigger('changeMonth', this.viewDate); + if (this.o.minViewMode === 1) { this._setDate(UTCDate(year, month, day,0,0,0,0)); } } else { var year = parseInt(target.text(), 10)||0; var day = 1; var month = 0; this.viewDate.setUTCFullYear(year); - this.element.trigger({ - type: 'changeYear', - date: this.viewDate - }); - if ( this.minViewMode == 2 ) { + this._trigger('changeYear', this.viewDate); + if (this.o.minViewMode === 2) { this._setDate(UTCDate(year, month, day,0,0,0,0)); } } this.showMode(-1); this.fill(); @@ -5044,28 +5620,25 @@ } }, _setDate: function(date, which){ if (!which || which == 'date') - this.date = date; + this.date = new Date(date); if (!which || which == 'view') - this.viewDate = date; + this.viewDate = new Date(date); this.fill(); this.setValue(); - this.element.trigger({ - type: 'changeDate', - date: this.date - }); + this._trigger('changeDate'); var element; if (this.isInput) { element = this.element; } else if (this.component){ element = this.element.find('input'); } if (element) { element.change(); - if (this.autoclose && (!which || which == 'date')) { + if (this.o.autoclose && (!which || which == 'date')) { this.hide(); } } }, @@ -5112,11 +5685,11 @@ moveYear: function(date, dir){ return this.moveMonth(date, dir*12); }, dateWithinRange: function(date){ - return date >= this.startDate && date <= this.endDate; + return date >= this.o.startDate && date <= this.o.endDate; }, keydown: function(e){ if (this.picker.is(':not(:visible)')){ if (e.keyCode == 27) // allow escape to hide and re-show picker @@ -5131,11 +5704,11 @@ this.hide(); e.preventDefault(); break; case 37: // left case 39: // right - if (!this.keyboardNavigation) break; + if (!this.o.keyboardNavigation) break; dir = e.keyCode == 37 ? -1 : 1; if (e.ctrlKey){ newDate = this.moveYear(this.date, dir); newViewDate = this.moveYear(this.viewDate, dir); } else if (e.shiftKey){ @@ -5156,11 +5729,11 @@ dateChanged = true; } break; case 38: // up case 40: // down - if (!this.keyboardNavigation) break; + if (!this.o.keyboardNavigation) break; dir = e.keyCode == 38 ? -1 : 1; if (e.ctrlKey){ newDate = this.moveYear(this.date, dir); newViewDate = this.moveYear(this.viewDate, dir); } else if (e.shiftKey){ @@ -5188,14 +5761,11 @@ case 9: // tab this.hide(); break; } if (dateChanged){ - this.element.trigger({ - type: 'changeDate', - date: this.date - }); + this._trigger('changeDate'); var element; if (this.isInput) { element = this.element; } else if (this.component){ element = this.element.find('input'); @@ -5206,11 +5776,11 @@ } }, showMode: function(dir) { if (dir) { - this.viewMode = Math.max(this.minViewMode, Math.min(2, this.viewMode + dir)); + this.viewMode = Math.max(this.o.minViewMode, Math.min(2, this.viewMode + dir)); } /* vitalets: fixing bug of very special conditions: jquery 1.7.1 + webkit + show inline datepicker in bootstrap popover. Method show() does not set display css correctly and datepicker is not shown. @@ -5223,37 +5793,165 @@ this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).css('display', 'block'); this.updateNavArrows(); } }; - $.fn.datepicker = function ( option ) { + var DateRangePicker = function(element, options){ + this.element = $(element); + this.inputs = $.map(options.inputs, function(i){ return i.jquery ? i[0] : i; }); + delete options.inputs; + + $(this.inputs) + .datepicker(options) + .bind('changeDate', $.proxy(this.dateUpdated, this)); + + this.pickers = $.map(this.inputs, function(i){ return $(i).data('datepicker'); }); + this.updateDates(); + }; + DateRangePicker.prototype = { + updateDates: function(){ + this.dates = $.map(this.pickers, function(i){ return i.date; }); + this.updateRanges(); + }, + updateRanges: function(){ + var range = $.map(this.dates, function(d){ return d.valueOf(); }); + $.each(this.pickers, function(i, p){ + p.setRange(range); + }); + }, + dateUpdated: function(e){ + var dp = $(e.target).data('datepicker'), + new_date = dp.getUTCDate(), + i = $.inArray(e.target, this.inputs), + l = this.inputs.length; + if (i == -1) return; + + if (new_date < this.dates[i]){ + // Date being moved earlier/left + while (i>=0 && new_date < this.dates[i]){ + this.pickers[i--].setUTCDate(new_date); + } + } + else if (new_date > this.dates[i]){ + // Date being moved later/right + while (i<l && new_date > this.dates[i]){ + this.pickers[i++].setUTCDate(new_date); + } + } + this.updateDates(); + }, + remove: function(){ + $.map(this.pickers, function(p){ p.remove(); }); + delete this.element.data().datepicker; + } + }; + + function opts_from_el(el, prefix){ + // Derive options from element data-attrs + var data = $(el).data(), + out = {}, inkey, + replace = new RegExp('^' + prefix.toLowerCase() + '([A-Z])'), + prefix = new RegExp('^' + prefix.toLowerCase()); + for (var key in data) + if (prefix.test(key)){ + inkey = key.replace(replace, function(_,a){ return a.toLowerCase(); }); + out[inkey] = data[key]; + } + return out; + } + + function opts_from_locale(lang){ + // Derive options from locale plugins + var out = {}; + // Check if "de-DE" style date is available, if not language should + // fallback to 2 letter code eg "de" + if (!dates[lang]) { + lang = lang.split('-')[0] + if (!dates[lang]) + return; + } + var d = dates[lang]; + $.each(locale_opts, function(i,k){ + if (k in d) + out[k] = d[k]; + }); + return out; + } + + var old = $.fn.datepicker; + var datepicker = $.fn.datepicker = function ( option ) { var args = Array.apply(null, arguments); args.shift(); - return this.each(function () { + var internal_return, + this_return; + this.each(function () { var $this = $(this), data = $this.data('datepicker'), options = typeof option == 'object' && option; if (!data) { - $this.data('datepicker', (data = new Datepicker(this, $.extend({}, $.fn.datepicker.defaults,options)))); + var elopts = opts_from_el(this, 'date'), + // Preliminary otions + xopts = $.extend({}, defaults, elopts, options), + locopts = opts_from_locale(xopts.language), + // Options priority: js args, data-attrs, locales, defaults + opts = $.extend({}, defaults, locopts, elopts, options); + if ($this.is('.input-daterange') || opts.inputs){ + var ropts = { + inputs: opts.inputs || $this.find('input').toArray() + }; + $this.data('datepicker', (data = new DateRangePicker(this, $.extend(opts, ropts)))); + } + else{ + $this.data('datepicker', (data = new Datepicker(this, opts))); + } } if (typeof option == 'string' && typeof data[option] == 'function') { - data[option].apply(data, args); + internal_return = data[option].apply(data, args); + if (internal_return !== undefined) + return false; } }); + if (internal_return !== undefined) + return internal_return; + else + return this; }; - $.fn.datepicker.defaults = { + var defaults = $.fn.datepicker.defaults = { + autoclose: false, + beforeShowDay: $.noop, + calendarWeeks: false, + clearBtn: false, + daysOfWeekDisabled: [], + endDate: Infinity, + forceParse: true, + format: 'mm/dd/yyyy', + keyboardNavigation: true, + language: 'en', + minViewMode: 0, + rtl: false, + startDate: -Infinity, + startView: 0, + todayBtn: false, + todayHighlight: false, + weekStart: 0 }; + var locale_opts = $.fn.datepicker.locale_opts = [ + 'format', + 'rtl', + 'weekStart' + ]; $.fn.datepicker.Constructor = Datepicker; var dates = $.fn.datepicker.dates = { en: { days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"], months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], - today: "Today" + today: "Today", + clear: "Clear" } }; var DPGlobal = { modes: [ @@ -5290,10 +5988,12 @@ } return {separators: separators, parts: parts}; }, parseDate: function(date, format, language) { if (date instanceof Date) return date; + if (typeof format === 'string') + format = DPGlobal.parseFormat(format); if (/^[\-+]\d+[dmwy]([\s,]+[\-+]\d+[dmwy])*$/.test(date)) { var part_re = /([\-+]\d+)([dmwy])/, parts = date.match(/([\-+]\d+)([dmwy])/g), part, dir; date = new Date(); @@ -5380,10 +6080,12 @@ } } return date; }, formatDate: function(date, format, language){ + if (typeof format === 'string') + format = DPGlobal.parseFormat(format); var val = { d: date.getUTCDate(), D: dates[language].daysShort[date.getUTCDay()], DD: dates[language].days[date.getUTCDay()], m: date.getUTCMonth() + 1, @@ -5394,26 +6096,26 @@ }; val.dd = (val.d < 10 ? '0' : '') + val.d; val.mm = (val.m < 10 ? '0' : '') + val.m; var date = [], seps = $.extend([], format.separators); - for (var i=0, cnt = format.parts.length; i < cnt; i++) { + for (var i=0, cnt = format.parts.length; i <= cnt; i++) { if (seps.length) date.push(seps.shift()); date.push(val[format.parts[i]]); } return date.join(''); }, headTemplate: '<thead>'+ '<tr>'+ '<th class="prev"><i class="icon-arrow-left"/></th>'+ - '<th colspan="5" class="switch"></th>'+ + '<th colspan="5" class="datepicker-switch"></th>'+ '<th class="next"><i class="icon-arrow-right"/></th>'+ '</tr>'+ '</thead>', contTemplate: '<tbody><tr><td colspan="7"></td></tr></tbody>', - footTemplate: '<tfoot><tr><th colspan="7" class="today"></th></tr></tfoot>' + footTemplate: '<tfoot><tr><th colspan="7" class="today"></th></tr><tr><th colspan="7" class="clear"></th></tr></tfoot>' }; DPGlobal.template = '<div class="datepicker">'+ '<div class="datepicker-days">'+ '<table class=" table-condensed">'+ DPGlobal.headTemplate+ @@ -5437,275 +6139,669 @@ '</div>'+ '</div>'; $.fn.datepicker.DPGlobal = DPGlobal; -}( 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' ...]`. - User can submit one of these strings or any text entered in input (even if it is not matching source). - -* **objects**: - When `source` defined as array of objects, e.g. `[{value: 1, text: "text1"}, {value: 2, text: "text2"}, ...]`. - User can submit only values that are in source (otherwise `null` is submitted). This is more like *dropdown* behavior. + /* DATEPICKER NO CONFLICT + * =================== */ -@class typeahead -@extends list -@since 1.4.1 + $.fn.datepicker.noConflict = function(){ + $.fn.datepicker = old; + return this; + }; + + + /* DATEPICKER DATA-API + * ================== */ + + $(document).on( + 'focus.datepicker.data-api click.datepicker.data-api', + '[data-provide="datepicker"]', + function(e){ + var $this = $(this); + if ($this.data('datepicker')) return; + e.preventDefault(); + // component click requires us to explicitly show it + datepicker.call($this, 'show'); + } + ); + $(function(){ + //$('[data-provide="datepicker-inline"]').datepicker(); + //vit: changed to support noConflict() + datepicker.call($('[data-provide="datepicker-inline"]')); + }); + +}( window.jQuery )); + +/** +Bootstrap-datepicker. +Description and examples: https://github.com/eternicode/bootstrap-datepicker. +For **i18n** you should include js file from here: https://github.com/eternicode/bootstrap-datepicker/tree/master/js/locales +and set `language` option. +Since 1.4.0 date has different appearance in **popup** and **inline** modes. + +@class date +@extends abstractinput @final @example -<a href="#" id="country" data-type="typeahead" data-pk="1" data-url="/post" data-original-title="Input country"></a> +<a href="#" id="dob" data-type="date" data-pk="1" data-url="/post" data-title="Select date">15/05/1984</a> <script> $(function(){ - $('#country').editable({ - value: 'ru', - source: [ - {value: 'gb', text: 'Great Britain'}, - {value: 'us', text: 'United States'}, - {value: 'ru', text: 'Russia'} - ] + $('#dob').editable({ + format: 'yyyy-mm-dd', + viewformat: 'dd/mm/yyyy', + datepicker: { + weekStart: 1 + } + } }); }); </script> **/ (function ($) { - - 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, { - //set default methods for typeahead to work with objects - matcher: this.matcher, - sorter: this.sorter, - highlighter: this.highlighter, - updater: this.updater - }, options.typeahead); + "use strict"; + + //store bootstrap-datepicker as bdateicker to exclude conflict with jQuery UI one + $.fn.bdatepicker = $.fn.datepicker.noConflict(); + if(!$.fn.datepicker) { //if there were no other datepickers, keep also original name + $.fn.datepicker = $.fn.bdatepicker; + } + + var Date = function (options) { + this.init('date', options, Date.defaults); + this.initPicker(options, Date.defaults); }; - $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.list); + $.fn.editableutils.inherit(Date, $.fn.editabletypes.abstractinput); + + $.extend(Date.prototype, { + initPicker: function(options, defaults) { + //'format' is set directly from settings or data-* attributes - $.extend(Constructor.prototype, { - renderList: function() { - this.$input = this.$tpl.is('input') ? this.$tpl : this.$tpl.find('input[type="text"]'); + //by default viewformat equals to format + if(!this.options.viewformat) { + this.options.viewformat = this.options.format; + } - //set source of typeahead - this.options.typeahead.source = this.sourceData; + //try parse datepicker config defined as json string in data-datepicker + options.datepicker = $.fn.editableutils.tryParseJson(options.datepicker, true); - //apply typeahead - this.$input.typeahead(this.options.typeahead); + //overriding datepicker config (as by default jQuery extend() is not recursive) + //since 1.4 datepicker internally uses viewformat instead of format. Format is for submit only + this.options.datepicker = $.extend({}, defaults.datepicker, options.datepicker, { + format: this.options.viewformat + }); - //patch some methods in typeahead - var ta = this.$input.data('typeahead'); - ta.render = $.proxy(this.typeaheadRender, ta); - ta.select = $.proxy(this.typeaheadSelect, ta); - ta.move = $.proxy(this.typeaheadMove, ta); + //language + this.options.datepicker.language = this.options.datepicker.language || 'en'; - this.renderClear(); - this.setClass(); - this.setAttr('placeholder'); + //store DPglobal + this.dpg = $.fn.bdatepicker.DPGlobal; + + //store parsed formats + this.parsedFormat = this.dpg.parseFormat(this.options.format); + this.parsedViewFormat = this.dpg.parseFormat(this.options.viewformat); }, - - value2htmlFinal: function(value, element) { - if(this.getIsObjects()) { - var items = $.fn.editableutils.itemsByValue(value, this.sourceData); - $(element).text(items.length ? items[0].text : ''); - } else { - $(element).text(value); - } - }, - html2value: function (html) { - return html ? html : null; + render: function () { + this.$input.bdatepicker(this.options.datepicker); + + //"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) { + var text = value ? this.dpg.formatDate(value, this.parsedViewFormat, this.options.datepicker.language) : ''; + Date.superclass.value2html.call(this, text, element); + }, + + html2value: function(html) { + return this.parseDate(html, this.parsedViewFormat); + }, + + value2str: function(value) { + return value ? this.dpg.formatDate(value, this.parsedFormat, this.options.datepicker.language) : ''; + }, + + str2value: function(str) { + return this.parseDate(str, this.parsedFormat); + }, + + value2submit: function(value) { + return this.value2str(value); + }, + value2input: function(value) { - if(this.getIsObjects()) { - var items = $.fn.editableutils.itemsByValue(value, this.sourceData); - this.$input.data('value', value).val(items.length ? items[0].text : ''); - } else { - this.$input.val(value); - } + this.$input.bdatepicker('update', value); }, - - input2value: function() { - if(this.getIsObjects()) { - var value = this.$input.data('value'), - items = $.fn.editableutils.itemsByValue(value, this.sourceData); - - if(items.length && items[0].text.toLowerCase() === this.$input.val().toLowerCase()) { - return value; - } else { - return null; //entered string not found in source - } - } else { - return this.$input.val(); + + input2value: function() { + return this.$input.data('datepicker').date; + }, + + activate: function() { + }, + + clear: function() { + this.$input.data('datepicker').date = null; + this.$input.find('.active').removeClass('active'); + if(!this.options.showbuttons) { + this.$input.closest('form').submit(); } }, - - /* - if in sourceData values <> texts, typeahead in "objects" mode: - user must pick some value from list, otherwise `null` returned. - if all values == texts put typeahead in "strings" mode: - anything what entered is submited. - */ - getIsObjects: function() { - if(this.isObjects === undefined) { - this.isObjects = false; - for(var i=0; i<this.sourceData.length; i++) { - if(this.sourceData[i].value !== this.sourceData[i].text) { - this.isObjects = true; - break; - } + + autosubmit: function() { + this.$input.on('mouseup', '.day', function(e){ + if($(e.currentTarget).is('.old') || $(e.currentTarget).is('.new')) { + return; } - } - return this.isObjects; - }, - - /* - Methods borrowed from text input - */ - activate: $.fn.editabletypes.text.prototype.activate, - renderClear: $.fn.editabletypes.text.prototype.renderClear, - postrender: $.fn.editabletypes.text.prototype.postrender, - toggleClear: $.fn.editabletypes.text.prototype.toggleClear, - clear: function() { - $.fn.editabletypes.text.prototype.clear.call(this); - this.$input.data('value', ''); + var $form = $(this).closest('form'); + setTimeout(function() { + $form.submit(); + }, 200); + }); + //changedate is not suitable as it triggered when showing datepicker. see #149 + /* + this.$input.on('changeDate', function(e){ + var $form = $(this).closest('form'); + setTimeout(function() { + $form.submit(); + }, 200); + }); + */ + }, + + /* + For incorrect date bootstrap-datepicker returns current date that is not suitable + for datefield. + This function returns null for incorrect date. + */ + parseDate: function(str, format) { + var date = null, formattedBack; + if(str) { + date = this.dpg.parseDate(str, format, this.options.datepicker.language); + if(typeof str === 'string') { + formattedBack = this.dpg.formatDate(date, format, this.options.datepicker.language); + if(str !== formattedBack) { + date = null; + } + } + } + return date; + } + + }); + + Date.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</code> + + @property format + @type string + @default yyyy-mm-dd + **/ + format:'yyyy-mm-dd', + /** + 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 datepicker. + Full list of options: http://bootstrap-datepicker.readthedocs.org/en/latest/options.html + + @property datepicker + @type object + @default { + weekStart: 0, + startView: 0, + minViewMode: 0, + autoclose: false + } + **/ + datepicker:{ + weekStart: 0, + startView: 0, + minViewMode: 0, + 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.date = Date; + +}(window.jQuery)); + +/** +Bootstrap datefield input - modification for inline mode. +Shows normal <input type="text"> and binds popup datepicker. +Automatically shown in inline mode. + +@class datefield +@extends date + +@since 1.4.0 +**/ +(function ($) { + "use strict"; + + var DateField = function (options) { + this.init('datefield', options, DateField.defaults); + this.initPicker(options, DateField.defaults); + }; + + $.fn.editableutils.inherit(DateField, $.fn.editabletypes.date); + + $.extend(DateField.prototype, { + render: function () { + this.$input = this.$tpl.find('input'); + this.setClass(); + this.setAttr('placeholder'); + + //bootstrap-datepicker is set `bdateicker` to exclude conflict with jQuery UI one. (in date.js) + this.$tpl.bdatepicker(this.options.datepicker); + + //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.bdatepicker('update'); + }, this)); + + }, + value2input: function(value) { + this.$input.val(value ? this.dpg.formatDate(value, this.parsedViewFormat, this.options.datepicker.language) : ''); + this.$tpl.bdatepicker('update'); + }, - /* - Typeahead option methods used as defaults - */ - /*jshint eqeqeq:false, curly: false, laxcomma: true, asi: true*/ - matcher: function (item) { - return $.fn.typeahead.Constructor.prototype.matcher.call(this, item.text); - }, - sorter: function (items) { - var beginswith = [] - , caseSensitive = [] - , caseInsensitive = [] - , item - , text; + input2value: function() { + return this.html2value(this.$input.val()); + }, + + activate: function() { + $.fn.editabletypes.text.prototype.activate.call(this); + }, + + autosubmit: function() { + //reset autosubmit to empty + } + }); + + DateField.defaults = $.extend({}, $.fn.editabletypes.date.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-small' + **/ + inputclass: 'input-small', + + /* datepicker config */ + datepicker: { + weekStart: 0, + startView: 0, + minViewMode: 0, + autoclose: true + } + }); + + $.fn.editabletypes.datefield = DateField; - while (item = items.shift()) { - text = item.text; - if (!text.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item); - else if (~text.indexOf(this.query)) caseSensitive.push(item); - else caseInsensitive.push(item); +}(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; } + + //try parse datetimepicker config defined as json string in data-datetimepicker + options.datetimepicker = $.fn.editableutils.tryParseJson(options.datetimepicker, true); - return beginswith.concat(caseSensitive, caseInsensitive); + //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); }, - highlighter: function (item) { - return $.fn.typeahead.Constructor.prototype.highlighter.call(this, item.text); - }, - updater: function (item) { - this.$element.data('value', item.value); - return item.text; - }, - - - /* - Overwrite typeahead's render method to store objects. - There are a lot of disscussion in bootstrap repo on this point and still no result. - See https://github.com/twitter/bootstrap/issues/5967 - - This function just store item via jQuery data() method instead of attr('data-value') - */ - typeaheadRender: function (items) { - var that = this; - items = $(items).map(function (i, item) { -// i = $(that.options.item).attr('data-value', item) - i = $(that.options.item).data('item', item); - i.find('a').html(that.highlighter(item)); - return i[0]; + render: function () { + this.$input.datetimepicker(this.options.datetimepicker); + + //adjust container position when viewMode changes + //see https://github.com/smalot/bootstrap-datetimepicker/pull/80 + this.$input.on('changeMode', function(e) { + var f = $(this).closest('form').parent(); + //timeout here, otherwise container changes position before form has new size + setTimeout(function(){ + f.triggerHandler('resize'); + }, 0); }); - //add option to disable autoselect of first line - //see https://github.com/twitter/bootstrap/pull/4164 - if (this.options.autoSelect) { - items.first().addClass('active'); + //"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)); } - this.$menu.html(items); - return this; }, - - //add option to disable autoselect of first line - //see https://github.com/twitter/bootstrap/pull/4164 - typeaheadSelect: function () { - var val = this.$menu.find('.active').data('item') - if(this.options.autoSelect || val){ - this.$element - .val(this.updater(val)) - .change() - } - return this.hide() + + 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.call(this, text, element); + } else { + return text; + } }, - - /* - if autoSelect = false and nothing matched we need extra press onEnter that is not convinient. - This patch fixes it. - */ - typeaheadMove: function (e) { - if (!this.shown) return - switch(e.keyCode) { - case 9: // tab - case 13: // enter - case 27: // escape - if (!this.$menu.find('.active').length) return - e.preventDefault() - break + html2value: function(html) { + //parseDate return utc date! + var value = this.parseDate(html, this.parsedViewFormat); + return value ? this.fromUTC(value) : null; + }, - case 38: // up arrow - e.preventDefault() - this.prev() - break + value2str: function(value) { + //formatDate works with UTCDate! + return value ? this.dpg.formatDate(this.toUTC(value), this.parsedFormat, this.options.datetimepicker.language, this.options.formatType) : ''; + }, - case 40: // down arrow - e.preventDefault() - this.next() - break - } + str2value: function(str) { + //parseDate return utc date! + var value = this.parseDate(str, this.parsedFormat); + return value ? this.fromUTC(value) : null; + }, - e.stopPropagation() - } - - /*jshint eqeqeq: true, curly: true, laxcomma: false, asi: false*/ - - }); + value2submit: function(value) { + return this.value2str(value); + }, - Constructor.defaults = $.extend({}, $.fn.editabletypes.list.defaults, { + 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; + }, + + /* + For incorrect date bootstrap-datetimepicker returns current date that is not suitable + for datetimefield. + This function returns null for incorrect date. + */ + parseDate: function(str, format) { + var date = null, formattedBack; + if(str) { + date = this.dpg.parseDate(str, format, this.options.datetimepicker.language, this.options.formatType); + if(typeof str === 'string') { + formattedBack = this.dpg.formatDate(date, format, this.options.datetimepicker.language, this.options.formatType); + if(str !== formattedBack) { + date = null; + } + } + } + return date; + } + + }); + + DateTime.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { /** @property tpl - @default <input type="text"> + @default <div></div> **/ - tpl:'<input type="text">', + tpl:'<div class="editable-date well"></div>', /** - Configuration of typeahead. [Full list of options](http://twitter.github.com/bootstrap/javascript.html#typeahead). - - @property typeahead - @type object + @property inputclass @default null **/ - typeahead: null, + inputclass: null, /** - Whether to show `clear` button + 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 - @default true + @type boolean|string + @default 'x clear' **/ - clear: true + clear: '&times; clear' }); - $.fn.editabletypes.typeahead = Constructor; + $.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)); \ No newline at end of file