/*! X-editable - v1.5.1
* In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery
* http://github.com/vitalets/x-editable
* Copyright (c) 2013 Vitaliy Potapov; Licensed MIT */
/**
Form with single input element, two buttons and two states: normal/loading.
Applied as jQuery method to DIV tag (not to form tag!). This is because form can be in loading state when spinner shown.
Editableform is linked with one of input types, e.g. 'text', 'select' etc.
@class editableform
@uses text
@uses textarea
**/
(function ($) {
"use strict";
var EditableForm = function (div, options) {
this.options = $.extend({}, $.fn.editableform.defaults, options);
this.$div = $(div); //div, containing form. Not form tag. Not editable-element.
if(!this.options.scope) {
this.options.scope = this;
}
//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|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 $('$().editable()
. You should subscribe on it's events (save / cancel) to get profit of it.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 $(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 $('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] = $('