/*! X-editable - v1.1.1 * In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery * http://github.com/vitalets/x-editable * Copyright (c) 2012 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!) Editableform is linked with one of input types, e.g. 'text' or 'select'. @class editableform @uses text @uses textarea **/ (function ($) { var EditableForm = function (element, options) { this.options = $.extend({}, $.fn.editableform.defaults, options); this.$element = $(element); //div, containing form. Not form tag! Not editable-element. this.initInput(); }; EditableForm.prototype = { constructor: EditableForm, initInput: function() { //called once var TypeConstructor, typeOptions; //create input of specified type if(typeof $.fn.editableform.types[this.options.type] === 'function') { TypeConstructor = $.fn.editableform.types[this.options.type]; typeOptions = $.fn.editableform.utils.sliceObj(this.options, $.fn.editableform.utils.objectKeys(TypeConstructor.defaults)); this.input = new TypeConstructor(typeOptions); } else { $.error('Unknown type: '+ this.options.type); return; } 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() { this.$loading = $($.fn.editableform.loading); this.$element.empty().append(this.$loading); this.showLoading(); //init form template and buttons this.initTemplate(); if(this.options.showbuttons) { this.initButtons(); } else { this.$form.find('.editable-buttons').remove(); } /** Fired when rendering starts @event rendering @param {Object} event event object **/ this.$element.triggerHandler('rendering'); //render input $.when(this.input.render()) .then($.proxy(function () { //input this.$form.find('div.editable-input').append(this.input.$input); //automatically submit inputs when no buttons shown if(!this.options.showbuttons) { this.input.autosubmit(); } //"clear" link if(this.input.$clear) { this.$form.find('div.editable-input').append($('
').append(this.input.$clear)); } //append form to container this.$element.append(this.$form); //attach 'cancel' handler this.$form.find('.editable-cancel').click($.proxy(this.cancel, this)); // this.$form.find('.editable-buttons button').eq(1).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); } else { this.error(false); this.input.$input.removeAttr('disabled'); this.$form.find('.editable-submit').removeAttr('disabled'); this.input.value2input(this.value); this.$form.submit($.proxy(this.submit, this)); } /** Fired when form is rendered @event rendered @param {Object} event event object **/ this.$element.triggerHandler('rendered'); this.showForm(); }, this)); }, cancel: function() { /** Fired when form was cancelled by user @event cancel @param {Object} event event object **/ this.$element.triggerHandler('cancel'); }, showLoading: function() { var w; if(this.$form) { //set loading size equal to form this.$loading.width(this.$form.outerWidth()); this.$loading.height(this.$form.outerHeight()); 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() { this.$loading.hide(); this.$form.show(); this.input.activate(); /** Fired when form is shown @event show @param {Object} event event object **/ this.$element.triggerHandler('show'); }, error: function(msg) { var $group = this.$form.find('.control-group'), $block = this.$form.find('.editable-error-block'); if(msg === false) { $group.removeClass($.fn.editableform.errorGroupClass); $block.removeClass($.fn.editableform.errorBlockClass).empty().hide(); } else { $group.addClass($.fn.editableform.errorGroupClass); $block.addClass($.fn.editableform.errorBlockClass).text(msg).show(); } }, submit: function(e) { e.stopPropagation(); e.preventDefault(); var error, newValue = this.input.input2value(), //get new value from input newValueStr; //validation if (error = this.validate(newValue)) { this.error(error); this.showForm(); return; } //value as string newValueStr = this.input.value2str(newValue); //if value not changed --> cancel /*jslint eqeq: true*/ if (newValueStr == this.input.value2str(this.value)) { /*jslint eqeq: false*/ this.cancel(); return; } //sending data to server $.when(this.save(newValueStr)) .done($.proxy(function(response) { //run success callback var res = typeof this.options.success === 'function' ? this.options.success.call(this, response, newValue) : null; //if success callback returns string --> show error if(res && 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.$element.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(value) { var pk = (typeof this.options.pk === 'function') ? this.options.pk.call(this) : this.options.pk, send = !!(typeof this.options.url === 'function' || (this.options.url && ((this.options.send === 'always') || (this.options.send === 'auto' && pk)))), params, ajaxOptions; if (send) { //send to server this.showLoading(); //standard params params = { name: this.options.name || '', value: value, pk: pk }; //additional params if(typeof this.options.params === 'function') { $.extend(params, this.options.params.call(this, params)); } else { //try parse json in single quotes (from data-params attribute) this.options.params = $.fn.editableform.utils.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, params); } else { //send ajax to server and return deferred object ajaxOptions = $.extend({ url : this.options.url, data : params, type : 'post', dataType: 'json' }, this.options.ajaxOptions); return $.ajax(ajaxOptions); } } }, validate: function (value) { if (value === undefined) { value = this.value; } if (typeof this.options.validate === 'function') { return this.options.validate.call(this, value); } }, option: function(key, value) { this.options[key] = value; if(key === 'value') { this.setValue(value); } }, setValue: function(value, convertStr) { if(convertStr) { this.value = this.input.str2value(value); } else { this.value = 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('field cannot be "abc"'); //returning error via deferred object } else { someModel.set(params.name, params.value); //save data in some js model } } **/ url:null, /** Additional params for submit. Function can be used to calculate params dynamically @example params: function(params) { return { a: 1 }; } @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 dinamically 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**. Usefull 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: function(response, newValue) {}, /** Additional options for ajax request. List of values: http://api.jquery.com/jQuery.ajax @property ajaxOptions @type object @default null **/ ajaxOptions: null, /** Wether to show buttons or not. Form without buttons can be auto-submitted by input or by onblur = 'submit'. @property showbuttons @type boolean @default true **/ showbuttons: true /*todo: Submit strategy. Can be normal|never submitmode='never' usefull for turning into classic form several inputs and submitting them together manually. Works pretty with showbuttons=false @property submitmode @type string @default normal */ // submitmode: 'normal' }; /* 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 attahced to control-group $.fn.editableform.errorGroupClass = null; //error class attahced to editable-error-block $.fn.editableform.errorBlockClass = 'editable-error'; //input types $.fn.editableform.types = {}; //utils $.fn.editableform.utils = {}; }(window.jQuery)); /** * EditableForm utilites */ (function ($) { $.fn.editableform.utils = { /** * 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)) { data[k] = v; } }); return data; }, 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; } } }; }(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 EditableContainer = function (element, options) { this.init(element, options); }; //methods EditableContainer.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.editableform.utils.getConfigData(this.$element), options); this.splitOptions(); 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 handlers (once) 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); //if click inside some editableContainer --> no nothing if($target.is('.editable-container') || $target.parents('.editable-container').length || $target.parents('.ui-datepicker-header').length) { return; } else { //close all open containers (except one) EditableContainer.prototype.closeOthers(e.target); } }); $(document).data('editable-handlers-attached', true); } }, //split options on containerOptions and formOptions splitOptions: function() { this.containerOptions = {}; this.formOptions = {}; var cDef = $.fn[this.containerName].defaults; for(var k in this.options) { if(k in cDef) { this.containerOptions[k] = this.options[k]; } else { this.formOptions[k] = this.options[k]; } } }, initContainer: function(){ this.call(this.containerOptions); }, initForm: function() { this.$form = $('
') .editableform(this.formOptions) .on({ save: $.proxy(this.save, this), cancel: $.proxy(this.cancel, this), show: $.proxy(this.setPosition, this), //re-position container every time form is shown (after loading state) rendering: $.proxy(this.setPosition, this), //this allows to place container correctly when loading shown rendered: $.proxy(function(){ /** Fired when container is shown and form is rendered (for select will wait for loading dropdown options) @event shown @param {Object} event event object @example $('#username').on('shown', function() { var $tip = $(this).data('editableContainer').tip(); $tip.find('input').val('overwriting value of input..'); }); **/ this.$element.triggerHandler('shown'); }, this) }); return this.$form; }, /* Returns jquery object of container @method tip() */ tip: function() { return this.container().$tip; }, container: function() { return this.$element.data(this.containerName); }, call: function() { this.$element[this.containerName].apply(this.$element, arguments); }, /** Shows container with form @method show() @param {boolean} closeAll Wether to close all other editable containers when showing this one. Default true. **/ show: function (closeAll) { this.$element.addClass('editable-open'); if(closeAll !== false) { //close all open containers (except this) this.closeOthers(this.$element[0]); } this.innerShow(); }, /* internal show method. To be overwritten in child classes */ innerShow: function () { this.call('show'); this.tip().addClass('editable-container'); this.initForm(); this.tip().find(this.innerCss).empty().append(this.$form); this.$form.editableform('render'); }, /** Hides container with form @method hide() **/ hide: function() { 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 **/ this.$element.triggerHandler('hidden'); }, /* internal hide method. To be overwritten in child classes */ innerHide: function () { this.call('hide'); }, /** Toggles container visibility (show / hide) @method toggle() @param {boolean} closeAll Wether to close all other editable containers when showing this one. Default true. **/ toggle: function(closeAll) { if(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 }, cancel: function() { if(this.options.autohide) { this.hide(); } /** Fired when form was cancelled by user @event cancel @param {Object} event event object **/ this.$element.triggerHandler('cancel'); }, save: function(e, params) { if(this.options.autohide) { this.hide(); } /** 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.call('destroy'); }, /* 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 if(el === element) { 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(); } 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; if (!data) { $this.data(dataKey, (data = new EditableContainer(this, options))); } if (typeof option === 'string') { //call method data[option].apply(data, Array.prototype.slice.call(args, 1)); } }); }; //store constructor $.fn.editableContainer.Constructor = EditableContainer; //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', /** Wether 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' **/ onblur: 'cancel' }; /* * 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)); /** 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.editableform.utils.getConfigData(this.$element), options); this.init(); }; Editable.prototype = { constructor: Editable, init: function () { var TypeConstructor, isValueByText = false, doAutotext, finalize; //initialization flag this.isInit = true; //editableContainer must be defined if(!$.fn.editableContainer) { $.error('You must define $.fn.editableContainer via including corresponding file (e.g. editable-popover.js)'); return; } //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 if(typeof $.fn.editableform.types[this.options.type] === 'function') { TypeConstructor = $.fn.editableform.types[this.options.type]; this.typeOptions = $.fn.editableform.utils.sliceObj(this.options, $.fn.editableform.utils.objectKeys(TypeConstructor.defaults)); this.input = new TypeConstructor(this.typeOptions); } else { $.error('Unknown type: '+ this.options.type); 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 { if(typeof this.options.value === 'string') { this.options.value = $.trim(this.options.value); } this.value = this.input.str2value(this.options.value); } //add 'editable' class 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.input.value2html(this.value, this.$element) : true).then($.proxy(function() { if(this.options.disabled) { this.disable(); } else { this.enable(); } /** Fired each time when element's text is rendered. Occurs on initialization and on each update of value. Can be used for display customization. @event render @param {Object} event event object @param {Object} editable editable instance @example $('#action').on('render', function(e, editable) { var colors = {0: "gray", 1: "green", 2: "blue", 3: "red"}; $(this).css("color", colors[editable.value]); }); **/ this.$element.triggerHandler('render', this); this.isInit = false; }, this)); }, /** 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') { if(value) { this.disable(); } else { this.enable(); } return; } //value if(key === 'value') { this.setValue(value); } //transfer new option to container! if(this.container) { this.container.option(key, value); } }, /* * set emptytext if element is empty (reverse: remove emptytext if needed) */ handleEmpty: function () { 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 Wether 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, autohide: false //element will take care to show/hide container }); this.$element.editableContainer(containerOptions); this.$element.on({ save: $.proxy(this.save, this), cancel: $.proxy(this.hide, 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(); } //return focus on element if (this.options.enablefocus && this.options.toggle === 'click') { this.$element.focus(); } }, /** Toggles container visibility (show / hide) @method toggle() @param {boolean} closeAll Wether 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' && 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.hide(); this.setValue(params.newValue); /** 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) { //assuming server response: '{success: true}' var pk = $(this).data('editable').options.pk; if(params.response && params.response.success) { alert('value: ' + params.newValue + ' with pk: ' + pk + ' saved!'); } else { alert('error!'); } }); **/ //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 wether to convert value from string to internal format **/ setValue: function(value, convertStr) { if(convertStr) { this.value = this.input.str2value(value); } else { this.value = value; } if(this.container) { this.container.option('value', this.value); } $.when(this.input.value2html(this.value, this.$element)) .then($.proxy(function() { this.handleEmpty(); this.$element.triggerHandler('render', this); }, this)); }, /** Activates input of visible container (e.g. set focus) @method activate() **/ activate: function() { if(this.container) { this.container.activate(); } } }; /* 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 requied", 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.value2str(data.value); } }); return result; /** This method collects values from several editable elements and submit them all to server. It is designed mainly for creating new records. @method submit(options) @param {object} options @param {object} options.url url to submit data @param {object} options.data additional data to submit @param {function} options.error(obj) error handler (called on both client-side and server-side validation errors) @param {function} options.success(obj) 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(typeof config.error !== 'function') { config.error = function() {}; } if($.isEmptyObject(errors)) { values = this.editable('getValue'); if(config.data) { $.extend(values, config.data); } $.ajax({ type: 'POST', url: config.url, data: values, dataType: 'json' }).success(function(response) { if(typeof response === 'object' && response.id) { $elems.editable('option', 'pk', response.id); $elems.removeClass('editable-unsaved'); if(typeof config.success === 'function') { config.success.apply($elems, arguments); } } else { //server-side validation error config.error.apply($elems, arguments); } }).error(function(){ //ajax error config.error.apply($elems, arguments); }); } else { //client-side validation error config.error.call($elems, {errors: 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. Usefull 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', /** Wether to return focus on element after form is closed. This allows fully keyboard input. @property enablefocus @type boolean @default false **/ enablefocus: false, /** Initial value of input. Taken from data-value or element's text. @property value @type mixed @default element's text **/ value: null }; }(window.jQuery)); /** Abstract editable input class. To create your own input you should inherit from this class. @class abstract **/ (function ($) { var Abstract = function () { }; Abstract.prototype = { /** Iinitializes input @method init() **/ init: function(type, options, defaults) { this.type = type; this.options = $.extend({}, defaults, options); this.$input = null; this.$clear = null; this.error = null; }, /** Renders input. Can return jQuery deferred object. @method render() **/ render: function() { this.$input = $(this.options.tpl); if(this.options.inputclass) { this.$input.addClass(this.options.inputclass); } if (this.options.placeholder) { this.$input.attr('placeholder', this.options.placeholder); } }, /** Sets element's html by value. @method value2html(value, element) @param {mixed} value @param {DOMElement} element **/ value2html: function(value, element) { var html = this.escape(value); $(element).html(html); }, /** 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 submiting to server) @method value2str(value) @param {mixed} value @returns {string} **/ value2str: function(value) { return value; }, /** Converts string received from server into value. @method str2value(str) @param {string} str @returns {mixed} **/ str2value: function(str) { return str; }, /** 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(); } }, /** Creares 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 (usefull when buttons not shown) **/ autosubmit: function() { } }; Abstract.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 span2 **/ inputclass: 'span2', /** Name attribute of input @property name @type string @default null **/ name: null }; $.extend($.fn.editableform.types, {abstract: Abstract}); }(window.jQuery)); /** List - abstract class for inputs that have source option loaded from js array or via ajax @class list @extends abstract **/ (function ($) { var List = function (options) { }; $.fn.editableform.utils.inherit(List, $.fn.editableform.types.abstract); $.extend(List.prototype, { render: function () { List.superclass.render.call(this); var deferred = $.Deferred(); this.error = null; this.sourceData = null; this.prependData = 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) { var deferred = $.Deferred(); this.onSourceReady(function () { this.value2htmlFinal(value, element); deferred.resolve(); }, function () { List.superclass.value2html(this.options.sourceError, element); 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.editableform.utils.tryParseJson(this.options.source, false); } catch (e) { error.call(this); return; } //loading from url if (typeof this.options.source === 'string') { var cacheID = this.options.source + (this.options.name ? '-' + this.options.name : ''), 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; 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; 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, data: this.options.name ? {name: this.options.name} : {}, dataType: 'json', success: $.proxy(function (data) { cache.loading = false; this.sourceData = this.makeArray(data); if($.isArray(this.sourceData)) { this.doPrepend(); //store result in cache cache.sourceData = this.sourceData; success.call(this); $.each(cache.callbacks, function () { this.call(); }); //run success callbacks for other fields } else { error.call(this); $.each(cache.err_callbacks, function () { this.call(); }); //run error callbacks for other fields } }, this), error: $.proxy(function () { cache.loading = false; error.call(this); $.each(cache.err_callbacks, function () { this.call(); }); //run error callbacks for other fields }, this) }); } else { //options as json/array 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.editableform.utils.tryParseJson(this.options.prepend, true); if (typeof this.options.prepend === 'string') { this.options.prepend = {'': this.options.prepend}; } 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; }, //search for item by particular value itemByVal: function(val) { if($.isArray(this.sourceData)) { for(var i=0; i[{value: 1, text: "text"}, {...}]
For compability it also supports format {value1: "text1", value2: "text2" ...} but it does not guarantee elements order. @property source @type string|array|object @default null **/ source:null, /** Data automatically prepended to the begining of dropdown list. @property prepend @type string|array|object @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' }); $.fn.editableform.types.list = List; }(window.jQuery)); /** Text input @class text @extends abstract @final @example awesome **/ (function ($) { var Text = function (options) { this.init('text', options, Text.defaults); }; $.fn.editableform.utils.inherit(Text, $.fn.editableform.types.abstract); $.extend(Text.prototype, { activate: function() { if(this.$input.is(':visible')) { this.$input.focus(); $.fn.editableform.utils.setCursorPosition(this.$input.get(0), this.$input.val().length); } } }); Text.defaults = $.extend({}, $.fn.editableform.types.abstract.defaults, { /** @property tpl @default **/ tpl: '', /** Placeholder attribute of input. Shown when input is empty. @property placeholder @type string @default null **/ placeholder: null }); $.fn.editableform.types.text = Text; }(window.jQuery)); /** Textarea input @class textarea @extends abstract @final @example awesome comment! **/ (function ($) { var Textarea = function (options) { this.init('textarea', options, Textarea.defaults); }; $.fn.editableform.utils.inherit(Textarea, $.fn.editableform.types.abstract); $.extend(Textarea.prototype, { render: function () { Textarea.superclass.render.call(this); //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 lines = html.split(//i); for (var i = 0; i < lines.length; i++) { lines[i] = $('
').html(lines[i]).text(); } return lines.join("\n"); }, activate: function() { if(this.$input.is(':visible')) { $.fn.editableform.utils.setCursorPosition(this.$input.get(0), this.$input.val().length); this.$input.focus(); } } }); Textarea.defaults = $.extend({}, $.fn.editableform.types.abstract.defaults, { /** @property tpl @default **/ tpl:'', /** @property inputclass @default span3 **/ inputclass:'span3', /** Placeholder attribute of input. Shown when input is empty. @property placeholder @type string @default null **/ placeholder: null }); $.fn.editableform.types.textarea = Textarea; }(window.jQuery)); /** Select (dropdown) @class select @extends list @final @example **/ (function ($) { var Select = function (options) { this.init('select', options, Select.defaults); }; $.fn.editableform.utils.inherit(Select, $.fn.editableform.types.list); $.extend(Select.prototype, { renderList: function() { if(!$.isArray(this.sourceData)) { return; } for(var i=0; i', {value: this.sourceData[i].value}).text(this.sourceData[i].text)); } }, value2htmlFinal: function(value, element) { var text = '', item = this.itemByVal(value); if(item) { text = item.text; } Select.superclass.constructor.superclass.value2html(text, element); }, autosubmit: function() { this.$input.on('change', function(){ $(this).closest('form').submit(); }); } }); Select.defaults = $.extend({}, $.fn.editableform.types.list.defaults, { /** @property tpl @default **/ tpl:'' }); $.fn.editableform.types.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.editableform.utils.inherit(Checklist, $.fn.editableform.types.list); $.extend(Checklist.prototype, { renderList: function() { var $label, $div; 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.$input); } }, value2str: function(value) { return $.isArray(value) ? value.join($.trim(this.options.separator)) : ''; //it is also possible to sent as array //return value; }, //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) { var $checks = this.$input.find('input[type="checkbox"]'); $checks.removeAttr('checked'); if($.isArray(value) && value.length) { $checks.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.find('input:checked').each(function(i, el) { checked.push($(el).val()); }); return checked; }, //collect text of checked boxes value2htmlFinal: function(value, element) { var selected = [], item, i, html = ''; if($.isArray(value) && value.length <= this.options.limit) { for(i=0; i').text(item.text).html()); } } html = selected.join(this.options.viewseparator); } else { html = this.options.limitText.replace('{checked}', $.isArray(value) ? value.length : 0).replace('{count}', this.sourceData.length); } $(element).html(html); }, activate: function() { this.$input.find('input[type="checkbox"]').first().focus(); }, autosubmit: function() { this.$input.find('input[type="checkbox"]').on('keydown', function(e){ if (e.which === 13) { $(this).closest('form').submit(); } }); } }); Checklist.defaults = $.extend({}, $.fn.editableform.types.list.defaults, { /** @property tpl @default
**/ tpl:'
', /** @property inputclass @type string @default span2 editable-checklist **/ inputclass: 'span2 editable-checklist', /** Separator of values in string when sending to server @property separator @type string @default ', ' **/ separator: ',', /** Separator of text when display as element content. @property viewseparator @type string @default '
' **/ viewseparator: '
', /** Maximum number of items shown as element content. If checked more items - limitText will be shown. @property limit @type integer @default 4 **/ limit: 4, /** Text shown when count of checked items is greater than limit parameter. You can use {checked} and {count} placeholders. @property limitText @type string @default 'Selected {checked} of {count}' **/ limitText: 'Selected {checked} of {count}' }); $.fn.editableform.types.checklist = Checklist; }(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.Constructor.prototype, { containerName: 'popover', innerCss: '.popover-content p', initContainer: function(){ $.extend(this.containerOptions, { trigger: 'manual', selector: false, content: 'dfgh' }); this.call(this.containerOptions); }, 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*/ } }); //defaults /* $.fn.editableContainer.defaults = $.extend({}, $.fn.popover.defaults, $.fn.editableContainer.defaults, { }); */ }(window.jQuery)); /** Bootstrap-datepicker. Description and examples: http://vitalets.github.com/bootstrap-datepicker. For localization you can include js file from here: https://github.com/eternicode/bootstrap-datepicker/tree/master/js/locales @class date @extends abstract @final @example 15/05/1984 **/ (function ($) { var Date = function (options) { this.init('date', options, Date.defaults); //set popular options directly from settings or data-* attributes var directOptions = $.fn.editableform.utils.sliceObj(this.options, ['format']); //overriding datepicker config (as by default jQuery extend() is not recursive) this.options.datepicker = $.extend({}, Date.defaults.datepicker, directOptions, options.datepicker); //by default viewformat equals to format if(!this.options.viewformat) { this.options.viewformat = this.options.datepicker.format; } //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.datepicker.format); this.parsedViewFormat = this.dpg.parseFormat(this.options.viewformat); }; $.fn.editableform.utils.inherit(Date, $.fn.editableform.types.abstract); $.extend(Date.prototype, { render: function () { Date.superclass.render.call(this); this.$input.datepicker(this.options.datepicker); if(this.options.clear) { this.$clear = $('').html(this.options.clear).click($.proxy(function(e){ e.preventDefault(); e.stopPropagation(); this.clear(); }, this)); } }, 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; }, 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.editableform.types.abstract.defaults, { /** @property tpl @default
**/ tpl:'
', /** @property inputclass @default editable-date well **/ inputclass: 'editable-date well', /** 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.editableform.types.date = Date; }(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.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; if (this.isInput) { //single input this.element.on({ focus: $.proxy(this.show, this), keyup: $.proxy(this.update, this), keydown: $.proxy(this.keydown, this) }); } else if(this.component && this.hasInput) { //component: input + button // For components that are not readonly, allow keyboard nav this.element.find('input').on({ focus: $.proxy(this.show, this), keyup: $.proxy(this.update, this), keydown: $.proxy(this.keydown, this) }); this.component.on('click', $.proxy(this.show, this)); } else if(this.element.is('div')) { //inline datepicker this.isInline = true; } else { this.element.on('click', $.proxy(this.show, this)); } 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('dropdown-menu'); } $(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'); } 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; case 0: case 'month': default: this.viewMode = this.startViewMode = 0; 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.setStartDate(options.startDate||this.element.data('date-startdate')); this.setEndDate(options.endDate||this.element.data('date-enddate')); this.fillDow(); this.fillMonths(); this.update(); this.showMode(); if(this.isInline) { this.show(); } }; Datepicker.prototype = { constructor: Datepicker, show: function(e) { this.picker.show(); this.height = this.component ? this.component.outerHeight() : this.element.outerHeight(); this.update(); this.place(); $(window).on('resize', $.proxy(this.place, this)); if (e ) { e.stopPropagation(); e.preventDefault(); } this.element.trigger({ type: 'show', date: this.date }); }, hide: function(e){ if(this.isInline) return; this.picker.hide(); $(window).off('resize', this.place); this.viewMode = this.startViewMode; this.showMode(); if (!this.isInput) { $(document).off('mousedown', this.hide); } if (e && e.currentTarget.value) this.setValue(); this.element.trigger({ type: 'hide', date: this.date }); }, getDate: function() { var d = this.getUTCDate(); return new Date(d.getTime() + (d.getTimezoneOffset()*60000)) }, getUTCDate: function() { return this.date; }, setDate: function(d) { this.setUTCDate(new Date(d.getTime() - (d.getTimezoneOffset()*60000))); }, setUTCDate: function(d) { this.date = d; this.setValue(); }, setValue: function() { var formatted = this.getFormattedDate(); if (!this.isInput) { if (this.component){ this.element.find('input').prop('value', formatted); } this.element.data('date', formatted); } else { this.element.prop('value', formatted); } }, getFormattedDate: function(format) { if(format == undefined) format = this.format; return DPGlobal.formatDate(this.date, format, this.language); }, setStartDate: function(startDate){ this.startDate = startDate||-Infinity; if (this.startDate !== -Infinity) { this.startDate = DPGlobal.parseDate(this.startDate, this.format, this.language); } this.update(); this.updateNavArrows(); }, setEndDate: function(endDate){ this.endDate = endDate||Infinity; if (this.endDate !== Infinity) { this.endDate = DPGlobal.parseDate(this.endDate, this.format, this.language); } this.update(); this.updateNavArrows(); }, place: function(){ if(this.isInline) return; var zIndex = parseInt(this.element.parents().filter(function() { return $(this).css('z-index') != 'auto'; }).first().css('z-index'))+10; var offset = this.component ? this.component.offset() : this.element.offset(); this.picker.css({ top: offset.top + this.height, left: offset.left, zIndex: zIndex }); }, update: function(){ var date, fromArgs = false; if(arguments && arguments.length && (typeof arguments[0] === 'string' || arguments[0] instanceof Date)) { date = arguments[0]; fromArgs = true; } else { date = this.isInput ? this.element.prop('value') : this.element.data('date') || this.element.find('input').prop('value'); } this.date = DPGlobal.parseDate(date, this.format, this.language); if(fromArgs) this.setValue(); if (this.date < this.startDate) { this.viewDate = new Date(this.startDate); } else if (this.date > this.endDate) { this.viewDate = new Date(this.endDate); } else { this.viewDate = new Date(this.date); } this.fill(); }, fillDow: function(){ var dowCnt = this.weekStart; var 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 = ''; var 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.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); 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 (prevMonth.valueOf() == currentDate) { clsName += ' active'; } if (prevMonth.valueOf() < this.startDate || prevMonth.valueOf() > this.endDate) { 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.getUTCFullYear(); var months = this.picker.find('.datepicker-months') .find('th:eq(1)') .text(year) .end() .find('span').removeClass('active'); if (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.setUTCHours(0); date.setUTCMinutes(0); date.setUTCSeconds(0); date.setUTCMilliseconds(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) { 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)); } this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).show(); 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?|mm?|MM?|yy(?:yy)?/g, nonpunctuation: /[^ -\/:-@\[-`{-~\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 );