All Downloads are FREE. Search and download functionalities are using the official Maven repository.

admin.javascripts.vendor.bootstrap-editable.js Maven / Gradle / Ivy

There is a newer version: 0.5.2
Show newest version
// moment.js
// version : 2.0.0
// author : Tim Wood
// license : MIT
// momentjs.com
(function(e){function O(e,t){return function(n){return j(e.call(this,n),t)}}function M(e){return function(t){return this.lang().ordinal(e.call(this,t))}}function _(){}function D(e){H(this,e)}function P(e){var t=this._data={},n=e.years||e.year||e.y||0,r=e.months||e.month||e.M||0,i=e.weeks||e.week||e.w||0,s=e.days||e.day||e.d||0,o=e.hours||e.hour||e.h||0,u=e.minutes||e.minute||e.m||0,a=e.seconds||e.second||e.s||0,f=e.milliseconds||e.millisecond||e.ms||0;this._milliseconds=f+a*1e3+u*6e4+o*36e5,this._days=s+i*7,this._months=r+n*12,t.milliseconds=f%1e3,a+=B(f/1e3),t.seconds=a%60,u+=B(a/60),t.minutes=u%60,o+=B(u/60),t.hours=o%24,s+=B(o/24),s+=i*7,t.days=s%30,r+=B(s/30),t.months=r%12,n+=B(r/12),t.years=n}function H(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e}function B(e){return e<0?Math.ceil(e):Math.floor(e)}function j(e,t){var n=e+"";while(n.length68?1900:2e3);break;case"YYYY":case"YYYYY":s[0]=~~t;break;case"a":case"A":n._isPm=(t+"").toLowerCase()==="pm";break;case"H":case"HH":case"h":case"hh":s[3]=~~t;break;case"m":case"mm":s[4]=~~t;break;case"s":case"ss":s[5]=~~t;break;case"S":case"SS":case"SSS":s[6]=~~(("0."+t)*1e3);break;case"X":n._d=new Date(parseFloat(t)*1e3);break;case"Z":case"ZZ":n._useUTC=!0,r=(t+"").match(x),r&&r[1]&&(n._tzh=~~r[1]),r&&r[2]&&(n._tzm=~~r[2]),r&&r[0]==="+"&&(n._tzh=-n._tzh,n._tzm=-n._tzm)}t==null&&(n._isValid=!1)}function J(e){var t,n,r=[];if(e._d)return;for(t=0;t<7;t++)e._a[t]=r[t]=e._a[t]==null?t===2?1:0:e._a[t];r[3]+=e._tzh||0,r[4]+=e._tzm||0,n=new Date(0),e._useUTC?(n.setUTCFullYear(r[0],r[1],r[2]),n.setUTCHours(r[3],r[4],r[5],r[6])):(n.setFullYear(r[0],r[1],r[2]),n.setHours(r[3],r[4],r[5],r[6])),e._d=n}function K(e){var t=e._f.match(a),n=e._i,r,i;e._a=[];for(r=0;r0,f[4]=n,Z.apply({},f)}function tt(e,n,r){var i=r-n,s=r-e.day();return s>i&&(s-=7),s11?n?"pm":"PM":n?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[last] dddd [at] LT",sameElse:"L"},calendar:function(e,t){var n=this._calendar[e];return typeof n=="function"?n.apply(t):n},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(e,t,n,r){var i=this._relativeTime[n];return typeof i=="function"?i(e,t,n,r):i.replace(/%d/i,e)},pastFuture:function(e,t){var n=this._relativeTime[e>0?"future":"past"];return typeof n=="function"?n(t):n.replace(/%s/i,t)},ordinal:function(e){return this._ordinal.replace("%d",e)},_ordinal:"%d",preparse:function(e){return e},postformat:function(e){return e},week:function(e){return tt(e,this._week.dow,this._week.doy)},_week:{dow:0,doy:6}},t=function(e,t,n){return nt({_i:e,_f:t,_l:n,_isUTC:!1})},t.utc=function(e,t,n){return nt({_useUTC:!0,_isUTC:!0,_l:n,_i:e,_f:t})},t.unix=function(e){return t(e*1e3)},t.duration=function(e,n){var r=t.isDuration(e),i=typeof e=="number",s=r?e._data:i?{}:e,o;return i&&(n?s[n]=e:s.milliseconds=e),o=new P(s),r&&e.hasOwnProperty("_lang")&&(o._lang=e._lang),o},t.version=n,t.defaultFormat=E,t.lang=function(e,n){var r;if(!e)return t.fn._lang._abbr;n?R(e,n):s[e]||U(e),t.duration.fn._lang=t.fn._lang=U(e)},t.langData=function(e){return e&&e._lang&&e._lang._abbr&&(e=e._lang._abbr),U(e)},t.isMoment=function(e){return e instanceof D},t.isDuration=function(e){return e instanceof P},t.fn=D.prototype={clone:function(){return t(this)},valueOf:function(){return+this._d},unix:function(){return Math.floor(+this._d/1e3)},toString:function(){return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._d},toJSON:function(){return t.utc(this).format("YYYY-MM-DD[T]HH:mm:ss.SSS[Z]")},toArray:function(){var e=this;return[e.year(),e.month(),e.date(),e.hours(),e.minutes(),e.seconds(),e.milliseconds()]},isValid:function(){return this._isValid==null&&(this._a?this._isValid=!q(this._a,(this._isUTC?t.utc(this._a):t(this._a)).toArray()):this._isValid=!isNaN(this._d.getTime())),!!this._isValid},utc:function(){return this._isUTC=!0,this},local:function(){return this._isUTC=!1,this},format:function(e){var n=X(this,e||t.defaultFormat);return this.lang().postformat(n)},add:function(e,n){var r;return typeof e=="string"?r=t.duration(+n,e):r=t.duration(e,n),F(this,r,1),this},subtract:function(e,n){var r;return typeof e=="string"?r=t.duration(+n,e):r=t.duration(e,n),F(this,r,-1),this},diff:function(e,n,r){var i=this._isUTC?t(e).utc():t(e).local(),s=(this.zone()-i.zone())*6e4,o,u;return n&&(n=n.replace(/s$/,"")),n==="year"||n==="month"?(o=(this.daysInMonth()+i.daysInMonth())*432e5,u=(this.year()-i.year())*12+(this.month()-i.month()),u+=(this-t(this).startOf("month")-(i-t(i).startOf("month")))/o,n==="year"&&(u/=12)):(o=this-i-s,u=n==="second"?o/1e3:n==="minute"?o/6e4:n==="hour"?o/36e5:n==="day"?o/864e5:n==="week"?o/6048e5:o),r?u:B(u)},from:function(e,n){return t.duration(this.diff(e)).lang(this.lang()._abbr).humanize(!n)},fromNow:function(e){return this.from(t(),e)},calendar:function(){var e=this.diff(t().startOf("day"),"days",!0),n=e<-6?"sameElse":e<-1?"lastWeek":e<0?"lastDay":e<1?"sameDay":e<2?"nextDay":e<7?"nextWeek":"sameElse";return this.format(this.lang().calendar(n,this))},isLeapYear:function(){var e=this.year();return e%4===0&&e%100!==0||e%400===0},isDST:function(){return this.zone()+t(e).startOf(n)},isBefore:function(e,n){return n=typeof n!="undefined"?n:"millisecond",+this.clone().startOf(n)<+t(e).startOf(n)},isSame:function(e,n){return n=typeof n!="undefined"?n:"millisecond",+this.clone().startOf(n)===+t(e).startOf(n)},zone:function(){return this._isUTC?0:this._d.getTimezoneOffset()},daysInMonth:function(){return t.utc([this.year(),this.month()+1,0]).date()},dayOfYear:function(e){var n=r((t(this).startOf("day")-t(this).startOf("year"))/864e5)+1;return e==null?n:this.add("d",e-n)},isoWeek:function(e){var t=tt(this,1,4);return e==null?t:this.add("d",(e-t)*7)},week:function(e){var t=this.lang().week(this);return e==null?t:this.add("d",(e-t)*7)},lang:function(t){return t===e?this._lang:(this._lang=U(t),this)}};for(i=0;i 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; } //convert value for submitting to server var submitValue = this.input.value2submit(newValue); this.isSaving = true; //sending data to server $.when(this.save(submitValue)) .done($.proxy(function(response) { this.isSaving = false; //run success callback var res = typeof this.options.success === 'function' ? this.options.success.call(this.options.scope, response, newValue) : null; //if success callback returns false --> keep form open and do not activate input if(res === false) { this.error(false); this.showForm(false); return; } //if success callback returns string --> keep form open, show error and activate input if(typeof res === 'string') { this.error(res); this.showForm(); return; } //if success callback returns object like {newValue: } --> use that value instead of submitted //it is usefull if you want to chnage value in url-function if(res && typeof res === 'object' && res.hasOwnProperty('newValue')) { newValue = res.newValue; } //clear error message this.error(false); this.value = newValue; /** Fired when form is submitted @event save @param {Object} event event object @param {Object} params additional params @param {mixed} params.newValue raw new value @param {mixed} params.submitValue submitted value as string @param {Object} params.response ajax response @example $('#form-div').on('save'), function(e, params){ if(params.newValue === 'username') {...} }); **/ this.$div.triggerHandler('save', {newValue: newValue, submitValue: submitValue, response: response}); }, this)) .fail($.proxy(function(xhr) { this.isSaving = false; var msg; if(typeof this.options.error === 'function') { msg = this.options.error.call(this.options.scope, xhr, newValue); } else { msg = typeof xhr === 'string' ? xhr : xhr.responseText || xhr.statusText || 'Unknown error!'; } this.error(msg); this.showForm(); }, this)); }, save: function(submitValue) { //try parse composite pk defined as json string in data-pk this.options.pk = $.fn.editableutils.tryParseJson(this.options.pk, true); var pk = (typeof this.options.pk === 'function') ? this.options.pk.call(this.options.scope) : this.options.pk, /* send on server in following cases: 1. url is function 2. url is string AND (pk defined OR send option = always) */ send = !!(typeof this.options.url === 'function' || (this.options.url && ((this.options.send === 'always') || (this.options.send === 'auto' && pk !== null && pk !== undefined)))), params; if (send) { //send to server this.showLoading(); //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 should return deferred object to run fail/done callbacks. @property url @type string|function @default null @example url: function(params) { var d = new $.Deferred; if(params.value === 'abc') { return d.reject('error message'); //returning error via deferred object } else { //async saving data in js model someModel.asyncSaveMethod({ ..., success: function(){ d.resolve(); } }); return d.promise(); } } **/ 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, /** Value that will be displayed in input if original field value is empty (`null|undefined|''`). @property defaultValue @type string|object @default null @since 1.4.6 **/ defaultValue: null, /** Strategy for sending data on server. Can be `auto|always|never`. When 'auto' data will be sent on server **only if pk and url defined**, otherwise new value will be stored locally. @property send @type string @default 'auto' **/ send: 'auto', /** Function for client-side validation. If returns string - means validation not passed and string showed as error. @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: null, /** Error callback. Called when request failed (response status != 200). Usefull when you want to parse error response and display a custom message. Must return **string** - the message to be displayed in the error block. @property error @type function @default null @since 1.4.4 @example error: function(response, newValue) { if(response.status === 500) { return 'Service unavailable. Please try later.'; } else { return response.responseText; } } **/ error: null, /** Additional options for submit ajax request. List of values: http://api.jquery.com/jQuery.ajax @property ajaxOptions @type object @default null @since 1.1.1 @example ajaxOptions: { type: 'put', dataType: 'json' } **/ ajaxOptions: null, /** Where to show buttons: left(true)|bottom|false Form without buttons is auto-submitted. @property showbuttons @type boolean|string @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'; //engine $.fn.editableform.engine = 'jquery'; }(window.jQuery)); /** * EditableForm utilites */ (function ($) { "use strict"; //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, valueProp) { if(!sourceData || value === null) { return []; } if (typeof(valueProp) !== "function") { var idKey = valueProp || 'value'; valueProp = function (e) { return e[idKey]; }; } var isValArray = $.isArray(value), result = [], that = this; $.each(sourceData, function(i, o) { if(o.children) { result = result.concat(that.itemsByValue(value, o.children, valueProp)); } else { /*jslint eqeq: true*/ if(isValArray) { if($.grep(value, function(v){ return v == (o && typeof o === 'object' ? valueProp(o) : o); }).length) { result.push(o); } } else { var itemValue = (o && (typeof o === 'object')) ? valueProp(o) : o; if(value == itemValue) { result.push(o); } } /*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'; } } //`datetime` should be datetimefield in 'inline' mode if(type === 'datetime' && options.mode === 'inline') { type = 'datetimefield'; } //change wysihtml5 to textarea for jquery UI and plain versions if(type === 'wysihtml5' && !$.fn.editabletypes[type]) { type = 'textarea'; } //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; } }, //see http://stackoverflow.com/questions/7264899/detect-css-transitions-using-javascript-and-without-modernizr supportsTransitions: function () { var b = document.body || document.documentElement, s = b.style, p = 'transition', v = ['Moz', 'Webkit', 'Khtml', 'O', 'ms']; if(typeof s[p] === 'string') { return true; } // Tests for vendor specific prop p = p.charAt(0).toUpperCase() + p.substr(1); for(var i=0; i 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 ($) { "use strict"; var Popup = function (element, options) { this.init(element, options); }; var Inline = function (element, options) { this.init(element, options); }; //methods Popup.prototype = { containerName: null, //method to call container on element containerDataName: null, //object name in element's .data() innerCss: null, //tbd in child class containerClass: 'editable-container editable-popup', //css class applied to container element defaults: {}, //container itself defaults init: function(element, options) { this.$element = $(element); //since 1.4.1 container do not use data-* directly as they already merged into options. this.options = $.extend({}, $.fn.editableContainer.defaults, options); this.splitOptions(); //set scope of form callbacks to element this.formOptions.scope = this.$element[0]; this.initContainer(); //flag to hide container, when saving value will finish this.delayedHide = false; //bind 'destroyed' listener to destroy container when element is removed from dom this.$element.on('destroyed', $.proxy(function(){ this.destroy(); }, this)); //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 //(mousedown could be better than click, it closes everything also on drag drop) $(document).on('click.editable', function(e) { var $target = $(e.target), i, exclude_classes = ['.editable-container', '.ui-datepicker-header', '.datepicker', //in inline mode datepicker is rendered into body '.modal-backdrop', '.bootstrap-wysihtml5-insert-image-modal', '.bootstrap-wysihtml5-insert-link-modal' ]; //check if element is detached. It occurs when clicking in bootstrap datepicker if (!$.contains(document.documentElement, e.target)) { return; } //for some reason FF 20 generates extra event (click) in select2 widget with e.target = document //we need to filter it via construction below. See https://github.com/vitalets/x-editable/issues/199 //Possibly related to http://stackoverflow.com/questions/10119793/why-does-firefox-react-differently-from-webkit-and-ie-to-click-event-on-selec if($target.is(document)) { return; } //if click inside one of exclude classes --> no nothing for(i=0; i container changes size before hide. */ //if form already exist - delete previous data if(this.$form) { //todo: destroy prev data! //this.$form.destroy(); } this.$form = $('
'); //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; } //if form is saving value, schedule hide if(this.$form.data('editableform').isSaving) { this.delayedHide = {reason: reason}; return; } else { this.delayedHide = false; } this.$element.removeClass('editable-open'); this.innerHide(); /** Fired when container was hidden. It occurs on both save or cancel. **Note:** Bootstrap popover has own `hidden` event that now cannot be separated from x-editable's one. The workaround is to check `arguments.length` that is always `2` for x-editable. @event hidden @param {object} event event object @param {string} reason Reason caused hiding. Can be save|cancel|onblur|nochange|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 || 'manual'); }, /* internal show method. To be overwritten in child classes */ innerShow: function () { }, /* internal hide method. To be overwritten in child classes */ innerHide: function () { }, /** Toggles container visibility (show / hide) @method toggle() @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. **/ 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) { /** 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); //hide must be after trigger, as saving value may require methods of plugin, applied to input this.hide('save'); }, /** 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 only) @property anim @type string @default false **/ anim: false, /** 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 ($) { "use strict"; //copy prototype from EditableContainer //extend methods $.extend($.fn.editableContainer.Inline.prototype, $.fn.editableContainer.Popup.prototype, { containerName: 'editableform', innerCss: '.editable-inline', containerClass: 'editable-container editable-inline', //css class applied to container element initContainer: function(){ //container is element this.$tip = $(''); //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.innerDestroy(); }, this)); }, innerDestroy: function() { if(this.tip()) { this.tip().empty().remove(); } } }); }(window.jQuery)); /** Makes editable any HTML element on the page. Applied as jQuery method. @class editable @uses editableContainer **/ (function ($) { "use strict"; var Editable = function (element, options) { this.$element = $(element); //data-* has more priority over js options: because dynamically created elements may change data-* this.options = $.extend({}, $.fn.editable.defaults, options, $.fn.editableutils.getConfigData(this.$element)); if(this.options.selector) { this.initLive(); } else { this.init(); } //check for transition support if(this.options.highlight && !$.fn.editableutils.supportsTransitions()) { this.options.highlight = false; } }; Editable.prototype = { constructor: Editable, init: function () { var isValueByText = false, doAutotext, finalize; //name this.options.name = this.options.name || this.$element.attr('id'); //create input of specified type. Input needed already here to convert value for initial display (e.g. show text by id for select) //also we set scope option to have access to element inside input specific callbacks (e. g. source as function) this.options.scope = this.$element[0]; 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'); //specifically for "textarea" add class .editable-pre-wrapped to keep linebreaks if(this.input.type === 'textarea') { this.$element.addClass('editable-pre-wrapped'); } //attach handler activating editable. In disabled mode it just prevent default action (useful for links) if(this.options.toggle !== 'manual') { this.$element.addClass('editable-click'); this.$element.on(this.options.toggle + '.editable', $.proxy(function(e){ //prevent following link if editable enabled if(!this.options.disabled) { e.preventDefault(); } //stop propagation not required because in document click handler it checks event target //e.stopPropagation(); if(this.options.toggle === 'mouseenter') { //for hover only show container this.show(); } else { //when toggle='click' we should not close all other containers as they will be closed automatically in document click listener var closeAll = (this.options.toggle !== 'click'); this.toggle(closeAll); } }, this)); } else { this.$element.attr('tabindex', -1); //do not stop focus on element when toggled manually } //if display is function it's far more convinient to have autotext = always to render correctly on init //see https://github.com/vitalets/x-editable-yii/issues/34 if(typeof this.options.display === 'function') { this.options.autotext = 'always'; } //check conditions for autotext: switch(this.options.autotext) { case 'always': doAutotext = true; break; case 'auto': //if element text is empty and value is defined and value not generated by text --> run autotext doAutotext = !$.trim(this.$element.text()).length && this.value !== null && this.value !== undefined && !isValueByText; break; default: doAutotext = false; } //depending on autotext run render() or just finilize init $.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. Please note that you should setup `init` handler **before** applying `editable`. @event init @param {Object} event event object @param {Object} editable editable instance (as here it cannot accessed via data('editable')) @since 1.2.0 @example $('#username').on('init', function(e, editable) { alert('initialized ' + editable.options.name); }); $('#username').editable(); **/ this.$element.triggerHandler('init', this); }, this)); }, /* Initializes parent element for live editables */ initLive: function() { //store selector var selector = this.options.selector; //modify options for child elements this.options.selector = false; this.options.autotext = 'never'; //listen toggle events this.$element.on(this.options.toggle + '.editable', selector, $.proxy(function(e){ var $target = $(e.target); if(!$target.data('editable')) { //if delegated element initially empty, we need to clear it's text (that was manually set to `empty` by user) //see https://github.com/vitalets/x-editable/issues/137 if($target.hasClass(this.options.emptyclass)) { $target.empty(); } $target.editable(this.options).trigger(e); } }, 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 input has `value2htmlFinal` method, we pass callback in third param to be called when source is loaded if(this.input.value2htmlFinal) { 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(this.isEmpty); 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(this.isEmpty); //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 */ handleEmpty: function (isEmpty) { //do not handle empty if we do not display anything if(this.options.display === false) { return; } /* isEmpty may be set directly as param of method. It is required when we enable/disable field and can't rely on content as node content is text: "Empty" that is not empty %) */ if(isEmpty !== undefined) { this.isEmpty = isEmpty; } else { //detect empty //for some inputs we need more smart check //e.g. wysihtml5 may have
,

, if(typeof(this.input.isEmpty) === 'function') { this.isEmpty = this.input.isEmpty(this.$element); } else { this.isEmpty = $.trim(this.$element.html()) === ''; } } //emptytext shown only for enabled if(!this.options.disabled) { if (this.isEmpty) { this.$element.html(this.options.emptytext); if(this.options.emptyclass) { this.$element.addClass(this.options.emptyclass); } } else if(this.options.emptyclass) { this.$element.removeClass(this.options.emptyclass); } } else { //below required if element disable property was changed if(this.isEmpty) { this.$element.empty(); if(this.options.emptyclass) { this.$element.removeClass(this.options.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); //listen `save` event 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) { //mark element with unsaved class if needed if(this.options.unsavedclass) { /* Add unsaved css to element if: - url is not user's function - value was not sent to server - params.response === undefined, that means data was not sent - value changed */ var sent = false; sent = sent || typeof this.options.url === 'function'; sent = sent || this.options.display === false; sent = sent || params.response !== undefined; sent = sent || (this.options.savenochange && this.input.value2str(this.value) !== this.input.value2str(params.newValue)); if(sent) { this.$element.removeClass(this.options.unsavedclass); } else { this.$element.addClass(this.options.unsavedclass); } } //highlight when saving if(this.options.highlight) { var $e = this.$element, bgColor = $e.css('background-color'); $e.css('background-color', this.options.highlight); setTimeout(function(){ if(bgColor === 'transparent') { bgColor = ''; } $e.css('background-color', bgColor); $e.addClass('editable-bg-transition'); setTimeout(function(){ $e.removeClass('editable-bg-transition'); }, 1700); }, 10); } //set new value this.setValue(params.newValue, false, params.response); /** Fired when new value was submitted. You can use $(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() { this.disable(); if(this.container) { this.container.destroy(); } this.input.destroy(); if(this.options.toggle !== 'manual') { this.$element.removeClass('editable-click'); this.$element.off(this.options.toggle + '.editable'); } this.$element.off("save.internal"); this.$element.removeClass('editable editable-open editable-disabled'); 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. Note that it returns an **object** with name-value pairs, not a value itself. It allows to get data from several elements. If value of some editable is `null` or `undefined` it is excluded from result object. When param `isSingle` is set to **true** - it is supposed you have single element and will return value of editable instead of object. @method getValue() @param {bool} isSingle whether to return just value of single element @returns {Object} object of element names and values @example $('#username, #fullname').editable('getValue'); //result: { username: "superuser", fullname: "John" } //isSingle = true $('#username').editable('getValue', true); //result "superuser" **/ case 'getValue': if(arguments.length === 2 && arguments[1] === true) { //isSingle = true result = this.eq(0).data(datakey).value; } else { this.each(function () { var $this = $(this), data = $this.data(datakey); if (data && data.value !== undefined && data.value !== null) { result[data.options.name] = data.input.value2submit(data.value); } }); } return result; /** This method collects values from several editable elements and submit them all to server. Internally it runs client-side validation for all fields and submits only in case of success. See
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; //for delegated targets do not store `editable` object for element //it's allows several different selectors. //see: https://github.com/vitalets/x-editable/issues/312 if(options && options.selector) { data = new Editable(this, options); return; } if (!data) { $this.data(datakey, (data = new Editable(this, options))); } if (typeof option === 'string') { //call method 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. Note, that if element's text is empty - text is automatically generated from value and can be customized (see `autotext` option). For example, to display currency sign: @example @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, /** Css class applied when editable text is empty. @property emptyclass @type string @since 1.4.1 @default editable-empty **/ emptyclass: 'editable-empty', /** Css class applied when value was stored but not sent to server (`pk` is empty or `send = 'never'`). You may set it to `null` if you work with editables locally and submit them together. @property unsavedclass @type string @since 1.4.1 @default editable-unsaved **/ unsavedclass: 'editable-unsaved', /** If selector is provided, editable will be delegated to the specified targets. Usefull for dynamically generated DOM elements. **Please note**, that delegated targets can't be initialized with `emptytext` and `autotext` options, as they actually become editable only after first click. You should manually set class `editable-click` to these elements. Also, if element originally empty you should add class `editable-empty`, set `data-value=""` and write emptytext into element: @property selector @type string @since 1.4.1 @default null @example **/ selector: null, /** Color used to highlight element after update. Implemented via CSS3 transition, works in modern browsers. @property highlight @type string|boolean @since 1.4.5 @default #FFFF80 **/ highlight: '#FFFF80' }; }(window.jQuery)); /** AbstractInput - base class for all editable inputs. It defines interface to be implemented by any input type. To create your own input you can inherit from this class. @class abstractinput **/ (function ($) { "use strict"; //types $.fn.editabletypes = {}; var AbstractInput = function () { }; AbstractInput.prototype = { /** Initializes input @method init() **/ init: function(type, options, defaults) { this.type = type; this.options = $.extend({}, defaults, options); }, /* this method called before render to init $tpl that is inserted in DOM */ prerender: function() { this.$tpl = $(this.options.tpl); //whole tpl as jquery object 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)[this.options.escape ? 'text' : 'html']($.trim(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() { }, /** Additional actions when destroying element **/ destroy: function() { }, // -------- helper functions -------- setClass: function() { if(this.options.inputclass) { this.$input.addClass(this.options.inputclass); } }, setAttr: function(attr) { if (this.options[attr] !== undefined && this.options[attr] !== null) { this.$input.attr(attr, this.options[attr]); } }, 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 null **/ inputclass: null, /** If `true` - html will be escaped in content of element via $.text() method. If `false` - html will not be escaped, $.html() used. When you use own `display` function, this option obviosly has no effect. @property escape @type boolean @since 1.5.0 @default true **/ escape: true, //scope for external methods (e.g. source defined as function) //for internal use only scope: null, //need to re-declare showbuttons here to get it's value from common config (passed only options existing in defaults) showbuttons: true }; $.extend($.fn.editabletypes, {abstractinput: AbstractInput}); }(window.jQuery)); /** List - abstract class for inputs that have source option loaded from js array or via ajax @class list @extends abstractinput **/ (function ($) { "use strict"; var List = function (options) { }; $.fn.editableutils.inherit(List, $.fn.editabletypes.abstractinput); $.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) { //run source if it function var source; if ($.isFunction(this.options.source)) { source = this.options.source.call(this.options.scope); this.sourceData = null; //note: if function returns the same source as URL - sourceData will be taken from cahce and no extra request performed } else { source = this.options.source; } //if allready loaded just call success if(this.options.sourceCache && $.isArray(this.sourceData)) { success.call(this); return; } //try parse json in single quotes (for double quotes jquery does automatically) try { source = $.fn.editableutils.tryParseJson(source, false); } catch (e) { error.call(this); return; } //loading from url if (typeof source === 'string') { //try to get sourceData from cache if(this.options.sourceCache) { var cacheID = 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 = []; } } //ajaxOptions for source. Can be overwritten bt options.sourceOptions var ajaxOptions = $.extend({ url: 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) }, this.options.sourceOptions); //loading sourceData from server $.ajax(ajaxOptions); } else { //options as json/array this.sourceData = this.makeArray(source); if($.isArray(this.sourceData)) { this.doPrepend(); success.call(this); } else { error.call(this); } } }, doPrepend: function () { if(this.options.prepend === null || this.options.prepend === undefined) { return; } if(!$.isArray(this.prependData)) { //run prepend if it is function (once) if ($.isFunction(this.options.prepend)) { this.options.prepend = this.options.prepend.call(this.options.scope); } //try parse json in single quotes this.options.prepend = $.fn.editableutils.tryParseJson(this.options.prepend, true); //convert prepend from string to object 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 = [], item, iterateItem; if(!data || typeof data === 'string') { return null; } if($.isArray(data)) { //array /* function to iterate inside item of array if item is object. Caclulates count of keys in item and store in obj. */ iterateItem = function (k, v) { obj = {value: k, text: v}; if(count++ >= 2) { return false;// exit from `each` if item has more than one key. } }; for(var i = 0; i < data.length; i++) { item = data[i]; if(typeof item === 'object') { count = 0; //count of keys inside item $.each(item, iterateItem); //case: [{val1: 'text1'}, {val2: 'text2} ...] if(count === 1) { result.push(obj); //case: [{value: 1, text: 'text1'}, {value: 2, text: 'text2'}, ...] } else if(count > 1) { //removed check of existance: item.hasOwnProperty('value') && item.hasOwnProperty('text') if(item.children) { item.children = this.makeArray(item.children); } result.push(item); } } else { //case: ['text1', 'text2' ...] result.push({value: item, text: item}); } } } else { //case: {val1: 'text1', val2: 'text2, ...} $.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"}, {value: 2, text: "text2"}, ...]` 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). Since 1.4.1 key `children` supported to render OPTGROUP (for **select** input only). `[{text: "group1", children: [{value: 1, text: "text1"}, {value: 2, text: "text2"}]}, ...]` @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. Usefull for editable column in grid to prevent extra requests. @property sourceCache @type boolean @default true @since 1.2.0 **/ sourceCache: true, /** Additional ajax options to be used in $.ajax() when loading list from server. Useful to send extra parameters (`data` key) or change request method (`type` key). @property sourceOptions @type object|function @default null @since 1.5.0 **/ sourceOptions: null }); $.fn.editabletypes.list = List; }(window.jQuery)); /** Text input @class text @extends abstractinput @final @example awesome **/ (function ($) { "use strict"; 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', 24) .keyup($.proxy(function(e) { //arrows, enter, tab, etc if(~$.inArray(e.keyCode, [40,38,9,13,27])) { return; } clearTimeout(this.t); var that = this; this.t = setTimeout(function() { that.toggleClear(e); }, 100); }, this)) .parent().css('position', 'relative'); this.$clear.click($.proxy(this.clear, this)); } }, postrender: function() { /* //now `clear` is positioned via css if(this.$clear) { //can position clear button only here, when form is shown and height can be calculated // var h = this.$input.outerHeight(true) || 20, var h = this.$clear.parent().height(), delta = (h - this.$clear.height()) / 2; //this.$clear.css({bottom: delta, right: delta}); } */ }, //show / hide clear button toggleClear: function(e) { if(!this.$clear) { return; } var len = this.$input.val().length, visible = this.$clear.is(':visible'); if(len && !visible) { this.$clear.show(); } if(!len && visible) { this.$clear.hide(); } }, clear: function() { this.$clear.hide(); this.$input.val('').focus(); } }); 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 ($) { "use strict"; 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(); } }); }, //using `white-space: pre-wrap` solves \n <--> BR conversion very elegant! /* value2html: function(value, element) { var html = '', lines; if(value) { lines = value.split("\n"); for (var i = 0; i < lines.length; i++) { lines[i] = $('
').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 ($) { "use strict"; 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(); var fillItems = function($el, data) { var attr; if($.isArray(data)) { for(var i=0; i', attr), data[i].children)); } else { attr.value = data[i].value; if(data[i].disabled) { attr.disabled = true; } $el.append($('
').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; } else { value = [str]; } return value; }, //set checked on required checkboxes value2input: function(value) { this.$input.prop('checked', false); 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.prop('checked', true); } }); }); } }, 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), escape = this.options.escape; if(checked.length) { $.each(checked, function(i, v) { var text = escape ? $.fn.editableutils.escape(v.text) : v.text; html.push(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 * time Learn more about html5 inputs: http://www.w3.org/wiki/HTML5_form_additions To check browser compatibility please see: https://developer.mozilla.org/en-US/docs/HTML/Element/Input @class html5types @extends text @final @since 1.3.0 @example **/ /** @property tpl @default depends on type **/ /* Password */ (function ($) { "use strict"; 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 ($) { "use strict"; var Email = function (options) { this.init('email', options, Email.defaults); }; $.fn.editableutils.inherit(Email, $.fn.editabletypes.text); Email.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { tpl: '' }); $.fn.editabletypes.email = Email; }(window.jQuery)); /* Url */ (function ($) { "use strict"; var Url = function (options) { this.init('url', options, Url.defaults); }; $.fn.editableutils.inherit(Url, $.fn.editabletypes.text); Url.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { tpl: '' }); $.fn.editabletypes.url = Url; }(window.jQuery)); /* Tel */ (function ($) { "use strict"; var Tel = function (options) { this.init('tel', options, Tel.defaults); }; $.fn.editableutils.inherit(Tel, $.fn.editabletypes.text); Tel.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { tpl: '' }); $.fn.editabletypes.tel = Tel; }(window.jQuery)); /* Number */ (function ($) { "use strict"; var NumberInput = function (options) { this.init('number', options, NumberInput.defaults); }; $.fn.editableutils.inherit(NumberInput, $.fn.editabletypes.text); $.extend(NumberInput.prototype, { render: function () { NumberInput.superclass.render.call(this); this.setAttr('min'); this.setAttr('max'); this.setAttr('step'); }, postrender: function() { if(this.$clear) { //increase right ffset for up/down arrows this.$clear.css({right: 24}); /* //can position clear button only here, when form is shown and height can be calculated var h = this.$input.outerHeight(true) || 20, delta = (h - this.$clear.height()) / 2; //add 12px to offset right for up/down arrows this.$clear.css({top: delta, right: delta + 16}); */ } } }); NumberInput.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { tpl: '', inputclass: 'input-mini', min: null, max: null, step: null }); $.fn.editabletypes.number = NumberInput; }(window.jQuery)); /* Range (inherit from number) */ (function ($) { "use strict"; var Range = function (options) { this.init('range', options, Range.defaults); }; $.fn.editableutils.inherit(Range, $.fn.editabletypes.number); $.extend(Range.prototype, { 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)); /* Time */ (function ($) { "use strict"; var Time = function (options) { this.init('time', options, Time.defaults); }; //inherit from abstract, as inheritance from text gives selection error. $.fn.editableutils.inherit(Time, $.fn.editabletypes.abstractinput); $.extend(Time.prototype, { render: function() { this.setClass(); } }); Time.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { tpl: '' }); $.fn.editabletypes.time = Time; }(window.jQuery)); /** Select2 input. Based on amazing work of Igor Vaynberg https://github.com/ivaynberg/select2. Please see [original select2 docs](http://ivaynberg.github.com/select2) for detailed description and options. You should manually download and include select2 distributive: To make it **bootstrap-styled** you can use css from [here](https://github.com/t0m/select2-bootstrap-css): **Note:** currently `autotext` feature does not work for select2 with `ajax` remote source. You need initially put both `data-value` and element's text youself: Text1 @class select2 @extends abstractinput @since 1.4.1 @final @example **/ (function ($) { "use strict"; var Constructor = function (options) { this.init('select2', options, Constructor.defaults); options.select2 = options.select2 || {}; this.sourceData = null; //placeholder if(options.placeholder) { options.select2.placeholder = options.placeholder; } //if not `tags` mode, use source if(!options.select2.tags && options.source) { var source = options.source; //if source is function, call it (once!) if ($.isFunction(options.source)) { source = options.source.call(options.scope); } if (typeof source === 'string') { options.select2.ajax = options.select2.ajax || {}; //some default ajax params if(!options.select2.ajax.data) { options.select2.ajax.data = function(term) {return { query:term };}; } if(!options.select2.ajax.results) { options.select2.ajax.results = function(data) { return {results:data };}; } options.select2.ajax.url = source; } else { //check format and convert x-editable format to select2 format (if needed) this.sourceData = this.convertSource(source); options.select2.data = this.sourceData; } } //overriding objects in config (as by default jQuery extend() is not recursive) this.options.select2 = $.extend({}, Constructor.defaults.select2, options.select2); //detect whether it is multi-valued this.isMultiple = this.options.select2.tags || this.options.select2.multiple; this.isRemote = ('ajax' in this.options.select2); //store function returning ID of item //should be here as used inautotext for local source this.idFunc = this.options.select2.id; if (typeof(this.idFunc) !== "function") { var idKey = this.idFunc || 'id'; this.idFunc = function (e) { return e[idKey]; }; } //store function that renders text in select2 this.formatSelection = this.options.select2.formatSelection; if (typeof(this.formatSelection) !== "function") { this.formatSelection = function (e) { return e.text; }; } }; $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.abstractinput); $.extend(Constructor.prototype, { render: function() { this.setClass(); //can not apply select2 here as it calls initSelection //over input that does not have correct value yet. //apply select2 only in value2input //this.$input.select2(this.options.select2); //when data is loaded via ajax, we need to know when it's done to populate listData if(this.isRemote) { //listen to loaded event to populate data this.$input.on('select2-loaded', $.proxy(function(e) { this.sourceData = e.items.results; }, this)); } //trigger resize of editableform to re-position container in multi-valued mode if(this.isMultiple) { this.$input.on('change', function() { $(this).closest('form').parent().triggerHandler('resize'); }); } }, value2html: function(value, element) { var text = '', data, that = this; if(this.options.select2.tags) { //in tags mode just assign value data = value; //data = $.fn.editableutils.itemsByValue(value, this.options.select2.tags, this.idFunc); } else if(this.sourceData) { data = $.fn.editableutils.itemsByValue(value, this.sourceData, this.idFunc); } else { //can not get list of possible values //(e.g. autotext for select2 with ajax source) } //data may be array (when multiple values allowed) if($.isArray(data)) { //collect selected data and show with separator text = []; $.each(data, function(k, v){ text.push(v && typeof v === 'object' ? that.formatSelection(v) : v); }); } else if(data) { text = that.formatSelection(data); } text = $.isArray(text) ? text.join(this.options.viewseparator) : text; //$(element).text(text); Constructor.superclass.value2html.call(this, text, element); }, html2value: function(html) { return this.options.select2.tags ? this.str2value(html, this.options.viewseparator) : null; }, value2input: function(value) { //for local source use data directly from source (to allow autotext) /* if(!this.isRemote && !this.isMultiple) { var items = $.fn.editableutils.itemsByValue(value, this.sourceData, this.idFunc); if(items.length) { this.$input.select2('data', items[0]); return; } } */ //for remote source just set value, text is updated by initSelection if(!this.$input.data('select2')) { this.$input.val(value); this.$input.select2(this.options.select2); } else { //second argument needed to separate initial change from user's click (for autosubmit) this.$input.val(value).trigger('change', true); } //if remote source AND no user's initSelection provided --> try to use element's text if(this.isRemote && !this.isMultiple && !this.options.select2.initSelection) { var customId = this.options.select2.id, customText = this.options.select2.formatSelection; if(!customId && !customText) { var data = {id: value, text: $(this.options.scope).text()}; this.$input.select2('data', data); } } }, input2value: function() { return this.$input.select2('val'); }, str2value: function(str, separator) { if(typeof str !== 'string' || !this.isMultiple) { return str; } separator = separator || this.options.select2.separator || $.fn.select2.defaults.separator; var val, i, l; if (str === null || str.length < 1) { return null; } val = str.split(separator); for (i = 0, l = val.length; i < l; i = i + 1) { val[i] = $.trim(val[i]); } return val; }, autosubmit: function() { this.$input.on('change', function(e, isInitial){ if(!isInitial) { $(this).closest('form').submit(); } }); }, /* Converts source from x-editable format: {value: 1, text: "1"} to select2 format: {id: 1, text: "1"} */ convertSource: function(source) { if($.isArray(source) && source.length && source[0].value !== undefined) { for(var i = 0; i **/ tpl:'', /** Configuration of select2. [Full list of options](http://ivaynberg.github.com/select2). @property select2 @type object @default null **/ select2: null, /** Placeholder attribute of select @property placeholder @type string @default null **/ placeholder: null, /** Source data for select. It will be assigned to select2 `data` property and kept here just for convenience. Please note, that format is different from simple `select` input: use 'id' instead of 'value'. E.g. `[{id: 1, text: "text1"}, {id: 2, text: "text2"}, ...]`. @property source @type array|string|function @default null **/ source: null, /** Separator used to display tags. @property viewseparator @type string @default ', ' **/ viewseparator: ', ' }); $.fn.editabletypes.select2 = Constructor; }(window.jQuery)); /** * Combodate - 1.0.4 * Dropdown date and time picker. * Converts text input into dropdowns to pick day, month, year, hour, minute and second. * Uses momentjs as datetime library http://momentjs.com. * For internalization include corresponding file from https://github.com/timrwood/moment/tree/master/lang * * Confusion at noon and midnight - see http://en.wikipedia.org/wiki/12-hour_clock#Confusion_at_noon_and_midnight * In combodate: * 12:00 pm --> 12:00 (24-h format, midday) * 12:00 am --> 00:00 (24-h format, midnight, start of day) * * Differs from momentjs parse rules: * 00:00 pm, 12:00 pm --> 12:00 (24-h format, day not change) * 00:00 am, 12:00 am --> 00:00 (24-h format, day not change) * * * Author: Vitaliy Potapov * Project page: http://github.com/vitalets/combodate * Copyright (c) 2012 Vitaliy Potapov. Released under MIT License. **/ (function ($) { 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 = [], relTime; if(this.options.firstItem === 'name') { //need both to support moment ver < 2 and >= 2 relTime = moment.relativeTime || moment.langData()._relativeTime; var header = typeof relTime[key] === 'function' ? relTime[key](1, true, key, false) : relTime[key]; //take last entry (see momentjs lang files structure) header = header.split(' ').reverse()[0]; values.push(['', header]); } else if(this.options.firstItem === 'empty') { values.push(['', '']); } return values; }, /* render items to string of '); } 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) { //see https://github.com/timrwood/momentjs.com/pull/36 name = moment().date(1).month(i).format('MMMM'); } else if(shortNames) { name = moment().date(1).month(i).format('MMM'); } else if(twoDigit) { name = this.leadZero(i+1); } else { name = i+1; } items.push([i, name]); } return items; }, /* fill year */ fillYear: function() { var items = [], name, i, longNames = this.options.template.indexOf('YYYY') !== -1; for(i=this.options.maxYear; i>=this.options.minYear; i--) { name = longNames ? i : (i+'').substring(2); items[this.options.yearDescending ? 'push' : 'unshift']([i, name]); } items = this.initItems('y').concat(items); 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, min = h12 ? 1 : 0, max = h12 ? 12 : 23; for(i=min; i<=max; i++) { name = twoDigit ? this.leadZero(i) : i; items.push([i, name]); } return items; }, /* fill minute */ fillMinute: function() { var items = this.initItems('m'), name, i, 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 from combos. 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 12h --> 24h if(this.$ampm) { //12:00 pm --> 12:00 (24-h format, midday), 12:00 am --> 00:00 (24-h format, midnight, start of day) if(values.hour === 12) { values.hour = this.$ampm.val() === 'am' ? 0 : 12; } else { values.hour = this.$ampm.val() === 'am' ? values.hour : values.hour+12; } } dt = moment([values.year, values.month, values.day, values.hour, values.minute, values.second]); //highlight invalid date 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 = {}; //function to find nearest value in select options function getNearest($select, value) { var delta = {}; $select.children('option').each(function(i, opt){ var optValue = $(opt).attr('value'), distance; if(optValue === '') return; distance = Math.abs(optValue - value); if(typeof delta.distance === 'undefined' || distance < delta.distance) { delta = {value: optValue, distance: distance}; } }); return delta.value; } if(dt.isValid()) { //read values from date object $.each(this.map, function(k, v) { if(k === 'ampm') { return; } values[k] = dt[v[1]](); }); if(this.$ampm) { //12:00 pm --> 12:00 (24-h format, midday), 12:00 am --> 00:00 (24-h format, midnight, start of day) if(values.hour >= 12) { values.ampm = 'pm'; if(values.hour > 12) { values.hour -= 12; } } else { values.ampm = 'am'; if(values.hour === 0) { values.hour = 12; } } } $.each(values, function(k, v) { //call val() for each existing combo, e.g. this.$hour.val() if(that['$'+k]) { if(k === 'minute' && that.options.minuteStep > 1 && that.options.roundTime) { v = getNearest(that['$'+k], v); } if(k === 'second' && that.options.secondStep > 1 && that.options.roundTime) { v = getNearest(that['$'+k], v); } that['$'+k].val(v); } }); this.$element.val(dt.format(this.options.format)); } }, /* 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, yearDescending: true, minuteStep: 5, secondStep: 1, firstItem: 'empty', //'name', 'empty', 'none' errorClass: null, roundTime: true //whether to round minutes and seconds if step > 1 }; }(window.jQuery)); /** Combodate input - dropdown date and time picker. Based on [combodate](http://vitalets.github.com/combodate) plugin (included). To use it you should manually include [momentjs](http://momentjs.com). 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 ($) { "use strict"; 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; } //try parse combodate config defined as json string in data-combodate options.combodate = $.fn.editableutils.tryParseJson(options.combodate, true); //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); if($.fn.editableform.engine === 'bs3') { this.$input.siblings().find('select').addClass('form-control'); } if(this.options.inputclass) { this.$input.siblings().find('select').addClass(this.options.inputclass); } //"clear" link /* if(this.options.clear) { this.$clear = $('').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); Constructor.superclass.value2html.call(this, text, element); }, 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 3 */ (function ($) { "use strict"; //store parent methods var pInitInput = $.fn.editableform.Constructor.prototype.initInput; $.extend($.fn.editableform.Constructor.prototype, { initTemplate: function() { this.$form = $($.fn.editableform.template); this.$form.find('.control-group').addClass('form-group'); this.$form.find('.editable-error-block').addClass('help-block'); }, initInput: function() { pInitInput.apply(this); //for bs3 set default class `input-sm` to standard inputs var emptyInputClass = this.input.options.inputclass === null || this.input.options.inputclass === false; var defaultClass = 'input-sm'; //bs3 add `form-control` class to standard inputs var stdtypes = 'text,select,textarea,password,email,url,tel,number,range,time,typeaheadjs'.split(','); if(~$.inArray(this.input.type, stdtypes)) { this.input.$input.addClass('form-control'); if(emptyInputClass) { this.input.options.inputclass = defaultClass; this.input.$input.addClass(defaultClass); } } //apply bs3 size class also to buttons (to fit size of control) var $btn = this.$form.find('.editable-buttons'); var classes = emptyInputClass ? [defaultClass] : this.input.options.inputclass.split(' '); for(var i=0; i'+ ''+ ''; //error classes $.fn.editableform.errorGroupClass = 'has-error'; $.fn.editableform.errorBlockClass = null; //engine $.fn.editableform.engine = 'bs3'; }(window.jQuery)); /** * Editable Popover3 (for Bootstrap 3) * --------------------- * requires bootstrap-popover.js */ (function ($) { "use strict"; //extend methods $.extend($.fn.editableContainer.Popup.prototype, { containerName: 'popover', containerDataName: 'bs.popover', innerCss: '.popover-content', defaults: $.fn.popover.Constructor.DEFAULTS, initContainer: function(){ $.extend(this.containerOptions, { trigger: 'manual', selector: false, content: ' ', template: this.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, eqeqeq: false*/ setPosition: function () { (function() { /* var $tip = this.tip() , inside , pos , actualWidth , actualHeight , placement , tp , tpt , tpb , tpl , tpr; 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; placement = inside ? placement.split(' ')[1] : placement; tpb = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2}; tpt = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2}; tpl = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth}; tpr = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width}; switch (placement) { case 'bottom': if ((tpb.top + actualHeight) > ($(window).scrollTop() + $(window).height())) { if (tpt.top > $(window).scrollTop()) { placement = 'top'; } else if ((tpr.left + actualWidth) < ($(window).scrollLeft() + $(window).width())) { placement = 'right'; } else if (tpl.left > $(window).scrollLeft()) { placement = 'left'; } else { placement = 'right'; } } break; case 'top': if (tpt.top < $(window).scrollTop()) { if ((tpb.top + actualHeight) < ($(window).scrollTop() + $(window).height())) { placement = 'bottom'; } else if ((tpr.left + actualWidth) < ($(window).scrollLeft() + $(window).width())) { placement = 'right'; } else if (tpl.left > $(window).scrollLeft()) { placement = 'left'; } else { placement = 'right'; } } break; case 'left': if (tpl.left < $(window).scrollLeft()) { if ((tpr.left + actualWidth) < ($(window).scrollLeft() + $(window).width())) { placement = 'right'; } else if (tpt.top > $(window).scrollTop()) { placement = 'top'; } else if (tpt.top > $(window).scrollTop()) { placement = 'bottom'; } else { placement = 'right'; } } break; case 'right': if ((tpr.left + actualWidth) > ($(window).scrollLeft() + $(window).width())) { if (tpl.left > $(window).scrollLeft()) { placement = 'left'; } else if (tpt.top > $(window).scrollTop()) { placement = 'top'; } else if (tpt.top > $(window).scrollTop()) { placement = 'bottom'; } } break; } switch (placement) { case 'bottom': tp = tpb; break; case 'top': tp = tpt; break; case 'left': tp = tpl; break; case 'right': tp = tpr; break; } $tip .offset(tp) .addClass(placement) .addClass('in'); */ var $tip = this.tip(); var placement = typeof this.options.placement == 'function' ? this.options.placement.call(this, $tip[0], this.$element[0]) : this.options.placement; var pos = this.getPosition(); var actualWidth = $tip[0].offsetWidth; var actualHeight = $tip[0].offsetHeight; var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight); this.applyPlacement(calculatedOffset, placement); }).call(this.container()); /*jshint laxcomma: false, eqeqeq: true*/ } }); }(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._process_options(options); this.element = $(element); this.isInline = false; this.isInput = this.element.is('input'); this.component = this.element.is('.date') ? this.element.find('.add-on, .btn') : false; this.hasInput = this.component && this.element.find('input').length; if(this.component && this.component.length === 0) this.component = false; this.picker = $(DPGlobal.template); this._buildEvents(); this._attachEvents(); if(this.isInline) { this.picker.addClass('datepicker-inline').appendTo(this.element); } else { this.picker.addClass('datepicker-dropdown dropdown-menu'); } if (this.o.rtl){ this.picker.addClass('datepicker-rtl'); this.picker.find('.prev i, .next i') .toggleClass('icon-arrow-left icon-arrow-right'); } this.viewMode = this.o.startView; if (this.o.calendarWeeks) this.picker.find('tfoot th.today') .attr('colspan', function(i, val){ return parseInt(val) + 1; }); this._allow_update = false; this.setStartDate(this.o.startDate); this.setEndDate(this.o.endDate); this.setDaysOfWeekDisabled(this.o.daysOfWeekDisabled); this.fillDow(); this.fillMonths(); this._allow_update = true; this.update(); this.showMode(); if(this.isInline) { this.show(); } }; Datepicker.prototype = { constructor: Datepicker, _process_options: function(opts){ // Store raw options for reference this._o = $.extend({}, this._o, opts); // Processed options var o = this.o = $.extend({}, this._o); // Check if "de-DE" style date is available, if not language should // fallback to 2 letter code eg "de" var lang = o.language; if (!dates[lang]) { lang = lang.split('-')[0]; if (!dates[lang]) lang = defaults.language; } o.language = lang; switch(o.startView){ case 2: case 'decade': o.startView = 2; break; case 1: case 'year': o.startView = 1; break; default: o.startView = 0; } switch (o.minViewMode) { case 1: case 'months': o.minViewMode = 1; break; case 2: case 'years': o.minViewMode = 2; break; default: o.minViewMode = 0; } o.startView = Math.max(o.startView, o.minViewMode); o.weekStart %= 7; o.weekEnd = ((o.weekStart + 6) % 7); var format = DPGlobal.parseFormat(o.format) if (o.startDate !== -Infinity) { o.startDate = DPGlobal.parseDate(o.startDate, format, o.language); } if (o.endDate !== Infinity) { o.endDate = DPGlobal.parseDate(o.endDate, format, o.language); } o.daysOfWeekDisabled = o.daysOfWeekDisabled||[]; if (!$.isArray(o.daysOfWeekDisabled)) o.daysOfWeekDisabled = o.daysOfWeekDisabled.split(/[,\s]*/); o.daysOfWeekDisabled = $.map(o.daysOfWeekDisabled, function (d) { return parseInt(d, 10); }); }, _events: [], _secondaryEvents: [], _applyEvents: function(evs){ for (var i=0, el, ev; i this.o.endDate) { this.viewDate = new Date(this.o.endDate); } else { this.viewDate = new Date(this.date); } this.fill(); }, fillDow: function(){ var dowCnt = this.o.weekStart, html = ''; if(this.o.calendarWeeks){ var cell = ' '; html += cell; this.picker.find('.datepicker-days thead tr:first-child').prepend(cell); } while (dowCnt < this.o.weekStart + 7) { html += ''+dates[this.o.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.o.language].monthsShort[i++]+''; } this.picker.find('.datepicker-months td').html(html); }, setRange: function(range){ if (!range || !range.length) delete this.range; else this.range = $.map(range, function(d){ return d.valueOf(); }); this.fill(); }, getClassNames: function(date){ var cls = [], year = this.viewDate.getUTCFullYear(), month = this.viewDate.getUTCMonth(), currentDate = this.date.valueOf(), today = new Date(); if (date.getUTCFullYear() < year || (date.getUTCFullYear() == year && date.getUTCMonth() < month)) { cls.push('old'); } else if (date.getUTCFullYear() > year || (date.getUTCFullYear() == year && date.getUTCMonth() > month)) { cls.push('new'); } // Compare internal UTC date with local today, not UTC today if (this.o.todayHighlight && date.getUTCFullYear() == today.getFullYear() && date.getUTCMonth() == today.getMonth() && date.getUTCDate() == today.getDate()) { cls.push('today'); } if (currentDate && date.valueOf() == currentDate) { cls.push('active'); } if (date.valueOf() < this.o.startDate || date.valueOf() > this.o.endDate || $.inArray(date.getUTCDay(), this.o.daysOfWeekDisabled) !== -1) { cls.push('disabled'); } if (this.range){ if (date > this.range[0] && date < this.range[this.range.length-1]){ cls.push('range'); } if ($.inArray(date.valueOf(), this.range) != -1){ cls.push('selected'); } } return cls; }, fill: function() { var d = new Date(this.viewDate), year = d.getUTCFullYear(), month = d.getUTCMonth(), startYear = this.o.startDate !== -Infinity ? this.o.startDate.getUTCFullYear() : -Infinity, startMonth = this.o.startDate !== -Infinity ? this.o.startDate.getUTCMonth() : -Infinity, endYear = this.o.endDate !== Infinity ? this.o.endDate.getUTCFullYear() : Infinity, endMonth = this.o.endDate !== Infinity ? this.o.endDate.getUTCMonth() : Infinity, currentDate = this.date && this.date.valueOf(), tooltip; this.picker.find('.datepicker-days thead th.datepicker-switch') .text(dates[this.o.language].months[month]+' '+year); this.picker.find('tfoot th.today') .text(dates[this.o.language].today) .toggle(this.o.todayBtn !== false); this.picker.find('tfoot th.clear') .text(dates[this.o.language].clear) .toggle(this.o.clearBtn !== false); this.updateNavArrows(); this.fillMonths(); var prevMonth = UTCDate(year, month-1, 28,0,0,0,0), day = DPGlobal.getDaysInMonth(prevMonth.getUTCFullYear(), prevMonth.getUTCMonth()); prevMonth.setUTCDate(day); prevMonth.setUTCDate(day - (prevMonth.getUTCDay() - this.o.weekStart + 7)%7); var nextMonth = new Date(prevMonth); nextMonth.setUTCDate(nextMonth.getUTCDate() + 42); nextMonth = nextMonth.valueOf(); var html = []; var clsName; while(prevMonth.valueOf() < nextMonth) { if (prevMonth.getUTCDay() == this.o.weekStart) { html.push(''); if(this.o.calendarWeeks){ // ISO 8601: First week contains first thursday. // ISO also states week starts on Monday, but we can be more abstract here. var // Start of current week: based on weekstart/current date ws = new Date(+prevMonth + (this.o.weekStart - prevMonth.getUTCDay() - 7) % 7 * 864e5), // Thursday of this week th = new Date(+ws + (7 + 4 - ws.getUTCDay()) % 7 * 864e5), // First Thursday of year, year from thursday yth = new Date(+(yth = UTCDate(th.getUTCFullYear(), 0, 1)) + (7 + 4 - yth.getUTCDay())%7*864e5), // Calendar week: ms between thursdays, div ms per day, div 7 days calWeek = (th - yth) / 864e5 / 7 + 1; html.push(''+ calWeek +''); } } clsName = this.getClassNames(prevMonth); clsName.push('day'); var before = this.o.beforeShowDay(prevMonth); if (before === undefined) before = {}; else if (typeof(before) === 'boolean') before = {enabled: before}; else if (typeof(before) === 'string') before = {classes: before}; if (before.enabled === false) clsName.push('disabled'); if (before.classes) clsName = clsName.concat(before.classes.split(/\s+/)); if (before.tooltip) tooltip = before.tooltip; clsName = $.unique(clsName); html.push(''+prevMonth.getUTCDate() + ''); if (prevMonth.getUTCDay() == this.o.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() { if (!this._allow_update) return; var d = new Date(this.viewDate), year = d.getUTCFullYear(), month = d.getUTCMonth(); switch (this.viewMode) { case 0: if (this.o.startDate !== -Infinity && year <= this.o.startDate.getUTCFullYear() && month <= this.o.startDate.getUTCMonth()) { this.picker.find('.prev').css({visibility: 'hidden'}); } else { this.picker.find('.prev').css({visibility: 'visible'}); } if (this.o.endDate !== Infinity && year >= this.o.endDate.getUTCFullYear() && month >= this.o.endDate.getUTCMonth()) { this.picker.find('.next').css({visibility: 'hidden'}); } else { this.picker.find('.next').css({visibility: 'visible'}); } break; case 1: case 2: if (this.o.startDate !== -Infinity && year <= this.o.startDate.getUTCFullYear()) { this.picker.find('.prev').css({visibility: 'hidden'}); } else { this.picker.find('.prev').css({visibility: 'visible'}); } if (this.o.endDate !== Infinity && year >= this.o.endDate.getUTCFullYear()) { this.picker.find('.next').css({visibility: 'hidden'}); } else { this.picker.find('.next').css({visibility: 'visible'}); } break; } }, click: function(e) { e.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 'datepicker-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.o.todayBtn == 'linked' ? null : 'view'; this._setDate(date, which); break; case 'clear': var element; if (this.isInput) element = this.element; else if (this.component) element = this.element.find('input'); if (element) element.val("").change(); this._trigger('changeDate'); this.update(); if (this.o.autoclose) this.hide(); break; } break; case 'span': if (!target.is('.disabled')) { this.viewDate.setUTCDate(1); if (target.is('.month')) { var day = 1; var month = target.parent().find('span').index(target); var year = this.viewDate.getUTCFullYear(); this.viewDate.setUTCMonth(month); this._trigger('changeMonth', this.viewDate); if (this.o.minViewMode === 1) { this._setDate(UTCDate(year, month, day,0,0,0,0)); } } else { var year = parseInt(target.text(), 10)||0; var day = 1; var month = 0; this.viewDate.setUTCFullYear(year); this._trigger('changeYear', this.viewDate); if (this.o.minViewMode === 2) { this._setDate(UTCDate(year, month, day,0,0,0,0)); } } 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 = new Date(date); if (!which || which == 'view') this.viewDate = new Date(date); this.fill(); this.setValue(); this._trigger('changeDate'); var element; if (this.isInput) { element = this.element; } else if (this.component){ element = this.element.find('input'); } if (element) { element.change(); if (this.o.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.o.startDate && date <= this.o.endDate; }, keydown: function(e){ if (this.picker.is(':not(:visible)')){ if (e.keyCode == 27) // allow escape to hide and re-show picker 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.o.keyboardNavigation) break; dir = e.keyCode == 37 ? -1 : 1; if (e.ctrlKey){ newDate = this.moveYear(this.date, dir); newViewDate = this.moveYear(this.viewDate, dir); } else if (e.shiftKey){ 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.o.keyboardNavigation) break; dir = e.keyCode == 38 ? -1 : 1; if (e.ctrlKey){ newDate = this.moveYear(this.date, dir); newViewDate = this.moveYear(this.viewDate, dir); } else if (e.shiftKey){ 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._trigger('changeDate'); 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(this.o.minViewMode, Math.min(2, this.viewMode + dir)); } /* vitalets: fixing bug of very special conditions: jquery 1.7.1 + webkit + show inline datepicker in bootstrap popover. Method show() does not set display css correctly and datepicker is not shown. 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(); } }; var DateRangePicker = function(element, options){ this.element = $(element); this.inputs = $.map(options.inputs, function(i){ return i.jquery ? i[0] : i; }); delete options.inputs; $(this.inputs) .datepicker(options) .bind('changeDate', $.proxy(this.dateUpdated, this)); this.pickers = $.map(this.inputs, function(i){ return $(i).data('datepicker'); }); this.updateDates(); }; DateRangePicker.prototype = { updateDates: function(){ this.dates = $.map(this.pickers, function(i){ return i.date; }); this.updateRanges(); }, updateRanges: function(){ var range = $.map(this.dates, function(d){ return d.valueOf(); }); $.each(this.pickers, function(i, p){ p.setRange(range); }); }, dateUpdated: function(e){ var dp = $(e.target).data('datepicker'), new_date = dp.getUTCDate(), i = $.inArray(e.target, this.inputs), l = this.inputs.length; if (i == -1) return; if (new_date < this.dates[i]){ // Date being moved earlier/left while (i>=0 && new_date < this.dates[i]){ this.pickers[i--].setUTCDate(new_date); } } else if (new_date > this.dates[i]){ // Date being moved later/right while (i this.dates[i]){ this.pickers[i++].setUTCDate(new_date); } } this.updateDates(); }, remove: function(){ $.map(this.pickers, function(p){ p.remove(); }); delete this.element.data().datepicker; } }; function opts_from_el(el, prefix){ // Derive options from element data-attrs var data = $(el).data(), out = {}, inkey, replace = new RegExp('^' + prefix.toLowerCase() + '([A-Z])'), prefix = new RegExp('^' + prefix.toLowerCase()); for (var key in data) if (prefix.test(key)){ inkey = key.replace(replace, function(_,a){ return a.toLowerCase(); }); out[inkey] = data[key]; } return out; } function opts_from_locale(lang){ // Derive options from locale plugins var out = {}; // Check if "de-DE" style date is available, if not language should // fallback to 2 letter code eg "de" if (!dates[lang]) { lang = lang.split('-')[0] if (!dates[lang]) return; } var d = dates[lang]; $.each(locale_opts, function(i,k){ if (k in d) out[k] = d[k]; }); return out; } var old = $.fn.datepicker; var datepicker = $.fn.datepicker = function ( option ) { var args = Array.apply(null, arguments); args.shift(); var internal_return, this_return; this.each(function () { var $this = $(this), data = $this.data('datepicker'), options = typeof option == 'object' && option; if (!data) { var elopts = opts_from_el(this, 'date'), // Preliminary otions xopts = $.extend({}, defaults, elopts, options), locopts = opts_from_locale(xopts.language), // Options priority: js args, data-attrs, locales, defaults opts = $.extend({}, defaults, locopts, elopts, options); if ($this.is('.input-daterange') || opts.inputs){ var ropts = { inputs: opts.inputs || $this.find('input').toArray() }; $this.data('datepicker', (data = new DateRangePicker(this, $.extend(opts, ropts)))); } else{ $this.data('datepicker', (data = new Datepicker(this, opts))); } } if (typeof option == 'string' && typeof data[option] == 'function') { internal_return = data[option].apply(data, args); if (internal_return !== undefined) return false; } }); if (internal_return !== undefined) return internal_return; else return this; }; var defaults = $.fn.datepicker.defaults = { autoclose: false, beforeShowDay: $.noop, calendarWeeks: false, clearBtn: false, daysOfWeekDisabled: [], endDate: Infinity, forceParse: true, format: 'mm/dd/yyyy', keyboardNavigation: true, language: 'en', minViewMode: 0, rtl: false, startDate: -Infinity, startView: 0, todayBtn: false, todayHighlight: false, weekStart: 0 }; var locale_opts = $.fn.datepicker.locale_opts = [ 'format', 'rtl', 'weekStart' ]; $.fn.datepicker.Constructor = Datepicker; var dates = $.fn.datepicker.dates = { en: { days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"], months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], today: "Today", clear: "Clear" } }; 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 (typeof format === 'string') format = DPGlobal.parseFormat(format); if (/^[\-+]\d+[dmwy]([\s,]+[\-+]\d+[dmwy])*$/.test(date)) { var part_re = /([\-+]\d+)([dmwy])/, parts = date.match(/([\-+]\d+)([dmwy])/g), part, dir; date = new Date(); 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; /* DATEPICKER NO CONFLICT * =================== */ $.fn.datepicker.noConflict = function(){ $.fn.datepicker = old; return this; }; /* DATEPICKER DATA-API * ================== */ $(document).on( 'focus.datepicker.data-api click.datepicker.data-api', '[data-provide="datepicker"]', function(e){ var $this = $(this); if ($this.data('datepicker')) return; e.preventDefault(); // component click requires us to explicitly show it datepicker.call($this, 'show'); } ); $(function(){ //$('[data-provide="datepicker-inline"]').datepicker(); //vit: changed to support noConflict() datepicker.call($('[data-provide="datepicker-inline"]')); }); }( window.jQuery )); /** Bootstrap-datepicker. Description and examples: https://github.com/eternicode/bootstrap-datepicker. For **i18n** you should include js file from here: https://github.com/eternicode/bootstrap-datepicker/tree/master/js/locales and set `language` option. Since 1.4.0 date has different appearance in **popup** and **inline** modes. @class date @extends abstractinput @final @example 15/05/1984 **/ (function ($) { "use strict"; //store bootstrap-datepicker as bdateicker to exclude conflict with jQuery UI one $.fn.bdatepicker = $.fn.datepicker.noConflict(); if(!$.fn.datepicker) { //if there were no other datepickers, keep also original name $.fn.datepicker = $.fn.bdatepicker; } var Date = function (options) { this.init('date', options, Date.defaults); this.initPicker(options, Date.defaults); }; $.fn.editableutils.inherit(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; } //try parse datepicker config defined as json string in data-datepicker options.datepicker = $.fn.editableutils.tryParseJson(options.datepicker, true); //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.bdatepicker.DPGlobal; //store parsed formats this.parsedFormat = this.dpg.parseFormat(this.options.format); this.parsedViewFormat = this.dpg.parseFormat(this.options.viewformat); }, render: function () { this.$input.bdatepicker(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.call(this, text, element); }, html2value: function(html) { return this.parseDate(html, this.parsedViewFormat); }, value2str: function(value) { return value ? this.dpg.formatDate(value, this.parsedFormat, this.options.datepicker.language) : ''; }, str2value: function(str) { return this.parseDate(str, this.parsedFormat); }, value2submit: function(value) { return this.value2str(value); }, value2input: function(value) { this.$input.bdatepicker('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'); if(!this.options.showbuttons) { this.$input.closest('form').submit(); } }, autosubmit: function() { this.$input.on('mouseup', '.day', function(e){ if($(e.currentTarget).is('.old') || $(e.currentTarget).is('.new')) { return; } var $form = $(this).closest('form'); setTimeout(function() { $form.submit(); }, 200); }); //changedate is not suitable as it triggered when showing datepicker. see #149 /* this.$input.on('changeDate', function(e){ var $form = $(this).closest('form'); setTimeout(function() { $form.submit(); }, 200); }); */ }, /* For incorrect date bootstrap-datepicker returns current date that is not suitable for datefield. This function returns null for incorrect date. */ parseDate: function(str, format) { var date = null, formattedBack; if(str) { date = this.dpg.parseDate(str, format, this.options.datepicker.language); if(typeof str === 'string') { formattedBack = this.dpg.formatDate(date, format, this.options.datepicker.language); if(str !== formattedBack) { date = null; } } } return date; } }); Date.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { /** @property tpl @default
**/ 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, minViewMode: 0, autoclose: false } **/ datepicker:{ weekStart: 0, startView: 0, minViewMode: 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 ($) { "use strict"; var DateField = function (options) { this.init('datefield', options, DateField.defaults); this.initPicker(options, DateField.defaults); }; $.fn.editableutils.inherit(DateField, $.fn.editabletypes.date); $.extend(DateField.prototype, { render: function () { this.$input = this.$tpl.find('input'); this.setClass(); this.setAttr('placeholder'); //bootstrap-datepicker is set `bdateicker` to exclude conflict with jQuery UI one. (in date.js) this.$tpl.bdatepicker(this.options.datepicker); //need to disable original event handlers this.$input.off('focus keydown'); //update value of datepicker this.$input.keyup($.proxy(function(){ this.$tpl.removeData('date'); this.$tpl.bdatepicker('update'); }, this)); }, value2input: function(value) { this.$input.val(value ? this.dpg.formatDate(value, this.parsedViewFormat, this.options.datepicker.language) : ''); this.$tpl.bdatepicker('update'); }, 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, minViewMode: 0, autoclose: true } }); $.fn.editabletypes.datefield = DateField; }(window.jQuery)); /** Bootstrap-datetimepicker. Based on [smalot bootstrap-datetimepicker plugin](https://github.com/smalot/bootstrap-datetimepicker). Before usage you should manually include dependent js and css: For **i18n** you should include js file from here: https://github.com/smalot/bootstrap-datetimepicker/tree/master/js/locales and set `language` option. @class datetime @extends abstractinput @final @since 1.4.4 @example 15/03/2013 12:45 **/ (function ($) { "use strict"; var DateTime = function (options) { this.init('datetime', options, DateTime.defaults); this.initPicker(options, DateTime.defaults); }; $.fn.editableutils.inherit(DateTime, $.fn.editabletypes.abstractinput); $.extend(DateTime.prototype, { initPicker: function(options, defaults) { //'format' is set directly from settings or data-* attributes //by default viewformat equals to format if(!this.options.viewformat) { this.options.viewformat = this.options.format; } //try parse datetimepicker config defined as json string in data-datetimepicker options.datetimepicker = $.fn.editableutils.tryParseJson(options.datetimepicker, true); //overriding datetimepicker config (as by default jQuery extend() is not recursive) //since 1.4 datetimepicker internally uses viewformat instead of format. Format is for submit only this.options.datetimepicker = $.extend({}, defaults.datetimepicker, options.datetimepicker, { format: this.options.viewformat }); //language this.options.datetimepicker.language = this.options.datetimepicker.language || 'en'; //store DPglobal this.dpg = $.fn.datetimepicker.DPGlobal; //store parsed formats this.parsedFormat = this.dpg.parseFormat(this.options.format, this.options.formatType); this.parsedViewFormat = this.dpg.parseFormat(this.options.viewformat, this.options.formatType); }, render: function () { this.$input.datetimepicker(this.options.datetimepicker); //adjust container position when viewMode changes //see https://github.com/smalot/bootstrap-datetimepicker/pull/80 this.$input.on('changeMode', function(e) { var f = $(this).closest('form').parent(); //timeout here, otherwise container changes position before form has new size setTimeout(function(){ f.triggerHandler('resize'); }, 0); }); //"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) { //formatDate works with UTCDate! var text = value ? this.dpg.formatDate(this.toUTC(value), this.parsedViewFormat, this.options.datetimepicker.language, this.options.formatType) : ''; if(element) { DateTime.superclass.value2html.call(this, text, element); } else { return text; } }, html2value: function(html) { //parseDate return utc date! var value = this.parseDate(html, this.parsedViewFormat); return value ? this.fromUTC(value) : null; }, value2str: function(value) { //formatDate works with UTCDate! return value ? this.dpg.formatDate(this.toUTC(value), this.parsedFormat, this.options.datetimepicker.language, this.options.formatType) : ''; }, str2value: function(str) { //parseDate return utc date! var value = this.parseDate(str, this.parsedFormat); return value ? this.fromUTC(value) : null; }, value2submit: function(value) { return this.value2str(value); }, value2input: function(value) { if(value) { this.$input.data('datetimepicker').setDate(value); } }, input2value: function() { //date may be cleared, in that case getDate() triggers error var dt = this.$input.data('datetimepicker'); return dt.date ? dt.getDate() : null; }, activate: function() { }, clear: function() { this.$input.data('datetimepicker').date = null; this.$input.find('.active').removeClass('active'); if(!this.options.showbuttons) { this.$input.closest('form').submit(); } }, autosubmit: function() { this.$input.on('mouseup', '.minute', function(e){ var $form = $(this).closest('form'); setTimeout(function() { $form.submit(); }, 200); }); }, //convert date from local to utc toUTC: function(value) { return value ? new Date(value.valueOf() - value.getTimezoneOffset() * 60000) : value; }, //convert date from utc to local fromUTC: function(value) { return value ? new Date(value.valueOf() + value.getTimezoneOffset() * 60000) : value; }, /* For incorrect date bootstrap-datetimepicker returns current date that is not suitable for datetimefield. This function returns null for incorrect date. */ parseDate: function(str, format) { var date = null, formattedBack; if(str) { date = this.dpg.parseDate(str, format, this.options.datetimepicker.language, this.options.formatType); if(typeof str === 'string') { formattedBack = this.dpg.formatDate(date, format, this.options.datetimepicker.language, this.options.formatType); if(str !== formattedBack) { date = null; } } } return date; } }); DateTime.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { /** @property tpl @default
**/ 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, h, i @property format @type string @default yyyy-mm-dd hh:ii **/ format:'yyyy-mm-dd hh:ii', formatType:'standard', /** Format used for displaying date. Also applied when converting date from element's text on init. If not specified equals to format @property viewformat @type string @default null **/ viewformat: null, /** Configuration of datetimepicker. Full list of options: https://github.com/smalot/bootstrap-datetimepicker @property datetimepicker @type object @default { } **/ datetimepicker:{ todayHighlight: false, autoclose: false }, /** Text shown as clear date button. If false clear button will not be rendered. @property clear @type boolean|string @default 'x clear' **/ clear: '× clear' }); $.fn.editabletypes.datetime = DateTime; }(window.jQuery)); /** Bootstrap datetimefield input - datetime input for inline mode. Shows normal and binds popup datetimepicker. Automatically shown in inline mode. @class datetimefield @extends datetime **/ (function ($) { "use strict"; var DateTimeField = function (options) { this.init('datetimefield', options, DateTimeField.defaults); this.initPicker(options, DateTimeField.defaults); }; $.fn.editableutils.inherit(DateTimeField, $.fn.editabletypes.datetime); $.extend(DateTimeField.prototype, { render: function () { this.$input = this.$tpl.find('input'); this.setClass(); this.setAttr('placeholder'); this.$tpl.datetimepicker(this.options.datetimepicker); //need to disable original event handlers this.$input.off('focus keydown'); //update value of datepicker this.$input.keyup($.proxy(function(){ this.$tpl.removeData('date'); this.$tpl.datetimepicker('update'); }, this)); }, value2input: function(value) { this.$input.val(this.value2html(value)); this.$tpl.datetimepicker('update'); }, input2value: function() { return this.html2value(this.$input.val()); }, activate: function() { $.fn.editabletypes.text.prototype.activate.call(this); }, autosubmit: function() { //reset autosubmit to empty } }); DateTimeField.defaults = $.extend({}, $.fn.editabletypes.datetime.defaults, { /** @property tpl **/ tpl:'
', /** @property inputclass @default 'input-medium' **/ inputclass: 'input-medium', /* datetimepicker config */ datetimepicker:{ todayHighlight: false, autoclose: true } }); $.fn.editabletypes.datetimefield = DateTimeField; }(window.jQuery)); /** Address editable input. Internally value stored as {city: "Moscow", street: "Lenina", building: "15"} @class address @extends abstractinput @final @example awesome **/ (function ($) { "use strict"; var Address = function (options) { this.init('address', options, Address.defaults); }; //inherit from Abstract input $.fn.editableutils.inherit(Address, $.fn.editabletypes.abstractinput); $.extend(Address.prototype, { /** Renders input from tpl @method render() **/ render: function() { this.$input = this.$tpl.find('input'); }, /** Default method to show value in element. Can be overwritten by display option. @method value2html(value, element) **/ value2html: function(value, element) { if(!value) { $(element).empty(); return; } var html = $('
').text(value.city).html() + ', ' + $('
').text(value.street).html() + ' st., bld. ' + $('
').text(value.building).html(); $(element).html(html); }, /** Gets value from element's html @method html2value(html) **/ html2value: function(html) { /* you may write parsing method to get value by element's html e.g. "Moscow, st. Lenina, bld. 15" => {city: "Moscow", street: "Lenina", building: "15"} but for complex structures it's not recommended. Better set value directly via javascript, e.g. editable({ value: { city: "Moscow", street: "Lenina", building: "15" } }); */ return null; }, /** Converts value to string. It is used in internal comparing (not for sending to server). @method value2str(value) **/ value2str: function(value) { var str = ''; if(value) { for(var k in value) { str = str + k + ':' + value[k] + ';'; } } return str; }, /* Converts string to value. Used for reading value from 'data-value' attribute. @method str2value(str) */ str2value: function(str) { /* this is mainly for parsing value defined in data-value attribute. If you will always set value by javascript, no need to overwrite it */ return str; }, /** Sets value of input. @method value2input(value) @param {mixed} value **/ value2input: function(value) { if(!value) { return; } this.$input.filter('[name="city"]').val(value.city); this.$input.filter('[name="street"]').val(value.street); this.$input.filter('[name="building"]').val(value.building); }, /** Returns value of input. @method input2value() **/ input2value: function() { return { city: this.$input.filter('[name="city"]').val(), street: this.$input.filter('[name="street"]').val(), building: this.$input.filter('[name="building"]').val() }; }, /** Activates input: sets focus on the first field. @method activate() **/ activate: function() { this.$input.filter('[name="city"]').focus(); }, /** Attaches handler to submit form in case of 'showbuttons=false' mode @method autosubmit() **/ autosubmit: function() { this.$input.keydown(function (e) { if (e.which === 13) { $(this).closest('form').submit(); } }); } }); Address.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { tpl: '
'+ '
'+ '
', inputclass: '' }); $.fn.editabletypes.address = Address; }(window.jQuery));