2013-10-10 20:42:55 +04:00

636 lines
22 KiB
JavaScript

/**
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 <br> for more pretty error display
if(msg) {
lines = (''+msg).split('\n');
for (var i = 0; i < lines.length; i++) {
lines[i] = $('<div>').text(lines[i]).html();
}
msg = lines.join('<br>');
}
$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: <something>} --> 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 = $('&lt;div&gt;').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 <code>text|textarea|select|date|checklist</code>
@property type
@type string
@default 'text'
**/
type: 'text',
/**
Url for submit, e.g. <code>'/post'</code>
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 <code>object</code> - it is **appended** to original ajax data (pk, name and value).
If defined as <code>function</code> - 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 <code>id</code> 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. <code>{id: 1, lang: 'en'}</code>.
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 <code>{success: true}</code>
or <code>{success: false, msg: "server error"}</code> 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** <code>{newValue: &lt;something&gt;}</code> - 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 <code>null</code> 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 = '<form class="form-inline editableform">'+
'<div class="control-group">' +
'<div><div class="editable-input"></div><div class="editable-buttons"></div></div>'+
'<div class="editable-error-block"></div>' +
'</div>' +
'</form>';
//loading div
$.fn.editableform.loading = '<div class="editableform-loading"></div>';
//buttons
$.fn.editableform.buttons = '<button type="submit" class="editable-submit">ok</button>'+
'<button type="button" class="editable-cancel">cancel</button>';
//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));