/*! X-editable - v1.4.0 * 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 ($) { 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; } //nothing shown after init }; EditableForm.prototype = { constructor: EditableForm, initInput: function() { //called once //take input from options (as it is created in editable-element) this.input = this.options.input; //set initial value this.value = this.input.str2value(this.options.value); }, initTemplate: function() { this.$form = $($.fn.editableform.template); }, initButtons: function() { this.$form.find('.editable-buttons').append($.fn.editableform.buttons); }, /** Renders editableform @method render **/ render: function() { //init loader this.$loading = $($.fn.editableform.loading); this.$div.empty().append(this.$loading); //init form template and buttons this.initTemplate(); if(this.options.showbuttons) { this.initButtons(); } else { this.$form.find('.editable-buttons').remove(); } //show loading state this.showLoading(); /** Fired when rendering starts @event rendering @param {Object} event event object **/ this.$div.triggerHandler('rendering'); //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()) .then($.proxy(function () { //setup input to submit automatically when no buttons shown if(!this.options.showbuttons) { this.input.autosubmit(); } //attach 'cancel' handler this.$form.find('.editable-cancel').click($.proxy(this.cancel, this)); if(this.input.error) { this.error(this.input.error); this.$form.find('.editable-submit').attr('disabled', true); this.input.$input.attr('disabled', true); //prevent form from submitting 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); //attach submit handler this.$form.submit($.proxy(this.submit, this)); } /** Fired when form is rendered @event rendered @param {Object} event event object **/ this.$div.triggerHandler('rendered'); this.showForm(); //call postrender method to perform actions required visibility of form if(this.input.postrender) { this.input.postrender(); } }, this)); }, cancel: function() { /** Fired when form was cancelled by user @event cancel @param {Object} event event object **/ this.$div.triggerHandler('cancel'); }, showLoading: function() { var w, h; if(this.$form) { //set loading size equal to form w = this.$form.outerWidth(); h = this.$form.outerHeight(); if(w) { this.$loading.width(w); } if(h) { this.$loading.height(h); } this.$form.hide(); } else { //stretch loading to fill container width w = this.$loading.parent().width(); if(w) { this.$loading.width(w); } } this.$loading.show(); }, showForm: function(activate) { this.$loading.hide(); this.$form.show(); if(activate !== false) { this.input.activate(); } /** Fired when form is shown @event show @param {Object} event event object **/ this.$div.triggerHandler('show'); }, error: function(msg) { var $group = this.$form.find('.control-group'), $block = this.$form.find('.editable-error-block'), lines; if(msg === false) { $group.removeClass($.fn.editableform.errorGroupClass); $block.removeClass($.fn.editableform.errorBlockClass).empty().hide(); } else { //convert newline to
for more pretty error display if(msg) { lines = msg.split("\n"); for (var i = 0; i < lines.length; i++) { lines[i] = $('
').text(lines[i]).html(); } msg = lines.join('
'); } $group.addClass($.fn.editableform.errorGroupClass); $block.addClass($.fn.editableform.errorBlockClass).html(msg).show(); } }, submit: function(e) { e.stopPropagation(); e.preventDefault(); var error, newValue = this.input.input2value(); //get new value from input //validation if (error = this.validate(newValue)) { this.error(error); this.showForm(); return; } //if value not changed --> trigger 'nochange' event and return /*jslint eqeq: true*/ if (!this.options.savenochange && this.input.value2str(newValue) == this.input.value2str(this.value)) { /*jslint eqeq: false*/ /** Fired when value not changed but form is submitted. Requires savenochange = false. @event nochange @param {Object} event event object **/ this.$div.triggerHandler('nochange'); return; } //sending data to server $.when(this.save(newValue)) .done($.proxy(function(response) { //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: } --> use that value instead of submitted 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 {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)) .fail($.proxy(function(xhr) { this.error(typeof xhr === 'string' ? xhr : xhr.responseText || xhr.statusText || 'Unknown error!'); this.showForm(); }, this)); }, save: function(newValue) { //convert value for submitting to server var submitValue = this.input.value2submit(newValue); //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)))), params; if (send) { //send to server this.showLoading(); //standard params params = { name: this.options.name || '', value: submitValue, pk: pk }; //additional params if(typeof this.options.params === 'function') { params = this.options.params.call(this.options.scope, params); } else { //try parse json in single quotes (from data-params attribute) this.options.params = $.fn.editableutils.tryParseJson(this.options.params, true); $.extend(params, this.options.params); } if(typeof this.options.url === 'function') { //user's function return this.options.url.call(this.options.scope, params); } else { //send ajax to server and return deferred object return $.ajax($.extend({ url : this.options.url, data : params, type : 'POST' }, this.options.ajaxOptions)); } } }, validate: function (value) { if (value === undefined) { value = this.value; } if (typeof this.options.validate === 'function') { return this.options.validate.call(this.options.scope, value); } }, option: function(key, value) { if(key in this.options) { this.options[key] = value; } if(key === 'value') { this.setValue(value); } //do not pass option to input as it is passed in editable-element }, setValue: function(value, convertStr) { if(convertStr) { this.value = this.input.str2value(value); } else { this.value = value; } //if form is visible, update input if(this.$form && this.$form.is(':visible')) { this.input.value2input(this.value); } } }; /* Initialize editableform. Applied to jQuery object. @method $().editableform(options) @params {Object} options @example var $form = $('<div>').editableform({ type: 'text', name: 'username', url: '/post', value: 'vitaliy' }); //to display form you should call 'render' method $form.editableform('render'); */ $.fn.editableform = function (option) { var args = arguments; return this.each(function () { var $this = $(this), data = $this.data('editableform'), options = typeof option === 'object' && option; if (!data) { $this.data('editableform', (data = new EditableForm(this, options))); } if (typeof option === 'string') { //call method data[option].apply(data, Array.prototype.slice.call(args, 1)); } }); }; //keep link to constructor to allow inheritance $.fn.editableform.Constructor = EditableForm; //defaults $.fn.editableform.defaults = { /* see also defaults for input */ /** Type of input. Can be text|textarea|select|date|checklist @property type @type string @default 'text' **/ type: 'text', /** Url for submit, e.g. '/post' If function - it will be called instead of ajax. Function can return deferred object to run fail/done callbacks. @property url @type string|function @default null @example url: function(params) { if(params.value === 'abc') { var d = new $.Deferred; return d.reject('error message'); //returning error via deferred object } else { someModel.set(params.name, params.value); //save data in some js model } } **/ url:null, /** Additional params for submit. If defined as object - it is **appended** to original ajax data (pk, name and value). If defined as function - returned object **overwrites** original ajax data. @example params: function(params) { //originally params contain pk, name and value params.a = 1; return params; } @property params @type object|function @default null **/ params:null, /** Name of field. Will be submitted on server. Can be taken from id attribute @property name @type string @default null **/ name: null, /** Primary key of editable object (e.g. record id in database). For composite keys use object, e.g. {id: 1, lang: 'en'}. Can be calculated dynamically via function. @property pk @type string|object|function @default null **/ pk: null, /** Initial value. If not defined - will be taken from element's content. For __select__ type should be defined (as it is ID of shown text). @property value @type string|object @default null **/ value: null, /** Strategy for sending data on server. Can be auto|always|never. When 'auto' data will be sent on server only if pk defined, otherwise new value will be stored in element. @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. @property validate @type function @default null @example validate: function(value) { if($.trim(value) == '') { return 'This field is required'; } } **/ 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 {success: true} or {success: false, msg: "server error"} 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** {newValue: <something>} - it overwrites value, submitted by user. Otherwise newValue simply rendered into element. @property success @type function @default null @example success: function(response, newValue) { if(!response.success) return response.msg; } **/ success: null, /** Additional options for ajax request. List of values: http://api.jquery.com/jQuery.ajax @property ajaxOptions @type object @default null @since 1.1.1 @example ajaxOptions: { type: 'put', dataType: 'json' } **/ ajaxOptions: null, /** Whether to show buttons or not. Form without buttons is auto-submitted. @property showbuttons @type boolean @default true @since 1.1.1 **/ showbuttons: true, /** Scope for callback methods (success, validate). If null means editableform instance itself. @property scope @type DOMElement|object @default null @since 1.2.0 @private **/ scope: null, /** Whether to save or cancel value when it was not changed but form was submitted @property savenochange @type boolean @default false @since 1.2.0 **/ savenochange: false }; /* Note: following params could redefined in engine: bootstrap or jqueryui: Classes 'control-group' and 'editable-error-block' must always present! */ $.fn.editableform.template = '
'+ '
' + '
'+ '
' + '
' + '
'; //loading div $.fn.editableform.loading = '
'; //buttons $.fn.editableform.buttons = ''+ ''; //error class attached to control-group $.fn.editableform.errorGroupClass = null; //error class attached to editable-error-block $.fn.editableform.errorBlockClass = 'editable-error'; }(window.jQuery)); /** * EditableForm utilites */ (function ($) { //utils $.fn.editableutils = { /** * classic JS inheritance function */ inherit: function (Child, Parent) { var F = function() { }; F.prototype = Parent.prototype; Child.prototype = new F(); Child.prototype.constructor = Child; Child.superclass = Parent.prototype; }, /** * set caret position in input * see http://stackoverflow.com/questions/499126/jquery-set-cursor-position-in-text-area */ setCursorPosition: function(elem, pos) { if (elem.setSelectionRange) { elem.setSelectionRange(pos, pos); } else if (elem.createTextRange) { var range = elem.createTextRange(); range.collapse(true); range.moveEnd('character', pos); range.moveStart('character', pos); range.select(); } }, /** * function to parse JSON in *single* quotes. (jquery automatically parse only double quotes) * That allows such code as: * safe = true --> means no exception will be thrown * for details see http://stackoverflow.com/questions/7410348/how-to-set-json-format-to-html5-data-attributes-in-the-jquery */ tryParseJson: function(s, safe) { if (typeof s === 'string' && s.length && s.match(/^[\{\[].*[\}\]]$/)) { if (safe) { try { /*jslint evil: true*/ s = (new Function('return ' + s))(); /*jslint evil: false*/ } catch (e) {} finally { return s; } } else { /*jslint evil: true*/ s = (new Function('return ' + s))(); /*jslint evil: false*/ } } return s; }, /** * slice object by specified keys */ sliceObj: function(obj, keys, caseSensitive /* default: false */) { var key, keyLower, newObj = {}; if (!$.isArray(keys) || !keys.length) { return newObj; } for (var i = 0; i < keys.length; i++) { key = keys[i]; if (obj.hasOwnProperty(key)) { newObj[key] = obj[key]; } if(caseSensitive === true) { continue; } //when getting data-* attributes via $.data() it's converted to lowercase. //details: http://stackoverflow.com/questions/7602565/using-data-attributes-with-jquery //workaround is code below. keyLower = key.toLowerCase(); if (obj.hasOwnProperty(keyLower)) { newObj[key] = obj[keyLower]; } } return newObj; }, /* exclude complex objects from $.data() before pass to config */ getConfigData: function($element) { var data = {}; $.each($element.data(), function(k, v) { if(typeof v !== 'object' || (v && typeof v === 'object' && (v.constructor === Object || v.constructor === Array))) { data[k] = v; } }); return data; }, /* returns keys of object */ objectKeys: function(o) { if (Object.keys) { return Object.keys(o); } else { if (o !== Object(o)) { throw new TypeError('Object.keys called on a non-object'); } var k=[], p; for (p in o) { if (Object.prototype.hasOwnProperty.call(o,p)) { k.push(p); } } return k; } }, /** method to escape html. **/ escape: function(str) { return $('
').text(str).html(); }, /* returns array items from sourceData having value property equal or inArray of 'value' */ itemsByValue: function(value, sourceData) { if(!sourceData || value === null) { return []; } //convert to array if(!$.isArray(value)) { value = [value]; } /*jslint eqeq: true*/ var result = $.grep(sourceData, function(o){ return $.grep(value, function(v){ return v == o.value; }).length; }); /*jslint eqeq: false*/ return result; }, /* Returns input by options: type, mode. */ createInput: function(options) { var TypeConstructor, typeOptions, input, type = options.type; //`date` is some kind of virtual type that is transformed to one of exact types //depending on mode and core lib if(type === 'date') { //inline if(options.mode === 'inline') { if($.fn.editabletypes.datefield) { type = 'datefield'; } else if($.fn.editabletypes.dateuifield) { type = 'dateuifield'; } //popup } else { if($.fn.editabletypes.date) { type = 'date'; } else if($.fn.editabletypes.dateui) { type = 'dateui'; } } //if type still `date` and not exist in types, replace with `combodate` that is base input if(type === 'date' && !$.fn.editabletypes.date) { type = 'combodate'; } } //change wysihtml5 to textarea for jquery UI and plain versions if(type === 'wysihtml5' && !$.fn.editabletypes[type]) { type = 'textarea'; } //create input of specified type. Input will be used for converting value, not in form if(typeof $.fn.editabletypes[type] === 'function') { TypeConstructor = $.fn.editabletypes[type]; typeOptions = this.sliceObj(options, this.objectKeys(TypeConstructor.defaults)); input = new TypeConstructor(typeOptions); return input; } else { $.error('Unknown type: '+ type); 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.
This method applied internally in $().editable(). You should subscribe on it's events (save / cancel) to get profit of it.
Final realization can be different: bootstrap-popover, jqueryui-tooltip, poshytip, inline-div. It depends on which js file you include.
Applied as jQuery method. @class editableContainer @uses editableform **/ (function ($) { var Popup = function (element, options) { this.init(element, options); }; var Inline = function (element, options) { this.init(element, options); }; //methods Popup.prototype = { containerName: null, //tbd in child class innerCss: null, //tbd in child class init: function(element, options) { this.$element = $(element); //todo: what is in priority: data or js? this.options = $.extend({}, $.fn.editableContainer.defaults, $.fn.editableutils.getConfigData(this.$element), options); this.splitOptions(); //set scope of form callbacks to element this.formOptions.scope = this.$element[0]; this.initContainer(); //bind 'destroyed' listener to destroy container when element is removed from dom this.$element.on('destroyed', $.proxy(function(){ this.destroy(); }, this)); //attach document handler to close containers on click / escape if(!$(document).data('editable-handlers-attached')) { //close all on escape $(document).on('keyup.editable', function (e) { if (e.which === 27) { $('.editable-open').editableContainer('hide'); //todo: return focus on element } }); //close containers when click outside $(document).on('click.editable', function(e) { var $target = $(e.target), i, exclude_classes = ['.editable-container', '.ui-datepicker-header', '.modal-backdrop', '.bootstrap-wysihtml5-insert-image-modal', '.bootstrap-wysihtml5-insert-link-modal']; //if click inside one of exclude classes --> no nothing for(i=0; i'); //insert form into container body if(this.tip().is(this.innerCss)) { //for inline container this.tip().append(this.$form); } else { this.tip().find(this.innerCss).append(this.$form); } //render form this.renderForm(); }, /** Hides container with form @method hide() @param {string} reason Reason caused hiding. Can be save|cancel|onblur|nochange|undefined (=manual) **/ hide: function(reason) { if(!this.tip() || !this.tip().is(':visible') || !this.$element.hasClass('editable-open')) { return; } this.$element.removeClass('editable-open'); this.innerHide(); /** Fired when container was hidden. It occurs on both save or cancel. @event hidden @param {object} event event object @param {string} reason Reason caused hiding. Can be save|cancel|onblur|nochange|undefined (=manual) @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); }, /* 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. **/ toggle: function(closeAll) { if(this.container() && this.tip() && this.tip().is(':visible')) { this.hide(); } else { this.show(closeAll); } }, /* Updates the position of container when content changed. @method setPosition() */ setPosition: function() { //tbd in child class }, save: function(e, params) { this.hide('save'); /** Fired when new value was submitted. You can use $(this).data('editableContainer') inside handler to access to editableContainer instance @event save @param {Object} event event object @param {Object} params additional params @param {mixed} params.newValue submitted value @param {Object} params.response ajax response @example $('#username').on('save', function(e, params) { //assuming server response: '{success: true}' var pk = $(this).data('editableContainer').options.pk; if(params.response && params.response.success) { alert('value: ' + params.newValue + ' with pk: ' + pk + ' saved!'); } else { alert('error!'); } }); **/ this.$element.triggerHandler('save', params); }, /** Sets new option @method option(key, value) @param {string} key @param {mixed} value **/ option: function(key, value) { this.options[key] = value; if(key in this.containerOptions) { this.containerOptions[key] = value; this.setContainerOption(key, value); } else { this.formOptions[key] = value; if(this.$form) { this.$form.editableform('option', key, value); } } }, setContainerOption: function(key, value) { this.call('option', key, value); }, /** Destroys the container instance @method destroy() **/ destroy: function() { this.hide(); this.innerDestroy(); this.$element.off('destroyed'); this.$element.removeData('editableContainer'); }, /* to be overwritten in child classes */ innerDestroy: function() { }, /* Closes other containers except one related to passed element. Other containers can be cancelled or submitted (depends on onblur option) */ closeOthers: function(element) { $('.editable-open').each(function(i, el){ //do nothing with passed element and it's children if(el === element || $(el).find(element).length) { return; } //otherwise cancel or submit all open containers var $el = $(el), ec = $el.data('editableContainer'); if(!ec) { return; } if(ec.options.onblur === 'cancel') { $el.data('editableContainer').hide('onblur'); } else if(ec.options.onblur === 'submit') { $el.data('editableContainer').tip().find('form').submit(); } }); }, /** Activates input of visible container (e.g. set focus) @method activate() **/ activate: function() { if(this.tip && this.tip().is(':visible') && this.$form) { this.$form.data('editableform').input.activate(); } } }; /** jQuery method to initialize editableContainer. @method $().editableContainer(options) @params {Object} options @example $('#edit').editableContainer({ type: 'text', url: '/post', pk: 1, value: 'hello' }); **/ $.fn.editableContainer = function (option) { var args = arguments; return this.each(function () { var $this = $(this), dataKey = 'editableContainer', data = $this.data(dataKey), options = typeof option === 'object' && option, Constructor = (options.mode === 'inline') ? Inline : Popup; if (!data) { $this.data(dataKey, (data = new Constructor(this, options))); } if (typeof option === 'string') { //call method data[option].apply(data, Array.prototype.slice.call(args, 1)); } }); }; //store constructors $.fn.editableContainer.Popup = Popup; $.fn.editableContainer.Inline = Inline; //defaults $.fn.editableContainer.defaults = { /** Initial value of form input @property value @type mixed @default null @private **/ value: null, /** Placement of container relative to element. Can be top|right|bottom|left. Not used for inline container. @property placement @type string @default 'top' **/ placement: 'top', /** Whether to hide container on save/cancel. @property autohide @type boolean @default true @private **/ autohide: true, /** Action when user clicks outside the container. Can be cancel|submit|ignore. Setting ignore allows to have several containers open. @property onblur @type string @default 'cancel' @since 1.1.1 **/ onblur: 'cancel', /** Animation speed (inline mode) @property anim @type string @default 'fast' **/ anim: 'fast', /** Mode of editable, can be `popup` or `inline` @property mode @type string @default 'popup' @since 1.4.0 **/ mode: 'popup' }; /* * workaround to have 'destroyed' event to destroy popover when element is destroyed * see http://stackoverflow.com/questions/2200494/jquery-trigger-event-when-an-element-is-removed-from-the-dom */ jQuery.event.special.destroyed = { remove: function(o) { if (o.handler) { o.handler(); } } }; }(window.jQuery)); /** * Editable Inline * --------------------- */ (function ($) { //copy prototype from EditableContainer //extend methods $.extend($.fn.editableContainer.Inline.prototype, $.fn.editableContainer.Popup.prototype, { containerName: 'editableform', innerCss: '.editable-inline', initContainer: function(){ //container is element this.$tip = $('').addClass('editable-inline'); //convert anim to miliseconds (int) if(!this.options.anim) { this.options.anim = 0; } }, splitOptions: function() { //all options are passed to form this.containerOptions = {}; this.formOptions = this.options; }, tip: function() { return this.$tip; }, innerShow: function () { this.$element.hide(); this.tip().insertAfter(this.$element).show(); }, innerHide: function () { this.$tip.hide(this.options.anim, $.proxy(function() { this.$element.show(); this.tip().empty().remove(); }, this)); }, innerDestroy: function() { this.tip().remove(); } }); }(window.jQuery)); /** Makes editable any HTML element on the page. Applied as jQuery method. @class editable @uses editableContainer **/ (function ($) { var Editable = function (element, options) { this.$element = $(element); this.options = $.extend({}, $.fn.editable.defaults, $.fn.editableutils.getConfigData(this.$element), options); this.init(); }; Editable.prototype = { constructor: Editable, init: function () { var isValueByText = false, doAutotext, finalize; //name this.options.name = this.options.name || this.$element.attr('id'); //create input of specified type. Input will be used for converting value, not in form this.input = $.fn.editableutils.createInput(this.options); if(!this.input) { return; } //set value from settings or by element's text if (this.options.value === undefined || this.options.value === null) { this.value = this.input.html2value($.trim(this.$element.html())); isValueByText = true; } else { /* value can be string when received from 'data-value' attribute for complext objects value can be set as json string in data-value attribute, e.g. data-value="{city: 'Moscow', street: 'Lenina'}" */ this.options.value = $.fn.editableutils.tryParseJson(this.options.value, true); if(typeof this.options.value === 'string') { this.value = this.input.str2value(this.options.value); } else { this.value = this.options.value; } } //add 'editable' class to every editable element this.$element.addClass('editable'); //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){ e.preventDefault(); //stop propagation not required anymore because in document click handler it checks event target //e.stopPropagation(); if(this.options.toggle === 'mouseenter') { //for hover only show container 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 } //check conditions for autotext: //if value was generated by text or value is empty, no sense to run autotext doAutotext = !isValueByText && this.value !== null && this.value !== undefined; doAutotext &= (this.options.autotext === 'always') || (this.options.autotext === 'auto' && !this.$element.text().length); $.when(doAutotext ? this.render() : true).then($.proxy(function() { if(this.options.disabled) { this.disable(); } else { this.enable(); } /** Fired when element was initialized by editable method. @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); }); **/ this.$element.triggerHandler('init', this); }, this)); }, /* Renders value into element's text. Can call custom display method from options. Can return deferred object. @method render() @param {mixed} response server response (if exist) to pass into display function */ render: function(response) { //do not display anything if(this.options.display === false) { return; } //if it is input with source, we pass callback in third param to be called when source is loaded if(this.input.options.hasOwnProperty('source')) { return this.input.value2html(this.value, this.$element[0], this.options.display, response); //if display method defined --> use it } else if(typeof this.options.display === 'function') { return this.options.display.call(this.$element[0], this.value, response); //else use input's original value2html() method } else { return this.input.value2html(this.value, this.$element[0]); } }, /** Enables editable @method enable() **/ enable: function() { this.options.disabled = false; this.$element.removeClass('editable-disabled'); this.handleEmpty(); if(this.options.toggle !== 'manual') { if(this.$element.attr('tabindex') === '-1') { this.$element.removeAttr('tabindex'); } } }, /** Disables editable @method disable() **/ disable: function() { this.options.disabled = true; this.hide(); this.$element.addClass('editable-disabled'); this.handleEmpty(); //do not stop focus on this element this.$element.attr('tabindex', -1); }, /** Toggles enabled / disabled state of editable element @method toggleDisabled() **/ toggleDisabled: function() { if(this.options.disabled) { this.enable(); } else { this.disable(); } }, /** Sets new option @method option(key, value) @param {string|object} key option name or object with several options @param {mixed} value option new value @example $('.editable').editable('option', 'pk', 2); **/ option: function(key, value) { //set option(s) by object if(key && typeof key === 'object') { $.each(key, $.proxy(function(k, v){ this.option($.trim(k), v); }, this)); return; } //set option by string this.options[key] = value; //disabled if(key === 'disabled') { return value ? this.disable() : this.enable(); } //value if(key === 'value') { this.setValue(value); } //transfer new option to container! if(this.container) { this.container.option(key, value); } //pass option to input directly (as it points to the same in form) if(this.input.option) { this.input.option(key, value); } }, /* * set emptytext if element is empty (reverse: remove emptytext if needed) */ handleEmpty: function () { //do not handle empty if we do not display anything if(this.options.display === false) { return; } var emptyClass = 'editable-empty'; //emptytext shown only for enabled if(!this.options.disabled) { if ($.trim(this.$element.text()) === '') { this.$element.addClass(emptyClass).text(this.options.emptytext); } else { this.$element.removeClass(emptyClass); } } else { //below required if element disable property was changed if(this.$element.hasClass(emptyClass)) { this.$element.empty(); this.$element.removeClass(emptyClass); } } }, /** Shows container with form @method show() @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. **/ show: function (closeAll) { if(this.options.disabled) { return; } //init editableContainer: popover, tooltip, inline, etc.. if(!this.container) { var containerOptions = $.extend({}, this.options, { value: this.value, input: this.input //pass input to form (as it is already created) }); this.$element.editableContainer(containerOptions); this.$element.on("save.internal", $.proxy(this.save, this)); this.container = this.$element.data('editableContainer'); } else if(this.container.tip().is(':visible')) { return; } //show container this.container.show(closeAll); }, /** Hides container with form @method hide() **/ hide: function () { if(this.container) { this.container.hide(); } }, /** Toggles container visibility (show / hide) @method toggle() @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. **/ toggle: function(closeAll) { if(this.container && this.container.tip().is(':visible')) { this.hide(); } else { this.show(closeAll); } }, /* * called when form was submitted */ save: function(e, params) { //if url is not user's function and value was not sent to server and value changed --> mark element with unsaved css. if(typeof this.options.url !== 'function' && this.options.display !== false && params.response === undefined && this.input.value2str(this.value) !== this.input.value2str(params.newValue)) { this.$element.addClass('editable-unsaved'); } else { this.$element.removeClass('editable-unsaved'); } this.setValue(params.newValue, false, params.response); /** Fired when new value was submitted. You can use $(this).data('editable') to access to editable instance @event save @param {Object} event event object @param {Object} params additional params @param {mixed} params.newValue submitted value @param {Object} params.response ajax response @example $('#username').on('save', function(e, params) { alert('Saved value: ' + params.newValue); }); **/ //event itself is triggered by editableContainer. Description here is only for documentation }, validate: function () { if (typeof this.options.validate === 'function') { return this.options.validate.call(this, this.value); } }, /** Sets new value of editable @method setValue(value, convertStr) @param {mixed} value new value @param {boolean} convertStr whether to convert value from string to internal format **/ setValue: function(value, convertStr, response) { if(convertStr) { this.value = this.input.str2value(value); } else { this.value = value; } if(this.container) { this.container.option('value', this.value); } $.when(this.render(response)) .then($.proxy(function() { this.handleEmpty(); }, this)); }, /** Activates input of visible container (e.g. set focus) @method activate() **/ activate: function() { if(this.container) { this.container.activate(); } }, /** Removes editable feature from element @method destroy() **/ destroy: function() { if(this.container) { this.container.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.removeData('editable'); } }; /* EDITABLE PLUGIN DEFINITION * ======================= */ /** jQuery method to initialize editable element. @method $().editable(options) @params {Object} options @example $('#username').editable({ type: 'text', url: '/post', pk: 1 }); **/ $.fn.editable = function (option) { //special API methods returning non-jquery object var result = {}, args = arguments, datakey = 'editable'; switch (option) { /** Runs client-side validation for all matched editables @method validate() @returns {Object} validation errors map @example $('#username, #fullname').editable('validate'); // possible result: { username: "username is required", fullname: "fullname should be minimum 3 letters length" } **/ case 'validate': this.each(function () { var $this = $(this), data = $this.data(datakey), error; if (data && (error = data.validate())) { result[data.options.name] = error; } }); return result; /** Returns current values of editable elements. If value is null or undefined it will not be returned @method getValue() @returns {Object} object of element names and values @example $('#username, #fullname').editable('validate'); // possible result: { username: "superuser", fullname: "John" } **/ 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); } }); 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
creating new records for details. @method submit(options) @param {object} options @param {object} options.url url to submit data @param {object} options.data additional data to submit @param {object} options.ajaxOptions additional ajax options @param {function} options.error(obj) error handler @param {function} options.success(obj,config) success handler @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; if($.isEmptyObject(errors)) { values = this.editable('getValue'); if(config.data) { $.extend(values, config.data); } $.ajax($.extend({ url: config.url, data: values, type: 'POST' }, config.ajaxOptions)) .success(function(response) { //successful response 200 OK if(typeof config.success === 'function') { config.success.call($elems, response, config); } }) .error(function(){ //ajax error if(typeof config.error === 'function') { config.error.apply($elems, arguments); } }); } else { //client-side validation error if(typeof config.error === 'function') { config.error.call($elems, errors); } } return this; } //return jquery object return this.each(function () { var $this = $(this), data = $this.data(datakey), options = typeof option === 'object' && option; if (!data) { $this.data(datakey, (data = new Editable(this, options))); } if (typeof option === 'string') { //call method data[option].apply(data, Array.prototype.slice.call(args, 1)); } }); }; $.fn.editable.defaults = { /** Type of input. Can be text|textarea|select|date|checklist and more @property type @type string @default 'text' **/ type: 'text', /** Sets disabled state of editable @property disabled @type boolean @default false **/ disabled: false, /** How to toggle editable. Can be click|dblclick|mouseenter|manual. When set to manual you should manually call show/hide methods of editable. **Note**: if you call show or toggle inside **click** handler of some DOM element, you need to apply e.stopPropagation() because containers are being closed on any click on document. @example $('#edit-button').click(function(e) { e.stopPropagation(); $('#username').editable('toggle'); }); @property toggle @type string @default 'click' **/ toggle: 'click', /** Text shown when element is empty. @property emptytext @type string @default 'Empty' **/ emptytext: 'Empty', /** Allows to automatically set element's text based on it's value. Can be auto|always|never. Useful for select and date. For example, if dropdown list is {1: 'a', 2: 'b'} and element's value set to 1, it's html will be automatically set to 'a'. auto - text will be automatically set only if element is empty. always|never - always(never) try to set element's text. @property autotext @type string @default 'auto' **/ autotext: 'auto', /** Initial value of input. If not set, taken from element's text. @property value @type mixed @default element's text **/ value: null, /** Callback to perform custom displaying of value in element's text. If `null`, default input's display used. If `false`, no displaying methods will be called, element's text will never change. Runs under element's scope. _Parameters:_ * `value` current value to be displayed * `response` server response (if display called after ajax submit), since 1.4.0 For **inputs with source** (select, checklist) parameters are different: * `value` current value to be displayed * `sourceData` array of items for current input (e.g. dropdown items) * `response` server response (if display called after ajax submit), since 1.4.0 To get currently selected items use `$.fn.editableutils.itemsByValue(value, sourceData)`. @property display @type function|boolean @default null @since 1.2.0 @example display: function(value, sourceData) { //display checklist as comma-separated values var html = [], checked = $.fn.editableutils.itemsByValue(value, sourceData); if(checked.length) { $.each(checked, function(i, v) { html.push($.fn.editableutils.escape(v.text)); }); $(this).html(html.join(', ')); } else { $(this).empty(); } } **/ display: null }; }(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 ($) { //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 this.$input = this.$tpl; //control itself, can be changed in render method this.$clear = null; //clear button this.error = null; //error message, if input cannot be rendered }, /** Renders input from tpl. Can return jQuery deferred object. Can be overwritten in child objects @method render() **/ render: function() { }, /** Sets element's html by value. @method value2html(value, element) @param {mixed} value @param {DOMElement} element **/ value2html: function(value, element) { $(element).text(value); }, /** Converts element's html to value @method html2value(html) @param {string} html @returns {mixed} **/ html2value: function(html) { return $('
').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) @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 $('
').text(str).html(); }, /** attach handler to automatically submit form when value changed (useful when buttons not shown) **/ autosubmit: function() { }, // -------- helper functions -------- setClass: function() { if(this.options.inputclass) { this.$input.addClass(this.options.inputclass); } }, setAttr: function(attr) { if (this.options[attr]) { this.$input.attr(attr, this.options[attr]); } }, option: function(key, value) { this.options[key] = value; } }; AbstractInput.defaults = { /** HTML template of input. Normally you should not change it. @property tpl @type string @default '' **/ tpl: '', /** CSS class automatically applied to input @property inputclass @type string @default input-medium **/ inputclass: 'input-medium', /** Name attribute of input @property name @type string @default null **/ name: null }; $.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 ($) { var List = function (options) { }; $.fn.editableutils.inherit(List, $.fn.editabletypes.abstractinput); $.extend(List.prototype, { render: function () { var deferred = $.Deferred(); this.error = null; this.onSourceReady(function () { this.renderList(); deferred.resolve(); }, function () { this.error = this.options.sourceError; deferred.resolve(); }); return deferred.promise(); }, html2value: function (html) { return null; //can't set value by text }, value2html: function (value, element, display, response) { var deferred = $.Deferred(), success = function () { if(typeof display === 'function') { //custom display method display.call(element, value, this.sourceData, response); } else { this.value2htmlFinal(value, element); } deferred.resolve(); }; //for null value just call success without loading source if(value === null) { success.call(this); } else { this.onSourceReady(success, function () { deferred.resolve(); }); } return deferred.promise(); }, // ------------- additional functions ------------ onSourceReady: function (success, error) { //if allready loaded just call success if($.isArray(this.sourceData)) { success.call(this); return; } // try parse json in single quotes (for double quotes jquery does automatically) try { this.options.source = $.fn.editableutils.tryParseJson(this.options.source, false); } catch (e) { error.call(this); return; } //loading from url if (typeof this.options.source === 'string') { //try to get from cache if(this.options.sourceCache) { var cacheID = this.options.source, cache; if (!$(document).data(cacheID)) { $(document).data(cacheID, {}); } cache = $(document).data(cacheID); //check for cached data if (cache.loading === false && cache.sourceData) { //take source from cache this.sourceData = cache.sourceData; this.doPrepend(); success.call(this); return; } else if (cache.loading === true) { //cache is loading, put callback in stack to be called later cache.callbacks.push($.proxy(function () { this.sourceData = cache.sourceData; this.doPrepend(); success.call(this); }, this)); //also collecting error callbacks cache.err_callbacks.push($.proxy(error, this)); return; } else { //no cache yet, activate it cache.loading = true; cache.callbacks = []; cache.err_callbacks = []; } } //loading sourceData from server $.ajax({ url: this.options.source, type: 'get', cache: false, dataType: 'json', success: $.proxy(function (data) { if(cache) { cache.loading = false; } this.sourceData = this.makeArray(data); if($.isArray(this.sourceData)) { if(cache) { //store result in cache cache.sourceData = this.sourceData; //run success callbacks for other fields waiting for this source $.each(cache.callbacks, function () { this.call(); }); } this.doPrepend(); success.call(this); } else { error.call(this); if(cache) { //run error callbacks for other fields waiting for this source $.each(cache.err_callbacks, function () { this.call(); }); } } }, this), error: $.proxy(function () { error.call(this); if(cache) { cache.loading = false; //run error callbacks for other fields $.each(cache.err_callbacks, function () { this.call(); }); } }, this) }); } else { //options as json/array/function if (typeof this.options.source === 'function') { this.sourceData = this.makeArray(this.options.source()); } else { this.sourceData = this.makeArray(this.options.source); } if($.isArray(this.sourceData)) { this.doPrepend(); success.call(this); } else { error.call(this); } } }, doPrepend: function () { if(this.options.prepend === null || this.options.prepend === undefined) { return; } if(!$.isArray(this.prependData)) { //try parse json in single quotes this.options.prepend = $.fn.editableutils.tryParseJson(this.options.prepend, true); if (typeof this.options.prepend === 'string') { this.options.prepend = {'': this.options.prepend}; } if (typeof this.options.prepend === 'function') { this.prependData = this.makeArray(this.options.prepend()); } else { this.prependData = this.makeArray(this.options.prepend); } } if($.isArray(this.prependData) && $.isArray(this.sourceData)) { this.sourceData = this.prependData.concat(this.sourceData); } }, /* renders input list */ renderList: function() { // this method should be overwritten in child class }, /* set element's html by value */ value2htmlFinal: function(value, element) { // this method should be overwritten in child class }, /** * convert data to array suitable for sourceData, e.g. [{value: 1, text: 'abc'}, {...}] */ makeArray: function(data) { var count, obj, result = [], iterateEl; if(!data || typeof data === 'string') { return null; } if($.isArray(data)) { //array iterateEl = function (k, v) { obj = {value: k, text: v}; if(count++ >= 2) { return false;// exit each if object has more than one value } }; for(var i = 0; i < data.length; i++) { if(typeof data[i] === 'object') { count = 0; $.each(data[i], iterateEl); if(count === 1) { result.push(obj); } else if(count > 1 && data[i].hasOwnProperty('value') && data[i].hasOwnProperty('text')) { result.push(data[i]); } else { //data contains incorrect objects } } else { result.push({value: data[i], text: data[i]}); } } } else { //object $.each(data, function (k, v) { result.push({value: k, text: v}); }); } return result; }, option: function(key, value) { this.options[key] = value; if(key === 'source') { this.sourceData = null; } if(key === 'prepend') { this.prependData = null; } } }); List.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { /** Source data for list. If **array** - it should be in format: `[{value: 1, text: "text1"}, {...}]` For compability, object format is also supported: `{"1": "text1", "2": "text2" ...}` but it does not guarantee elements order. If **string** - considered ajax url to load items. In that case results will be cached for fields with the same source and name. See also `sourceCache` option. If **function**, it should return data in format above (since 1.4.0). @property source @type string | array | object | function @default null **/ source: null, /** Data automatically prepended to the beginning of dropdown list. @property prepend @type string | array | object | function @default false **/ prepend: false, /** Error message when list cannot be loaded (e.g. ajax error) @property sourceError @type string @default Error when loading list **/ sourceError: 'Error when loading list', /** if true and source is **string url** - results will be cached for fields with the same source and name. Usefull for editable grids. @property sourceCache @type boolean @default true @since 1.2.0 **/ sourceCache: true }); $.fn.editabletypes.list = List; }(window.jQuery)); /** Text input @class text @extends abstractinput @final @example awesome **/ (function ($) { var Text = function (options) { this.init('text', options, Text.defaults); }; $.fn.editableutils.inherit(Text, $.fn.editabletypes.abstractinput); $.extend(Text.prototype, { render: function() { this.renderClear(); this.setClass(); this.setAttr('placeholder'); }, activate: function() { if(this.$input.is(':visible')) { this.$input.focus(); $.fn.editableutils.setCursorPosition(this.$input.get(0), this.$input.val().length); if(this.toggleClear) { this.toggleClear(); } } }, //render clear button renderClear: function() { if (this.options.clear) { this.$clear = $(''); this.$input.after(this.$clear) .css('padding-right', 20) .keyup($.proxy(this.toggleClear, this)) .parent().css('position', 'relative'); this.$clear.click($.proxy(function(){ this.$clear.hide(); this.$input.val('').focus(); }, this)); } }, postrender: function() { if(this.$clear) { //can position clear button only here, when form is shown and height can be calculated var h = this.$input.outerHeight() || 20, delta = (h - this.$clear.height()) / 2; //workaround for plain-popup if(delta < 3) { delta = 3; } this.$clear.css({top: delta, right: delta}); } }, //show / hide clear button toggleClear: function() { if(!this.$clear) { return; } if(this.$input.val()) { this.$clear.show(); } else { this.$clear.hide(); } } }); Text.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { /** @property tpl @default **/ tpl: '', /** Placeholder attribute of input. Shown when input is empty. @property placeholder @type string @default null **/ placeholder: null, /** Whether to show `clear` button @property clear @type boolean @default true **/ clear: true }); $.fn.editabletypes.text = Text; }(window.jQuery)); /** Textarea input @class textarea @extends abstractinput @final @example awesome comment! **/ (function ($) { var Textarea = function (options) { this.init('textarea', options, Textarea.defaults); }; $.fn.editableutils.inherit(Textarea, $.fn.editabletypes.abstractinput); $.extend(Textarea.prototype, { render: function () { this.setClass(); this.setAttr('placeholder'); this.setAttr('rows'); //ctrl + enter this.$input.keydown(function (e) { if (e.ctrlKey && e.which === 13) { $(this).closest('form').submit(); } }); }, value2html: function(value, element) { var html = '', lines; if(value) { lines = value.split("\n"); for (var i = 0; i < lines.length; i++) { lines[i] = $('
').text(lines[i]).html(); } html = lines.join('
'); } $(element).html(html); }, html2value: function(html) { if(!html) { return ''; } var regex = new RegExp(String.fromCharCode(10), 'g'); var lines = html.split(//i); for (var i = 0; i < lines.length; i++) { var text = $('
').html(lines[i]).text(); // Remove newline characters (\n) to avoid them being converted by value2html() method // thus adding extra
tags text = text.replace(regex, ''); lines[i] = text; } return lines.join("\n"); }, activate: function() { $.fn.editabletypes.text.prototype.activate.call(this); } }); Textarea.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { /** @property tpl @default **/ tpl:'', /** @property inputclass @default input-large **/ inputclass: 'input-large', /** Placeholder attribute of input. Shown when input is empty. @property placeholder @type string @default null **/ placeholder: null, /** Number of rows in textarea @property rows @type integer @default 7 **/ rows: 7 }); $.fn.editabletypes.textarea = Textarea; }(window.jQuery)); /** Select (dropdown) @class select @extends list @final @example **/ (function ($) { var Select = function (options) { this.init('select', options, Select.defaults); }; $.fn.editableutils.inherit(Select, $.fn.editabletypes.list); $.extend(Select.prototype, { renderList: function() { this.$input.empty(); if(!$.isArray(this.sourceData)) { return; } for(var i=0; i', {value: this.sourceData[i].value}).text(this.sourceData[i].text)); } this.setClass(); //enter submit this.$input.on('keydown.editable', function (e) { if (e.which === 13) { $(this).closest('form').submit(); } }); }, value2htmlFinal: function(value, element) { var text = '', items = $.fn.editableutils.itemsByValue(value, this.sourceData); if(items.length) { text = items[0].text; } $(element).text(text); }, autosubmit: function() { this.$input.off('keydown.editable').on('change.editable', function(){ $(this).closest('form').submit(); }); } }); Select.defaults = $.extend({}, $.fn.editabletypes.list.defaults, { /** @property tpl @default **/ tpl:'' }); $.fn.editabletypes.select = Select; }(window.jQuery)); /** List of checkboxes. Internally value stored as javascript array of values. @class checklist @extends list @final @example **/ (function ($) { var Checklist = function (options) { this.init('checklist', options, Checklist.defaults); }; $.fn.editableutils.inherit(Checklist, $.fn.editabletypes.list); $.extend(Checklist.prototype, { renderList: function() { var $label, $div; this.$tpl.empty(); if(!$.isArray(this.sourceData)) { return; } for(var i=0; i').append($('', { type: 'checkbox', value: this.sourceData[i].value, name: this.options.name })) .append($('').text(' '+this.sourceData[i].text)); $('
').append($label).appendTo(this.$tpl); } this.$input = this.$tpl.find('input[type="checkbox"]'); this.setClass(); }, value2str: function(value) { return $.isArray(value) ? value.sort().join($.trim(this.options.separator)) : ''; }, //parse separated string str2value: function(str) { var reg, value = null; 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; } return value; }, //set checked on required checkboxes value2input: function(value) { this.$input.removeAttr('checked'); if($.isArray(value) && value.length) { this.$input.each(function(i, el) { var $el = $(el); // cannot use $.inArray as it performs strict comparison $.each(value, function(j, val){ /*jslint eqeq: true*/ if($el.val() == val) { /*jslint eqeq: false*/ $el.attr('checked', 'checked'); } }); }); } }, input2value: function() { var checked = []; this.$input.filter(':checked').each(function(i, el) { checked.push($(el).val()); }); return checked; }, //collect text of checked boxes value2htmlFinal: function(value, element) { var html = [], checked = $.fn.editableutils.itemsByValue(value, this.sourceData); if(checked.length) { $.each(checked, function(i, v) { html.push($.fn.editableutils.escape(v.text)); }); $(element).html(html.join('
')); } else { $(element).empty(); } }, activate: function() { this.$input.first().focus(); }, autosubmit: function() { this.$input.on('keydown', function(e){ if (e.which === 13) { $(this).closest('form').submit(); } }); } }); Checklist.defaults = $.extend({}, $.fn.editabletypes.list.defaults, { /** @property tpl @default
**/ tpl:'
', /** @property inputclass @type string @default null **/ inputclass: null, /** Separator of values when reading from `data-value` attribute @property separator @type string @default ',' **/ separator: ',' }); $.fn.editabletypes.checklist = Checklist; }(window.jQuery)); /** HTML5 input types. Following types are supported: * password * email * url * tel * number * range 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 @class html5types @extends text @final @since 1.3.0 @example admin@example.com **/ /** @property tpl @default depends on type **/ /* Password */ (function ($) { var Password = function (options) { this.init('password', options, Password.defaults); }; $.fn.editableutils.inherit(Password, $.fn.editabletypes.text); $.extend(Password.prototype, { //do not display password, show '[hidden]' instead value2html: function(value, element) { if(value) { $(element).text('[hidden]'); } else { $(element).empty(); } }, //as password not displayed, should not set value by html html2value: function(html) { return null; } }); Password.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { tpl: '' }); $.fn.editabletypes.password = Password; }(window.jQuery)); /* Email */ (function ($) { var Email = function (options) { this.init('email', options, Email.defaults); }; $.fn.editableutils.inherit(Email, $.fn.editabletypes.text); Email.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { tpl: '' }); $.fn.editabletypes.email = Email; }(window.jQuery)); /* Url */ (function ($) { var Url = function (options) { this.init('url', options, Url.defaults); }; $.fn.editableutils.inherit(Url, $.fn.editabletypes.text); Url.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { tpl: '' }); $.fn.editabletypes.url = Url; }(window.jQuery)); /* Tel */ (function ($) { var Tel = function (options) { this.init('tel', options, Tel.defaults); }; $.fn.editableutils.inherit(Tel, $.fn.editabletypes.text); Tel.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { tpl: '' }); $.fn.editabletypes.tel = Tel; }(window.jQuery)); /* Number */ (function ($) { 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'); } }); NumberInput.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { tpl: '', inputclass: 'input-mini', min: null, max: null, step: null }); $.fn.editabletypes.number = NumberInput; }(window.jQuery)); /* Range (inherit from number) */ (function ($) { var Range = function (options) { this.init('range', options, Range.defaults); }; $.fn.editableutils.inherit(Range, $.fn.editabletypes.number); $.extend(Range.prototype, { render: function () { this.$input = this.$tpl.filter('input'); this.setClass(); this.setAttr('min'); this.setAttr('max'); this.setAttr('step'); this.$input.on('input', function(){ $(this).siblings('output').text($(this).val()); }); }, activate: function() { this.$input.focus(); } }); Range.defaults = $.extend({}, $.fn.editabletypes.number.defaults, { tpl: '', inputclass: 'input-medium' }); $.fn.editabletypes.range = Range; }(window.jQuery)); /** * Combodate - 1.0.1 * 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 * * Author: Vitaliy Potapov * Project page: http://github.com/vitalets/combodate * Copyright (c) 2012 Vitaliy Potapov. Released under MIT License. **/ (function ($) { var Combodate = function (element, options) { this.$element = $(element); if(!this.$element.is('input')) { $.error('Combodate should be applied to INPUT element'); return; } this.options = $.extend({}, $.fn.combodate.defaults, options, this.$element.data()); this.init(); }; Combodate.prototype = { constructor: Combodate, init: function () { this.map = { //key regexp moment.method day: ['D', 'date'], month: ['M', 'month'], year: ['Y', 'year'], hour: ['[Hh]', 'hours'], minute: ['m', 'minutes'], second: ['s', 'seconds'], ampm: ['[Aa]', ''] }; this.$widget = $('').html(this.getTemplate()); this.initCombos(); //update original input on change this.$widget.on('change', 'select', $.proxy(function(){ this.$element.val(this.getValue()); }, this)); this.$widget.find('select').css('width', 'auto'); //hide original input and insert widget this.$element.hide().after(this.$widget); //set initial value this.setValue(this.$element.val() || this.options.value); }, /* Replace tokens in template with '); }); return tpl; }, /* 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)); } }); }, /* Initialize items of combos. Handles `firstItem` option */ initItems: function(key) { var values = []; if(this.options.firstItem === 'name') { var header = typeof moment.relativeTime[key] === 'function' ? moment.relativeTime[key](1, true, key, false) : moment.relativeTime[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 '); } 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++) { name = twoDigit ? this.leadZero(i) : i; items.push([i, name]); } return items; }, /* fill month */ fillMonth: function() { var items = this.initItems('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.months[i]; } else if(shortNames) { name = moment.monthsShort[i]; } else if(twoDigit) { name = this.leadZero(i+1); } else { name = i+1; } items.push([i, name]); } return items; }, /* fill year */ fillYear: function() { var items = this.initItems('y'), 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]); } return items; }, /* fill hour */ fillHour: function() { var items = this.initItems('h'), name, i, h12 = this.options.template.indexOf('h') !== -1, h24 = this.options.template.indexOf('H') !== -1, twoDigit = this.options.template.toLowerCase().indexOf('hh') !== -1, max = h12 ? 12 : 23; for(i=0; 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, 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]); } return items; }, /* fill second */ fillSecond: function() { var items = this.initItems('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]); } return items; }, /* fill ampm */ fillAmpm: function() { var ampmL = this.options.template.indexOf('a') !== -1, ampmU = this.options.template.indexOf('A') !== -1, items = [ ['am', ampmL ? 'am' : 'AM'], ['pm', ampmL ? 'pm' : 'PM'] ]; return items; }, /* Returns current date value. If format not specified - `options.format` used. If format = `null` - Moment object returned. */ getValue: function(format) { var dt, values = {}, that = this, notSelected = false; //getting selected values $.each(this.map, function(k, v) { if(k === 'ampm') { return; } var def = k === 'day' ? 1 : 0; values[k] = that['$'+k] ? parseInt(that['$'+k].val(), 10) : def; if(isNaN(values[k])) { notSelected = true; return false; } }); //if at least one visible combo not selected - return empty string if(notSelected) { return ''; } //convert hours if 12h format if(this.$ampm) { values.hour = this.$ampm.val() === 'am' ? values.hour : values.hour+12; if(values.hour === 24) { values.hour = 0; } } dt = moment([values.year, values.month, values.day, values.hour, values.minute, values.second]); //highlight invalid date this.highlight(dt); format = format === undefined ? this.options.format : format; if(format === null) { return dt.isValid() ? dt : null; } else { return dt.isValid() ? dt.format(format) : ''; } }, setValue: function(value) { if(!value) { return; } var dt = typeof value === 'string' ? moment(value, this.options.format) : moment(value), that = this, values = {}; if(dt.isValid()) { //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'; } } $.each(values, function(k, v) { if(that['$'+k]) { that['$'+k].val(v); } }); this.$element.val(dt.format(this.options.format)); } }, /* highlight combos if date is invalid */ highlight: function(dt) { if(!dt.isValid()) { if(this.options.errorClass) { this.$widget.addClass(this.options.errorClass); } else { //store original border color if(!this.borderColor) { this.borderColor = this.$widget.find('select').css('border-color'); } this.$widget.find('select').css('border-color', 'red'); } } else { if(this.options.errorClass) { this.$widget.removeClass(this.options.errorClass); } else { this.$widget.find('select').css('border-color', this.borderColor); } } }, leadZero: function(v) { return v <= 9 ? '0' + v : v; }, destroy: function() { this.$widget.remove(); this.$element.removeData('combodate').show(); } //todo: clear method }; $.fn.combodate = function ( option ) { var d, args = Array.apply(null, arguments); args.shift(); //getValue returns date as string / object (not jQuery object) if(option === 'getValue' && this.length && (d = this.eq(0).data('combodate'))) { return d.getValue.apply(d, args); } return this.each(function () { var $this = $(this), data = $this.data('combodate'), options = typeof option == 'object' && option; if (!data) { $this.data('combodate', (data = new Combodate(this, options))); } if (typeof option == 'string' && typeof data[option] == 'function') { data[option].apply(data, args); } }); }; $.fn.combodate.defaults = { //in this format value stored in original input format: 'DD-MM-YYYY HH:mm', //in this format items in dropdowns are displayed template: 'D / MMM / YYYY H : mm', //initial value, can be `new Date()` value: null, minYear: 1970, maxYear: 2015, minuteStep: 5, secondStep: 1, firstItem: 'empty', //'name', 'empty', 'none' errorClass: null }; }(window.jQuery)); /** Combodate input - dropdown date and time picker. Based on [combodate](http://vitalets.github.com/combodate) plugin. To use it you should manually include [momentjs](http://momentjs.com). Allows to input: * only date * only time * both date and time Please note, that format is taken from momentjs and **not compatible** with bootstrap-datepicker / jquery UI datepicker. Internally value stored as `momentjs` object. @class combodate @extends abstractinput @final @since 1.4.0 @example **/ /*global moment*/ (function ($) { var Constructor = function (options) { this.init('combodate', options, Constructor.defaults); //by default viewformat equals to format if(!this.options.viewformat) { this.options.viewformat = this.options.format; } //overriding combodate config (as by default jQuery extend() is not recursive) this.options.combodate = $.extend({}, Constructor.defaults.combodate, options.combodate, { format: this.options.format, template: this.options.template }); }; $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.abstractinput); $.extend(Constructor.prototype, { render: function () { this.$input.combodate(this.options.combodate); //"clear" link /* if(this.options.clear) { this.$clear = $('').html(this.options.clear).click($.proxy(function(e){ e.preventDefault(); e.stopPropagation(); this.clear(); }, this)); this.$tpl.parent().append($('
').append(this.$clear)); } */ }, value2html: function(value, element) { var text = value ? value.format(this.options.viewformat) : ''; $(element).text(text); }, html2value: function(html) { return html ? moment(html, this.options.viewformat) : null; }, value2str: function(value) { return value ? value.format(this.options.format) : ''; }, str2value: function(str) { return str ? moment(str, this.options.format) : null; }, value2submit: function(value) { return this.value2str(value); }, value2input: function(value) { this.$input.combodate('setValue', value); }, input2value: function() { return this.$input.combodate('getValue', null); }, activate: function() { this.$input.siblings('.combodate').find('select').eq(0).focus(); }, /* clear: function() { this.$input.data('datepicker').date = null; this.$input.find('.active').removeClass('active'); }, */ autosubmit: function() { } }); Constructor.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { /** @property tpl @default **/ tpl:'', /** @property inputclass @default null **/ inputclass: null, /** Format used for sending value to server. Also applied when converting date from data-value attribute.
See list of tokens in [momentjs docs](http://momentjs.com/docs/#/parsing/string-format) @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 `format`. @property viewformat @type string @default null **/ viewformat: null, /** Template used for displaying dropdowns. @property template @type string @default D / MMM / YYYY **/ template: 'D / MMM / YYYY', /** Configuration of combodate. Full list of options: http://vitalets.github.com/combodate/#docs @property combodate @type object @default null **/ combodate: null /* (not implemented yet) Text shown as clear date button. If false clear button will not be rendered. @property clear @type boolean|string @default 'x clear' */ //clear: '× clear' }); $.fn.editabletypes.combodate = Constructor; }(window.jQuery)); /* Editableform based on Twitter Bootstrap */ (function ($) { $.extend($.fn.editableform.Constructor.prototype, { initTemplate: function() { this.$form = $($.fn.editableform.template); this.$form.find('.editable-error-block').addClass('help-block'); } }); //buttons $.fn.editableform.buttons = ''+ ''; //error classes $.fn.editableform.errorGroupClass = 'error'; $.fn.editableform.errorBlockClass = null; }(window.jQuery)); /** * Editable Popover * --------------------- * requires bootstrap-popover.js */ (function ($) { //extend methods $.extend($.fn.editableContainer.Popup.prototype, { containerName: 'popover', //for compatibility with bootstrap <= 2.2.1 (content inserted into

instead of directly .popover-content) innerCss: $($.fn.popover.defaults.template).find('p').length ? '.popover-content p' : '.popover-content', initContainer: function(){ $.extend(this.containerOptions, { trigger: 'manual', selector: false, content: ' ', template: $.fn.popover.defaults.template }); //as template property is used in inputs, hide it from popover var t; if(this.$element.data('template')) { t = this.$element.data('template'); this.$element.removeData('template'); } this.call(this.containerOptions); if(t) { //restore data('template') this.$element.data('template', t); } }, /* show */ innerShow: function () { this.call('show'); }, /* hide */ innerHide: function () { this.call('hide'); }, /* destroy */ innerDestroy: function() { this.call('destroy'); }, setContainerOption: function(key, value) { this.container().options[key] = value; }, /** * move popover to new position. This function mainly copied from bootstrap-popover. */ /*jshint laxcomma: true*/ setPosition: function () { (function() { var $tip = this.tip() , inside , pos , actualWidth , actualHeight , placement , tp; placement = typeof this.options.placement === 'function' ? this.options.placement.call(this, $tip[0], this.$element[0]) : this.options.placement; inside = /in/.test(placement); $tip // .detach() //vitalets: remove any placement class because otherwise they dont influence on re-positioning of visible popover .removeClass('top right bottom left') .css({ top: 0, left: 0, display: 'block' }); // .insertAfter(this.$element); pos = this.getPosition(inside); actualWidth = $tip[0].offsetWidth; actualHeight = $tip[0].offsetHeight; switch (inside ? placement.split(' ')[1] : placement) { case 'bottom': tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2}; break; case 'top': tp = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2}; break; case 'left': tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth}; break; case 'right': tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width}; break; } $tip .offset(tp) .addClass(placement) .addClass('in'); }).call(this.container()); /*jshint laxcomma: false*/ } }); }(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 15/05/1984 **/ (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; } //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'; //store DPglobal this.dpg = $.fn.datepicker.DPGlobal; //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 = $('').html(this.options.clear).click($.proxy(function(e){ e.preventDefault(); e.stopPropagation(); this.clear(); }, this)); this.$tpl.parent().append($('

').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); }, 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); }, 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('changeDate', function(e){ var $form = $(this).closest('form'); setTimeout(function() { $form.submit(); }, 200); }); } }); Date.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { /** @property tpl @default
**/ tpl:'
', /** @property inputclass @default null **/ inputclass: null, /** Format used for sending value to server. Also applied when converting date from data-value attribute.
Possible tokens are: d, dd, m, mm, yy, yyyy @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 format @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, autoclose: false } **/ datepicker:{ weekStart: 0, startView: 0, autoclose: false }, /** Text shown as clear date button. If false clear button will not be rendered. @property clear @type boolean|string @default 'x clear' **/ clear: '× clear' }); $.fn.editabletypes.date = Date; }(window.jQuery)); /** Bootstrap datefield input - modification for inline mode. Shows normal 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 } }); DateField.defaults = $.extend({}, $.fn.editabletypes.date.defaults, { /** @property tpl **/ tpl:'
', /** @property inputclass @default 'input-small' **/ inputclass: 'input-small', /* datepicker config */ datepicker: { weekStart: 0, startView: 0, autoclose: true } }); $.fn.editabletypes.datefield = DateField; }(window.jQuery)); /* ========================================================= * bootstrap-datepicker.js * http://www.eyecon.ro/bootstrap-datepicker * ========================================================= * Copyright 2012 Stefan Petre * Improvements by Andrew Rowls * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * 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 UTCDate(){ return new Date(Date.UTC.apply(Date, arguments)); } function UTCToday(){ var today = new Date(); return UTCDate(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate()); } // Picker object var Datepicker = function(element, options) { var that = this; this.element = $(element); this.language = options.language||this.element.data('date-language')||"en"; 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')||'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.hasInput = this.component && this.element.find('input').length; if(this.component && this.component.length === 0) this.component = false; this._attachEvents(); this.forceParse = true; if ('forceParse' in options) { this.forceParse = options.forceParse; } else if ('dateForceParse' in this.element.data()) { this.forceParse = this.element.data('date-force-parse'); } this.picker = $(DPGlobal.template) .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'); } else { this.picker.addClass('datepicker-dropdown dropdown-menu'); } if (this.isRTL){ this.picker.addClass('datepicker-rtl'); this.picker.find('.prev i, .next i') .toggleClass('icon-arrow-left icon-arrow-right'); } $(document).on('mousedown', function (e) { // Clicked outside the datepicker, hide it if ($(e.target).closest('.datepicker').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.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.todayBtn = (options.todayBtn||this.element.data('date-today-btn')||false); this.todayHighlight = (options.todayHighlight||this.element.data('date-today-highlight')||false); this.weekStart = ((options.weekStart||this.element.data('date-weekstart')||dates[this.language].weekStart||0) % 7); this.weekEnd = ((this.weekStart + 6) % 7); this.startDate = -Infinity; this.endDate = Infinity; this.daysOfWeekDisabled = []; this.setStartDate(options.startDate||this.element.data('date-startdate')); this.setEndDate(options.endDate||this.element.data('date-enddate')); this.setDaysOfWeekDisabled(options.daysOfWeekDisabled||this.element.data('date-days-of-week-disabled')); this.fillDow(); this.fillMonths(); this.update(); this.showMode(); if(this.isInline) { this.show(); } }; Datepicker.prototype = { constructor: Datepicker, _events: [], _attachEvents: function(){ this._detachEvents(); if (this.isInput) { // single input this._events = [ [this.element, { focus: $.proxy(this.show, this), keyup: $.proxy(this.update, this), keydown: $.proxy(this.keydown, this) }] ]; } else if (this.component && this.hasInput){ // component: input + button this._events = [ // For components that are not readonly, allow keyboard nav [this.element.find('input'), { focus: $.proxy(this.show, this), keyup: $.proxy(this.update, this), keydown: $.proxy(this.keydown, this) }], [this.component, { click: $.proxy(this.show, this) }] ]; } 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.endDate) { this.viewDate = new Date(this.endDate); } else { this.viewDate = new Date(this.date); } if (oldViewDate && oldViewDate.getTime() != this.viewDate.getTime()){ this.element.trigger({ type: 'changeDate', date: this.viewDate }); } this.fill(); }, fillDow: function(){ var dowCnt = this.weekStart, html = ''; while (dowCnt < this.weekStart + 7) { html += ''+dates[this.language].daysMin[(dowCnt++)%7]+''; } html += ''; this.picker.find('.datepicker-days thead').append(html); }, fillMonths: function(){ var html = '', i = 0; while (i < 12) { html += ''+dates[this.language].monthsShort[i++]+''; } this.picker.find('.datepicker-months td').html(html); }, 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, currentDate = this.date && this.date.valueOf(), today = new Date(); this.picker.find('.datepicker-days thead th:eq(1)') .text(dates[this.language].months[month]+' '+year); this.picker.find('tfoot th.today') .text(dates[this.language].today) .toggle(this.todayBtn !== 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); 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) { html.push(''); } 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(''+prevMonth.getUTCDate() + ''); if (prevMonth.getUTCDay() == this.weekEnd) { html.push(''); } prevMonth.setUTCDate(prevMonth.getUTCDate()+1); } this.picker.find('.datepicker-days tbody').empty().append(html.join('')); var currentYear = this.date && this.date.getUTCFullYear(); var months = this.picker.find('.datepicker-months') .find('th:eq(1)') .text(year) .end() .find('span').removeClass('active'); if (currentYear && currentYear == year) { months.eq(this.date.getUTCMonth()).addClass('active'); } if (year < startYear || year > endYear) { months.addClass('disabled'); } if (year == startYear) { months.slice(0, startMonth).addClass('disabled'); } if (year == endYear) { months.slice(endMonth+1).addClass('disabled'); } html = ''; year = parseInt(year/10, 10) * 10; var yearCont = this.picker.find('.datepicker-years') .find('th:eq(1)') .text(year + '-' + (year + 9)) .end() .find('td'); year -= 1; for (var i = -1; i < 11; i++) { html += ''+year+''; year += 1; } yearCont.html(html); }, updateNavArrows: function() { 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()) { 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()) { 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()) { this.picker.find('.prev').css({visibility: 'hidden'}); } else { this.picker.find('.prev').css({visibility: 'visible'}); } if (this.endDate !== Infinity && year >= this.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': this.showMode(1); break; case 'prev': case 'next': var dir = DPGlobal.modes[this.viewMode].navStep * (target[0].className == 'prev' ? -1 : 1); switch(this.viewMode){ case 0: this.viewDate = this.moveMonth(this.viewDate, dir); break; case 1: case 2: this.viewDate = this.moveYear(this.viewDate, dir); break; } this.fill(); break; 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'; this._setDate(date, which); break; } break; case 'span': if (!target.is('.disabled')) { this.viewDate.setUTCDate(1); if (target.is('.month')) { var month = target.parent().find('span').index(target); this.viewDate.setUTCMonth(month); this.element.trigger({ type: 'changeMonth', date: this.viewDate }); } else { var year = parseInt(target.text(), 10)||0; this.viewDate.setUTCFullYear(year); this.element.trigger({ type: 'changeYear', date: this.viewDate }); } this.showMode(-1); this.fill(); } break; case 'td': if (target.is('.day') && !target.is('.disabled')){ var day = parseInt(target.text(), 10)||1; var year = this.viewDate.getUTCFullYear(), month = this.viewDate.getUTCMonth(); if (target.is('.old')) { if (month === 0) { month = 11; year -= 1; } else { month -= 1; } } else if (target.is('.new')) { if (month == 11) { month = 0; year += 1; } else { month += 1; } } this._setDate(UTCDate(year, month, day,0,0,0,0)); } break; } } }, _setDate: function(date, which){ if (!which || which == 'date') this.date = date; if (!which || which == 'view') this.viewDate = date; this.fill(); this.setValue(); this.element.trigger({ type: 'changeDate', date: this.date }); 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')) { this.hide(); } } }, moveMonth: function(date, dir){ if (!dir) return date; var new_date = new Date(date.valueOf()), day = new_date.getUTCDate(), month = new_date.getUTCMonth(), mag = Math.abs(dir), new_month, test; dir = dir > 0 ? 1 : -1; if (mag == 1){ test = dir == -1 // If going back one month, make sure month is not current month // (eg, Mar 31 -> Feb 31 == Feb 28, not Mar 02) ? function(){ return new_date.getUTCMonth() == month; } // If going forward one month, make sure month is as expected // (eg, Jan 31 -> Feb 31 == Feb 28, not Mar 02) : function(){ return new_date.getUTCMonth() != new_month; }; new_month = month + dir; new_date.setUTCMonth(new_month); // Dec -> Jan (12) or Jan -> Dec (-1) -- limit expected date to 0-11 if (new_month < 0 || new_month > 11) new_month = (new_month + 12) % 12; } else { // For magnitudes >1, move one month at a time... for (var i=0; i= this.startDate && date <= this.endDate; }, keydown: function(e){ if (this.picker.is(':not(:visible)')){ if (e.keyCode == 27) // allow escape to hide and re-show picker this.show(); return; } var dateChanged = false, dir, day, month, newDate, newViewDate; switch(e.keyCode){ case 27: // escape this.hide(); e.preventDefault(); break; case 37: // left case 39: // right if (!this.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){ newDate = this.moveMonth(this.date, dir); newViewDate = this.moveMonth(this.viewDate, dir); } else { newDate = new Date(this.date); newDate.setUTCDate(this.date.getUTCDate() + dir); newViewDate = new Date(this.viewDate); newViewDate.setUTCDate(this.viewDate.getUTCDate() + dir); } if (this.dateWithinRange(newDate)){ this.date = newDate; this.viewDate = newViewDate; this.setValue(); this.update(); e.preventDefault(); dateChanged = true; } break; case 38: // up case 40: // down if (!this.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){ newDate = this.moveMonth(this.date, dir); newViewDate = this.moveMonth(this.viewDate, dir); } else { newDate = new Date(this.date); newDate.setUTCDate(this.date.getUTCDate() + dir * 7); newViewDate = new Date(this.viewDate); newViewDate.setUTCDate(this.viewDate.getUTCDate() + dir * 7); } if (this.dateWithinRange(newDate)){ this.date = newDate; this.viewDate = newViewDate; this.setValue(); this.update(); e.preventDefault(); dateChanged = true; } break; case 13: // enter this.hide(); e.preventDefault(); break; case 9: // tab this.hide(); break; } if (dateChanged){ this.element.trigger({ type: 'changeDate', date: this.date }); var element; if (this.isInput) { element = this.element; } else if (this.component){ element = this.element.find('input'); } if (element) { element.change(); } } }, showMode: function(dir) { if (dir) { this.viewMode = Math.max(0, 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. Changed to .css('display', 'block') solve the problem. See https://github.com/vitalets/x-editable/issues/37 In jquery 1.7.2+ everything works fine. */ //this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).show(); this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).css('display', 'block'); this.updateNavArrows(); } }; $.fn.datepicker = function ( option ) { var args = Array.apply(null, arguments); args.shift(); 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)))); } if (typeof option == 'string' && typeof data[option] == 'function') { data[option].apply(data, args); } }); }; $.fn.datepicker.defaults = { }; $.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" } }; var DPGlobal = { modes: [ { clsName: 'days', navFnc: 'Month', navStep: 1 }, { clsName: 'months', navFnc: 'FullYear', navStep: 1 }, { clsName: 'years', navFnc: 'FullYear', navStep: 10 }], isLeapYear: function (year) { return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0)); }, getDaysInMonth: function (year, month) { return [31, (DPGlobal.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]; }, validParts: /dd?|DD?|mm?|MM?|yy(?:yy)?/g, nonpunctuation: /[^ -\/:-@\[\u3400-\u9fff-`{-~\t\n\r]+/g, parseFormat: function(format){ // IE treats \0 as a string end in inputs (truncating the value), // so it's a bad format delimiter, anyway var separators = format.replace(this.validParts, '\0').split('\0'), parts = format.match(this.validParts); if (!separators || !separators.length || !parts || parts.length === 0){ throw new Error("Invalid date format."); } return {separators: separators, parts: parts}; }, parseDate: function(date, format, language) { if (date instanceof Date) return date; if (/^[\-+]\d+[dmwy]([\s,]+[\-+]\d+[dmwy])*$/.test(date)) { var part_re = /([\-+]\d+)([dmwy])/, parts = date.match(/([\-+]\d+)([dmwy])/g), part, dir; date = new Date(); for (var i=0; i'+ ''+ ''+ ''+ ''+ ''+ '', contTemplate: '', footTemplate: '' }; DPGlobal.template = '
'+ '
'+ ''+ DPGlobal.headTemplate+ ''+ DPGlobal.footTemplate+ '
'+ '
'+ '
'+ ''+ DPGlobal.headTemplate+ DPGlobal.contTemplate+ DPGlobal.footTemplate+ '
'+ '
'+ '
'+ ''+ DPGlobal.headTemplate+ DPGlobal.contTemplate+ DPGlobal.footTemplate+ '
'+ '
'+ '
'; $.fn.datepicker.DPGlobal = DPGlobal; }( window.jQuery );