5118 lines
174 KiB
JavaScript
5118 lines
174 KiB
JavaScript
/*! 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 <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));
|
|
|
|
/**
|
|
* 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: <a data-source="{'a': 'b', 'c': 'd'}">
|
|
* 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 $('<div>').text(str).html();
|
|
},
|
|
|
|
/*
|
|
returns array items from sourceData having value property equal or inArray of 'value'
|
|
*/
|
|
itemsByValue: function(value, sourceData, valueProp) {
|
|
if(!sourceData || value === null) {
|
|
return [];
|
|
}
|
|
|
|
if (typeof(valueProp) !== "function") {
|
|
var idKey = valueProp || 'value';
|
|
valueProp = function (e) { return e[idKey]; };
|
|
}
|
|
|
|
var isValArray = $.isArray(value),
|
|
result = [],
|
|
that = this;
|
|
|
|
$.each(sourceData, function(i, o) {
|
|
if(o.children) {
|
|
result = result.concat(that.itemsByValue(value, o.children, valueProp));
|
|
} else {
|
|
/*jslint eqeq: true*/
|
|
if(isValArray) {
|
|
if($.grep(value, function(v){ return v == (o && typeof o === 'object' ? valueProp(o) : o); }).length) {
|
|
result.push(o);
|
|
}
|
|
} else {
|
|
var itemValue = (o && (typeof o === 'object')) ? valueProp(o) : o;
|
|
if(value == itemValue) {
|
|
result.push(o);
|
|
}
|
|
}
|
|
/*jslint eqeq: false*/
|
|
}
|
|
});
|
|
|
|
return result;
|
|
},
|
|
|
|
/*
|
|
Returns input by options: type, mode.
|
|
*/
|
|
createInput: function(options) {
|
|
var TypeConstructor, typeOptions, input,
|
|
type = options.type;
|
|
|
|
//`date` is some kind of virtual type that is transformed to one of exact types
|
|
//depending on mode and core lib
|
|
if(type === 'date') {
|
|
//inline
|
|
if(options.mode === 'inline') {
|
|
if($.fn.editabletypes.datefield) {
|
|
type = 'datefield';
|
|
} else if($.fn.editabletypes.dateuifield) {
|
|
type = 'dateuifield';
|
|
}
|
|
//popup
|
|
} else {
|
|
if($.fn.editabletypes.date) {
|
|
type = 'date';
|
|
} else if($.fn.editabletypes.dateui) {
|
|
type = 'dateui';
|
|
}
|
|
}
|
|
|
|
//if type still `date` and not exist in types, replace with `combodate` that is base input
|
|
if(type === 'date' && !$.fn.editabletypes.date) {
|
|
type = 'combodate';
|
|
}
|
|
}
|
|
|
|
//`datetime` should be datetimefield in 'inline' mode
|
|
if(type === 'datetime' && options.mode === 'inline') {
|
|
type = 'datetimefield';
|
|
}
|
|
|
|
//change wysihtml5 to textarea for jquery UI and plain versions
|
|
if(type === 'wysihtml5' && !$.fn.editabletypes[type]) {
|
|
type = 'textarea';
|
|
}
|
|
|
|
//create input of specified type. Input will be used for converting value, not in form
|
|
if(typeof $.fn.editabletypes[type] === 'function') {
|
|
TypeConstructor = $.fn.editabletypes[type];
|
|
typeOptions = this.sliceObj(options, this.objectKeys(TypeConstructor.defaults));
|
|
input = new TypeConstructor(typeOptions);
|
|
return input;
|
|
} else {
|
|
$.error('Unknown type: '+ type);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
//see http://stackoverflow.com/questions/7264899/detect-css-transitions-using-javascript-and-without-modernizr
|
|
supportsTransitions: function () {
|
|
var b = document.body || document.documentElement,
|
|
s = b.style,
|
|
p = 'transition',
|
|
v = ['Moz', 'Webkit', 'Khtml', 'O', 'ms'];
|
|
|
|
if(typeof s[p] === 'string') {
|
|
return true;
|
|
}
|
|
|
|
// Tests for vendor specific prop
|
|
p = p.charAt(0).toUpperCase() + p.substr(1);
|
|
for(var i=0; i<v.length; i++) {
|
|
if(typeof s[v[i] + p] === 'string') {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
};
|
|
}(window.jQuery));
|
|
|
|
/**
|
|
Attaches stand-alone container with editable-form to HTML element. Element is used only for positioning, value is not stored anywhere.<br>
|
|
This method applied internally in <code>$().editable()</code>. You should subscribe on it's events (save / cancel) to get profit of it.<br>
|
|
Final realization can be different: bootstrap-popover, jqueryui-tooltip, poshytip, inline-div. It depends on which js file you include.<br>
|
|
Applied as jQuery method.
|
|
|
|
@class editableContainer
|
|
@uses editableform
|
|
**/
|
|
(function ($) {
|
|
"use strict";
|
|
|
|
var Popup = function (element, options) {
|
|
this.init(element, options);
|
|
};
|
|
|
|
var Inline = function (element, options) {
|
|
this.init(element, options);
|
|
};
|
|
|
|
//methods
|
|
Popup.prototype = {
|
|
containerName: null, //method to call container on element
|
|
containerDataName: null, //object name in element's .data()
|
|
innerCss: null, //tbd in child class
|
|
containerClass: 'editable-container editable-popup', //css class applied to container element
|
|
defaults: {}, //container itself defaults
|
|
|
|
init: function(element, options) {
|
|
this.$element = $(element);
|
|
//since 1.4.1 container do not use data-* directly as they already merged into options.
|
|
this.options = $.extend({}, $.fn.editableContainer.defaults, options);
|
|
this.splitOptions();
|
|
|
|
//set scope of form callbacks to element
|
|
this.formOptions.scope = this.$element[0];
|
|
|
|
this.initContainer();
|
|
|
|
//flag to hide container, when saving value will finish
|
|
this.delayedHide = false;
|
|
|
|
//bind 'destroyed' listener to destroy container when element is removed from dom
|
|
this.$element.on('destroyed', $.proxy(function(){
|
|
this.destroy();
|
|
}, this));
|
|
|
|
//attach document handler to close containers on click / escape
|
|
if(!$(document).data('editable-handlers-attached')) {
|
|
//close all on escape
|
|
$(document).on('keyup.editable', function (e) {
|
|
if (e.which === 27) {
|
|
$('.editable-open').editableContainer('hide');
|
|
//todo: return focus on element
|
|
}
|
|
});
|
|
|
|
//close containers when click outside
|
|
//(mousedown could be better than click, it closes everything also on drag drop)
|
|
$(document).on('click.editable', function(e) {
|
|
var $target = $(e.target), i,
|
|
exclude_classes = ['.editable-container',
|
|
'.ui-datepicker-header',
|
|
'.datepicker', //in inline mode datepicker is rendered into body
|
|
'.modal-backdrop',
|
|
'.bootstrap-wysihtml5-insert-image-modal',
|
|
'.bootstrap-wysihtml5-insert-link-modal'
|
|
];
|
|
|
|
//check if element is detached. It occurs when clicking in bootstrap datepicker
|
|
if (!$.contains(document.documentElement, e.target)) {
|
|
return;
|
|
}
|
|
|
|
//for some reason FF 20 generates extra event (click) in select2 widget with e.target = document
|
|
//we need to filter it via construction below. See https://github.com/vitalets/x-editable/issues/199
|
|
//Possibly related to http://stackoverflow.com/questions/10119793/why-does-firefox-react-differently-from-webkit-and-ie-to-click-event-on-selec
|
|
if($target.is(document)) {
|
|
return;
|
|
}
|
|
|
|
//if click inside one of exclude classes --> no nothing
|
|
for(i=0; i<exclude_classes.length; i++) {
|
|
if($target.is(exclude_classes[i]) || $target.parents(exclude_classes[i]).length) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
//close all open containers (except one - target)
|
|
Popup.prototype.closeOthers(e.target);
|
|
});
|
|
|
|
$(document).data('editable-handlers-attached', true);
|
|
}
|
|
},
|
|
|
|
//split options on containerOptions and formOptions
|
|
splitOptions: function() {
|
|
this.containerOptions = {};
|
|
this.formOptions = {};
|
|
|
|
if(!$.fn[this.containerName]) {
|
|
throw new Error(this.containerName + ' not found. Have you included corresponding js file?');
|
|
}
|
|
|
|
//keys defined in container defaults go to container, others go to form
|
|
for(var k in this.options) {
|
|
if(k in this.defaults) {
|
|
this.containerOptions[k] = this.options[k];
|
|
} else {
|
|
this.formOptions[k] = this.options[k];
|
|
}
|
|
}
|
|
},
|
|
|
|
/*
|
|
Returns jquery object of container
|
|
@method tip()
|
|
*/
|
|
tip: function() {
|
|
return this.container() ? this.container().$tip : null;
|
|
},
|
|
|
|
/* returns container object */
|
|
container: function() {
|
|
var container;
|
|
//first, try get it by `containerDataName`
|
|
if(this.containerDataName) {
|
|
if(container = this.$element.data(this.containerDataName)) {
|
|
return container;
|
|
}
|
|
}
|
|
//second, try `containerName`
|
|
container = this.$element.data(this.containerName);
|
|
return container;
|
|
},
|
|
|
|
/* call native method of underlying container, e.g. this.$element.popover('method') */
|
|
call: function() {
|
|
this.$element[this.containerName].apply(this.$element, arguments);
|
|
},
|
|
|
|
initContainer: function(){
|
|
this.call(this.containerOptions);
|
|
},
|
|
|
|
renderForm: function() {
|
|
this.$form
|
|
.editableform(this.formOptions)
|
|
.on({
|
|
save: $.proxy(this.save, this), //click on submit button (value changed)
|
|
nochange: $.proxy(function(){ this.hide('nochange'); }, this), //click on submit button (value NOT changed)
|
|
cancel: $.proxy(function(){ this.hide('cancel'); }, this), //click on calcel button
|
|
show: $.proxy(function() {
|
|
if(this.delayedHide) {
|
|
this.hide(this.delayedHide.reason);
|
|
this.delayedHide = false;
|
|
} else {
|
|
this.setPosition();
|
|
}
|
|
}, this), //re-position container every time form is shown (occurs each time after loading state)
|
|
rendering: $.proxy(this.setPosition, this), //this allows to place container correctly when loading shown
|
|
resize: $.proxy(this.setPosition, this), //this allows to re-position container when form size is changed
|
|
rendered: $.proxy(function(){
|
|
/**
|
|
Fired when container is shown and form is rendered (for select will wait for loading dropdown options).
|
|
**Note:** Bootstrap popover has own `shown` 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 shown
|
|
@param {Object} event event object
|
|
@example
|
|
$('#username').on('shown', function(e, editable) {
|
|
editable.input.$input.val('overwriting value of input..');
|
|
});
|
|
**/
|
|
/*
|
|
TODO: added second param mainly to distinguish from bootstrap's shown event. It's a hotfix that will be solved in future versions via namespaced events.
|
|
*/
|
|
this.$element.triggerHandler('shown', $(this.options.scope).data('editable'));
|
|
}, this)
|
|
})
|
|
.editableform('render');
|
|
},
|
|
|
|
/**
|
|
Shows container with form
|
|
@method show()
|
|
@param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
|
|
**/
|
|
/* Note: poshytip owerwrites this method totally! */
|
|
show: function (closeAll) {
|
|
this.$element.addClass('editable-open');
|
|
if(closeAll !== false) {
|
|
//close all open containers (except this)
|
|
this.closeOthers(this.$element[0]);
|
|
}
|
|
|
|
//show container itself
|
|
this.innerShow();
|
|
this.tip().addClass(this.containerClass);
|
|
|
|
/*
|
|
Currently, form is re-rendered on every show.
|
|
The main reason is that we dont know, what will container do with content when closed:
|
|
remove(), detach() or just hide() - it depends on container.
|
|
|
|
Detaching form itself before hide and re-insert before show is good solution,
|
|
but visually it looks ugly --> container changes size before hide.
|
|
*/
|
|
|
|
//if form already exist - delete previous data
|
|
if(this.$form) {
|
|
//todo: destroy prev data!
|
|
//this.$form.destroy();
|
|
}
|
|
|
|
this.$form = $('<div>');
|
|
|
|
//insert form into container body
|
|
if(this.tip().is(this.innerCss)) {
|
|
//for inline container
|
|
this.tip().append(this.$form);
|
|
} else {
|
|
this.tip().find(this.innerCss).append(this.$form);
|
|
}
|
|
|
|
//render form
|
|
this.renderForm();
|
|
},
|
|
|
|
/**
|
|
Hides container with form
|
|
@method hide()
|
|
@param {string} reason Reason caused hiding. Can be <code>save|cancel|onblur|nochange|undefined (=manual)</code>
|
|
**/
|
|
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 <code>save|cancel|onblur|nochange|manual</code>
|
|
@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 <code>$(this).data('editableContainer')</code> 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 <code>top|right|bottom|left</code>. 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 <code>cancel|submit|ignore</code>.
|
|
Setting <code>ignore</code> 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 <span> element
|
|
this.$tip = $('<span></span>');
|
|
|
|
//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 <br>, <p></p>, <img>
|
|
if(typeof(this.input.isEmpty) === 'function') {
|
|
this.isEmpty = this.input.isEmpty(this.$element);
|
|
} else {
|
|
this.isEmpty = $.trim(this.$element.html()) === '';
|
|
}
|
|
}
|
|
|
|
//emptytext shown only for enabled
|
|
if(!this.options.disabled) {
|
|
if (this.isEmpty) {
|
|
this.$element.html(this.options.emptytext);
|
|
if(this.options.emptyclass) {
|
|
this.$element.addClass(this.options.emptyclass);
|
|
}
|
|
} else if(this.options.emptyclass) {
|
|
this.$element.removeClass(this.options.emptyclass);
|
|
}
|
|
} else {
|
|
//below required if element disable property was changed
|
|
if(this.isEmpty) {
|
|
this.$element.empty();
|
|
if(this.options.emptyclass) {
|
|
this.$element.removeClass(this.options.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,
|
|
input: this.input //pass input to form (as it is already created)
|
|
});
|
|
this.$element.editableContainer(containerOptions);
|
|
//listen `save` event
|
|
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) {
|
|
//mark element with unsaved class if needed
|
|
if(this.options.unsavedclass) {
|
|
/*
|
|
Add unsaved css to element if:
|
|
- url is not user's function
|
|
- value was not sent to server
|
|
- params.response === undefined, that means data was not sent
|
|
- value changed
|
|
*/
|
|
var sent = false;
|
|
sent = sent || typeof this.options.url === 'function';
|
|
sent = sent || this.options.display === false;
|
|
sent = sent || params.response !== undefined;
|
|
sent = sent || (this.options.savenochange && this.input.value2str(this.value) !== this.input.value2str(params.newValue));
|
|
|
|
if(sent) {
|
|
this.$element.removeClass(this.options.unsavedclass);
|
|
} else {
|
|
this.$element.addClass(this.options.unsavedclass);
|
|
}
|
|
}
|
|
|
|
//highlight when saving
|
|
if(this.options.highlight) {
|
|
var $e = this.$element,
|
|
bgColor = $e.css('background-color');
|
|
|
|
$e.css('background-color', this.options.highlight);
|
|
setTimeout(function(){
|
|
if(bgColor === 'transparent') {
|
|
bgColor = '';
|
|
}
|
|
$e.css('background-color', bgColor);
|
|
$e.addClass('editable-bg-transition');
|
|
setTimeout(function(){
|
|
$e.removeClass('editable-bg-transition');
|
|
}, 1700);
|
|
}, 10);
|
|
}
|
|
|
|
//set new value
|
|
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();
|
|
}
|
|
},
|
|
|
|
/**
|
|
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 <a href="#newrecord">creating new records</a> 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 <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.
|
|
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
|
|
<a id="price" data-type="text" data-value="100"></a>
|
|
<script>
|
|
$('#price').editable({
|
|
...
|
|
display: function(value) {
|
|
$(this).text(value + '$');
|
|
}
|
|
})
|
|
</script>
|
|
|
|
@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
|
|
<div id="user">
|
|
<!-- empty -->
|
|
<a href="#" data-name="username" data-type="text" class="editable-click editable-empty" data-value="" title="Username">Empty</a>
|
|
<!-- non-empty -->
|
|
<a href="#" data-name="group" data-type="select" data-source="/groups" data-value="1" class="editable-click" title="Group">Operator</a>
|
|
</div>
|
|
|
|
<script>
|
|
$('#user').editable({
|
|
selector: 'a',
|
|
url: '/post',
|
|
pk: 1
|
|
});
|
|
</script>
|
|
**/
|
|
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 $('<div>').html(html).text();
|
|
},
|
|
|
|
/**
|
|
Converts value to string (for internal compare). For submitting to server used value2submit().
|
|
|
|
@method value2str(value)
|
|
@param {mixed} value
|
|
@returns {string}
|
|
**/
|
|
value2str: function(value) {
|
|
return value;
|
|
},
|
|
|
|
/**
|
|
Converts string received from server into value. Usually from `data-value` attribute.
|
|
|
|
@method str2value(str)
|
|
@param {string} str
|
|
@returns {mixed}
|
|
**/
|
|
str2value: function(str) {
|
|
return str;
|
|
},
|
|
|
|
/**
|
|
Converts value for submitting to server. Result can be string or object.
|
|
|
|
@method value2submit(value)
|
|
@param {mixed} value
|
|
@returns {mixed}
|
|
**/
|
|
value2submit: function(value) {
|
|
return value;
|
|
},
|
|
|
|
/**
|
|
Sets value of input.
|
|
|
|
@method value2input(value)
|
|
@param {mixed} value
|
|
**/
|
|
value2input: function(value) {
|
|
this.$input.val(value);
|
|
},
|
|
|
|
/**
|
|
Returns value of input. Value can be object (e.g. datepicker)
|
|
|
|
@method input2value()
|
|
**/
|
|
input2value: function() {
|
|
return this.$input.val();
|
|
},
|
|
|
|
/**
|
|
Activates input. For text it sets focus.
|
|
|
|
@method activate()
|
|
**/
|
|
activate: function() {
|
|
if(this.$input.is(':visible')) {
|
|
this.$input.focus();
|
|
}
|
|
},
|
|
|
|
/**
|
|
Creates input.
|
|
|
|
@method clear()
|
|
**/
|
|
clear: function() {
|
|
this.$input.val(null);
|
|
},
|
|
|
|
/**
|
|
method to escape html.
|
|
**/
|
|
escape: function(str) {
|
|
return $('<div>').text(str).html();
|
|
},
|
|
|
|
/**
|
|
attach handler to automatically submit form when value changed (useful when buttons not shown)
|
|
**/
|
|
autosubmit: function() {
|
|
|
|
},
|
|
|
|
/**
|
|
Additional actions when destroying element
|
|
**/
|
|
destroy: function() {
|
|
},
|
|
|
|
// -------- helper functions --------
|
|
setClass: function() {
|
|
if(this.options.inputclass) {
|
|
this.$input.addClass(this.options.inputclass);
|
|
}
|
|
},
|
|
|
|
setAttr: function(attr) {
|
|
if (this.options[attr] !== undefined && this.options[attr] !== null) {
|
|
this.$input.attr(attr, this.options[attr]);
|
|
}
|
|
},
|
|
|
|
option: function(key, value) {
|
|
this.options[key] = value;
|
|
}
|
|
|
|
};
|
|
|
|
AbstractInput.defaults = {
|
|
/**
|
|
HTML template of input. Normally you should not change it.
|
|
|
|
@property tpl
|
|
@type string
|
|
@default ''
|
|
**/
|
|
tpl: '',
|
|
/**
|
|
CSS class automatically applied to input
|
|
|
|
@property inputclass
|
|
@type string
|
|
@default null
|
|
**/
|
|
inputclass: null,
|
|
|
|
/**
|
|
If `true` - html will be escaped in content of element via $.text() method.
|
|
If `false` - html will not be escaped, $.html() used.
|
|
When you use own `display` function, this option obviosly has no effect.
|
|
|
|
@property escape
|
|
@type boolean
|
|
@since 1.5.0
|
|
@default true
|
|
**/
|
|
escape: true,
|
|
|
|
//scope for external methods (e.g. source defined as function)
|
|
//for internal use only
|
|
scope: null,
|
|
|
|
//need to re-declare showbuttons here to get it's value from common config (passed only options existing in defaults)
|
|
showbuttons: true
|
|
};
|
|
|
|
$.extend($.fn.editabletypes, {abstractinput: AbstractInput});
|
|
|
|
}(window.jQuery));
|
|
|
|
/**
|
|
List - abstract class for inputs that have source option loaded from js array or via ajax
|
|
|
|
@class list
|
|
@extends abstractinput
|
|
**/
|
|
(function ($) {
|
|
"use strict";
|
|
|
|
var List = function (options) {
|
|
|
|
};
|
|
|
|
$.fn.editableutils.inherit(List, $.fn.editabletypes.abstractinput);
|
|
|
|
$.extend(List.prototype, {
|
|
render: function () {
|
|
var deferred = $.Deferred();
|
|
|
|
this.error = null;
|
|
this.onSourceReady(function () {
|
|
this.renderList();
|
|
deferred.resolve();
|
|
}, function () {
|
|
this.error = this.options.sourceError;
|
|
deferred.resolve();
|
|
});
|
|
|
|
return deferred.promise();
|
|
},
|
|
|
|
html2value: function (html) {
|
|
return null; //can't set value by text
|
|
},
|
|
|
|
value2html: function (value, element, display, response) {
|
|
var deferred = $.Deferred(),
|
|
success = function () {
|
|
if(typeof display === 'function') {
|
|
//custom display method
|
|
display.call(element, value, this.sourceData, response);
|
|
} else {
|
|
this.value2htmlFinal(value, element);
|
|
}
|
|
deferred.resolve();
|
|
};
|
|
|
|
//for null value just call success without loading source
|
|
if(value === null) {
|
|
success.call(this);
|
|
} else {
|
|
this.onSourceReady(success, function () { deferred.resolve(); });
|
|
}
|
|
|
|
return deferred.promise();
|
|
},
|
|
|
|
// ------------- additional functions ------------
|
|
|
|
onSourceReady: function (success, error) {
|
|
//run source if it function
|
|
var source;
|
|
if ($.isFunction(this.options.source)) {
|
|
source = this.options.source.call(this.options.scope);
|
|
this.sourceData = null;
|
|
//note: if function returns the same source as URL - sourceData will be taken from cahce and no extra request performed
|
|
} else {
|
|
source = this.options.source;
|
|
}
|
|
|
|
//if allready loaded just call success
|
|
if(this.options.sourceCache && $.isArray(this.sourceData)) {
|
|
success.call(this);
|
|
return;
|
|
}
|
|
|
|
//try parse json in single quotes (for double quotes jquery does automatically)
|
|
try {
|
|
source = $.fn.editableutils.tryParseJson(source, false);
|
|
} catch (e) {
|
|
error.call(this);
|
|
return;
|
|
}
|
|
|
|
//loading from url
|
|
if (typeof source === 'string') {
|
|
//try to get sourceData from cache
|
|
if(this.options.sourceCache) {
|
|
var cacheID = source,
|
|
cache;
|
|
|
|
if (!$(document).data(cacheID)) {
|
|
$(document).data(cacheID, {});
|
|
}
|
|
cache = $(document).data(cacheID);
|
|
|
|
//check for cached data
|
|
if (cache.loading === false && cache.sourceData) { //take source from cache
|
|
this.sourceData = cache.sourceData;
|
|
this.doPrepend();
|
|
success.call(this);
|
|
return;
|
|
} else if (cache.loading === true) { //cache is loading, put callback in stack to be called later
|
|
cache.callbacks.push($.proxy(function () {
|
|
this.sourceData = cache.sourceData;
|
|
this.doPrepend();
|
|
success.call(this);
|
|
}, this));
|
|
|
|
//also collecting error callbacks
|
|
cache.err_callbacks.push($.proxy(error, this));
|
|
return;
|
|
} else { //no cache yet, activate it
|
|
cache.loading = true;
|
|
cache.callbacks = [];
|
|
cache.err_callbacks = [];
|
|
}
|
|
}
|
|
|
|
//ajaxOptions for source. Can be overwritten bt options.sourceOptions
|
|
var ajaxOptions = $.extend({
|
|
url: source,
|
|
type: 'get',
|
|
cache: false,
|
|
dataType: 'json',
|
|
success: $.proxy(function (data) {
|
|
if(cache) {
|
|
cache.loading = false;
|
|
}
|
|
this.sourceData = this.makeArray(data);
|
|
if($.isArray(this.sourceData)) {
|
|
if(cache) {
|
|
//store result in cache
|
|
cache.sourceData = this.sourceData;
|
|
//run success callbacks for other fields waiting for this source
|
|
$.each(cache.callbacks, function () { this.call(); });
|
|
}
|
|
this.doPrepend();
|
|
success.call(this);
|
|
} else {
|
|
error.call(this);
|
|
if(cache) {
|
|
//run error callbacks for other fields waiting for this source
|
|
$.each(cache.err_callbacks, function () { this.call(); });
|
|
}
|
|
}
|
|
}, this),
|
|
error: $.proxy(function () {
|
|
error.call(this);
|
|
if(cache) {
|
|
cache.loading = false;
|
|
//run error callbacks for other fields
|
|
$.each(cache.err_callbacks, function () { this.call(); });
|
|
}
|
|
}, this)
|
|
}, this.options.sourceOptions);
|
|
|
|
//loading sourceData from server
|
|
$.ajax(ajaxOptions);
|
|
|
|
} else { //options as json/array
|
|
this.sourceData = this.makeArray(source);
|
|
|
|
if($.isArray(this.sourceData)) {
|
|
this.doPrepend();
|
|
success.call(this);
|
|
} else {
|
|
error.call(this);
|
|
}
|
|
}
|
|
},
|
|
|
|
doPrepend: function () {
|
|
if(this.options.prepend === null || this.options.prepend === undefined) {
|
|
return;
|
|
}
|
|
|
|
if(!$.isArray(this.prependData)) {
|
|
//run prepend if it is function (once)
|
|
if ($.isFunction(this.options.prepend)) {
|
|
this.options.prepend = this.options.prepend.call(this.options.scope);
|
|
}
|
|
|
|
//try parse json in single quotes
|
|
this.options.prepend = $.fn.editableutils.tryParseJson(this.options.prepend, true);
|
|
|
|
//convert prepend from string to object
|
|
if (typeof this.options.prepend === 'string') {
|
|
this.options.prepend = {'': this.options.prepend};
|
|
}
|
|
|
|
this.prependData = this.makeArray(this.options.prepend);
|
|
}
|
|
|
|
if($.isArray(this.prependData) && $.isArray(this.sourceData)) {
|
|
this.sourceData = this.prependData.concat(this.sourceData);
|
|
}
|
|
},
|
|
|
|
/*
|
|
renders input list
|
|
*/
|
|
renderList: function() {
|
|
// this method should be overwritten in child class
|
|
},
|
|
|
|
/*
|
|
set element's html by value
|
|
*/
|
|
value2htmlFinal: function(value, element) {
|
|
// this method should be overwritten in child class
|
|
},
|
|
|
|
/**
|
|
* convert data to array suitable for sourceData, e.g. [{value: 1, text: 'abc'}, {...}]
|
|
*/
|
|
makeArray: function(data) {
|
|
var count, obj, result = [], item, iterateItem;
|
|
if(!data || typeof data === 'string') {
|
|
return null;
|
|
}
|
|
|
|
if($.isArray(data)) { //array
|
|
/*
|
|
function to iterate inside item of array if item is object.
|
|
Caclulates count of keys in item and store in obj.
|
|
*/
|
|
iterateItem = function (k, v) {
|
|
obj = {value: k, text: v};
|
|
if(count++ >= 2) {
|
|
return false;// exit from `each` if item has more than one key.
|
|
}
|
|
};
|
|
|
|
for(var i = 0; i < data.length; i++) {
|
|
item = data[i];
|
|
if(typeof item === 'object') {
|
|
count = 0; //count of keys inside item
|
|
$.each(item, iterateItem);
|
|
//case: [{val1: 'text1'}, {val2: 'text2} ...]
|
|
if(count === 1) {
|
|
result.push(obj);
|
|
//case: [{value: 1, text: 'text1'}, {value: 2, text: 'text2'}, ...]
|
|
} else if(count > 1) {
|
|
//removed check of existance: item.hasOwnProperty('value') && item.hasOwnProperty('text')
|
|
if(item.children) {
|
|
item.children = this.makeArray(item.children);
|
|
}
|
|
result.push(item);
|
|
}
|
|
} else {
|
|
//case: ['text1', 'text2' ...]
|
|
result.push({value: item, text: item});
|
|
}
|
|
}
|
|
} else { //case: {val1: 'text1', val2: 'text2, ...}
|
|
$.each(data, function (k, v) {
|
|
result.push({value: k, text: v});
|
|
});
|
|
}
|
|
return result;
|
|
},
|
|
|
|
option: function(key, value) {
|
|
this.options[key] = value;
|
|
if(key === 'source') {
|
|
this.sourceData = null;
|
|
}
|
|
if(key === 'prepend') {
|
|
this.prependData = null;
|
|
}
|
|
}
|
|
|
|
});
|
|
|
|
List.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
|
|
/**
|
|
Source data for list.
|
|
If **array** - it should be in format: `[{value: 1, text: "text1"}, {value: 2, text: "text2"}, ...]`
|
|
For compability, object format is also supported: `{"1": "text1", "2": "text2" ...}` but it does not guarantee elements order.
|
|
|
|
If **string** - considered ajax url to load items. In that case results will be cached for fields with the same source and name. See also `sourceCache` option.
|
|
|
|
If **function**, it should return data in format above (since 1.4.0).
|
|
|
|
Since 1.4.1 key `children` supported to render OPTGROUP (for **select** input only).
|
|
`[{text: "group1", children: [{value: 1, text: "text1"}, {value: 2, text: "text2"}]}, ...]`
|
|
|
|
|
|
@property source
|
|
@type string | array | object | function
|
|
@default null
|
|
**/
|
|
source: null,
|
|
/**
|
|
Data automatically prepended to the beginning of dropdown list.
|
|
|
|
@property prepend
|
|
@type string | array | object | function
|
|
@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',
|
|
/**
|
|
if <code>true</code> 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
|
|
<a href="#" id="username" data-type="text" data-pk="1">awesome</a>
|
|
<script>
|
|
$(function(){
|
|
$('#username').editable({
|
|
url: '/post',
|
|
title: 'Enter username'
|
|
});
|
|
});
|
|
</script>
|
|
**/
|
|
(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 = $('<span class="editable-clear-x"></span>');
|
|
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 <input type="text">
|
|
**/
|
|
tpl: '<input type="text">',
|
|
/**
|
|
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
|
|
<a href="#" id="comments" data-type="textarea" data-pk="1">awesome comment!</a>
|
|
<script>
|
|
$(function(){
|
|
$('#comments').editable({
|
|
url: '/post',
|
|
title: 'Enter comments',
|
|
rows: 10
|
|
});
|
|
});
|
|
</script>
|
|
**/
|
|
(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] = $('<div>').text(lines[i]).html();
|
|
}
|
|
html = lines.join('<br>');
|
|
}
|
|
$(element).html(html);
|
|
},
|
|
|
|
html2value: function(html) {
|
|
if(!html) {
|
|
return '';
|
|
}
|
|
|
|
var regex = new RegExp(String.fromCharCode(10), 'g');
|
|
var lines = html.split(/<br\s*\/?>/i);
|
|
for (var i = 0; i < lines.length; i++) {
|
|
var text = $('<div>').html(lines[i]).text();
|
|
|
|
// Remove newline characters (\n) to avoid them being converted by value2html() method
|
|
// thus adding extra <br> tags
|
|
text = text.replace(regex, '');
|
|
|
|
lines[i] = text;
|
|
}
|
|
return lines.join("\n");
|
|
},
|
|
*/
|
|
activate: function() {
|
|
$.fn.editabletypes.text.prototype.activate.call(this);
|
|
}
|
|
});
|
|
|
|
Textarea.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
|
|
/**
|
|
@property tpl
|
|
@default <textarea></textarea>
|
|
**/
|
|
tpl:'<textarea></textarea>',
|
|
/**
|
|
@property inputclass
|
|
@default input-large
|
|
**/
|
|
inputclass: 'input-large',
|
|
/**
|
|
Placeholder attribute of input. Shown when input is empty.
|
|
|
|
@property placeholder
|
|
@type string
|
|
@default null
|
|
**/
|
|
placeholder: null,
|
|
/**
|
|
Number of rows in textarea
|
|
|
|
@property rows
|
|
@type integer
|
|
@default 7
|
|
**/
|
|
rows: 7
|
|
});
|
|
|
|
$.fn.editabletypes.textarea = Textarea;
|
|
|
|
}(window.jQuery));
|
|
|
|
/**
|
|
Select (dropdown)
|
|
|
|
@class select
|
|
@extends list
|
|
@final
|
|
@example
|
|
<a href="#" id="status" data-type="select" data-pk="1" data-url="/post" data-title="Select status"></a>
|
|
<script>
|
|
$(function(){
|
|
$('#status').editable({
|
|
value: 2,
|
|
source: [
|
|
{value: 1, text: 'Active'},
|
|
{value: 2, text: 'Blocked'},
|
|
{value: 3, text: 'Deleted'}
|
|
]
|
|
});
|
|
});
|
|
</script>
|
|
**/
|
|
(function ($) {
|
|
"use strict";
|
|
|
|
var Select = function (options) {
|
|
this.init('select', options, Select.defaults);
|
|
};
|
|
|
|
$.fn.editableutils.inherit(Select, $.fn.editabletypes.list);
|
|
|
|
$.extend(Select.prototype, {
|
|
renderList: function() {
|
|
this.$input.empty();
|
|
|
|
var fillItems = function($el, data) {
|
|
var attr;
|
|
if($.isArray(data)) {
|
|
for(var i=0; i<data.length; i++) {
|
|
attr = {};
|
|
if(data[i].children) {
|
|
attr.label = data[i].text;
|
|
$el.append(fillItems($('<optgroup>', attr), data[i].children));
|
|
} else {
|
|
attr.value = data[i].value;
|
|
if(data[i].disabled) {
|
|
attr.disabled = true;
|
|
}
|
|
$el.append($('<option>', attr).text(data[i].text));
|
|
}
|
|
}
|
|
}
|
|
return $el;
|
|
};
|
|
|
|
fillItems(this.$input, this.sourceData);
|
|
|
|
this.setClass();
|
|
|
|
//enter submit
|
|
this.$input.on('keydown.editable', function (e) {
|
|
if (e.which === 13) {
|
|
$(this).closest('form').submit();
|
|
}
|
|
});
|
|
},
|
|
|
|
value2htmlFinal: function(value, element) {
|
|
var text = '',
|
|
items = $.fn.editableutils.itemsByValue(value, this.sourceData);
|
|
|
|
if(items.length) {
|
|
text = items[0].text;
|
|
}
|
|
|
|
//$(element).text(text);
|
|
$.fn.editabletypes.abstractinput.prototype.value2html.call(this, text, element);
|
|
},
|
|
|
|
autosubmit: function() {
|
|
this.$input.off('keydown.editable').on('change.editable', function(){
|
|
$(this).closest('form').submit();
|
|
});
|
|
}
|
|
});
|
|
|
|
Select.defaults = $.extend({}, $.fn.editabletypes.list.defaults, {
|
|
/**
|
|
@property tpl
|
|
@default <select></select>
|
|
**/
|
|
tpl:'<select></select>'
|
|
});
|
|
|
|
$.fn.editabletypes.select = Select;
|
|
|
|
}(window.jQuery));
|
|
|
|
/**
|
|
List of checkboxes.
|
|
Internally value stored as javascript array of values.
|
|
|
|
@class checklist
|
|
@extends list
|
|
@final
|
|
@example
|
|
<a href="#" id="options" data-type="checklist" data-pk="1" data-url="/post" data-title="Select options"></a>
|
|
<script>
|
|
$(function(){
|
|
$('#options').editable({
|
|
value: [2, 3],
|
|
source: [
|
|
{value: 1, text: 'option1'},
|
|
{value: 2, text: 'option2'},
|
|
{value: 3, text: 'option3'}
|
|
]
|
|
});
|
|
});
|
|
</script>
|
|
**/
|
|
(function ($) {
|
|
"use strict";
|
|
|
|
var Checklist = function (options) {
|
|
this.init('checklist', options, Checklist.defaults);
|
|
};
|
|
|
|
$.fn.editableutils.inherit(Checklist, $.fn.editabletypes.list);
|
|
|
|
$.extend(Checklist.prototype, {
|
|
renderList: function() {
|
|
var $label, $div;
|
|
|
|
this.$tpl.empty();
|
|
|
|
if(!$.isArray(this.sourceData)) {
|
|
return;
|
|
}
|
|
|
|
for(var i=0; i<this.sourceData.length; i++) {
|
|
$label = $('<label>').append($('<input>', {
|
|
type: 'checkbox',
|
|
value: this.sourceData[i].value
|
|
}))
|
|
.append($('<span>').text(' '+this.sourceData[i].text));
|
|
|
|
$('<div>').append($label).appendTo(this.$tpl);
|
|
}
|
|
|
|
this.$input = this.$tpl.find('input[type="checkbox"]');
|
|
this.setClass();
|
|
},
|
|
|
|
value2str: function(value) {
|
|
return $.isArray(value) ? value.sort().join($.trim(this.options.separator)) : '';
|
|
},
|
|
|
|
//parse separated string
|
|
str2value: function(str) {
|
|
var reg, value = null;
|
|
if(typeof str === 'string' && str.length) {
|
|
reg = new RegExp('\\s*'+$.trim(this.options.separator)+'\\s*');
|
|
value = str.split(reg);
|
|
} else if($.isArray(str)) {
|
|
value = str;
|
|
} else {
|
|
value = [str];
|
|
}
|
|
return value;
|
|
},
|
|
|
|
//set checked on required checkboxes
|
|
value2input: function(value) {
|
|
this.$input.prop('checked', false);
|
|
if($.isArray(value) && value.length) {
|
|
this.$input.each(function(i, el) {
|
|
var $el = $(el);
|
|
// cannot use $.inArray as it performs strict comparison
|
|
$.each(value, function(j, val){
|
|
/*jslint eqeq: true*/
|
|
if($el.val() == val) {
|
|
/*jslint eqeq: false*/
|
|
$el.prop('checked', true);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
},
|
|
|
|
input2value: function() {
|
|
var checked = [];
|
|
this.$input.filter(':checked').each(function(i, el) {
|
|
checked.push($(el).val());
|
|
});
|
|
return checked;
|
|
},
|
|
|
|
//collect text of checked boxes
|
|
value2htmlFinal: function(value, element) {
|
|
var html = [],
|
|
checked = $.fn.editableutils.itemsByValue(value, this.sourceData),
|
|
escape = this.options.escape;
|
|
|
|
if(checked.length) {
|
|
$.each(checked, function(i, v) {
|
|
var text = escape ? $.fn.editableutils.escape(v.text) : v.text;
|
|
html.push(text);
|
|
});
|
|
$(element).html(html.join('<br>'));
|
|
} else {
|
|
$(element).empty();
|
|
}
|
|
},
|
|
|
|
activate: function() {
|
|
this.$input.first().focus();
|
|
},
|
|
|
|
autosubmit: function() {
|
|
this.$input.on('keydown', function(e){
|
|
if (e.which === 13) {
|
|
$(this).closest('form').submit();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
Checklist.defaults = $.extend({}, $.fn.editabletypes.list.defaults, {
|
|
/**
|
|
@property tpl
|
|
@default <div></div>
|
|
**/
|
|
tpl:'<div class="editable-checklist"></div>',
|
|
|
|
/**
|
|
@property inputclass
|
|
@type string
|
|
@default null
|
|
**/
|
|
inputclass: null,
|
|
|
|
/**
|
|
Separator of values when reading from `data-value` attribute
|
|
|
|
@property separator
|
|
@type string
|
|
@default ','
|
|
**/
|
|
separator: ','
|
|
});
|
|
|
|
$.fn.editabletypes.checklist = Checklist;
|
|
|
|
}(window.jQuery));
|
|
|
|
/**
|
|
HTML5 input types.
|
|
Following types are supported:
|
|
|
|
* password
|
|
* email
|
|
* url
|
|
* tel
|
|
* number
|
|
* range
|
|
* time
|
|
|
|
Learn more about html5 inputs:
|
|
http://www.w3.org/wiki/HTML5_form_additions
|
|
To check browser compatibility please see:
|
|
https://developer.mozilla.org/en-US/docs/HTML/Element/Input
|
|
|
|
@class html5types
|
|
@extends text
|
|
@final
|
|
@since 1.3.0
|
|
@example
|
|
<a href="#" id="email" data-type="email" data-pk="1">admin@example.com</a>
|
|
<script>
|
|
$(function(){
|
|
$('#email').editable({
|
|
url: '/post',
|
|
title: 'Enter email'
|
|
});
|
|
});
|
|
</script>
|
|
**/
|
|
|
|
/**
|
|
@property tpl
|
|
@default depends on type
|
|
**/
|
|
|
|
/*
|
|
Password
|
|
*/
|
|
(function ($) {
|
|
"use strict";
|
|
|
|
var Password = function (options) {
|
|
this.init('password', options, Password.defaults);
|
|
};
|
|
$.fn.editableutils.inherit(Password, $.fn.editabletypes.text);
|
|
$.extend(Password.prototype, {
|
|
//do not display password, show '[hidden]' instead
|
|
value2html: function(value, element) {
|
|
if(value) {
|
|
$(element).text('[hidden]');
|
|
} else {
|
|
$(element).empty();
|
|
}
|
|
},
|
|
//as password not displayed, should not set value by html
|
|
html2value: function(html) {
|
|
return null;
|
|
}
|
|
});
|
|
Password.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
|
|
tpl: '<input type="password">'
|
|
});
|
|
$.fn.editabletypes.password = Password;
|
|
}(window.jQuery));
|
|
|
|
|
|
/*
|
|
Email
|
|
*/
|
|
(function ($) {
|
|
"use strict";
|
|
|
|
var Email = function (options) {
|
|
this.init('email', options, Email.defaults);
|
|
};
|
|
$.fn.editableutils.inherit(Email, $.fn.editabletypes.text);
|
|
Email.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
|
|
tpl: '<input type="email">'
|
|
});
|
|
$.fn.editabletypes.email = Email;
|
|
}(window.jQuery));
|
|
|
|
|
|
/*
|
|
Url
|
|
*/
|
|
(function ($) {
|
|
"use strict";
|
|
|
|
var Url = function (options) {
|
|
this.init('url', options, Url.defaults);
|
|
};
|
|
$.fn.editableutils.inherit(Url, $.fn.editabletypes.text);
|
|
Url.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
|
|
tpl: '<input type="url">'
|
|
});
|
|
$.fn.editabletypes.url = Url;
|
|
}(window.jQuery));
|
|
|
|
|
|
/*
|
|
Tel
|
|
*/
|
|
(function ($) {
|
|
"use strict";
|
|
|
|
var Tel = function (options) {
|
|
this.init('tel', options, Tel.defaults);
|
|
};
|
|
$.fn.editableutils.inherit(Tel, $.fn.editabletypes.text);
|
|
Tel.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
|
|
tpl: '<input type="tel">'
|
|
});
|
|
$.fn.editabletypes.tel = Tel;
|
|
}(window.jQuery));
|
|
|
|
|
|
/*
|
|
Number
|
|
*/
|
|
(function ($) {
|
|
"use strict";
|
|
|
|
var NumberInput = function (options) {
|
|
this.init('number', options, NumberInput.defaults);
|
|
};
|
|
$.fn.editableutils.inherit(NumberInput, $.fn.editabletypes.text);
|
|
$.extend(NumberInput.prototype, {
|
|
render: function () {
|
|
NumberInput.superclass.render.call(this);
|
|
this.setAttr('min');
|
|
this.setAttr('max');
|
|
this.setAttr('step');
|
|
},
|
|
postrender: function() {
|
|
if(this.$clear) {
|
|
//increase right ffset for up/down arrows
|
|
this.$clear.css({right: 24});
|
|
/*
|
|
//can position clear button only here, when form is shown and height can be calculated
|
|
var h = this.$input.outerHeight(true) || 20,
|
|
delta = (h - this.$clear.height()) / 2;
|
|
|
|
//add 12px to offset right for up/down arrows
|
|
this.$clear.css({top: delta, right: delta + 16});
|
|
*/
|
|
}
|
|
}
|
|
});
|
|
NumberInput.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
|
|
tpl: '<input type="number">',
|
|
inputclass: 'input-mini',
|
|
min: null,
|
|
max: null,
|
|
step: null
|
|
});
|
|
$.fn.editabletypes.number = NumberInput;
|
|
}(window.jQuery));
|
|
|
|
|
|
/*
|
|
Range (inherit from number)
|
|
*/
|
|
(function ($) {
|
|
"use strict";
|
|
|
|
var Range = function (options) {
|
|
this.init('range', options, Range.defaults);
|
|
};
|
|
$.fn.editableutils.inherit(Range, $.fn.editabletypes.number);
|
|
$.extend(Range.prototype, {
|
|
render: function () {
|
|
this.$input = this.$tpl.filter('input');
|
|
|
|
this.setClass();
|
|
this.setAttr('min');
|
|
this.setAttr('max');
|
|
this.setAttr('step');
|
|
|
|
this.$input.on('input', function(){
|
|
$(this).siblings('output').text($(this).val());
|
|
});
|
|
},
|
|
activate: function() {
|
|
this.$input.focus();
|
|
}
|
|
});
|
|
Range.defaults = $.extend({}, $.fn.editabletypes.number.defaults, {
|
|
tpl: '<input type="range"><output style="width: 30px; display: inline-block"></output>',
|
|
inputclass: 'input-medium'
|
|
});
|
|
$.fn.editabletypes.range = Range;
|
|
}(window.jQuery));
|
|
|
|
/*
|
|
Time
|
|
*/
|
|
(function ($) {
|
|
"use strict";
|
|
|
|
var Time = function (options) {
|
|
this.init('time', options, Time.defaults);
|
|
};
|
|
//inherit from abstract, as inheritance from text gives selection error.
|
|
$.fn.editableutils.inherit(Time, $.fn.editabletypes.abstractinput);
|
|
$.extend(Time.prototype, {
|
|
render: function() {
|
|
this.setClass();
|
|
}
|
|
});
|
|
Time.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
|
|
tpl: '<input type="time">'
|
|
});
|
|
$.fn.editabletypes.time = Time;
|
|
}(window.jQuery));
|
|
|
|
/**
|
|
Select2 input. Based on amazing work of Igor Vaynberg https://github.com/ivaynberg/select2.
|
|
Please see [original select2 docs](http://ivaynberg.github.com/select2) for detailed description and options.
|
|
|
|
You should manually download and include select2 distributive:
|
|
|
|
<link href="select2/select2.css" rel="stylesheet" type="text/css"></link>
|
|
<script src="select2/select2.js"></script>
|
|
|
|
To make it **bootstrap-styled** you can use css from [here](https://github.com/t0m/select2-bootstrap-css):
|
|
|
|
<link href="select2-bootstrap.css" rel="stylesheet" type="text/css"></link>
|
|
|
|
**Note:** currently `autotext` feature does not work for select2 with `ajax` remote source.
|
|
You need initially put both `data-value` and element's text youself:
|
|
|
|
<a href="#" data-type="select2" data-value="1">Text1</a>
|
|
|
|
|
|
@class select2
|
|
@extends abstractinput
|
|
@since 1.4.1
|
|
@final
|
|
@example
|
|
<a href="#" id="country" data-type="select2" data-pk="1" data-value="ru" data-url="/post" data-title="Select country"></a>
|
|
<script>
|
|
$(function(){
|
|
//local source
|
|
$('#country').editable({
|
|
source: [
|
|
{id: 'gb', text: 'Great Britain'},
|
|
{id: 'us', text: 'United States'},
|
|
{id: 'ru', text: 'Russia'}
|
|
],
|
|
select2: {
|
|
multiple: true
|
|
}
|
|
});
|
|
//remote source (simple)
|
|
$('#country').editable({
|
|
source: '/getCountries',
|
|
select2: {
|
|
placeholder: 'Select Country',
|
|
minimumInputLength: 1
|
|
}
|
|
});
|
|
//remote source (advanced)
|
|
$('#country').editable({
|
|
select2: {
|
|
placeholder: 'Select Country',
|
|
allowClear: true,
|
|
minimumInputLength: 3,
|
|
id: function (item) {
|
|
return item.CountryId;
|
|
},
|
|
ajax: {
|
|
url: '/getCountries',
|
|
dataType: 'json',
|
|
data: function (term, page) {
|
|
return { query: term };
|
|
},
|
|
results: function (data, page) {
|
|
return { results: data };
|
|
}
|
|
},
|
|
formatResult: function (item) {
|
|
return item.CountryName;
|
|
},
|
|
formatSelection: function (item) {
|
|
return item.CountryName;
|
|
},
|
|
initSelection: function (element, callback) {
|
|
return $.get('/getCountryById', { query: element.val() }, function (data) {
|
|
callback(data);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
**/
|
|
(function ($) {
|
|
"use strict";
|
|
|
|
var Constructor = function (options) {
|
|
this.init('select2', options, Constructor.defaults);
|
|
|
|
options.select2 = options.select2 || {};
|
|
|
|
this.sourceData = null;
|
|
|
|
//placeholder
|
|
if(options.placeholder) {
|
|
options.select2.placeholder = options.placeholder;
|
|
}
|
|
|
|
//if not `tags` mode, use source
|
|
if(!options.select2.tags && options.source) {
|
|
var source = options.source;
|
|
//if source is function, call it (once!)
|
|
if ($.isFunction(options.source)) {
|
|
source = options.source.call(options.scope);
|
|
}
|
|
|
|
if (typeof source === 'string') {
|
|
options.select2.ajax = options.select2.ajax || {};
|
|
//some default ajax params
|
|
if(!options.select2.ajax.data) {
|
|
options.select2.ajax.data = function(term) {return { query:term };};
|
|
}
|
|
if(!options.select2.ajax.results) {
|
|
options.select2.ajax.results = function(data) { return {results:data };};
|
|
}
|
|
options.select2.ajax.url = source;
|
|
} else {
|
|
//check format and convert x-editable format to select2 format (if needed)
|
|
this.sourceData = this.convertSource(source);
|
|
options.select2.data = this.sourceData;
|
|
}
|
|
}
|
|
|
|
//overriding objects in config (as by default jQuery extend() is not recursive)
|
|
this.options.select2 = $.extend({}, Constructor.defaults.select2, options.select2);
|
|
|
|
//detect whether it is multi-valued
|
|
this.isMultiple = this.options.select2.tags || this.options.select2.multiple;
|
|
this.isRemote = ('ajax' in this.options.select2);
|
|
|
|
//store function returning ID of item
|
|
//should be here as used inautotext for local source
|
|
this.idFunc = this.options.select2.id;
|
|
if (typeof(this.idFunc) !== "function") {
|
|
var idKey = this.idFunc || 'id';
|
|
this.idFunc = function (e) { return e[idKey]; };
|
|
}
|
|
|
|
//store function that renders text in select2
|
|
this.formatSelection = this.options.select2.formatSelection;
|
|
if (typeof(this.formatSelection) !== "function") {
|
|
this.formatSelection = function (e) { return e.text; };
|
|
}
|
|
};
|
|
|
|
$.fn.editableutils.inherit(Constructor, $.fn.editabletypes.abstractinput);
|
|
|
|
$.extend(Constructor.prototype, {
|
|
render: function() {
|
|
this.setClass();
|
|
|
|
//can not apply select2 here as it calls initSelection
|
|
//over input that does not have correct value yet.
|
|
//apply select2 only in value2input
|
|
//this.$input.select2(this.options.select2);
|
|
|
|
//when data is loaded via ajax, we need to know when it's done to populate listData
|
|
if(this.isRemote) {
|
|
//listen to loaded event to populate data
|
|
this.$input.on('select2-loaded', $.proxy(function(e) {
|
|
this.sourceData = e.items.results;
|
|
}, this));
|
|
}
|
|
|
|
//trigger resize of editableform to re-position container in multi-valued mode
|
|
if(this.isMultiple) {
|
|
this.$input.on('change', function() {
|
|
$(this).closest('form').parent().triggerHandler('resize');
|
|
});
|
|
}
|
|
},
|
|
|
|
value2html: function(value, element) {
|
|
var text = '', data,
|
|
that = this;
|
|
|
|
if(this.options.select2.tags) { //in tags mode just assign value
|
|
data = value;
|
|
//data = $.fn.editableutils.itemsByValue(value, this.options.select2.tags, this.idFunc);
|
|
} else if(this.sourceData) {
|
|
data = $.fn.editableutils.itemsByValue(value, this.sourceData, this.idFunc);
|
|
} else {
|
|
//can not get list of possible values
|
|
//(e.g. autotext for select2 with ajax source)
|
|
}
|
|
|
|
//data may be array (when multiple values allowed)
|
|
if($.isArray(data)) {
|
|
//collect selected data and show with separator
|
|
text = [];
|
|
$.each(data, function(k, v){
|
|
text.push(v && typeof v === 'object' ? that.formatSelection(v) : v);
|
|
});
|
|
} else if(data) {
|
|
text = that.formatSelection(data);
|
|
}
|
|
|
|
text = $.isArray(text) ? text.join(this.options.viewseparator) : text;
|
|
|
|
//$(element).text(text);
|
|
Constructor.superclass.value2html.call(this, text, element);
|
|
},
|
|
|
|
html2value: function(html) {
|
|
return this.options.select2.tags ? this.str2value(html, this.options.viewseparator) : null;
|
|
},
|
|
|
|
value2input: function(value) {
|
|
// if value array => join it anyway
|
|
if($.isArray(value)) {
|
|
value = value.join(this.getSeparator());
|
|
}
|
|
|
|
//for remote source just set value, text is updated by initSelection
|
|
if(!this.$input.data('select2')) {
|
|
this.$input.val(value);
|
|
this.$input.select2(this.options.select2);
|
|
} else {
|
|
//second argument needed to separate initial change from user's click (for autosubmit)
|
|
this.$input.val(value).trigger('change', true);
|
|
|
|
//Uncaught Error: cannot call val() if initSelection() is not defined
|
|
//this.$input.select2('val', value);
|
|
}
|
|
|
|
// if defined remote source AND no multiple mode AND no user's initSelection provided -->
|
|
// we should somehow get text for provided id.
|
|
// The solution is to use element's text as text for that id (exclude empty)
|
|
if(this.isRemote && !this.isMultiple && !this.options.select2.initSelection) {
|
|
// customId and customText are methods to extract `id` and `text` from data object
|
|
// we can use this workaround only if user did not define these methods
|
|
// otherwise we cant construct data object
|
|
var customId = this.options.select2.id,
|
|
customText = this.options.select2.formatSelection;
|
|
|
|
if(!customId && !customText) {
|
|
var $el = $(this.options.scope);
|
|
if (!$el.data('editable').isEmpty) {
|
|
var data = {id: value, text: $el.text()};
|
|
this.$input.select2('data', data);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
input2value: function() {
|
|
return this.$input.select2('val');
|
|
},
|
|
|
|
str2value: function(str, separator) {
|
|
if(typeof str !== 'string' || !this.isMultiple) {
|
|
return str;
|
|
}
|
|
|
|
separator = separator || this.getSeparator();
|
|
|
|
var val, i, l;
|
|
|
|
if (str === null || str.length < 1) {
|
|
return null;
|
|
}
|
|
val = str.split(separator);
|
|
for (i = 0, l = val.length; i < l; i = i + 1) {
|
|
val[i] = $.trim(val[i]);
|
|
}
|
|
|
|
return val;
|
|
},
|
|
|
|
autosubmit: function() {
|
|
this.$input.on('change', function(e, isInitial){
|
|
if(!isInitial) {
|
|
$(this).closest('form').submit();
|
|
}
|
|
});
|
|
},
|
|
|
|
getSeparator: function() {
|
|
return this.options.select2.separator || $.fn.select2.defaults.separator;
|
|
},
|
|
|
|
/*
|
|
Converts source from x-editable format: {value: 1, text: "1"} to
|
|
select2 format: {id: 1, text: "1"}
|
|
*/
|
|
convertSource: function(source) {
|
|
if($.isArray(source) && source.length && source[0].value !== undefined) {
|
|
for(var i = 0; i<source.length; i++) {
|
|
if(source[i].value !== undefined) {
|
|
source[i].id = source[i].value;
|
|
delete source[i].value;
|
|
}
|
|
}
|
|
}
|
|
return source;
|
|
},
|
|
|
|
destroy: function() {
|
|
if(this.$input.data('select2')) {
|
|
this.$input.select2('destroy');
|
|
}
|
|
}
|
|
|
|
});
|
|
|
|
Constructor.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
|
|
/**
|
|
@property tpl
|
|
@default <input type="hidden">
|
|
**/
|
|
tpl:'<input type="hidden">',
|
|
/**
|
|
Configuration of select2. [Full list of options](http://ivaynberg.github.com/select2).
|
|
|
|
@property select2
|
|
@type object
|
|
@default null
|
|
**/
|
|
select2: null,
|
|
/**
|
|
Placeholder attribute of select
|
|
|
|
@property placeholder
|
|
@type string
|
|
@default null
|
|
**/
|
|
placeholder: null,
|
|
/**
|
|
Source data for select. It will be assigned to select2 `data` property and kept here just for convenience.
|
|
Please note, that format is different from simple `select` input: use 'id' instead of 'value'.
|
|
E.g. `[{id: 1, text: "text1"}, {id: 2, text: "text2"}, ...]`.
|
|
|
|
@property source
|
|
@type array|string|function
|
|
@default null
|
|
**/
|
|
source: null,
|
|
/**
|
|
Separator used to display tags.
|
|
|
|
@property viewseparator
|
|
@type string
|
|
@default ', '
|
|
**/
|
|
viewseparator: ', '
|
|
});
|
|
|
|
$.fn.editabletypes.select2 = Constructor;
|
|
|
|
}(window.jQuery));
|
|
|
|
/**
|
|
* Combodate - 1.0.5
|
|
* Dropdown date and time picker.
|
|
* Converts text input into dropdowns to pick day, month, year, hour, minute and second.
|
|
* Uses momentjs as datetime library http://momentjs.com.
|
|
* For i18n include corresponding file from https://github.com/timrwood/moment/tree/master/lang
|
|
*
|
|
* Confusion at noon and midnight - see http://en.wikipedia.org/wiki/12-hour_clock#Confusion_at_noon_and_midnight
|
|
* In combodate:
|
|
* 12:00 pm --> 12:00 (24-h format, midday)
|
|
* 12:00 am --> 00:00 (24-h format, midnight, start of day)
|
|
*
|
|
* Differs from momentjs parse rules:
|
|
* 00:00 pm, 12:00 pm --> 12:00 (24-h format, day not change)
|
|
* 00:00 am, 12:00 am --> 00:00 (24-h format, day not change)
|
|
*
|
|
*
|
|
* Author: Vitaliy Potapov
|
|
* Project page: http://github.com/vitalets/combodate
|
|
* Copyright (c) 2012 Vitaliy Potapov. Released under MIT License.
|
|
**/
|
|
(function ($) {
|
|
|
|
var Combodate = function (element, options) {
|
|
this.$element = $(element);
|
|
if(!this.$element.is('input')) {
|
|
$.error('Combodate should be applied to INPUT element');
|
|
return;
|
|
}
|
|
this.options = $.extend({}, $.fn.combodate.defaults, options, this.$element.data());
|
|
this.init();
|
|
};
|
|
|
|
Combodate.prototype = {
|
|
constructor: Combodate,
|
|
init: function () {
|
|
this.map = {
|
|
//key regexp moment.method
|
|
day: ['D', 'date'],
|
|
month: ['M', 'month'],
|
|
year: ['Y', 'year'],
|
|
hour: ['[Hh]', 'hours'],
|
|
minute: ['m', 'minutes'],
|
|
second: ['s', 'seconds'],
|
|
ampm: ['[Aa]', '']
|
|
};
|
|
|
|
this.$widget = $('<span class="combodate"></span>').html(this.getTemplate());
|
|
|
|
this.initCombos();
|
|
|
|
//update original input on change
|
|
this.$widget.on('change', 'select', $.proxy(function(e) {
|
|
this.$element.val(this.getValue()).change();
|
|
// update days count if month or year changes
|
|
if (this.options.smartDays) {
|
|
if ($(e.target).is('.month') || $(e.target).is('.year')) {
|
|
this.fillCombo('day');
|
|
}
|
|
}
|
|
}, this));
|
|
|
|
this.$widget.find('select').css('width', 'auto');
|
|
|
|
// hide original input and insert widget
|
|
this.$element.hide().after(this.$widget);
|
|
|
|
// set initial value
|
|
this.setValue(this.$element.val() || this.options.value);
|
|
},
|
|
|
|
/*
|
|
Replace tokens in template with <select> elements
|
|
*/
|
|
getTemplate: function() {
|
|
var tpl = this.options.template;
|
|
|
|
//first pass
|
|
$.each(this.map, function(k, v) {
|
|
v = v[0];
|
|
var r = new RegExp(v+'+'),
|
|
token = v.length > 1 ? v.substring(1, 2) : v;
|
|
|
|
tpl = tpl.replace(r, '{'+token+'}');
|
|
});
|
|
|
|
//replace spaces with
|
|
tpl = tpl.replace(/ /g, ' ');
|
|
|
|
//second pass
|
|
$.each(this.map, function(k, v) {
|
|
v = v[0];
|
|
var token = v.length > 1 ? v.substring(1, 2) : v;
|
|
|
|
tpl = tpl.replace('{'+token+'}', '<select class="'+k+'"></select>');
|
|
});
|
|
|
|
return tpl;
|
|
},
|
|
|
|
/*
|
|
Initialize combos that presents in template
|
|
*/
|
|
initCombos: function() {
|
|
for (var k in this.map) {
|
|
var $c = this.$widget.find('.'+k);
|
|
// set properties like this.$day, this.$month etc.
|
|
this['$'+k] = $c.length ? $c : null;
|
|
// fill with items
|
|
this.fillCombo(k);
|
|
}
|
|
},
|
|
|
|
/*
|
|
Fill combo with items
|
|
*/
|
|
fillCombo: function(k) {
|
|
var $combo = this['$'+k];
|
|
if (!$combo) {
|
|
return;
|
|
}
|
|
|
|
// define method name to fill items, e.g `fillDays`
|
|
var f = 'fill' + k.charAt(0).toUpperCase() + k.slice(1);
|
|
var items = this[f]();
|
|
var value = $combo.val();
|
|
|
|
$combo.empty();
|
|
for(var i=0; i<items.length; i++) {
|
|
$combo.append('<option value="'+items[i][0]+'">'+items[i][1]+'</option>');
|
|
}
|
|
|
|
$combo.val(value);
|
|
},
|
|
|
|
/*
|
|
Initialize items of combos. Handles `firstItem` option
|
|
*/
|
|
fillCommon: function(key) {
|
|
var values = [],
|
|
relTime;
|
|
|
|
if(this.options.firstItem === 'name') {
|
|
//need both to support moment ver < 2 and >= 2
|
|
relTime = moment.relativeTime || moment.langData()._relativeTime;
|
|
var header = typeof relTime[key] === 'function' ? relTime[key](1, true, key, false) : relTime[key];
|
|
//take last entry (see momentjs lang files structure)
|
|
header = header.split(' ').reverse()[0];
|
|
values.push(['', header]);
|
|
} else if(this.options.firstItem === 'empty') {
|
|
values.push(['', '']);
|
|
}
|
|
return values;
|
|
},
|
|
|
|
|
|
/*
|
|
fill day
|
|
*/
|
|
fillDay: function() {
|
|
var items = this.fillCommon('d'), name, i,
|
|
twoDigit = this.options.template.indexOf('DD') !== -1,
|
|
daysCount = 31;
|
|
|
|
// detect days count (depends on month and year)
|
|
// originally https://github.com/vitalets/combodate/pull/7
|
|
if (this.options.smartDays && this.$month && this.$year) {
|
|
var month = parseInt(this.$month.val(), 10);
|
|
var year = parseInt(this.$year.val(), 10);
|
|
|
|
if (!isNaN(month) && !isNaN(year)) {
|
|
daysCount = moment([year, month]).daysInMonth();
|
|
}
|
|
}
|
|
|
|
for (i = 1; i <= daysCount; i++) {
|
|
name = twoDigit ? this.leadZero(i) : i;
|
|
items.push([i, name]);
|
|
}
|
|
return items;
|
|
},
|
|
|
|
/*
|
|
fill month
|
|
*/
|
|
fillMonth: function() {
|
|
var items = this.fillCommon('M'), name, i,
|
|
longNames = this.options.template.indexOf('MMMM') !== -1,
|
|
shortNames = this.options.template.indexOf('MMM') !== -1,
|
|
twoDigit = this.options.template.indexOf('MM') !== -1;
|
|
|
|
for(i=0; i<=11; i++) {
|
|
if(longNames) {
|
|
//see https://github.com/timrwood/momentjs.com/pull/36
|
|
name = moment().date(1).month(i).format('MMMM');
|
|
} else if(shortNames) {
|
|
name = moment().date(1).month(i).format('MMM');
|
|
} else if(twoDigit) {
|
|
name = this.leadZero(i+1);
|
|
} else {
|
|
name = i+1;
|
|
}
|
|
items.push([i, name]);
|
|
}
|
|
return items;
|
|
},
|
|
|
|
/*
|
|
fill year
|
|
*/
|
|
fillYear: function() {
|
|
var items = [], name, i,
|
|
longNames = this.options.template.indexOf('YYYY') !== -1;
|
|
|
|
for(i=this.options.maxYear; i>=this.options.minYear; i--) {
|
|
name = longNames ? i : (i+'').substring(2);
|
|
items[this.options.yearDescending ? 'push' : 'unshift']([i, name]);
|
|
}
|
|
|
|
items = this.fillCommon('y').concat(items);
|
|
|
|
return items;
|
|
},
|
|
|
|
/*
|
|
fill hour
|
|
*/
|
|
fillHour: function() {
|
|
var items = this.fillCommon('h'), name, i,
|
|
h12 = this.options.template.indexOf('h') !== -1,
|
|
h24 = this.options.template.indexOf('H') !== -1,
|
|
twoDigit = this.options.template.toLowerCase().indexOf('hh') !== -1,
|
|
min = h12 ? 1 : 0,
|
|
max = h12 ? 12 : 23;
|
|
|
|
for(i=min; i<=max; i++) {
|
|
name = twoDigit ? this.leadZero(i) : i;
|
|
items.push([i, name]);
|
|
}
|
|
return items;
|
|
},
|
|
|
|
/*
|
|
fill minute
|
|
*/
|
|
fillMinute: function() {
|
|
var items = this.fillCommon('m'), name, i,
|
|
twoDigit = this.options.template.indexOf('mm') !== -1;
|
|
|
|
for(i=0; i<=59; i+= this.options.minuteStep) {
|
|
name = twoDigit ? this.leadZero(i) : i;
|
|
items.push([i, name]);
|
|
}
|
|
return items;
|
|
},
|
|
|
|
/*
|
|
fill second
|
|
*/
|
|
fillSecond: function() {
|
|
var items = this.fillCommon('s'), name, i,
|
|
twoDigit = this.options.template.indexOf('ss') !== -1;
|
|
|
|
for(i=0; i<=59; i+= this.options.secondStep) {
|
|
name = twoDigit ? this.leadZero(i) : i;
|
|
items.push([i, name]);
|
|
}
|
|
return items;
|
|
},
|
|
|
|
/*
|
|
fill ampm
|
|
*/
|
|
fillAmpm: function() {
|
|
var ampmL = this.options.template.indexOf('a') !== -1,
|
|
ampmU = this.options.template.indexOf('A') !== -1,
|
|
items = [
|
|
['am', ampmL ? 'am' : 'AM'],
|
|
['pm', ampmL ? 'pm' : 'PM']
|
|
];
|
|
return items;
|
|
},
|
|
|
|
/*
|
|
Returns current date value from combos.
|
|
If format not specified - `options.format` used.
|
|
If format = `null` - Moment object returned.
|
|
*/
|
|
getValue: function(format) {
|
|
var dt, values = {},
|
|
that = this,
|
|
notSelected = false;
|
|
|
|
//getting selected values
|
|
$.each(this.map, function(k, v) {
|
|
if(k === 'ampm') {
|
|
return;
|
|
}
|
|
var def = k === 'day' ? 1 : 0;
|
|
|
|
values[k] = that['$'+k] ? parseInt(that['$'+k].val(), 10) : def;
|
|
|
|
if(isNaN(values[k])) {
|
|
notSelected = true;
|
|
return false;
|
|
}
|
|
});
|
|
|
|
//if at least one visible combo not selected - return empty string
|
|
if(notSelected) {
|
|
return '';
|
|
}
|
|
|
|
//convert hours 12h --> 24h
|
|
if(this.$ampm) {
|
|
//12:00 pm --> 12:00 (24-h format, midday), 12:00 am --> 00:00 (24-h format, midnight, start of day)
|
|
if(values.hour === 12) {
|
|
values.hour = this.$ampm.val() === 'am' ? 0 : 12;
|
|
} else {
|
|
values.hour = this.$ampm.val() === 'am' ? values.hour : values.hour+12;
|
|
}
|
|
}
|
|
|
|
dt = moment([values.year, values.month, values.day, values.hour, values.minute, values.second]);
|
|
|
|
//highlight invalid date
|
|
this.highlight(dt);
|
|
|
|
format = format === undefined ? this.options.format : format;
|
|
if(format === null) {
|
|
return dt.isValid() ? dt : null;
|
|
} else {
|
|
return dt.isValid() ? dt.format(format) : '';
|
|
}
|
|
},
|
|
|
|
setValue: function(value) {
|
|
if(!value) {
|
|
return;
|
|
}
|
|
|
|
var dt = typeof value === 'string' ? moment(value, this.options.format) : moment(value),
|
|
that = this,
|
|
values = {};
|
|
|
|
//function to find nearest value in select options
|
|
function getNearest($select, value) {
|
|
var delta = {};
|
|
$select.children('option').each(function(i, opt){
|
|
var optValue = $(opt).attr('value'),
|
|
distance;
|
|
|
|
if(optValue === '') return;
|
|
distance = Math.abs(optValue - value);
|
|
if(typeof delta.distance === 'undefined' || distance < delta.distance) {
|
|
delta = {value: optValue, distance: distance};
|
|
}
|
|
});
|
|
return delta.value;
|
|
}
|
|
|
|
if(dt.isValid()) {
|
|
//read values from date object
|
|
$.each(this.map, function(k, v) {
|
|
if(k === 'ampm') {
|
|
return;
|
|
}
|
|
values[k] = dt[v[1]]();
|
|
});
|
|
|
|
if(this.$ampm) {
|
|
//12:00 pm --> 12:00 (24-h format, midday), 12:00 am --> 00:00 (24-h format, midnight, start of day)
|
|
if(values.hour >= 12) {
|
|
values.ampm = 'pm';
|
|
if(values.hour > 12) {
|
|
values.hour -= 12;
|
|
}
|
|
} else {
|
|
values.ampm = 'am';
|
|
if(values.hour === 0) {
|
|
values.hour = 12;
|
|
}
|
|
}
|
|
}
|
|
|
|
$.each(values, function(k, v) {
|
|
//call val() for each existing combo, e.g. this.$hour.val()
|
|
if(that['$'+k]) {
|
|
|
|
if(k === 'minute' && that.options.minuteStep > 1 && that.options.roundTime) {
|
|
v = getNearest(that['$'+k], v);
|
|
}
|
|
|
|
if(k === 'second' && that.options.secondStep > 1 && that.options.roundTime) {
|
|
v = getNearest(that['$'+k], v);
|
|
}
|
|
|
|
that['$'+k].val(v);
|
|
}
|
|
});
|
|
|
|
// update days count
|
|
if (this.options.smartDays) {
|
|
this.fillCombo('day');
|
|
}
|
|
|
|
this.$element.val(dt.format(this.options.format)).change();
|
|
}
|
|
},
|
|
|
|
/*
|
|
highlight combos if date is invalid
|
|
*/
|
|
highlight: function(dt) {
|
|
if(!dt.isValid()) {
|
|
if(this.options.errorClass) {
|
|
this.$widget.addClass(this.options.errorClass);
|
|
} else {
|
|
//store original border color
|
|
if(!this.borderColor) {
|
|
this.borderColor = this.$widget.find('select').css('border-color');
|
|
}
|
|
this.$widget.find('select').css('border-color', 'red');
|
|
}
|
|
} else {
|
|
if(this.options.errorClass) {
|
|
this.$widget.removeClass(this.options.errorClass);
|
|
} else {
|
|
this.$widget.find('select').css('border-color', this.borderColor);
|
|
}
|
|
}
|
|
},
|
|
|
|
leadZero: function(v) {
|
|
return v <= 9 ? '0' + v : v;
|
|
},
|
|
|
|
destroy: function() {
|
|
this.$widget.remove();
|
|
this.$element.removeData('combodate').show();
|
|
}
|
|
|
|
//todo: clear method
|
|
};
|
|
|
|
$.fn.combodate = function ( option ) {
|
|
var d, args = Array.apply(null, arguments);
|
|
args.shift();
|
|
|
|
//getValue returns date as string / object (not jQuery object)
|
|
if(option === 'getValue' && this.length && (d = this.eq(0).data('combodate'))) {
|
|
return d.getValue.apply(d, args);
|
|
}
|
|
|
|
return this.each(function () {
|
|
var $this = $(this),
|
|
data = $this.data('combodate'),
|
|
options = typeof option == 'object' && option;
|
|
if (!data) {
|
|
$this.data('combodate', (data = new Combodate(this, options)));
|
|
}
|
|
if (typeof option == 'string' && typeof data[option] == 'function') {
|
|
data[option].apply(data, args);
|
|
}
|
|
});
|
|
};
|
|
|
|
$.fn.combodate.defaults = {
|
|
//in this format value stored in original input
|
|
format: 'DD-MM-YYYY HH:mm',
|
|
//in this format items in dropdowns are displayed
|
|
template: 'D / MMM / YYYY H : mm',
|
|
//initial value, can be `new Date()`
|
|
value: null,
|
|
minYear: 1970,
|
|
maxYear: 2015,
|
|
yearDescending: true,
|
|
minuteStep: 5,
|
|
secondStep: 1,
|
|
firstItem: 'empty', //'name', 'empty', 'none'
|
|
errorClass: null,
|
|
roundTime: true, // whether to round minutes and seconds if step > 1
|
|
smartDays: false // whether days in combo depend on selected month: 31, 30, 28
|
|
};
|
|
|
|
}(window.jQuery));
|
|
/**
|
|
Combodate input - dropdown date and time picker.
|
|
Based on [combodate](http://vitalets.github.com/combodate) plugin (included). To use it you should manually include [momentjs](http://momentjs.com).
|
|
|
|
<script src="js/moment.min.js"></script>
|
|
|
|
Allows to input:
|
|
|
|
* only date
|
|
* only time
|
|
* both date and time
|
|
|
|
Please note, that format is taken from momentjs and **not compatible** with bootstrap-datepicker / jquery UI datepicker.
|
|
Internally value stored as `momentjs` object.
|
|
|
|
@class combodate
|
|
@extends abstractinput
|
|
@final
|
|
@since 1.4.0
|
|
@example
|
|
<a href="#" id="dob" data-type="combodate" data-pk="1" data-url="/post" data-value="1984-05-15" data-title="Select date"></a>
|
|
<script>
|
|
$(function(){
|
|
$('#dob').editable({
|
|
format: 'YYYY-MM-DD',
|
|
viewformat: 'DD.MM.YYYY',
|
|
template: 'D / MMMM / YYYY',
|
|
combodate: {
|
|
minYear: 2000,
|
|
maxYear: 2015,
|
|
minuteStep: 1
|
|
}
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
**/
|
|
|
|
/*global moment*/
|
|
|
|
(function ($) {
|
|
"use strict";
|
|
|
|
var Constructor = function (options) {
|
|
this.init('combodate', options, Constructor.defaults);
|
|
|
|
//by default viewformat equals to format
|
|
if(!this.options.viewformat) {
|
|
this.options.viewformat = this.options.format;
|
|
}
|
|
|
|
//try parse combodate config defined as json string in data-combodate
|
|
options.combodate = $.fn.editableutils.tryParseJson(options.combodate, true);
|
|
|
|
//overriding combodate config (as by default jQuery extend() is not recursive)
|
|
this.options.combodate = $.extend({}, Constructor.defaults.combodate, options.combodate, {
|
|
format: this.options.format,
|
|
template: this.options.template
|
|
});
|
|
};
|
|
|
|
$.fn.editableutils.inherit(Constructor, $.fn.editabletypes.abstractinput);
|
|
|
|
$.extend(Constructor.prototype, {
|
|
render: function () {
|
|
this.$input.combodate(this.options.combodate);
|
|
|
|
if($.fn.editableform.engine === 'bs3') {
|
|
this.$input.siblings().find('select').addClass('form-control');
|
|
}
|
|
|
|
if(this.options.inputclass) {
|
|
this.$input.siblings().find('select').addClass(this.options.inputclass);
|
|
}
|
|
//"clear" link
|
|
/*
|
|
if(this.options.clear) {
|
|
this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
this.clear();
|
|
}, this));
|
|
|
|
this.$tpl.parent().append($('<div class="editable-clear">').append(this.$clear));
|
|
}
|
|
*/
|
|
},
|
|
|
|
value2html: function(value, element) {
|
|
var text = value ? value.format(this.options.viewformat) : '';
|
|
//$(element).text(text);
|
|
Constructor.superclass.value2html.call(this, text, element);
|
|
},
|
|
|
|
html2value: function(html) {
|
|
return html ? moment(html, this.options.viewformat) : null;
|
|
},
|
|
|
|
value2str: function(value) {
|
|
return value ? value.format(this.options.format) : '';
|
|
},
|
|
|
|
str2value: function(str) {
|
|
return str ? moment(str, this.options.format) : null;
|
|
},
|
|
|
|
value2submit: function(value) {
|
|
return this.value2str(value);
|
|
},
|
|
|
|
value2input: function(value) {
|
|
this.$input.combodate('setValue', value);
|
|
},
|
|
|
|
input2value: function() {
|
|
return this.$input.combodate('getValue', null);
|
|
},
|
|
|
|
activate: function() {
|
|
this.$input.siblings('.combodate').find('select').eq(0).focus();
|
|
},
|
|
|
|
/*
|
|
clear: function() {
|
|
this.$input.data('datepicker').date = null;
|
|
this.$input.find('.active').removeClass('active');
|
|
},
|
|
*/
|
|
|
|
autosubmit: function() {
|
|
|
|
}
|
|
|
|
});
|
|
|
|
Constructor.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
|
|
/**
|
|
@property tpl
|
|
@default <input type="text">
|
|
**/
|
|
tpl:'<input type="text">',
|
|
/**
|
|
@property inputclass
|
|
@default null
|
|
**/
|
|
inputclass: null,
|
|
/**
|
|
Format used for sending value to server. Also applied when converting date from <code>data-value</code> attribute.<br>
|
|
See list of tokens in [momentjs docs](http://momentjs.com/docs/#/parsing/string-format)
|
|
|
|
@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,
|
|
/**
|
|
Template used for displaying dropdowns.
|
|
|
|
@property template
|
|
@type string
|
|
@default D / MMM / YYYY
|
|
**/
|
|
template: 'D / MMM / YYYY',
|
|
/**
|
|
Configuration of combodate.
|
|
Full list of options: http://vitalets.github.com/combodate/#docs
|
|
|
|
@property combodate
|
|
@type object
|
|
@default null
|
|
**/
|
|
combodate: null
|
|
|
|
/*
|
|
(not implemented yet)
|
|
Text shown as clear date button.
|
|
If <code>false</code> clear button will not be rendered.
|
|
|
|
@property clear
|
|
@type boolean|string
|
|
@default 'x clear'
|
|
*/
|
|
//clear: '× clear'
|
|
});
|
|
|
|
$.fn.editabletypes.combodate = Constructor;
|
|
|
|
}(window.jQuery));
|
|
|
|
/**
|
|
* Editable Poshytip
|
|
* ---------------------
|
|
* requires jquery.poshytip.js
|
|
*/
|
|
(function ($) {
|
|
"use strict";
|
|
|
|
//extend methods
|
|
$.extend($.fn.editableContainer.Popup.prototype, {
|
|
containerName: 'poshytip',
|
|
innerCss: 'div.tip-inner',
|
|
defaults: $.fn.poshytip.defaults,
|
|
|
|
initContainer: function(){
|
|
this.handlePlacement();
|
|
|
|
$.extend(this.containerOptions, {
|
|
showOn: 'none',
|
|
content: '',
|
|
alignTo: 'target'
|
|
});
|
|
|
|
this.call(this.containerOptions);
|
|
},
|
|
|
|
/*
|
|
Overwrite totally show() method as poshytip requires content is set before show
|
|
*/
|
|
show: function (closeAll) {
|
|
this.$element.addClass('editable-open');
|
|
if(closeAll !== false) {
|
|
//close all open containers (except this)
|
|
this.closeOthers(this.$element[0]);
|
|
}
|
|
|
|
//render form
|
|
this.$form = $('<div>');
|
|
this.renderForm();
|
|
|
|
var $label = $('<label>').text(this.options.title || this.$element.data( "title") || this.$element.data( "originalTitle")),
|
|
$content = $('<div>').append($label).append(this.$form);
|
|
|
|
this.call('update', $content);
|
|
this.call('show');
|
|
|
|
this.tip().addClass(this.containerClass);
|
|
this.$form.data('editableform').input.activate();
|
|
},
|
|
|
|
/* hide */
|
|
innerHide: function () {
|
|
this.call('hide');
|
|
},
|
|
|
|
/* destroy */
|
|
innerDestroy: function() {
|
|
this.call('destroy');
|
|
},
|
|
|
|
setPosition: function() {
|
|
this.container().refresh(false);
|
|
},
|
|
|
|
handlePlacement: function() {
|
|
var x, y, ox = 0, oy = 0;
|
|
switch(this.options.placement) {
|
|
case 'top':
|
|
x = 'center';
|
|
y = 'top';
|
|
oy = 5;
|
|
break;
|
|
case 'right':
|
|
x = 'right';
|
|
y = 'center';
|
|
ox = 10;
|
|
break;
|
|
case 'bottom':
|
|
x = 'center';
|
|
y = 'bottom';
|
|
oy = 5;
|
|
break;
|
|
case 'left':
|
|
x = 'left';
|
|
y = 'center';
|
|
ox = 10;
|
|
break;
|
|
}
|
|
|
|
$.extend(this.containerOptions, {
|
|
alignX: x,
|
|
offsetX: ox,
|
|
alignY: y,
|
|
offsetY:oy
|
|
});
|
|
}
|
|
});
|
|
|
|
//defaults
|
|
$.fn.editableContainer.defaults = $.extend({}, $.fn.editableContainer.defaults, {
|
|
className: 'tip-yellowsimple'
|
|
});
|
|
|
|
|
|
/**
|
|
* Poshytip fix: disable incorrect table display
|
|
* see https://github.com/vadikom/poshytip/issues/7
|
|
*/
|
|
/*jshint eqeqeq:false, curly: false*/
|
|
if($.Poshytip) { //need this check, because in inline mode poshytip may not be loaded!
|
|
var tips = [],
|
|
reBgImage = /^url\(["']?([^"'\)]*)["']?\);?$/i,
|
|
rePNG = /\.png$/i,
|
|
ie6 = !!window.createPopup && document.documentElement.currentStyle.minWidth == 'undefined';
|
|
|
|
$.Poshytip.prototype.refresh = function(async) {
|
|
if (this.disabled)
|
|
return;
|
|
|
|
var currPos;
|
|
if (async) {
|
|
if (!this.$tip.data('active'))
|
|
return;
|
|
// save current position as we will need to animate
|
|
currPos = {left: this.$tip.css('left'), top: this.$tip.css('top')};
|
|
}
|
|
|
|
// reset position to avoid text wrapping, etc.
|
|
this.$tip.css({left: 0, top: 0}).appendTo(document.body);
|
|
|
|
// save default opacity
|
|
if (this.opacity === undefined)
|
|
this.opacity = this.$tip.css('opacity');
|
|
|
|
// check for images - this code is here (i.e. executed each time we show the tip and not on init) due to some browser inconsistencies
|
|
var bgImage = this.$tip.css('background-image').match(reBgImage),
|
|
arrow = this.$arrow.css('background-image').match(reBgImage);
|
|
|
|
if (bgImage) {
|
|
var bgImagePNG = rePNG.test(bgImage[1]);
|
|
// fallback to background-color/padding/border in IE6 if a PNG is used
|
|
if (ie6 && bgImagePNG) {
|
|
this.$tip.css('background-image', 'none');
|
|
this.$inner.css({margin: 0, border: 0, padding: 0});
|
|
bgImage = bgImagePNG = false;
|
|
} else {
|
|
this.$tip.prepend('<table class="fallback" border="0" cellpadding="0" cellspacing="0"><tr><td class="tip-top tip-bg-image" colspan="2"><span></span></td><td class="tip-right tip-bg-image" rowspan="2"><span></span></td></tr><tr><td class="tip-left tip-bg-image" rowspan="2"><span></span></td><td></td></tr><tr><td class="tip-bottom tip-bg-image" colspan="2"><span></span></td></tr></table>')
|
|
.css({border: 0, padding: 0, 'background-image': 'none', 'background-color': 'transparent'})
|
|
.find('.tip-bg-image').css('background-image', 'url("' + bgImage[1] +'")').end()
|
|
.find('td').eq(3).append(this.$inner);
|
|
}
|
|
// disable fade effect in IE due to Alpha filter + translucent PNG issue
|
|
if (bgImagePNG && !$.support.opacity)
|
|
this.opts.fade = false;
|
|
}
|
|
// IE arrow fixes
|
|
if (arrow && !$.support.opacity) {
|
|
// disable arrow in IE6 if using a PNG
|
|
if (ie6 && rePNG.test(arrow[1])) {
|
|
arrow = false;
|
|
this.$arrow.css('background-image', 'none');
|
|
}
|
|
// disable fade effect in IE due to Alpha filter + translucent PNG issue
|
|
this.opts.fade = false;
|
|
}
|
|
|
|
var $table = this.$tip.find('table.fallback');
|
|
if (ie6) {
|
|
// fix min/max-width in IE6
|
|
this.$tip[0].style.width = '';
|
|
$table.width('auto').find('td').eq(3).width('auto');
|
|
var tipW = this.$tip.width(),
|
|
minW = parseInt(this.$tip.css('min-width'), 10),
|
|
maxW = parseInt(this.$tip.css('max-width'), 10);
|
|
if (!isNaN(minW) && tipW < minW)
|
|
tipW = minW;
|
|
else if (!isNaN(maxW) && tipW > maxW)
|
|
tipW = maxW;
|
|
this.$tip.add($table).width(tipW).eq(0).find('td').eq(3).width('100%');
|
|
} else if ($table[0]) {
|
|
// fix the table width if we are using a background image
|
|
// IE9, FF4 use float numbers for width/height so use getComputedStyle for them to avoid text wrapping
|
|
// for details look at: http://vadikom.com/dailies/offsetwidth-offsetheight-useless-in-ie9-firefox4/
|
|
$table.width('auto').find('td').eq(3).width('auto').end().end().width(document.defaultView && document.defaultView.getComputedStyle && parseFloat(document.defaultView.getComputedStyle(this.$tip[0], null).width) || this.$tip.width()).find('td').eq(3).width('100%');
|
|
}
|
|
this.tipOuterW = this.$tip.outerWidth();
|
|
this.tipOuterH = this.$tip.outerHeight();
|
|
|
|
this.calcPos();
|
|
|
|
// position and show the arrow image
|
|
if (arrow && this.pos.arrow) {
|
|
this.$arrow[0].className = 'tip-arrow tip-arrow-' + this.pos.arrow;
|
|
this.$arrow.css('visibility', 'inherit');
|
|
}
|
|
|
|
if (async) {
|
|
this.asyncAnimating = true;
|
|
var self = this;
|
|
this.$tip.css(currPos).animate({left: this.pos.l, top: this.pos.t}, 200, function() { self.asyncAnimating = false; });
|
|
} else {
|
|
this.$tip.css({left: this.pos.l, top: this.pos.t});
|
|
}
|
|
};
|
|
}
|
|
/*jshinteqeqeq: true, curly: true*/
|
|
}(window.jQuery));
|
|
/**
|
|
jQuery UI Datepicker.
|
|
Description and examples: http://jqueryui.com/datepicker.
|
|
This input is also accessible as **date** type. Do not use it together with __bootstrap-datepicker__ as both apply <code>$().datepicker()</code> method.
|
|
For **i18n** you should include js file from here: https://github.com/jquery/jquery-ui/tree/master/ui/i18n.
|
|
|
|
@class dateui
|
|
@extends abstractinput
|
|
@final
|
|
@example
|
|
<a href="#" id="dob" data-type="date" data-pk="1" data-url="/post" data-title="Select date">15/05/1984</a>
|
|
<script>
|
|
$(function(){
|
|
$('#dob').editable({
|
|
format: 'yyyy-mm-dd',
|
|
viewformat: 'dd/mm/yyyy',
|
|
datepicker: {
|
|
firstDay: 1
|
|
}
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
**/
|
|
(function ($) {
|
|
"use strict";
|
|
|
|
var DateUI = function (options) {
|
|
this.init('dateui', options, DateUI.defaults);
|
|
this.initPicker(options, DateUI.defaults);
|
|
};
|
|
|
|
$.fn.editableutils.inherit(DateUI, $.fn.editabletypes.abstractinput);
|
|
|
|
$.extend(DateUI.prototype, {
|
|
initPicker: function(options, defaults) {
|
|
//by default viewformat equals to format
|
|
if(!this.options.viewformat) {
|
|
this.options.viewformat = this.options.format;
|
|
}
|
|
|
|
//correct formats: replace yyyy with yy (for compatibility with bootstrap datepicker)
|
|
this.options.viewformat = this.options.viewformat.replace('yyyy', 'yy');
|
|
this.options.format = this.options.format.replace('yyyy', 'yy');
|
|
|
|
//overriding datepicker config (as by default jQuery extend() is not recursive)
|
|
//since 1.4 datepicker internally uses viewformat instead of format. Format is for submit only
|
|
this.options.datepicker = $.extend({}, defaults.datepicker, options.datepicker, {
|
|
dateFormat: this.options.viewformat
|
|
});
|
|
},
|
|
|
|
render: function () {
|
|
this.$input.datepicker(this.options.datepicker);
|
|
|
|
//"clear" link
|
|
if(this.options.clear) {
|
|
this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
this.clear();
|
|
}, this));
|
|
|
|
this.$tpl.parent().append($('<div class="editable-clear">').append(this.$clear));
|
|
}
|
|
},
|
|
|
|
value2html: function(value, element) {
|
|
var text = $.datepicker.formatDate(this.options.viewformat, value);
|
|
DateUI.superclass.value2html.call(this, text, element);
|
|
},
|
|
|
|
html2value: function(html) {
|
|
if(typeof html !== 'string') {
|
|
return html;
|
|
}
|
|
|
|
//if string does not match format, UI datepicker throws exception
|
|
var d;
|
|
try {
|
|
d = $.datepicker.parseDate(this.options.viewformat, html);
|
|
} catch(e) {}
|
|
|
|
return d;
|
|
},
|
|
|
|
value2str: function(value) {
|
|
return $.datepicker.formatDate(this.options.format, value);
|
|
},
|
|
|
|
str2value: function(str) {
|
|
if(typeof str !== 'string') {
|
|
return str;
|
|
}
|
|
|
|
//if string does not match format, UI datepicker throws exception
|
|
var d;
|
|
try {
|
|
d = $.datepicker.parseDate(this.options.format, str);
|
|
} catch(e) {}
|
|
|
|
return d;
|
|
},
|
|
|
|
value2submit: function(value) {
|
|
return this.value2str(value);
|
|
},
|
|
|
|
value2input: function(value) {
|
|
this.$input.datepicker('setDate', value);
|
|
},
|
|
|
|
input2value: function() {
|
|
return this.$input.datepicker('getDate');
|
|
},
|
|
|
|
activate: function() {
|
|
},
|
|
|
|
clear: function() {
|
|
this.$input.datepicker('setDate', null);
|
|
// submit automatically whe that are no buttons
|
|
if(this.isAutosubmit) {
|
|
this.submit();
|
|
}
|
|
},
|
|
|
|
autosubmit: function() {
|
|
this.isAutosubmit = true;
|
|
this.$input.on('mouseup', 'table.ui-datepicker-calendar a.ui-state-default', $.proxy(this.submit, this));
|
|
},
|
|
|
|
submit: function() {
|
|
var $form = this.$input.closest('form');
|
|
setTimeout(function() {
|
|
$form.submit();
|
|
}, 200);
|
|
}
|
|
|
|
});
|
|
|
|
DateUI.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
|
|
/**
|
|
@property tpl
|
|
@default <div></div>
|
|
**/
|
|
tpl:'<div class="editable-date"></div>',
|
|
/**
|
|
@property inputclass
|
|
@default null
|
|
**/
|
|
inputclass: null,
|
|
/**
|
|
Format used for sending value to server. Also applied when converting date from <code>data-value</code> attribute.<br>
|
|
Full list of tokens: http://docs.jquery.com/UI/Datepicker/formatDate
|
|
|
|
@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 <code>format</code>
|
|
|
|
@property viewformat
|
|
@type string
|
|
@default null
|
|
**/
|
|
viewformat: null,
|
|
|
|
/**
|
|
Configuration of datepicker.
|
|
Full list of options: http://api.jqueryui.com/datepicker
|
|
|
|
@property datepicker
|
|
@type object
|
|
@default {
|
|
firstDay: 0,
|
|
changeYear: true,
|
|
changeMonth: true
|
|
}
|
|
**/
|
|
datepicker: {
|
|
firstDay: 0,
|
|
changeYear: true,
|
|
changeMonth: true,
|
|
showOtherMonths: true
|
|
},
|
|
/**
|
|
Text shown as clear date button.
|
|
If <code>false</code> clear button will not be rendered.
|
|
|
|
@property clear
|
|
@type boolean|string
|
|
@default 'x clear'
|
|
**/
|
|
clear: '× clear'
|
|
});
|
|
|
|
$.fn.editabletypes.dateui = DateUI;
|
|
|
|
}(window.jQuery));
|
|
|
|
/**
|
|
jQuery UI datefield input - modification for inline mode.
|
|
Shows normal <input type="text"> and binds popup datepicker.
|
|
Automatically shown in inline mode.
|
|
|
|
@class dateuifield
|
|
@extends dateui
|
|
|
|
@since 1.4.0
|
|
**/
|
|
(function ($) {
|
|
"use strict";
|
|
|
|
var DateUIField = function (options) {
|
|
this.init('dateuifield', options, DateUIField.defaults);
|
|
this.initPicker(options, DateUIField.defaults);
|
|
};
|
|
|
|
$.fn.editableutils.inherit(DateUIField, $.fn.editabletypes.dateui);
|
|
|
|
$.extend(DateUIField.prototype, {
|
|
render: function () {
|
|
// this.$input = this.$tpl.find('input');
|
|
this.$input.datepicker(this.options.datepicker);
|
|
$.fn.editabletypes.text.prototype.renderClear.call(this);
|
|
},
|
|
|
|
value2input: function(value) {
|
|
this.$input.val($.datepicker.formatDate(this.options.viewformat, value));
|
|
},
|
|
|
|
input2value: function() {
|
|
return this.html2value(this.$input.val());
|
|
},
|
|
|
|
activate: function() {
|
|
$.fn.editabletypes.text.prototype.activate.call(this);
|
|
},
|
|
|
|
toggleClear: function() {
|
|
$.fn.editabletypes.text.prototype.toggleClear.call(this);
|
|
},
|
|
|
|
autosubmit: function() {
|
|
//reset autosubmit to empty
|
|
}
|
|
});
|
|
|
|
DateUIField.defaults = $.extend({}, $.fn.editabletypes.dateui.defaults, {
|
|
/**
|
|
@property tpl
|
|
@default <input type="text">
|
|
**/
|
|
tpl: '<input type="text"/>',
|
|
/**
|
|
@property inputclass
|
|
@default null
|
|
**/
|
|
inputclass: null,
|
|
|
|
/* datepicker config */
|
|
datepicker: {
|
|
showOn: "button",
|
|
buttonImage: "http://jqueryui.com/resources/demos/datepicker/images/calendar.gif",
|
|
buttonImageOnly: true,
|
|
firstDay: 0,
|
|
changeYear: true,
|
|
changeMonth: true,
|
|
showOtherMonths: true
|
|
},
|
|
|
|
/* disable clear link */
|
|
clear: false
|
|
});
|
|
|
|
$.fn.editabletypes.dateuifield = DateUIField;
|
|
|
|
}(window.jQuery)); |