Files
src
containers
editable-form
element
editable-element.css
editable-element.js
img
inputs
inputs-ext
test
.gitignore
CHANGELOG.txt
LICENSE-MIT
README.md
grunt.js
package.json
x-editable/src/element/editable-element.js
2013-01-04 16:08:00 +04:00

577 lines
22 KiB
JavaScript

/**
Makes editable any HTML element on the page. Applied as jQuery method.
@class editable
@uses editableContainer
**/
(function ($) {
var Editable = function (element, options) {
this.$element = $(element);
this.options = $.extend({}, $.fn.editable.defaults, $.fn.editableutils.getConfigData(this.$element), options);
this.init();
};
Editable.prototype = {
constructor: Editable,
init: function () {
var isValueByText = false,
doAutotext, finalize;
//name
this.options.name = this.options.name || this.$element.attr('id');
//create input of specified type. Input will be used for converting value, not in form
this.input = $.fn.editableutils.createInput(this.options);
if(!this.input) {
return;
}
//set value from settings or by element's text
if (this.options.value === undefined || this.options.value === null) {
this.value = this.input.html2value($.trim(this.$element.html()));
isValueByText = true;
} else {
/*
value can be string when received from 'data-value' attribute
for complext objects value can be set as json string in data-value attribute,
e.g. data-value="{city: 'Moscow', street: 'Lenina'}"
*/
this.options.value = $.fn.editableutils.tryParseJson(this.options.value, true);
if(typeof this.options.value === 'string') {
this.value = this.input.str2value(this.options.value);
} else {
this.value = this.options.value;
}
}
//add 'editable' class to every editable element
this.$element.addClass('editable');
//attach handler activating editable. In disabled mode it just prevent default action (useful for links)
if(this.options.toggle !== 'manual') {
this.$element.addClass('editable-click');
this.$element.on(this.options.toggle + '.editable', $.proxy(function(e){
e.preventDefault();
//stop propagation not required anymore because in document click handler it checks event target
//e.stopPropagation();
if(this.options.toggle === 'mouseenter') {
//for hover only show container
this.show();
} else {
//when toggle='click' we should not close all other containers as they will be closed automatically in document click listener
var closeAll = (this.options.toggle !== 'click');
this.toggle(closeAll);
}
}, this));
} else {
this.$element.attr('tabindex', -1); //do not stop focus on element when toggled manually
}
//check conditions for autotext:
//if value was generated by text or value is empty, no sense to run autotext
doAutotext = !isValueByText && this.value !== null && this.value !== undefined;
doAutotext &= (this.options.autotext === 'always') || (this.options.autotext === 'auto' && !this.$element.text().length);
$.when(doAutotext ? this.render() : true).then($.proxy(function() {
if(this.options.disabled) {
this.disable();
} else {
this.enable();
}
/**
Fired when element was initialized by editable method.
@event init
@param {Object} event event object
@param {Object} editable editable instance (as here it cannot accessed via data('editable'))
@since 1.2.0
@example
$('#username').on('init', function(e, editable) {
alert('initialized ' + editable.options.name);
});
**/
this.$element.triggerHandler('init', this);
}, this));
},
/*
Renders value into element's text.
Can call custom display method from options.
Can return deferred object.
@method render()
@param {mixed} response server response (if exist) to pass into display function
*/
render: function(response) {
//do not display anything
if(this.options.display === false) {
return;
}
//if it is input with source, we pass callback in third param to be called when source is loaded
if(this.input.options.hasOwnProperty('source')) {
return this.input.value2html(this.value, this.$element[0], this.options.display, response);
//if display method defined --> use it
} else if(typeof this.options.display === 'function') {
return this.options.display.call(this.$element[0], this.value, response);
//else use input's original value2html() method
} else {
return this.input.value2html(this.value, this.$element[0]);
}
},
/**
Enables editable
@method enable()
**/
enable: function() {
this.options.disabled = false;
this.$element.removeClass('editable-disabled');
this.handleEmpty();
if(this.options.toggle !== 'manual') {
if(this.$element.attr('tabindex') === '-1') {
this.$element.removeAttr('tabindex');
}
}
},
/**
Disables editable
@method disable()
**/
disable: function() {
this.options.disabled = true;
this.hide();
this.$element.addClass('editable-disabled');
this.handleEmpty();
//do not stop focus on this element
this.$element.attr('tabindex', -1);
},
/**
Toggles enabled / disabled state of editable element
@method toggleDisabled()
**/
toggleDisabled: function() {
if(this.options.disabled) {
this.enable();
} else {
this.disable();
}
},
/**
Sets new option
@method option(key, value)
@param {string|object} key option name or object with several options
@param {mixed} value option new value
@example
$('.editable').editable('option', 'pk', 2);
**/
option: function(key, value) {
//set option(s) by object
if(key && typeof key === 'object') {
$.each(key, $.proxy(function(k, v){
this.option($.trim(k), v);
}, this));
return;
}
//set option by string
this.options[key] = value;
//disabled
if(key === 'disabled') {
if(value) {
this.disable();
} else {
this.enable();
}
return;
}
//value
if(key === 'value') {
this.setValue(value);
}
//transfer new option to container!
if(this.container) {
this.container.option(key, value);
}
},
/*
* set emptytext if element is empty (reverse: remove emptytext if needed)
*/
handleEmpty: function () {
//do not handle empty if we do not display anything
if(this.options.display === false) {
return;
}
var emptyClass = 'editable-empty';
//emptytext shown only for enabled
if(!this.options.disabled) {
if ($.trim(this.$element.text()) === '') {
this.$element.addClass(emptyClass).text(this.options.emptytext);
} else {
this.$element.removeClass(emptyClass);
}
} else {
//below required if element disable property was changed
if(this.$element.hasClass(emptyClass)) {
this.$element.empty();
this.$element.removeClass(emptyClass);
}
}
},
/**
Shows container with form
@method show()
@param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
**/
show: function (closeAll) {
if(this.options.disabled) {
return;
}
//init editableContainer: popover, tooltip, inline, etc..
if(!this.container) {
var containerOptions = $.extend({}, this.options, {
value: this.value
});
this.$element.editableContainer(containerOptions);
this.$element.on("save.internal", $.proxy(this.save, this));
this.container = this.$element.data('editableContainer');
} else if(this.container.tip().is(':visible')) {
return;
}
//show container
this.container.show(closeAll);
},
/**
Hides container with form
@method hide()
**/
hide: function () {
if(this.container) {
this.container.hide();
}
},
/**
Toggles container visibility (show / hide)
@method toggle()
@param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
**/
toggle: function(closeAll) {
if(this.container && this.container.tip().is(':visible')) {
this.hide();
} else {
this.show(closeAll);
}
},
/*
* called when form was submitted
*/
save: function(e, params) {
//if url is not user's function and value was not sent to server and value changed --> mark element with unsaved css.
if(typeof this.options.url !== 'function' && this.options.display !== false && params.response === undefined && this.input.value2str(this.value) !== this.input.value2str(params.newValue)) {
this.$element.addClass('editable-unsaved');
} else {
this.$element.removeClass('editable-unsaved');
}
this.setValue(params.newValue, false, params.response);
/**
Fired when new value was submitted. You can use <code>$(this).data('editable')</code> 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();
}
}
};
/* EDITABLE PLUGIN DEFINITION
* ======================= */
/**
jQuery method to initialize editable element.
@method $().editable(options)
@params {Object} options
@example
$('#username').editable({
type: 'text',
url: '/post',
pk: 1
});
**/
$.fn.editable = function (option) {
//special API methods returning non-jquery object
var result = {}, args = arguments, datakey = 'editable';
switch (option) {
/**
Runs client-side validation for all matched editables
@method validate()
@returns {Object} validation errors map
@example
$('#username, #fullname').editable('validate');
// possible result:
{
username: "username is required",
fullname: "fullname should be minimum 3 letters length"
}
**/
case 'validate':
this.each(function () {
var $this = $(this), data = $this.data(datakey), error;
if (data && (error = data.validate())) {
result[data.options.name] = error;
}
});
return result;
/**
Returns current values of editable elements. If value is <code>null</code> or <code>undefined</code> it will not be returned
@method getValue()
@returns {Object} object of element names and values
@example
$('#username, #fullname').editable('validate');
// possible result:
{
username: "superuser",
fullname: "John"
}
**/
case 'getValue':
this.each(function () {
var $this = $(this), data = $this.data(datakey);
if (data && data.value !== undefined && data.value !== null) {
result[data.options.name] = data.input.value2submit(data.value);
}
});
return result;
/**
This method collects values from several editable elements and submit them all to server.
Internally it runs client-side validation for all fields and submits only in case of success.
See <a href="#newrecord">creating new records</a> for details.
@method submit(options)
@param {object} options
@param {object} options.url url to submit data
@param {object} options.data additional data to submit
@param {object} options.ajaxOptions additional ajax options
@param {function} options.error(obj) error handler
@param {function} options.success(obj,config) success handler
@returns {Object} jQuery object
**/
case 'submit': //collects value, validate and submit to server for creating new record
var config = arguments[1] || {},
$elems = this,
errors = this.editable('validate'),
values;
if($.isEmptyObject(errors)) {
values = this.editable('getValue');
if(config.data) {
$.extend(values, config.data);
}
$.ajax($.extend({
url: config.url,
data: values,
type: 'POST'
}, config.ajaxOptions))
.success(function(response) {
//successful response 200 OK
if(typeof config.success === 'function') {
config.success.call($elems, response, config);
}
})
.error(function(){ //ajax error
if(typeof config.error === 'function') {
config.error.apply($elems, arguments);
}
});
} else { //client-side validation error
if(typeof config.error === 'function') {
config.error.call($elems, errors);
}
}
return this;
}
//return jquery object
return this.each(function () {
var $this = $(this),
data = $this.data(datakey),
options = typeof option === 'object' && option;
if (!data) {
$this.data(datakey, (data = new Editable(this, options)));
}
if (typeof option === 'string') { //call method
data[option].apply(data, Array.prototype.slice.call(args, 1));
}
});
};
$.fn.editable.defaults = {
/**
Type of input. Can be <code>text|textarea|select|date|checklist</code> 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 <code>click|dblclick|mouseenter|manual</code>.
When set to <code>manual</code> you should manually call <code>show/hide</code> methods of editable.
**Note**: if you call <code>show</code> or <code>toggle</code> inside **click** handler of some DOM element,
you need to apply <code>e.stopPropagation()</code> 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 <code>auto|always|never</code>. Useful for select and date.
For example, if dropdown list is <code>{1: 'a', 2: 'b'}</code> and element's value set to <code>1</code>, it's html will be automatically set to <code>'a'</code>.
<code>auto</code> - text will be automatically set only if element is empty.
<code>always|never</code> - always(never) try to set element's text.
@property autotext
@type string
@default 'auto'
**/
autotext: 'auto',
/**
Initial value of input. If not set, taken from element's text.
@property value
@type mixed
@default element's text
**/
value: null,
/**
Callback to perform custom displaying of value in element's text.
If `null`, default input's display used.
If `false`, no displaying methods will be called, element's text will never change.
Runs under element's scope.
_Parameters:_
* `value` current value to be displayed
* `response` server response (if display called after ajax submit), since 1.4.0
For **inputs with source** (select, checklist) parameters are different:
* `value` current value to be displayed
* `sourceData` array of items for current input (e.g. dropdown items)
* `response` server response (if display called after ajax submit), since 1.4.0
To get currently selected items use `$.fn.editableutils.itemsByValue(value, sourceData)`.
@property display
@type function|boolean
@default null
@since 1.2.0
@example
display: function(value, sourceData) {
//display checklist as comma-separated values
var html = [],
checked = $.fn.editableutils.itemsByValue(value, sourceData);
if(checked.length) {
$.each(checked, function(i, v) { html.push($.fn.editableutils.escape(v.text)); });
$(this).html(html.join(', '));
} else {
$(this).empty();
}
}
**/
display: null
};
}(window.jQuery));