636 lines
22 KiB
JavaScript
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 = $('<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 <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: <something>}</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));
|