/*! X-editable - v1.1.1 * In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery * http://github.com/vitalets/x-editable * Copyright (c) 2012 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!) Editableform is linked with one of input types, e.g. 'text' or 'select'. @class editableform @uses text @uses textarea **/ (function ($) { var EditableForm = function (element, options) { this.options = $.extend({}, $.fn.editableform.defaults, options); this.$element = $(element); //div, containing form. Not form tag! Not editable-element. this.initInput(); }; EditableForm.prototype = { constructor: EditableForm, initInput: function() { //called once var TypeConstructor, typeOptions; //create input of specified type if(typeof $.fn.editableform.types[this.options.type] === 'function') { TypeConstructor = $.fn.editableform.types[this.options.type]; typeOptions = $.fn.editableform.utils.sliceObj(this.options, $.fn.editableform.utils.objectKeys(TypeConstructor.defaults)); this.input = new TypeConstructor(typeOptions); } else { $.error('Unknown type: '+ this.options.type); return; } this.value = this.input.str2value(this.options.value); }, initTemplate: function() { this.$form = $($.fn.editableform.template); }, initButtons: function() { this.$form.find('.editable-buttons').append($.fn.editableform.buttons); }, /** Renders editableform @method render **/ render: function() { this.$loading = $($.fn.editableform.loading); this.$element.empty().append(this.$loading); this.showLoading(); //init form template and buttons this.initTemplate(); if(this.options.showbuttons) { this.initButtons(); } else { this.$form.find('.editable-buttons').remove(); } /** Fired when rendering starts @event rendering @param {Object} event event object **/ this.$element.triggerHandler('rendering'); //render input $.when(this.input.render()) .then($.proxy(function () { //input this.$form.find('div.editable-input').append(this.input.$input); //automatically submit inputs when no buttons shown if(!this.options.showbuttons) { this.input.autosubmit(); } //"clear" link if(this.input.$clear) { this.$form.find('div.editable-input').append($('
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 can return deferred object to run fail/done callbacks.
@property url
@type string|function
@default null
@example
url: function(params) {
if(params.value === 'abc') {
var d = new $.Deferred;
return d.reject('field cannot be "abc"'); //returning error via deferred object
} else {
someModel.set(params.name, params.value); //save data in some js model
}
}
**/
url:null,
/**
Additional params for submit. Function can be used to calculate params dynamically
@example
params: function(params) {
return { a: 1 };
}
@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 dinamically 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,
/**
Strategy for sending data on server. Can be auto|always|never
.
When 'auto' data will be sent on server only if pk defined, otherwise new value will be stored in element.
@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.
@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: function(response, newValue) {},
/**
Additional options for ajax request.
List of values: http://api.jquery.com/jQuery.ajax
@property ajaxOptions
@type object
@default null
**/
ajaxOptions: null,
/**
Wether to show buttons or not.
Form without buttons can be auto-submitted by input or by onblur = 'submit'.
@property showbuttons
@type boolean
@default true
**/
showbuttons: true
/*todo:
Submit strategy. Can be normal|never
submitmode='never'
usefull for turning into classic form several inputs and submitting them together manually.
Works pretty with showbuttons=false
@property submitmode
@type string
@default normal
*/
// submitmode: 'normal'
};
/*
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 attahced to control-group
$.fn.editableform.errorGroupClass = null;
//error class attahced to editable-error-block
$.fn.editableform.errorBlockClass = 'editable-error';
//input types
$.fn.editableform.types = {};
//utils
$.fn.editableform.utils = {};
}(window.jQuery));
/**
* EditableForm utilites
*/
(function ($) {
$.fn.editableform.utils = {
/**
* 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)) {
data[k] = v;
}
});
return data;
},
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;
}
}
};
}(window.jQuery));
/**
Attaches stand-alone container with editable-form to HTML element. Element is used only for positioning, value is not stored anywhere.$().editable()
. You should subscribe on it's events (save / cancel) to get profit of it.$(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);
},
/**
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.call('destroy');
},
/*
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
if(el === element) {
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();
} 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;
if (!data) {
$this.data(dataKey, (data = new EditableContainer(this, options)));
}
if (typeof option === 'string') { //call method
data[option].apply(data, Array.prototype.slice.call(args, 1));
}
});
};
//store constructor
$.fn.editableContainer.Constructor = EditableContainer;
//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',
/**
Wether 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'
**/
onblur: 'cancel'
};
/*
* 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));
/**
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.editableform.utils.getConfigData(this.$element), options);
this.init();
};
Editable.prototype = {
constructor: Editable,
init: function () {
var TypeConstructor,
isValueByText = false,
doAutotext,
finalize;
//initialization flag
this.isInit = true;
//editableContainer must be defined
if(!$.fn.editableContainer) {
$.error('You must define $.fn.editableContainer via including corresponding file (e.g. editable-popover.js)');
return;
}
//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
if(typeof $.fn.editableform.types[this.options.type] === 'function') {
TypeConstructor = $.fn.editableform.types[this.options.type];
this.typeOptions = $.fn.editableform.utils.sliceObj(this.options, $.fn.editableform.utils.objectKeys(TypeConstructor.defaults));
this.input = new TypeConstructor(this.typeOptions);
} else {
$.error('Unknown type: '+ this.options.type);
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 {
if(typeof this.options.value === 'string') {
this.options.value = $.trim(this.options.value);
}
this.value = this.input.str2value(this.options.value);
}
//add 'editable' class
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.input.value2html(this.value, this.$element) : true).then($.proxy(function() {
if(this.options.disabled) {
this.disable();
} else {
this.enable();
}
/**
Fired each time when element's text is rendered. Occurs on initialization and on each update of value.
Can be used for display customization.
@event render
@param {Object} event event object
@param {Object} editable editable instance
@example
$('#action').on('render', function(e, editable) {
var colors = {0: "gray", 1: "green", 2: "blue", 3: "red"};
$(this).css("color", colors[editable.value]);
});
**/
this.$element.triggerHandler('render', this);
this.isInit = false;
}, this));
},
/**
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 () {
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 Wether 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,
autohide: false //element will take care to show/hide container
});
this.$element.editableContainer(containerOptions);
this.$element.on({
save: $.proxy(this.save, this),
cancel: $.proxy(this.hide, 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();
}
//return focus on element
if (this.options.enablefocus && this.options.toggle === 'click') {
this.$element.focus();
}
},
/**
Toggles container visibility (show / hide)
@method toggle()
@param {boolean} closeAll Wether 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' && 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.hide();
this.setValue(params.newValue);
/**
Fired when new value was submitted. You can use $(this).data('editable')
to access to editable instance
@event save
@param {Object} event event object
@param {Object} params additional params
@param {mixed} params.newValue submitted value
@param {Object} params.response ajax response
@example
$('#username').on('save', function(e, params) {
//assuming server response: '{success: true}'
var pk = $(this).data('editable').options.pk;
if(params.response && params.response.success) {
alert('value: ' + params.newValue + ' with pk: ' + pk + ' saved!');
} else {
alert('error!');
}
});
**/
//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 wether to convert value from string to internal format
**/
setValue: function(value, convertStr) {
if(convertStr) {
this.value = this.input.str2value(value);
} else {
this.value = value;
}
if(this.container) {
this.container.option('value', this.value);
}
$.when(this.input.value2html(this.value, this.$element))
.then($.proxy(function() {
this.handleEmpty();
this.$element.triggerHandler('render', this);
}, 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 requied",
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 null
or undefined
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.value2str(data.value);
}
});
return result;
/**
This method collects values from several editable elements and submit them all to server.
It is designed mainly for creating new records.
@method submit(options)
@param {object} options
@param {object} options.url url to submit data
@param {object} options.data additional data to submit
@param {function} options.error(obj) error handler (called on both client-side and server-side validation errors)
@param {function} options.success(obj) 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(typeof config.error !== 'function') {
config.error = function() {};
}
if($.isEmptyObject(errors)) {
values = this.editable('getValue');
if(config.data) {
$.extend(values, config.data);
}
$.ajax({
type: 'POST',
url: config.url,
data: values,
dataType: 'json'
}).success(function(response) {
if(typeof response === 'object' && response.id) {
$elems.editable('option', 'pk', response.id);
$elems.removeClass('editable-unsaved');
if(typeof config.success === 'function') {
config.success.apply($elems, arguments);
}
} else { //server-side validation error
config.error.apply($elems, arguments);
}
}).error(function(){ //ajax error
config.error.apply($elems, arguments);
});
} else { //client-side validation error
config.error.call($elems, {errors: 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 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
. Usefull 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',
/**
Wether to return focus on element after form is closed.
This allows fully keyboard input.
@property enablefocus
@type boolean
@default false
**/
enablefocus: false,
/**
Initial value of input. Taken from data-value
or element's text.
@property value
@type mixed
@default element's text
**/
value: null
};
}(window.jQuery));
/**
Abstract editable input class.
To create your own input you should inherit from this class.
@class abstract
**/
(function ($) {
var Abstract = function () { };
Abstract.prototype = {
/**
Iinitializes input
@method init()
**/
init: function(type, options, defaults) {
this.type = type;
this.options = $.extend({}, defaults, options);
this.$input = null;
this.$clear = null;
this.error = null;
},
/**
Renders input. Can return jQuery deferred object.
@method render()
**/
render: function() {
this.$input = $(this.options.tpl);
if(this.options.inputclass) {
this.$input.addClass(this.options.inputclass);
}
if (this.options.placeholder) {
this.$input.attr('placeholder', this.options.placeholder);
}
},
/**
Sets element's html by value.
@method value2html(value, element)
@param {mixed} value
@param {DOMElement} element
**/
value2html: function(value, element) {
var html = this.escape(value);
$(element).html(html);
},
/**
Converts element's html to value
@method html2value(html)
@param {string} html
@returns {mixed}
**/
html2value: function(html) {
return $('{value1: "text1", value2: "text2" ...}
but it does not guarantee elements order.
@property source
@type string|array|object
@default null
**/
source:null,
/**
Data automatically prepended to the begining of dropdown list.
@property prepend
@type string|array|object
@default false
**/
prepend:false,
/**
Error message when list cannot be loaded (e.g. ajax error)
@property sourceError
@type string
@default Error when loading list
**/
sourceError: 'Error when loading list'
});
$.fn.editableform.types.list = List;
}(window.jQuery));
/**
Text input
@class text
@extends abstract
@final
@example
awesome
**/
(function ($) {
var Text = function (options) {
this.init('text', options, Text.defaults);
};
$.fn.editableform.utils.inherit(Text, $.fn.editableform.types.abstract);
$.extend(Text.prototype, {
activate: function() {
if(this.$input.is(':visible')) {
this.$input.focus();
$.fn.editableform.utils.setCursorPosition(this.$input.get(0), this.$input.val().length);
}
}
});
Text.defaults = $.extend({}, $.fn.editableform.types.abstract.defaults, {
/**
@property tpl
@default
**/
tpl: '',
/**
Placeholder attribute of input. Shown when input is empty.
@property placeholder
@type string
@default null
**/
placeholder: null
});
$.fn.editableform.types.text = Text;
}(window.jQuery));
/**
Textarea input
@class textarea
@extends abstract
@final
@example
awesome comment!
**/
(function ($) {
var Textarea = function (options) {
this.init('textarea', options, Textarea.defaults);
};
$.fn.editableform.utils.inherit(Textarea, $.fn.editableform.types.abstract);
$.extend(Textarea.prototype, {
render: function () {
Textarea.superclass.render.call(this);
//ctrl + enter
this.$input.keydown(function (e) {
if (e.ctrlKey && e.which === 13) {
$(this).closest('form').submit();
}
});
},
value2html: function(value, element) {
var html = '', lines;
if(value) {
lines = value.split("\n");
for (var i = 0; i < lines.length; i++) {
lines[i] = $('limitText
will be shown.
@property limit
@type integer
@default 4
**/
limit: 4,
/**
Text shown when count of checked items is greater than limit
parameter.
You can use {checked}
and {count}
placeholders.
@property limitText
@type string
@default 'Selected {checked} of {count}'
**/
limitText: 'Selected {checked} of {count}'
});
$.fn.editableform.types.checklist = Checklist;
}(window.jQuery));
/*
Editableform based on Twitter Bootstrap
*/
(function ($) {
$.extend($.fn.editableform.Constructor.prototype, {
initTemplate: function() {
this.$form = $($.fn.editableform.template);
this.$form.find('.editable-error-block').addClass('help-block');
}
});
//buttons
$.fn.editableform.buttons = ''+
'';
//error classes
$.fn.editableform.errorGroupClass = 'error';
$.fn.editableform.errorBlockClass = null;
}(window.jQuery));
/**
* Editable Popover
* ---------------------
* requires bootstrap-popover.js
*/
(function ($) {
//extend methods
$.extend($.fn.editableContainer.Constructor.prototype, {
containerName: 'popover',
innerCss: '.popover-content p',
initContainer: function(){
$.extend(this.containerOptions, {
trigger: 'manual',
selector: false,
content: 'dfgh'
});
this.call(this.containerOptions);
},
setContainerOption: function(key, value) {
this.container().options[key] = value;
},
/**
* move popover to new position. This function mainly copied from bootstrap-popover.
*/
/*jshint laxcomma: true*/
setPosition: function () {
(function() {
var $tip = this.tip()
, inside
, pos
, actualWidth
, actualHeight
, placement
, tp;
placement = typeof this.options.placement === 'function' ?
this.options.placement.call(this, $tip[0], this.$element[0]) :
this.options.placement;
inside = /in/.test(placement);
$tip
// .detach()
//vitalets: remove any placement class because otherwise they dont influence on re-positioning of visible popover
.removeClass('top right bottom left')
.css({ top: 0, left: 0, display: 'block' });
// .insertAfter(this.$element);
pos = this.getPosition(inside);
actualWidth = $tip[0].offsetWidth;
actualHeight = $tip[0].offsetHeight;
switch (inside ? placement.split(' ')[1] : placement) {
case 'bottom':
tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2};
break;
case 'top':
tp = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2};
break;
case 'left':
tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth};
break;
case 'right':
tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width};
break;
}
$tip
.offset(tp)
.addClass(placement)
.addClass('in');
}).call(this.container());
/*jshint laxcomma: false*/
}
});
//defaults
/*
$.fn.editableContainer.defaults = $.extend({}, $.fn.popover.defaults, $.fn.editableContainer.defaults, {
});
*/
}(window.jQuery));
/**
Bootstrap-datepicker.
Description and examples: http://vitalets.github.com/bootstrap-datepicker.
For localization you can include js file from here: https://github.com/eternicode/bootstrap-datepicker/tree/master/js/locales
@class date
@extends abstract
@final
@example
15/05/1984
**/
(function ($) {
var Date = function (options) {
this.init('date', options, Date.defaults);
//set popular options directly from settings or data-* attributes
var directOptions = $.fn.editableform.utils.sliceObj(this.options, ['format']);
//overriding datepicker config (as by default jQuery extend() is not recursive)
this.options.datepicker = $.extend({}, Date.defaults.datepicker, directOptions, options.datepicker);
//by default viewformat equals to format
if(!this.options.viewformat) {
this.options.viewformat = this.options.datepicker.format;
}
//language
this.options.datepicker.language = this.options.datepicker.language || 'en';
//store DPglobal
this.dpg = $.fn.datepicker.DPGlobal;
//store parsed formats
this.parsedFormat = this.dpg.parseFormat(this.options.datepicker.format);
this.parsedViewFormat = this.dpg.parseFormat(this.options.viewformat);
};
$.fn.editableform.utils.inherit(Date, $.fn.editableform.types.abstract);
$.extend(Date.prototype, {
render: function () {
Date.superclass.render.call(this);
this.$input.datepicker(this.options.datepicker);
if(this.options.clear) {
this.$clear = $('').html(this.options.clear).click($.proxy(function(e){
e.preventDefault();
e.stopPropagation();
this.clear();
}, this));
}
},
value2html: function(value, element) {
var text = value ? this.dpg.formatDate(value, this.parsedViewFormat, this.options.datepicker.language) : '';
Date.superclass.value2html(text, element);
},
html2value: function(html) {
return html ? this.dpg.parseDate(html, this.parsedViewFormat, this.options.datepicker.language) : null;
},
value2str: function(value) {
return value ? this.dpg.formatDate(value, this.parsedFormat, this.options.datepicker.language) : '';
},
str2value: function(str) {
return str ? this.dpg.parseDate(str, this.parsedFormat, this.options.datepicker.language) : null;
},
value2input: function(value) {
this.$input.datepicker('update', value);
},
input2value: function() {
return this.$input.data('datepicker').date;
},
activate: function() {
},
clear: function() {
this.$input.data('datepicker').date = null;
this.$input.find('.active').removeClass('active');
},
autosubmit: function() {
this.$input.on('changeDate', function(e){
var $form = $(this).closest('form');
setTimeout(function() {
$form.submit();
}, 200);
});
}
});
Date.defaults = $.extend({}, $.fn.editableform.types.abstract.defaults, {
/**
@property tpl
@default
**/
tpl:'',
/**
@property inputclass
@default editable-date well
**/
inputclass: 'editable-date well',
/**
Format used for sending value to server. Also applied when converting date from data-value
attribute.d, dd, m, mm, yy, yyyy
@property format
@type string
@default yyyy-mm-dd
**/
format:'yyyy-mm-dd',
/**
Format used for displaying date. Also applied when converting date from element's text on init.
If not specified equals to format
@property viewformat
@type string
@default null
**/
viewformat: null,
/**
Configuration of datepicker.
Full list of options: http://vitalets.github.com/bootstrap-datepicker
@property datepicker
@type object
@default {
weekStart: 0,
startView: 0,
autoclose: false
}
**/
datepicker:{
weekStart: 0,
startView: 0,
autoclose: false
},
/**
Text shown as clear date button.
If false
clear button will not be rendered.
@property clear
@type boolean|string
@default 'x clear'
**/
clear: '× clear'
});
$.fn.editableform.types.date = Date;
}(window.jQuery));
/* =========================================================
* bootstrap-datepicker.js
* http://www.eyecon.ro/bootstrap-datepicker
* =========================================================
* Copyright 2012 Stefan Petre
* Improvements by Andrew Rowls
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ========================================================= */
!function( $ ) {
function UTCDate(){
return new Date(Date.UTC.apply(Date, arguments));
}
function UTCToday(){
var today = new Date();
return UTCDate(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate());
}
// Picker object
var Datepicker = function(element, options) {
var that = this;
this.element = $(element);
this.language = options.language||this.element.data('date-language')||"en";
this.language = this.language in dates ? this.language : "en";
this.format = DPGlobal.parseFormat(options.format||this.element.data('date-format')||'mm/dd/yyyy');
this.isInline = false;
this.isInput = this.element.is('input');
this.component = this.element.is('.date') ? this.element.find('.add-on') : false;
this.hasInput = this.component && this.element.find('input').length;
if(this.component && this.component.length === 0)
this.component = false;
if (this.isInput) { //single input
this.element.on({
focus: $.proxy(this.show, this),
keyup: $.proxy(this.update, this),
keydown: $.proxy(this.keydown, this)
});
} else if(this.component && this.hasInput) { //component: input + button
// For components that are not readonly, allow keyboard nav
this.element.find('input').on({
focus: $.proxy(this.show, this),
keyup: $.proxy(this.update, this),
keydown: $.proxy(this.keydown, this)
});
this.component.on('click', $.proxy(this.show, this));
} else if(this.element.is('div')) { //inline datepicker
this.isInline = true;
} else {
this.element.on('click', $.proxy(this.show, this));
}
this.picker = $(DPGlobal.template)
.appendTo(this.isInline ? this.element : 'body')
.on({
click: $.proxy(this.click, this),
mousedown: $.proxy(this.mousedown, this)
});
if(this.isInline) {
this.picker.addClass('datepicker-inline');
} else {
this.picker.addClass('dropdown-menu');
}
$(document).on('mousedown', function (e) {
// Clicked outside the datepicker, hide it
if ($(e.target).closest('.datepicker').length == 0) {
that.hide();
}
});
this.autoclose = false;
if ('autoclose' in options) {
this.autoclose = options.autoclose;
} else if ('dateAutoclose' in this.element.data()) {
this.autoclose = this.element.data('date-autoclose');
}
this.keyboardNavigation = true;
if ('keyboardNavigation' in options) {
this.keyboardNavigation = options.keyboardNavigation;
} else if ('dateKeyboardNavigation' in this.element.data()) {
this.keyboardNavigation = this.element.data('date-keyboard-navigation');
}
switch(options.startView || this.element.data('date-start-view')){
case 2:
case 'decade':
this.viewMode = this.startViewMode = 2;
break;
case 1:
case 'year':
this.viewMode = this.startViewMode = 1;
break;
case 0:
case 'month':
default:
this.viewMode = this.startViewMode = 0;
break;
}
this.todayBtn = (options.todayBtn||this.element.data('date-today-btn')||false);
this.todayHighlight = (options.todayHighlight||this.element.data('date-today-highlight')||false);
this.weekStart = ((options.weekStart||this.element.data('date-weekstart')||dates[this.language].weekStart||0) % 7);
this.weekEnd = ((this.weekStart + 6) % 7);
this.startDate = -Infinity;
this.endDate = Infinity;
this.setStartDate(options.startDate||this.element.data('date-startdate'));
this.setEndDate(options.endDate||this.element.data('date-enddate'));
this.fillDow();
this.fillMonths();
this.update();
this.showMode();
if(this.isInline) {
this.show();
}
};
Datepicker.prototype = {
constructor: Datepicker,
show: function(e) {
this.picker.show();
this.height = this.component ? this.component.outerHeight() : this.element.outerHeight();
this.update();
this.place();
$(window).on('resize', $.proxy(this.place, this));
if (e ) {
e.stopPropagation();
e.preventDefault();
}
this.element.trigger({
type: 'show',
date: this.date
});
},
hide: function(e){
if(this.isInline) return;
this.picker.hide();
$(window).off('resize', this.place);
this.viewMode = this.startViewMode;
this.showMode();
if (!this.isInput) {
$(document).off('mousedown', this.hide);
}
if (e && e.currentTarget.value)
this.setValue();
this.element.trigger({
type: 'hide',
date: this.date
});
},
getDate: function() {
var d = this.getUTCDate();
return new Date(d.getTime() + (d.getTimezoneOffset()*60000))
},
getUTCDate: function() {
return this.date;
},
setDate: function(d) {
this.setUTCDate(new Date(d.getTime() - (d.getTimezoneOffset()*60000)));
},
setUTCDate: function(d) {
this.date = d;
this.setValue();
},
setValue: function() {
var formatted = this.getFormattedDate();
if (!this.isInput) {
if (this.component){
this.element.find('input').prop('value', formatted);
}
this.element.data('date', formatted);
} else {
this.element.prop('value', formatted);
}
},
getFormattedDate: function(format) {
if(format == undefined) format = this.format;
return DPGlobal.formatDate(this.date, format, this.language);
},
setStartDate: function(startDate){
this.startDate = startDate||-Infinity;
if (this.startDate !== -Infinity) {
this.startDate = DPGlobal.parseDate(this.startDate, this.format, this.language);
}
this.update();
this.updateNavArrows();
},
setEndDate: function(endDate){
this.endDate = endDate||Infinity;
if (this.endDate !== Infinity) {
this.endDate = DPGlobal.parseDate(this.endDate, this.format, this.language);
}
this.update();
this.updateNavArrows();
},
place: function(){
if(this.isInline) return;
var zIndex = parseInt(this.element.parents().filter(function() {
return $(this).css('z-index') != 'auto';
}).first().css('z-index'))+10;
var offset = this.component ? this.component.offset() : this.element.offset();
this.picker.css({
top: offset.top + this.height,
left: offset.left,
zIndex: zIndex
});
},
update: function(){
var date, fromArgs = false;
if(arguments && arguments.length && (typeof arguments[0] === 'string' || arguments[0] instanceof Date)) {
date = arguments[0];
fromArgs = true;
} else {
date = this.isInput ? this.element.prop('value') : this.element.data('date') || this.element.find('input').prop('value');
}
this.date = DPGlobal.parseDate(date, this.format, this.language);
if(fromArgs) this.setValue();
if (this.date < this.startDate) {
this.viewDate = new Date(this.startDate);
} else if (this.date > this.endDate) {
this.viewDate = new Date(this.endDate);
} else {
this.viewDate = new Date(this.date);
}
this.fill();
},
fillDow: function(){
var dowCnt = this.weekStart;
var html = '