(function ($) { "use strict"; var EditableForm = function (div, options) { this.options = $.extend({}, $.fn.editableform.defaults, options); this.$div = $(div); //div, containing form. Not form tag. Not editable-element. if(!this.options.scope) { this.options.scope = this; } //nothing shown after init }; EditableForm.prototype = { constructor: EditableForm, initInput: function() { //called once //take input from options (as it is created in editable-element) this.input = this.options.input; //set initial value //todo: may be add check: typeof str === 'string' ? this.value = this.input.str2value(this.options.value); //prerender: get input.$input this.input.prerender(); }, initTemplate: function() { this.$form = $($.fn.editableform.template); }, initButtons: function() { var $btn = this.$form.find('.editable-buttons'); $btn.append($.fn.editableform.buttons); if(this.options.showbuttons === 'bottom') { $btn.addClass('editable-buttons-bottom'); } }, /** Renders editableform @method render **/ render: function() { //init loader this.$loading = $($.fn.editableform.loading); this.$div.empty().append(this.$loading); //init form template and buttons this.initTemplate(); if(this.options.showbuttons) { this.initButtons(); } else { this.$form.find('.editable-buttons').remove(); } //show loading state this.showLoading(); //flag showing is form now saving value to server. //It is needed to wait when closing form. this.isSaving = false; /** Fired when rendering starts @event rendering @param {Object} event event object **/ this.$div.triggerHandler('rendering'); //init input this.initInput(); //append input to form this.$form.find('div.editable-input').append(this.input.$tpl); //append form to container this.$div.append(this.$form); //render input $.when(this.input.render()) .then($.proxy(function () { //setup input to submit automatically when no buttons shown if(!this.options.showbuttons) { this.input.autosubmit(); } //attach 'cancel' handler this.$form.find('.editable-cancel').click($.proxy(this.cancel, this)); if(this.input.error) { this.error(this.input.error); this.$form.find('.editable-submit').attr('disabled', true); this.input.$input.attr('disabled', true); //prevent form from submitting this.$form.submit(function(e){ e.preventDefault(); }); } else { this.error(false); this.input.$input.removeAttr('disabled'); this.$form.find('.editable-submit').removeAttr('disabled'); var value = (this.value === null || this.value === undefined || this.value === '') ? this.options.defaultValue : this.value; this.input.value2input(value); //attach submit handler this.$form.submit($.proxy(this.submit, this)); } /** Fired when form is rendered @event rendered @param {Object} event event object **/ this.$div.triggerHandler('rendered'); this.showForm(); //call postrender method to perform actions required visibility of form if(this.input.postrender) { this.input.postrender(); } }, this)); }, cancel: function() { /** Fired when form was cancelled by user @event cancel @param {Object} event event object **/ this.$div.triggerHandler('cancel'); }, showLoading: function() { var w, h; if(this.$form) { //set loading size equal to form w = this.$form.outerWidth(); h = this.$form.outerHeight(); if(w) { this.$loading.width(w); } if(h) { this.$loading.height(h); } this.$form.hide(); } else { //stretch loading to fill container width w = this.$loading.parent().width(); if(w) { this.$loading.width(w); } } this.$loading.show(); }, showForm: function(activate) { this.$loading.hide(); this.$form.show(); if(activate !== false) { this.input.activate(); } /** Fired when form is shown @event show @param {Object} event event object **/ this.$div.triggerHandler('show'); }, error: function(msg) { var $group = this.$form.find('.control-group'), $block = this.$form.find('.editable-error-block'), lines; if(msg === false) { $group.removeClass($.fn.editableform.errorGroupClass); $block.removeClass($.fn.editableform.errorBlockClass).empty().hide(); } else { //convert newline to
for more pretty error display if(msg) { lines = (''+msg).split('\n'); for (var i = 0; i < lines.length; i++) { lines[i] = $('
').text(lines[i]).html(); } msg = lines.join('
'); } $group.addClass($.fn.editableform.errorGroupClass); $block.addClass($.fn.editableform.errorBlockClass).html(msg).show(); } }, submit: function(e) { e.stopPropagation(); e.preventDefault(); //get new value from input var newValue = this.input.input2value(); //validation: if validate returns string or truthy value - means error //if returns object like {newValue: '...'} => submitted value is reassigned to it var error = this.validate(newValue); if ($.type(error) === 'object' && error.newValue !== undefined) { newValue = error.newValue; this.input.value2input(newValue); if(typeof error.msg === 'string') { this.error(error.msg); this.showForm(); return; } } else if (error) { this.error(error); this.showForm(); return; } //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. Since 1.5.1 you can modify submitted value by returning object from `validate`: `{newValue: '...'}` or `{newValue: '...', msg: '...'}` @property validate @type function @default null @example 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' ]; //select2 has extra body click in IE //see: https://github.com/ivaynberg/select2/issues/1058 if ($('.select2-drop-mask').is(':visible')) { return;//ACE } //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. Since 1.5.1 `submit` can be applied to single element to send data programmatically. In that case `url`, `success` and `error` is taken from initial options and you can just call `$('#username').editable('submit')`. @method submit(options) @param {object} options @param {object} options.url url to submit data @param {object} options.data additional data to submit @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'); // validation ok if($.isEmptyObject(errors)) { var ajaxOptions = {}; // for single element use url, success etc from options if($elems.length === 1) { var editable = $elems.data('editable'); //standard params var params = { name: editable.options.name || '', value: editable.input.value2submit(editable.value), pk: (typeof editable.options.pk === 'function') ? editable.options.pk.call(editable.options.scope) : editable.options.pk }; //additional params if(typeof editable.options.params === 'function') { params = editable.options.params.call(editable.options.scope, params); } else { //try parse json in single quotes (from data-params attribute) editable.options.params = $.fn.editableutils.tryParseJson(editable.options.params, true); $.extend(params, editable.options.params); } ajaxOptions = { url: editable.options.url, data: params, type: 'POST' }; // use success / error from options config.success = config.success || editable.options.success; config.error = config.error || editable.options.error; // multiple elements } else { var values = this.editable('getValue'); ajaxOptions = { url: config.url, data: values, type: 'POST' }; } // ajax success callabck (response 200 OK) ajaxOptions.success = typeof config.success === 'function' ? function(response) { config.success.call($elems, response, config); } : $.noop; // ajax error callabck ajaxOptions.error = typeof config.error === 'function' ? function() { config.error.apply($elems, arguments); } : $.noop; // extend ajaxOptions if(config.ajaxOptions) { $.extend(ajaxOptions, config.ajaxOptions); } // extra data if(config.data) { $.extend(ajaxOptions.data, config.data); } // perform ajax request $.ajax(ajaxOptions); } else { //client-side validation error if(typeof config.error === 'function') { config.error.call($elems, errors); } } 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($('