7007 lines
232 KiB
JavaScript
Executable File
7007 lines
232 KiB
JavaScript
Executable File
/*! 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));
|
|
|
|
/*
|
|
Editableform based on Twitter Bootstrap 2
|
|
*/
|
|
(function ($) {
|
|
"use strict";
|
|
|
|
//store parent methods
|
|
var pInitInput = $.fn.editableform.Constructor.prototype.initInput;
|
|
|
|
$.extend($.fn.editableform.Constructor.prototype, {
|
|
initTemplate: function() {
|
|
this.$form = $($.fn.editableform.template);
|
|
this.$form.find('.editable-error-block').addClass('help-block');
|
|
},
|
|
initInput: function() {
|
|
pInitInput.apply(this);
|
|
|
|
//for bs2 set default class `input-medium` to standard inputs
|
|
var emptyInputClass = this.input.options.inputclass === null || this.input.options.inputclass === false;
|
|
var defaultClass = 'input-medium';
|
|
|
|
//add bs2 default class to standard inputs
|
|
//if(this.input.$input.is('input,select,textarea')) {
|
|
var stdtypes = 'text,select,textarea,password,email,url,tel,number,range,time'.split(',');
|
|
if(~$.inArray(this.input.type, stdtypes) && emptyInputClass) {
|
|
this.input.options.inputclass = defaultClass;
|
|
this.input.$input.addClass(defaultClass);
|
|
}
|
|
}
|
|
});
|
|
|
|
//buttons
|
|
$.fn.editableform.buttons = '<button type="submit" class="btn btn-primary editable-submit"><i class="icon-ok icon-white"></i></button>'+
|
|
'<button type="button" class="btn editable-cancel"><i class="icon-remove"></i></button>';
|
|
|
|
//error classes
|
|
$.fn.editableform.errorGroupClass = 'error';
|
|
$.fn.editableform.errorBlockClass = null;
|
|
//engine
|
|
$.fn.editableform.engine = 'bs2';
|
|
|
|
}(window.jQuery));
|
|
/**
|
|
* Editable Popover
|
|
* ---------------------
|
|
* requires bootstrap-popover.js
|
|
*/
|
|
(function ($) {
|
|
"use strict";
|
|
|
|
//extend methods
|
|
$.extend($.fn.editableContainer.Popup.prototype, {
|
|
containerName: 'popover',
|
|
//for compatibility with bootstrap <= 2.2.1 (content inserted into <p> instead of directly .popover-content)
|
|
innerCss: $.fn.popover && $($.fn.popover.defaults.template).find('p').length ? '.popover-content p' : '.popover-content',
|
|
defaults: $.fn.popover.defaults,
|
|
|
|
initContainer: function(){
|
|
$.extend(this.containerOptions, {
|
|
trigger: 'manual',
|
|
selector: false,
|
|
content: ' ',
|
|
template: this.defaults.template
|
|
});
|
|
|
|
//as template property is used in inputs, hide it from popover
|
|
var t;
|
|
if(this.$element.data('template')) {
|
|
t = this.$element.data('template');
|
|
this.$element.removeData('template');
|
|
}
|
|
|
|
this.call(this.containerOptions);
|
|
|
|
if(t) {
|
|
//restore data('template')
|
|
this.$element.data('template', t);
|
|
}
|
|
},
|
|
|
|
/* show */
|
|
innerShow: function () {
|
|
this.call('show');
|
|
},
|
|
|
|
/* hide */
|
|
innerHide: function () {
|
|
this.call('hide');
|
|
},
|
|
|
|
/* destroy */
|
|
innerDestroy: function() {
|
|
this.call('destroy');
|
|
},
|
|
|
|
setContainerOption: function(key, value) {
|
|
this.container().options[key] = value;
|
|
},
|
|
|
|
/**
|
|
* move popover to new position. This function mainly copied from bootstrap-popover.
|
|
*/
|
|
/*jshint laxcomma: true*/
|
|
setPosition: function () {
|
|
|
|
(function() {
|
|
var $tip = this.tip()
|
|
, inside
|
|
, pos
|
|
, actualWidth
|
|
, actualHeight
|
|
, placement
|
|
, tp
|
|
, tpt
|
|
, tpb
|
|
, tpl
|
|
, tpr;
|
|
|
|
placement = typeof this.options.placement === 'function' ?
|
|
this.options.placement.call(this, $tip[0], this.$element[0]) :
|
|
this.options.placement;
|
|
|
|
inside = /in/.test(placement);
|
|
|
|
$tip
|
|
// .detach()
|
|
//vitalets: remove any placement class because otherwise they dont influence on re-positioning of visible popover
|
|
.removeClass('top right bottom left')
|
|
.css({ top: 0, left: 0, display: 'block' });
|
|
// .insertAfter(this.$element);
|
|
|
|
pos = this.getPosition(inside);
|
|
|
|
actualWidth = $tip[0].offsetWidth;
|
|
actualHeight = $tip[0].offsetHeight;
|
|
|
|
placement = inside ? placement.split(' ')[1] : placement;
|
|
|
|
tpb = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2};
|
|
tpt = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2};
|
|
tpl = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth};
|
|
tpr = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width};
|
|
|
|
switch (placement) {
|
|
case 'bottom':
|
|
if ((tpb.top + actualHeight) > ($(window).scrollTop() + $(window).height())) {
|
|
if (tpt.top > $(window).scrollTop()) {
|
|
placement = 'top';
|
|
} else if ((tpr.left + actualWidth) < ($(window).scrollLeft() + $(window).width())) {
|
|
placement = 'right';
|
|
} else if (tpl.left > $(window).scrollLeft()) {
|
|
placement = 'left';
|
|
} else {
|
|
placement = 'right';
|
|
}
|
|
}
|
|
break;
|
|
case 'top':
|
|
if (tpt.top < $(window).scrollTop()) {
|
|
if ((tpb.top + actualHeight) < ($(window).scrollTop() + $(window).height())) {
|
|
placement = 'bottom';
|
|
} else if ((tpr.left + actualWidth) < ($(window).scrollLeft() + $(window).width())) {
|
|
placement = 'right';
|
|
} else if (tpl.left > $(window).scrollLeft()) {
|
|
placement = 'left';
|
|
} else {
|
|
placement = 'right';
|
|
}
|
|
}
|
|
break;
|
|
case 'left':
|
|
if (tpl.left < $(window).scrollLeft()) {
|
|
if ((tpr.left + actualWidth) < ($(window).scrollLeft() + $(window).width())) {
|
|
placement = 'right';
|
|
} else if (tpt.top > $(window).scrollTop()) {
|
|
placement = 'top';
|
|
} else if (tpt.top > $(window).scrollTop()) {
|
|
placement = 'bottom';
|
|
} else {
|
|
placement = 'right';
|
|
}
|
|
}
|
|
break;
|
|
case 'right':
|
|
if ((tpr.left + actualWidth) > ($(window).scrollLeft() + $(window).width())) {
|
|
if (tpl.left > $(window).scrollLeft()) {
|
|
placement = 'left';
|
|
} else if (tpt.top > $(window).scrollTop()) {
|
|
placement = 'top';
|
|
} else if (tpt.top > $(window).scrollTop()) {
|
|
placement = 'bottom';
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
switch (placement) {
|
|
case 'bottom':
|
|
tp = tpb;
|
|
break;
|
|
case 'top':
|
|
tp = tpt;
|
|
break;
|
|
case 'left':
|
|
tp = tpl;
|
|
break;
|
|
case 'right':
|
|
tp = tpr;
|
|
break;
|
|
}
|
|
|
|
$tip
|
|
.offset(tp)
|
|
.addClass(placement)
|
|
.addClass('in');
|
|
|
|
}).call(this.container());
|
|
/*jshint laxcomma: false*/
|
|
}
|
|
});
|
|
|
|
}(window.jQuery));
|
|
|
|
/* =========================================================
|
|
* bootstrap-datepicker.js
|
|
* http://www.eyecon.ro/bootstrap-datepicker
|
|
* =========================================================
|
|
* Copyright 2012 Stefan Petre
|
|
* Improvements by Andrew Rowls
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
* ========================================================= */
|
|
|
|
(function( $ ) {
|
|
|
|
function UTCDate(){
|
|
return new Date(Date.UTC.apply(Date, arguments));
|
|
}
|
|
function UTCToday(){
|
|
var today = new Date();
|
|
return UTCDate(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate());
|
|
}
|
|
|
|
// Picker object
|
|
|
|
var Datepicker = function(element, options) {
|
|
var that = this;
|
|
|
|
this._process_options(options);
|
|
|
|
this.element = $(element);
|
|
this.isInline = false;
|
|
this.isInput = this.element.is('input');
|
|
this.component = this.element.is('.date') ? this.element.find('.add-on, .btn') : false;
|
|
this.hasInput = this.component && this.element.find('input').length;
|
|
if(this.component && this.component.length === 0)
|
|
this.component = false;
|
|
|
|
this.picker = $(DPGlobal.template);
|
|
this._buildEvents();
|
|
this._attachEvents();
|
|
|
|
if(this.isInline) {
|
|
this.picker.addClass('datepicker-inline').appendTo(this.element);
|
|
} else {
|
|
this.picker.addClass('datepicker-dropdown dropdown-menu');
|
|
}
|
|
|
|
if (this.o.rtl){
|
|
this.picker.addClass('datepicker-rtl');
|
|
this.picker.find('.prev i, .next i')
|
|
.toggleClass('icon-arrow-left icon-arrow-right');
|
|
}
|
|
|
|
|
|
this.viewMode = this.o.startView;
|
|
|
|
if (this.o.calendarWeeks)
|
|
this.picker.find('tfoot th.today')
|
|
.attr('colspan', function(i, val){
|
|
return parseInt(val) + 1;
|
|
});
|
|
|
|
this._allow_update = false;
|
|
|
|
this.setStartDate(this.o.startDate);
|
|
this.setEndDate(this.o.endDate);
|
|
this.setDaysOfWeekDisabled(this.o.daysOfWeekDisabled);
|
|
|
|
this.fillDow();
|
|
this.fillMonths();
|
|
|
|
this._allow_update = true;
|
|
|
|
this.update();
|
|
this.showMode();
|
|
|
|
if(this.isInline) {
|
|
this.show();
|
|
}
|
|
};
|
|
|
|
Datepicker.prototype = {
|
|
constructor: Datepicker,
|
|
|
|
_process_options: function(opts){
|
|
// Store raw options for reference
|
|
this._o = $.extend({}, this._o, opts);
|
|
// Processed options
|
|
var o = this.o = $.extend({}, this._o);
|
|
|
|
// Check if "de-DE" style date is available, if not language should
|
|
// fallback to 2 letter code eg "de"
|
|
var lang = o.language;
|
|
if (!dates[lang]) {
|
|
lang = lang.split('-')[0];
|
|
if (!dates[lang])
|
|
lang = defaults.language;
|
|
}
|
|
o.language = lang;
|
|
|
|
switch(o.startView){
|
|
case 2:
|
|
case 'decade':
|
|
o.startView = 2;
|
|
break;
|
|
case 1:
|
|
case 'year':
|
|
o.startView = 1;
|
|
break;
|
|
default:
|
|
o.startView = 0;
|
|
}
|
|
|
|
switch (o.minViewMode) {
|
|
case 1:
|
|
case 'months':
|
|
o.minViewMode = 1;
|
|
break;
|
|
case 2:
|
|
case 'years':
|
|
o.minViewMode = 2;
|
|
break;
|
|
default:
|
|
o.minViewMode = 0;
|
|
}
|
|
|
|
o.startView = Math.max(o.startView, o.minViewMode);
|
|
|
|
o.weekStart %= 7;
|
|
o.weekEnd = ((o.weekStart + 6) % 7);
|
|
|
|
var format = DPGlobal.parseFormat(o.format)
|
|
if (o.startDate !== -Infinity) {
|
|
o.startDate = DPGlobal.parseDate(o.startDate, format, o.language);
|
|
}
|
|
if (o.endDate !== Infinity) {
|
|
o.endDate = DPGlobal.parseDate(o.endDate, format, o.language);
|
|
}
|
|
|
|
o.daysOfWeekDisabled = o.daysOfWeekDisabled||[];
|
|
if (!$.isArray(o.daysOfWeekDisabled))
|
|
o.daysOfWeekDisabled = o.daysOfWeekDisabled.split(/[,\s]*/);
|
|
o.daysOfWeekDisabled = $.map(o.daysOfWeekDisabled, function (d) {
|
|
return parseInt(d, 10);
|
|
});
|
|
},
|
|
_events: [],
|
|
_secondaryEvents: [],
|
|
_applyEvents: function(evs){
|
|
for (var i=0, el, ev; i<evs.length; i++){
|
|
el = evs[i][0];
|
|
ev = evs[i][1];
|
|
el.on(ev);
|
|
}
|
|
},
|
|
_unapplyEvents: function(evs){
|
|
for (var i=0, el, ev; i<evs.length; i++){
|
|
el = evs[i][0];
|
|
ev = evs[i][1];
|
|
el.off(ev);
|
|
}
|
|
},
|
|
_buildEvents: function(){
|
|
if (this.isInput) { // single input
|
|
this._events = [
|
|
[this.element, {
|
|
focus: $.proxy(this.show, this),
|
|
keyup: $.proxy(this.update, this),
|
|
keydown: $.proxy(this.keydown, this)
|
|
}]
|
|
];
|
|
}
|
|
else if (this.component && this.hasInput){ // component: input + button
|
|
this._events = [
|
|
// For components that are not readonly, allow keyboard nav
|
|
[this.element.find('input'), {
|
|
focus: $.proxy(this.show, this),
|
|
keyup: $.proxy(this.update, this),
|
|
keydown: $.proxy(this.keydown, this)
|
|
}],
|
|
[this.component, {
|
|
click: $.proxy(this.show, this)
|
|
}]
|
|
];
|
|
}
|
|
else if (this.element.is('div')) { // inline datepicker
|
|
this.isInline = true;
|
|
}
|
|
else {
|
|
this._events = [
|
|
[this.element, {
|
|
click: $.proxy(this.show, this)
|
|
}]
|
|
];
|
|
}
|
|
|
|
this._secondaryEvents = [
|
|
[this.picker, {
|
|
click: $.proxy(this.click, this)
|
|
}],
|
|
[$(window), {
|
|
resize: $.proxy(this.place, this)
|
|
}],
|
|
[$(document), {
|
|
mousedown: $.proxy(function (e) {
|
|
// Clicked outside the datepicker, hide it
|
|
if (!(
|
|
this.element.is(e.target) ||
|
|
this.element.find(e.target).size() ||
|
|
this.picker.is(e.target) ||
|
|
this.picker.find(e.target).size()
|
|
)) {
|
|
this.hide();
|
|
}
|
|
}, this)
|
|
}]
|
|
];
|
|
},
|
|
_attachEvents: function(){
|
|
this._detachEvents();
|
|
this._applyEvents(this._events);
|
|
},
|
|
_detachEvents: function(){
|
|
this._unapplyEvents(this._events);
|
|
},
|
|
_attachSecondaryEvents: function(){
|
|
this._detachSecondaryEvents();
|
|
this._applyEvents(this._secondaryEvents);
|
|
},
|
|
_detachSecondaryEvents: function(){
|
|
this._unapplyEvents(this._secondaryEvents);
|
|
},
|
|
_trigger: function(event, altdate){
|
|
var date = altdate || this.date,
|
|
local_date = new Date(date.getTime() + (date.getTimezoneOffset()*60000));
|
|
|
|
this.element.trigger({
|
|
type: event,
|
|
date: local_date,
|
|
format: $.proxy(function(altformat){
|
|
var format = altformat || this.o.format;
|
|
return DPGlobal.formatDate(date, format, this.o.language);
|
|
}, this)
|
|
});
|
|
},
|
|
|
|
show: function(e) {
|
|
if (!this.isInline)
|
|
this.picker.appendTo('body');
|
|
this.picker.show();
|
|
this.height = this.component ? this.component.outerHeight() : this.element.outerHeight();
|
|
this.place();
|
|
this._attachSecondaryEvents();
|
|
if (e) {
|
|
e.preventDefault();
|
|
}
|
|
this._trigger('show');
|
|
},
|
|
|
|
hide: function(e){
|
|
if(this.isInline) return;
|
|
if (!this.picker.is(':visible')) return;
|
|
this.picker.hide().detach();
|
|
this._detachSecondaryEvents();
|
|
this.viewMode = this.o.startView;
|
|
this.showMode();
|
|
|
|
if (
|
|
this.o.forceParse &&
|
|
(
|
|
this.isInput && this.element.val() ||
|
|
this.hasInput && this.element.find('input').val()
|
|
)
|
|
)
|
|
this.setValue();
|
|
this._trigger('hide');
|
|
},
|
|
|
|
remove: function() {
|
|
this.hide();
|
|
this._detachEvents();
|
|
this._detachSecondaryEvents();
|
|
this.picker.remove();
|
|
delete this.element.data().datepicker;
|
|
if (!this.isInput) {
|
|
delete this.element.data().date;
|
|
}
|
|
},
|
|
|
|
getDate: function() {
|
|
var d = this.getUTCDate();
|
|
return new Date(d.getTime() + (d.getTimezoneOffset()*60000));
|
|
},
|
|
|
|
getUTCDate: function() {
|
|
return this.date;
|
|
},
|
|
|
|
setDate: function(d) {
|
|
this.setUTCDate(new Date(d.getTime() - (d.getTimezoneOffset()*60000)));
|
|
},
|
|
|
|
setUTCDate: function(d) {
|
|
this.date = d;
|
|
this.setValue();
|
|
},
|
|
|
|
setValue: function() {
|
|
var formatted = this.getFormattedDate();
|
|
if (!this.isInput) {
|
|
if (this.component){
|
|
this.element.find('input').val(formatted);
|
|
}
|
|
} else {
|
|
this.element.val(formatted);
|
|
}
|
|
},
|
|
|
|
getFormattedDate: function(format) {
|
|
if (format === undefined)
|
|
format = this.o.format;
|
|
return DPGlobal.formatDate(this.date, format, this.o.language);
|
|
},
|
|
|
|
setStartDate: function(startDate){
|
|
this._process_options({startDate: startDate});
|
|
this.update();
|
|
this.updateNavArrows();
|
|
},
|
|
|
|
setEndDate: function(endDate){
|
|
this._process_options({endDate: endDate});
|
|
this.update();
|
|
this.updateNavArrows();
|
|
},
|
|
|
|
setDaysOfWeekDisabled: function(daysOfWeekDisabled){
|
|
this._process_options({daysOfWeekDisabled: daysOfWeekDisabled});
|
|
this.update();
|
|
this.updateNavArrows();
|
|
},
|
|
|
|
place: function(){
|
|
if(this.isInline) return;
|
|
var zIndex = parseInt(this.element.parents().filter(function() {
|
|
return $(this).css('z-index') != 'auto';
|
|
}).first().css('z-index'))+10;
|
|
var offset = this.component ? this.component.parent().offset() : this.element.offset();
|
|
var height = this.component ? this.component.outerHeight(true) : this.element.outerHeight(true);
|
|
this.picker.css({
|
|
top: offset.top + height,
|
|
left: offset.left,
|
|
zIndex: zIndex
|
|
});
|
|
},
|
|
|
|
_allow_update: true,
|
|
update: function(){
|
|
if (!this._allow_update) return;
|
|
|
|
var date, fromArgs = false;
|
|
if(arguments && arguments.length && (typeof arguments[0] === 'string' || arguments[0] instanceof Date)) {
|
|
date = arguments[0];
|
|
fromArgs = true;
|
|
} else {
|
|
date = this.isInput ? this.element.val() : this.element.data('date') || this.element.find('input').val();
|
|
delete this.element.data().date;
|
|
}
|
|
|
|
this.date = DPGlobal.parseDate(date, this.o.format, this.o.language);
|
|
|
|
if(fromArgs) this.setValue();
|
|
|
|
if (this.date < this.o.startDate) {
|
|
this.viewDate = new Date(this.o.startDate);
|
|
} else if (this.date > this.o.endDate) {
|
|
this.viewDate = new Date(this.o.endDate);
|
|
} else {
|
|
this.viewDate = new Date(this.date);
|
|
}
|
|
this.fill();
|
|
},
|
|
|
|
fillDow: function(){
|
|
var dowCnt = this.o.weekStart,
|
|
html = '<tr>';
|
|
if(this.o.calendarWeeks){
|
|
var cell = '<th class="cw"> </th>';
|
|
html += cell;
|
|
this.picker.find('.datepicker-days thead tr:first-child').prepend(cell);
|
|
}
|
|
while (dowCnt < this.o.weekStart + 7) {
|
|
html += '<th class="dow">'+dates[this.o.language].daysMin[(dowCnt++)%7]+'</th>';
|
|
}
|
|
html += '</tr>';
|
|
this.picker.find('.datepicker-days thead').append(html);
|
|
},
|
|
|
|
fillMonths: function(){
|
|
var html = '',
|
|
i = 0;
|
|
while (i < 12) {
|
|
html += '<span class="month">'+dates[this.o.language].monthsShort[i++]+'</span>';
|
|
}
|
|
this.picker.find('.datepicker-months td').html(html);
|
|
},
|
|
|
|
setRange: function(range){
|
|
if (!range || !range.length)
|
|
delete this.range;
|
|
else
|
|
this.range = $.map(range, function(d){ return d.valueOf(); });
|
|
this.fill();
|
|
},
|
|
|
|
getClassNames: function(date){
|
|
var cls = [],
|
|
year = this.viewDate.getUTCFullYear(),
|
|
month = this.viewDate.getUTCMonth(),
|
|
currentDate = this.date.valueOf(),
|
|
today = new Date();
|
|
if (date.getUTCFullYear() < year || (date.getUTCFullYear() == year && date.getUTCMonth() < month)) {
|
|
cls.push('old');
|
|
} else if (date.getUTCFullYear() > year || (date.getUTCFullYear() == year && date.getUTCMonth() > month)) {
|
|
cls.push('new');
|
|
}
|
|
// Compare internal UTC date with local today, not UTC today
|
|
if (this.o.todayHighlight &&
|
|
date.getUTCFullYear() == today.getFullYear() &&
|
|
date.getUTCMonth() == today.getMonth() &&
|
|
date.getUTCDate() == today.getDate()) {
|
|
cls.push('today');
|
|
}
|
|
if (currentDate && date.valueOf() == currentDate) {
|
|
cls.push('active');
|
|
}
|
|
if (date.valueOf() < this.o.startDate || date.valueOf() > this.o.endDate ||
|
|
$.inArray(date.getUTCDay(), this.o.daysOfWeekDisabled) !== -1) {
|
|
cls.push('disabled');
|
|
}
|
|
if (this.range){
|
|
if (date > this.range[0] && date < this.range[this.range.length-1]){
|
|
cls.push('range');
|
|
}
|
|
if ($.inArray(date.valueOf(), this.range) != -1){
|
|
cls.push('selected');
|
|
}
|
|
}
|
|
return cls;
|
|
},
|
|
|
|
fill: function() {
|
|
var d = new Date(this.viewDate),
|
|
year = d.getUTCFullYear(),
|
|
month = d.getUTCMonth(),
|
|
startYear = this.o.startDate !== -Infinity ? this.o.startDate.getUTCFullYear() : -Infinity,
|
|
startMonth = this.o.startDate !== -Infinity ? this.o.startDate.getUTCMonth() : -Infinity,
|
|
endYear = this.o.endDate !== Infinity ? this.o.endDate.getUTCFullYear() : Infinity,
|
|
endMonth = this.o.endDate !== Infinity ? this.o.endDate.getUTCMonth() : Infinity,
|
|
currentDate = this.date && this.date.valueOf(),
|
|
tooltip;
|
|
this.picker.find('.datepicker-days thead th.datepicker-switch')
|
|
.text(dates[this.o.language].months[month]+' '+year);
|
|
this.picker.find('tfoot th.today')
|
|
.text(dates[this.o.language].today)
|
|
.toggle(this.o.todayBtn !== false);
|
|
this.picker.find('tfoot th.clear')
|
|
.text(dates[this.o.language].clear)
|
|
.toggle(this.o.clearBtn !== false);
|
|
this.updateNavArrows();
|
|
this.fillMonths();
|
|
var prevMonth = UTCDate(year, month-1, 28,0,0,0,0),
|
|
day = DPGlobal.getDaysInMonth(prevMonth.getUTCFullYear(), prevMonth.getUTCMonth());
|
|
prevMonth.setUTCDate(day);
|
|
prevMonth.setUTCDate(day - (prevMonth.getUTCDay() - this.o.weekStart + 7)%7);
|
|
var nextMonth = new Date(prevMonth);
|
|
nextMonth.setUTCDate(nextMonth.getUTCDate() + 42);
|
|
nextMonth = nextMonth.valueOf();
|
|
var html = [];
|
|
var clsName;
|
|
while(prevMonth.valueOf() < nextMonth) {
|
|
if (prevMonth.getUTCDay() == this.o.weekStart) {
|
|
html.push('<tr>');
|
|
if(this.o.calendarWeeks){
|
|
// ISO 8601: First week contains first thursday.
|
|
// ISO also states week starts on Monday, but we can be more abstract here.
|
|
var
|
|
// Start of current week: based on weekstart/current date
|
|
ws = new Date(+prevMonth + (this.o.weekStart - prevMonth.getUTCDay() - 7) % 7 * 864e5),
|
|
// Thursday of this week
|
|
th = new Date(+ws + (7 + 4 - ws.getUTCDay()) % 7 * 864e5),
|
|
// First Thursday of year, year from thursday
|
|
yth = new Date(+(yth = UTCDate(th.getUTCFullYear(), 0, 1)) + (7 + 4 - yth.getUTCDay())%7*864e5),
|
|
// Calendar week: ms between thursdays, div ms per day, div 7 days
|
|
calWeek = (th - yth) / 864e5 / 7 + 1;
|
|
html.push('<td class="cw">'+ calWeek +'</td>');
|
|
|
|
}
|
|
}
|
|
clsName = this.getClassNames(prevMonth);
|
|
clsName.push('day');
|
|
|
|
var before = this.o.beforeShowDay(prevMonth);
|
|
if (before === undefined)
|
|
before = {};
|
|
else if (typeof(before) === 'boolean')
|
|
before = {enabled: before};
|
|
else if (typeof(before) === 'string')
|
|
before = {classes: before};
|
|
if (before.enabled === false)
|
|
clsName.push('disabled');
|
|
if (before.classes)
|
|
clsName = clsName.concat(before.classes.split(/\s+/));
|
|
if (before.tooltip)
|
|
tooltip = before.tooltip;
|
|
|
|
clsName = $.unique(clsName);
|
|
html.push('<td class="'+clsName.join(' ')+'"' + (tooltip ? ' title="'+tooltip+'"' : '') + '>'+prevMonth.getUTCDate() + '</td>');
|
|
if (prevMonth.getUTCDay() == this.o.weekEnd) {
|
|
html.push('</tr>');
|
|
}
|
|
prevMonth.setUTCDate(prevMonth.getUTCDate()+1);
|
|
}
|
|
this.picker.find('.datepicker-days tbody').empty().append(html.join(''));
|
|
var currentYear = this.date && this.date.getUTCFullYear();
|
|
|
|
var months = this.picker.find('.datepicker-months')
|
|
.find('th:eq(1)')
|
|
.text(year)
|
|
.end()
|
|
.find('span').removeClass('active');
|
|
if (currentYear && currentYear == year) {
|
|
months.eq(this.date.getUTCMonth()).addClass('active');
|
|
}
|
|
if (year < startYear || year > endYear) {
|
|
months.addClass('disabled');
|
|
}
|
|
if (year == startYear) {
|
|
months.slice(0, startMonth).addClass('disabled');
|
|
}
|
|
if (year == endYear) {
|
|
months.slice(endMonth+1).addClass('disabled');
|
|
}
|
|
|
|
html = '';
|
|
year = parseInt(year/10, 10) * 10;
|
|
var yearCont = this.picker.find('.datepicker-years')
|
|
.find('th:eq(1)')
|
|
.text(year + '-' + (year + 9))
|
|
.end()
|
|
.find('td');
|
|
year -= 1;
|
|
for (var i = -1; i < 11; i++) {
|
|
html += '<span class="year'+(i == -1 ? ' old' : i == 10 ? ' new' : '')+(currentYear == year ? ' active' : '')+(year < startYear || year > endYear ? ' disabled' : '')+'">'+year+'</span>';
|
|
year += 1;
|
|
}
|
|
yearCont.html(html);
|
|
},
|
|
|
|
updateNavArrows: function() {
|
|
if (!this._allow_update) return;
|
|
|
|
var d = new Date(this.viewDate),
|
|
year = d.getUTCFullYear(),
|
|
month = d.getUTCMonth();
|
|
switch (this.viewMode) {
|
|
case 0:
|
|
if (this.o.startDate !== -Infinity && year <= this.o.startDate.getUTCFullYear() && month <= this.o.startDate.getUTCMonth()) {
|
|
this.picker.find('.prev').css({visibility: 'hidden'});
|
|
} else {
|
|
this.picker.find('.prev').css({visibility: 'visible'});
|
|
}
|
|
if (this.o.endDate !== Infinity && year >= this.o.endDate.getUTCFullYear() && month >= this.o.endDate.getUTCMonth()) {
|
|
this.picker.find('.next').css({visibility: 'hidden'});
|
|
} else {
|
|
this.picker.find('.next').css({visibility: 'visible'});
|
|
}
|
|
break;
|
|
case 1:
|
|
case 2:
|
|
if (this.o.startDate !== -Infinity && year <= this.o.startDate.getUTCFullYear()) {
|
|
this.picker.find('.prev').css({visibility: 'hidden'});
|
|
} else {
|
|
this.picker.find('.prev').css({visibility: 'visible'});
|
|
}
|
|
if (this.o.endDate !== Infinity && year >= this.o.endDate.getUTCFullYear()) {
|
|
this.picker.find('.next').css({visibility: 'hidden'});
|
|
} else {
|
|
this.picker.find('.next').css({visibility: 'visible'});
|
|
}
|
|
break;
|
|
}
|
|
},
|
|
|
|
click: function(e) {
|
|
e.preventDefault();
|
|
var target = $(e.target).closest('span, td, th');
|
|
if (target.length == 1) {
|
|
switch(target[0].nodeName.toLowerCase()) {
|
|
case 'th':
|
|
switch(target[0].className) {
|
|
case 'datepicker-switch':
|
|
this.showMode(1);
|
|
break;
|
|
case 'prev':
|
|
case 'next':
|
|
var dir = DPGlobal.modes[this.viewMode].navStep * (target[0].className == 'prev' ? -1 : 1);
|
|
switch(this.viewMode){
|
|
case 0:
|
|
this.viewDate = this.moveMonth(this.viewDate, dir);
|
|
break;
|
|
case 1:
|
|
case 2:
|
|
this.viewDate = this.moveYear(this.viewDate, dir);
|
|
break;
|
|
}
|
|
this.fill();
|
|
break;
|
|
case 'today':
|
|
var date = new Date();
|
|
date = UTCDate(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);
|
|
|
|
this.showMode(-2);
|
|
var which = this.o.todayBtn == 'linked' ? null : 'view';
|
|
this._setDate(date, which);
|
|
break;
|
|
case 'clear':
|
|
var element;
|
|
if (this.isInput)
|
|
element = this.element;
|
|
else if (this.component)
|
|
element = this.element.find('input');
|
|
if (element)
|
|
element.val("").change();
|
|
this._trigger('changeDate');
|
|
this.update();
|
|
if (this.o.autoclose)
|
|
this.hide();
|
|
break;
|
|
}
|
|
break;
|
|
case 'span':
|
|
if (!target.is('.disabled')) {
|
|
this.viewDate.setUTCDate(1);
|
|
if (target.is('.month')) {
|
|
var day = 1;
|
|
var month = target.parent().find('span').index(target);
|
|
var year = this.viewDate.getUTCFullYear();
|
|
this.viewDate.setUTCMonth(month);
|
|
this._trigger('changeMonth', this.viewDate);
|
|
if (this.o.minViewMode === 1) {
|
|
this._setDate(UTCDate(year, month, day,0,0,0,0));
|
|
}
|
|
} else {
|
|
var year = parseInt(target.text(), 10)||0;
|
|
var day = 1;
|
|
var month = 0;
|
|
this.viewDate.setUTCFullYear(year);
|
|
this._trigger('changeYear', this.viewDate);
|
|
if (this.o.minViewMode === 2) {
|
|
this._setDate(UTCDate(year, month, day,0,0,0,0));
|
|
}
|
|
}
|
|
this.showMode(-1);
|
|
this.fill();
|
|
}
|
|
break;
|
|
case 'td':
|
|
if (target.is('.day') && !target.is('.disabled')){
|
|
var day = parseInt(target.text(), 10)||1;
|
|
var year = this.viewDate.getUTCFullYear(),
|
|
month = this.viewDate.getUTCMonth();
|
|
if (target.is('.old')) {
|
|
if (month === 0) {
|
|
month = 11;
|
|
year -= 1;
|
|
} else {
|
|
month -= 1;
|
|
}
|
|
} else if (target.is('.new')) {
|
|
if (month == 11) {
|
|
month = 0;
|
|
year += 1;
|
|
} else {
|
|
month += 1;
|
|
}
|
|
}
|
|
this._setDate(UTCDate(year, month, day,0,0,0,0));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
_setDate: function(date, which){
|
|
if (!which || which == 'date')
|
|
this.date = new Date(date);
|
|
if (!which || which == 'view')
|
|
this.viewDate = new Date(date);
|
|
this.fill();
|
|
this.setValue();
|
|
this._trigger('changeDate');
|
|
var element;
|
|
if (this.isInput) {
|
|
element = this.element;
|
|
} else if (this.component){
|
|
element = this.element.find('input');
|
|
}
|
|
if (element) {
|
|
element.change();
|
|
if (this.o.autoclose && (!which || which == 'date')) {
|
|
this.hide();
|
|
}
|
|
}
|
|
},
|
|
|
|
moveMonth: function(date, dir){
|
|
if (!dir) return date;
|
|
var new_date = new Date(date.valueOf()),
|
|
day = new_date.getUTCDate(),
|
|
month = new_date.getUTCMonth(),
|
|
mag = Math.abs(dir),
|
|
new_month, test;
|
|
dir = dir > 0 ? 1 : -1;
|
|
if (mag == 1){
|
|
test = dir == -1
|
|
// If going back one month, make sure month is not current month
|
|
// (eg, Mar 31 -> Feb 31 == Feb 28, not Mar 02)
|
|
? function(){ return new_date.getUTCMonth() == month; }
|
|
// If going forward one month, make sure month is as expected
|
|
// (eg, Jan 31 -> Feb 31 == Feb 28, not Mar 02)
|
|
: function(){ return new_date.getUTCMonth() != new_month; };
|
|
new_month = month + dir;
|
|
new_date.setUTCMonth(new_month);
|
|
// Dec -> Jan (12) or Jan -> Dec (-1) -- limit expected date to 0-11
|
|
if (new_month < 0 || new_month > 11)
|
|
new_month = (new_month + 12) % 12;
|
|
} else {
|
|
// For magnitudes >1, move one month at a time...
|
|
for (var i=0; i<mag; i++)
|
|
// ...which might decrease the day (eg, Jan 31 to Feb 28, etc)...
|
|
new_date = this.moveMonth(new_date, dir);
|
|
// ...then reset the day, keeping it in the new month
|
|
new_month = new_date.getUTCMonth();
|
|
new_date.setUTCDate(day);
|
|
test = function(){ return new_month != new_date.getUTCMonth(); };
|
|
}
|
|
// Common date-resetting loop -- if date is beyond end of month, make it
|
|
// end of month
|
|
while (test()){
|
|
new_date.setUTCDate(--day);
|
|
new_date.setUTCMonth(new_month);
|
|
}
|
|
return new_date;
|
|
},
|
|
|
|
moveYear: function(date, dir){
|
|
return this.moveMonth(date, dir*12);
|
|
},
|
|
|
|
dateWithinRange: function(date){
|
|
return date >= this.o.startDate && date <= this.o.endDate;
|
|
},
|
|
|
|
keydown: function(e){
|
|
if (this.picker.is(':not(:visible)')){
|
|
if (e.keyCode == 27) // allow escape to hide and re-show picker
|
|
this.show();
|
|
return;
|
|
}
|
|
var dateChanged = false,
|
|
dir, day, month,
|
|
newDate, newViewDate;
|
|
switch(e.keyCode){
|
|
case 27: // escape
|
|
this.hide();
|
|
e.preventDefault();
|
|
break;
|
|
case 37: // left
|
|
case 39: // right
|
|
if (!this.o.keyboardNavigation) break;
|
|
dir = e.keyCode == 37 ? -1 : 1;
|
|
if (e.ctrlKey){
|
|
newDate = this.moveYear(this.date, dir);
|
|
newViewDate = this.moveYear(this.viewDate, dir);
|
|
} else if (e.shiftKey){
|
|
newDate = this.moveMonth(this.date, dir);
|
|
newViewDate = this.moveMonth(this.viewDate, dir);
|
|
} else {
|
|
newDate = new Date(this.date);
|
|
newDate.setUTCDate(this.date.getUTCDate() + dir);
|
|
newViewDate = new Date(this.viewDate);
|
|
newViewDate.setUTCDate(this.viewDate.getUTCDate() + dir);
|
|
}
|
|
if (this.dateWithinRange(newDate)){
|
|
this.date = newDate;
|
|
this.viewDate = newViewDate;
|
|
this.setValue();
|
|
this.update();
|
|
e.preventDefault();
|
|
dateChanged = true;
|
|
}
|
|
break;
|
|
case 38: // up
|
|
case 40: // down
|
|
if (!this.o.keyboardNavigation) break;
|
|
dir = e.keyCode == 38 ? -1 : 1;
|
|
if (e.ctrlKey){
|
|
newDate = this.moveYear(this.date, dir);
|
|
newViewDate = this.moveYear(this.viewDate, dir);
|
|
} else if (e.shiftKey){
|
|
newDate = this.moveMonth(this.date, dir);
|
|
newViewDate = this.moveMonth(this.viewDate, dir);
|
|
} else {
|
|
newDate = new Date(this.date);
|
|
newDate.setUTCDate(this.date.getUTCDate() + dir * 7);
|
|
newViewDate = new Date(this.viewDate);
|
|
newViewDate.setUTCDate(this.viewDate.getUTCDate() + dir * 7);
|
|
}
|
|
if (this.dateWithinRange(newDate)){
|
|
this.date = newDate;
|
|
this.viewDate = newViewDate;
|
|
this.setValue();
|
|
this.update();
|
|
e.preventDefault();
|
|
dateChanged = true;
|
|
}
|
|
break;
|
|
case 13: // enter
|
|
this.hide();
|
|
e.preventDefault();
|
|
break;
|
|
case 9: // tab
|
|
this.hide();
|
|
break;
|
|
}
|
|
if (dateChanged){
|
|
this._trigger('changeDate');
|
|
var element;
|
|
if (this.isInput) {
|
|
element = this.element;
|
|
} else if (this.component){
|
|
element = this.element.find('input');
|
|
}
|
|
if (element) {
|
|
element.change();
|
|
}
|
|
}
|
|
},
|
|
|
|
showMode: function(dir) {
|
|
if (dir) {
|
|
this.viewMode = Math.max(this.o.minViewMode, Math.min(2, this.viewMode + dir));
|
|
}
|
|
/*
|
|
vitalets: fixing bug of very special conditions:
|
|
jquery 1.7.1 + webkit + show inline datepicker in bootstrap popover.
|
|
Method show() does not set display css correctly and datepicker is not shown.
|
|
Changed to .css('display', 'block') solve the problem.
|
|
See https://github.com/vitalets/x-editable/issues/37
|
|
|
|
In jquery 1.7.2+ everything works fine.
|
|
*/
|
|
//this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).show();
|
|
this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).css('display', 'block');
|
|
this.updateNavArrows();
|
|
}
|
|
};
|
|
|
|
var DateRangePicker = function(element, options){
|
|
this.element = $(element);
|
|
this.inputs = $.map(options.inputs, function(i){ return i.jquery ? i[0] : i; });
|
|
delete options.inputs;
|
|
|
|
$(this.inputs)
|
|
.datepicker(options)
|
|
.bind('changeDate', $.proxy(this.dateUpdated, this));
|
|
|
|
this.pickers = $.map(this.inputs, function(i){ return $(i).data('datepicker'); });
|
|
this.updateDates();
|
|
};
|
|
DateRangePicker.prototype = {
|
|
updateDates: function(){
|
|
this.dates = $.map(this.pickers, function(i){ return i.date; });
|
|
this.updateRanges();
|
|
},
|
|
updateRanges: function(){
|
|
var range = $.map(this.dates, function(d){ return d.valueOf(); });
|
|
$.each(this.pickers, function(i, p){
|
|
p.setRange(range);
|
|
});
|
|
},
|
|
dateUpdated: function(e){
|
|
var dp = $(e.target).data('datepicker'),
|
|
new_date = dp.getUTCDate(),
|
|
i = $.inArray(e.target, this.inputs),
|
|
l = this.inputs.length;
|
|
if (i == -1) return;
|
|
|
|
if (new_date < this.dates[i]){
|
|
// Date being moved earlier/left
|
|
while (i>=0 && new_date < this.dates[i]){
|
|
this.pickers[i--].setUTCDate(new_date);
|
|
}
|
|
}
|
|
else if (new_date > this.dates[i]){
|
|
// Date being moved later/right
|
|
while (i<l && new_date > this.dates[i]){
|
|
this.pickers[i++].setUTCDate(new_date);
|
|
}
|
|
}
|
|
this.updateDates();
|
|
},
|
|
remove: function(){
|
|
$.map(this.pickers, function(p){ p.remove(); });
|
|
delete this.element.data().datepicker;
|
|
}
|
|
};
|
|
|
|
function opts_from_el(el, prefix){
|
|
// Derive options from element data-attrs
|
|
var data = $(el).data(),
|
|
out = {}, inkey,
|
|
replace = new RegExp('^' + prefix.toLowerCase() + '([A-Z])'),
|
|
prefix = new RegExp('^' + prefix.toLowerCase());
|
|
for (var key in data)
|
|
if (prefix.test(key)){
|
|
inkey = key.replace(replace, function(_,a){ return a.toLowerCase(); });
|
|
out[inkey] = data[key];
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function opts_from_locale(lang){
|
|
// Derive options from locale plugins
|
|
var out = {};
|
|
// Check if "de-DE" style date is available, if not language should
|
|
// fallback to 2 letter code eg "de"
|
|
if (!dates[lang]) {
|
|
lang = lang.split('-')[0]
|
|
if (!dates[lang])
|
|
return;
|
|
}
|
|
var d = dates[lang];
|
|
$.each(locale_opts, function(i,k){
|
|
if (k in d)
|
|
out[k] = d[k];
|
|
});
|
|
return out;
|
|
}
|
|
|
|
var old = $.fn.datepicker;
|
|
var datepicker = $.fn.datepicker = function ( option ) {
|
|
var args = Array.apply(null, arguments);
|
|
args.shift();
|
|
var internal_return,
|
|
this_return;
|
|
this.each(function () {
|
|
var $this = $(this),
|
|
data = $this.data('datepicker'),
|
|
options = typeof option == 'object' && option;
|
|
if (!data) {
|
|
var elopts = opts_from_el(this, 'date'),
|
|
// Preliminary otions
|
|
xopts = $.extend({}, defaults, elopts, options),
|
|
locopts = opts_from_locale(xopts.language),
|
|
// Options priority: js args, data-attrs, locales, defaults
|
|
opts = $.extend({}, defaults, locopts, elopts, options);
|
|
if ($this.is('.input-daterange') || opts.inputs){
|
|
var ropts = {
|
|
inputs: opts.inputs || $this.find('input').toArray()
|
|
};
|
|
$this.data('datepicker', (data = new DateRangePicker(this, $.extend(opts, ropts))));
|
|
}
|
|
else{
|
|
$this.data('datepicker', (data = new Datepicker(this, opts)));
|
|
}
|
|
}
|
|
if (typeof option == 'string' && typeof data[option] == 'function') {
|
|
internal_return = data[option].apply(data, args);
|
|
if (internal_return !== undefined)
|
|
return false;
|
|
}
|
|
});
|
|
if (internal_return !== undefined)
|
|
return internal_return;
|
|
else
|
|
return this;
|
|
};
|
|
|
|
var defaults = $.fn.datepicker.defaults = {
|
|
autoclose: false,
|
|
beforeShowDay: $.noop,
|
|
calendarWeeks: false,
|
|
clearBtn: false,
|
|
daysOfWeekDisabled: [],
|
|
endDate: Infinity,
|
|
forceParse: true,
|
|
format: 'mm/dd/yyyy',
|
|
keyboardNavigation: true,
|
|
language: 'en',
|
|
minViewMode: 0,
|
|
rtl: false,
|
|
startDate: -Infinity,
|
|
startView: 0,
|
|
todayBtn: false,
|
|
todayHighlight: false,
|
|
weekStart: 0
|
|
};
|
|
var locale_opts = $.fn.datepicker.locale_opts = [
|
|
'format',
|
|
'rtl',
|
|
'weekStart'
|
|
];
|
|
$.fn.datepicker.Constructor = Datepicker;
|
|
var dates = $.fn.datepicker.dates = {
|
|
en: {
|
|
days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
|
|
daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
|
|
daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"],
|
|
months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
|
|
monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
|
|
today: "Today",
|
|
clear: "Clear"
|
|
}
|
|
};
|
|
|
|
var DPGlobal = {
|
|
modes: [
|
|
{
|
|
clsName: 'days',
|
|
navFnc: 'Month',
|
|
navStep: 1
|
|
},
|
|
{
|
|
clsName: 'months',
|
|
navFnc: 'FullYear',
|
|
navStep: 1
|
|
},
|
|
{
|
|
clsName: 'years',
|
|
navFnc: 'FullYear',
|
|
navStep: 10
|
|
}],
|
|
isLeapYear: function (year) {
|
|
return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0));
|
|
},
|
|
getDaysInMonth: function (year, month) {
|
|
return [31, (DPGlobal.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month];
|
|
},
|
|
validParts: /dd?|DD?|mm?|MM?|yy(?:yy)?/g,
|
|
nonpunctuation: /[^ -\/:-@\[\u3400-\u9fff-`{-~\t\n\r]+/g,
|
|
parseFormat: function(format){
|
|
// IE treats \0 as a string end in inputs (truncating the value),
|
|
// so it's a bad format delimiter, anyway
|
|
var separators = format.replace(this.validParts, '\0').split('\0'),
|
|
parts = format.match(this.validParts);
|
|
if (!separators || !separators.length || !parts || parts.length === 0){
|
|
throw new Error("Invalid date format.");
|
|
}
|
|
return {separators: separators, parts: parts};
|
|
},
|
|
parseDate: function(date, format, language) {
|
|
if (date instanceof Date) return date;
|
|
if (typeof format === 'string')
|
|
format = DPGlobal.parseFormat(format);
|
|
if (/^[\-+]\d+[dmwy]([\s,]+[\-+]\d+[dmwy])*$/.test(date)) {
|
|
var part_re = /([\-+]\d+)([dmwy])/,
|
|
parts = date.match(/([\-+]\d+)([dmwy])/g),
|
|
part, dir;
|
|
date = new Date();
|
|
for (var i=0; i<parts.length; i++) {
|
|
part = part_re.exec(parts[i]);
|
|
dir = parseInt(part[1]);
|
|
switch(part[2]){
|
|
case 'd':
|
|
date.setUTCDate(date.getUTCDate() + dir);
|
|
break;
|
|
case 'm':
|
|
date = Datepicker.prototype.moveMonth.call(Datepicker.prototype, date, dir);
|
|
break;
|
|
case 'w':
|
|
date.setUTCDate(date.getUTCDate() + dir * 7);
|
|
break;
|
|
case 'y':
|
|
date = Datepicker.prototype.moveYear.call(Datepicker.prototype, date, dir);
|
|
break;
|
|
}
|
|
}
|
|
return UTCDate(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0);
|
|
}
|
|
var parts = date && date.match(this.nonpunctuation) || [],
|
|
date = new Date(),
|
|
parsed = {},
|
|
setters_order = ['yyyy', 'yy', 'M', 'MM', 'm', 'mm', 'd', 'dd'],
|
|
setters_map = {
|
|
yyyy: function(d,v){ return d.setUTCFullYear(v); },
|
|
yy: function(d,v){ return d.setUTCFullYear(2000+v); },
|
|
m: function(d,v){
|
|
v -= 1;
|
|
while (v<0) v += 12;
|
|
v %= 12;
|
|
d.setUTCMonth(v);
|
|
while (d.getUTCMonth() != v)
|
|
d.setUTCDate(d.getUTCDate()-1);
|
|
return d;
|
|
},
|
|
d: function(d,v){ return d.setUTCDate(v); }
|
|
},
|
|
val, filtered, part;
|
|
setters_map['M'] = setters_map['MM'] = setters_map['mm'] = setters_map['m'];
|
|
setters_map['dd'] = setters_map['d'];
|
|
date = UTCDate(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);
|
|
var fparts = format.parts.slice();
|
|
// Remove noop parts
|
|
if (parts.length != fparts.length) {
|
|
fparts = $(fparts).filter(function(i,p){
|
|
return $.inArray(p, setters_order) !== -1;
|
|
}).toArray();
|
|
}
|
|
// Process remainder
|
|
if (parts.length == fparts.length) {
|
|
for (var i=0, cnt = fparts.length; i < cnt; i++) {
|
|
val = parseInt(parts[i], 10);
|
|
part = fparts[i];
|
|
if (isNaN(val)) {
|
|
switch(part) {
|
|
case 'MM':
|
|
filtered = $(dates[language].months).filter(function(){
|
|
var m = this.slice(0, parts[i].length),
|
|
p = parts[i].slice(0, m.length);
|
|
return m == p;
|
|
});
|
|
val = $.inArray(filtered[0], dates[language].months) + 1;
|
|
break;
|
|
case 'M':
|
|
filtered = $(dates[language].monthsShort).filter(function(){
|
|
var m = this.slice(0, parts[i].length),
|
|
p = parts[i].slice(0, m.length);
|
|
return m == p;
|
|
});
|
|
val = $.inArray(filtered[0], dates[language].monthsShort) + 1;
|
|
break;
|
|
}
|
|
}
|
|
parsed[part] = val;
|
|
}
|
|
for (var i=0, s; i<setters_order.length; i++){
|
|
s = setters_order[i];
|
|
if (s in parsed && !isNaN(parsed[s]))
|
|
setters_map[s](date, parsed[s]);
|
|
}
|
|
}
|
|
return date;
|
|
},
|
|
formatDate: function(date, format, language){
|
|
if (typeof format === 'string')
|
|
format = DPGlobal.parseFormat(format);
|
|
var val = {
|
|
d: date.getUTCDate(),
|
|
D: dates[language].daysShort[date.getUTCDay()],
|
|
DD: dates[language].days[date.getUTCDay()],
|
|
m: date.getUTCMonth() + 1,
|
|
M: dates[language].monthsShort[date.getUTCMonth()],
|
|
MM: dates[language].months[date.getUTCMonth()],
|
|
yy: date.getUTCFullYear().toString().substring(2),
|
|
yyyy: date.getUTCFullYear()
|
|
};
|
|
val.dd = (val.d < 10 ? '0' : '') + val.d;
|
|
val.mm = (val.m < 10 ? '0' : '') + val.m;
|
|
var date = [],
|
|
seps = $.extend([], format.separators);
|
|
for (var i=0, cnt = format.parts.length; i <= cnt; i++) {
|
|
if (seps.length)
|
|
date.push(seps.shift());
|
|
date.push(val[format.parts[i]]);
|
|
}
|
|
return date.join('');
|
|
},
|
|
headTemplate: '<thead>'+
|
|
'<tr>'+
|
|
'<th class="prev"><i class="icon-arrow-left"/></th>'+
|
|
'<th colspan="5" class="datepicker-switch"></th>'+
|
|
'<th class="next"><i class="icon-arrow-right"/></th>'+
|
|
'</tr>'+
|
|
'</thead>',
|
|
contTemplate: '<tbody><tr><td colspan="7"></td></tr></tbody>',
|
|
footTemplate: '<tfoot><tr><th colspan="7" class="today"></th></tr><tr><th colspan="7" class="clear"></th></tr></tfoot>'
|
|
};
|
|
DPGlobal.template = '<div class="datepicker">'+
|
|
'<div class="datepicker-days">'+
|
|
'<table class=" table-condensed">'+
|
|
DPGlobal.headTemplate+
|
|
'<tbody></tbody>'+
|
|
DPGlobal.footTemplate+
|
|
'</table>'+
|
|
'</div>'+
|
|
'<div class="datepicker-months">'+
|
|
'<table class="table-condensed">'+
|
|
DPGlobal.headTemplate+
|
|
DPGlobal.contTemplate+
|
|
DPGlobal.footTemplate+
|
|
'</table>'+
|
|
'</div>'+
|
|
'<div class="datepicker-years">'+
|
|
'<table class="table-condensed">'+
|
|
DPGlobal.headTemplate+
|
|
DPGlobal.contTemplate+
|
|
DPGlobal.footTemplate+
|
|
'</table>'+
|
|
'</div>'+
|
|
'</div>';
|
|
|
|
$.fn.datepicker.DPGlobal = DPGlobal;
|
|
|
|
|
|
/* DATEPICKER NO CONFLICT
|
|
* =================== */
|
|
|
|
$.fn.datepicker.noConflict = function(){
|
|
$.fn.datepicker = old;
|
|
return this;
|
|
};
|
|
|
|
|
|
/* DATEPICKER DATA-API
|
|
* ================== */
|
|
|
|
$(document).on(
|
|
'focus.datepicker.data-api click.datepicker.data-api',
|
|
'[data-provide="datepicker"]',
|
|
function(e){
|
|
var $this = $(this);
|
|
if ($this.data('datepicker')) return;
|
|
e.preventDefault();
|
|
// component click requires us to explicitly show it
|
|
datepicker.call($this, 'show');
|
|
}
|
|
);
|
|
$(function(){
|
|
//$('[data-provide="datepicker-inline"]').datepicker();
|
|
//vit: changed to support noConflict()
|
|
datepicker.call($('[data-provide="datepicker-inline"]'));
|
|
});
|
|
|
|
}( window.jQuery ));
|
|
|
|
/**
|
|
Bootstrap-datepicker.
|
|
Description and examples: https://github.com/eternicode/bootstrap-datepicker.
|
|
For **i18n** you should include js file from here: https://github.com/eternicode/bootstrap-datepicker/tree/master/js/locales
|
|
and set `language` option.
|
|
Since 1.4.0 date has different appearance in **popup** and **inline** modes.
|
|
|
|
@class date
|
|
@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: {
|
|
weekStart: 1
|
|
}
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
**/
|
|
(function ($) {
|
|
"use strict";
|
|
|
|
//store bootstrap-datepicker as bdateicker to exclude conflict with jQuery UI one
|
|
$.fn.bdatepicker = $.fn.datepicker.noConflict();
|
|
if(!$.fn.datepicker) { //if there were no other datepickers, keep also original name
|
|
$.fn.datepicker = $.fn.bdatepicker;
|
|
}
|
|
|
|
var Date = function (options) {
|
|
this.init('date', options, Date.defaults);
|
|
this.initPicker(options, Date.defaults);
|
|
};
|
|
|
|
$.fn.editableutils.inherit(Date, $.fn.editabletypes.abstractinput);
|
|
|
|
$.extend(Date.prototype, {
|
|
initPicker: function(options, defaults) {
|
|
//'format' is set directly from settings or data-* attributes
|
|
|
|
//by default viewformat equals to format
|
|
if(!this.options.viewformat) {
|
|
this.options.viewformat = this.options.format;
|
|
}
|
|
|
|
//try parse datepicker config defined as json string in data-datepicker
|
|
options.datepicker = $.fn.editableutils.tryParseJson(options.datepicker, true);
|
|
|
|
//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, {
|
|
format: this.options.viewformat
|
|
});
|
|
|
|
//language
|
|
this.options.datepicker.language = this.options.datepicker.language || 'en';
|
|
|
|
//store DPglobal
|
|
this.dpg = $.fn.bdatepicker.DPGlobal;
|
|
|
|
//store parsed formats
|
|
this.parsedFormat = this.dpg.parseFormat(this.options.format);
|
|
this.parsedViewFormat = this.dpg.parseFormat(this.options.viewformat);
|
|
},
|
|
|
|
render: function () {
|
|
this.$input.bdatepicker(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 = value ? this.dpg.formatDate(value, this.parsedViewFormat, this.options.datepicker.language) : '';
|
|
Date.superclass.value2html.call(this, text, element);
|
|
},
|
|
|
|
html2value: function(html) {
|
|
return this.parseDate(html, this.parsedViewFormat);
|
|
},
|
|
|
|
value2str: function(value) {
|
|
return value ? this.dpg.formatDate(value, this.parsedFormat, this.options.datepicker.language) : '';
|
|
},
|
|
|
|
str2value: function(str) {
|
|
return this.parseDate(str, this.parsedFormat);
|
|
},
|
|
|
|
value2submit: function(value) {
|
|
return this.value2str(value);
|
|
},
|
|
|
|
value2input: function(value) {
|
|
this.$input.bdatepicker('update', value);
|
|
},
|
|
|
|
input2value: function() {
|
|
return this.$input.data('datepicker').date;
|
|
},
|
|
|
|
activate: function() {
|
|
},
|
|
|
|
clear: function() {
|
|
this.$input.data('datepicker').date = null;
|
|
this.$input.find('.active').removeClass('active');
|
|
if(!this.options.showbuttons) {
|
|
this.$input.closest('form').submit();
|
|
}
|
|
},
|
|
|
|
autosubmit: function() {
|
|
this.$input.on('mouseup', '.day', function(e){
|
|
if($(e.currentTarget).is('.old') || $(e.currentTarget).is('.new')) {
|
|
return;
|
|
}
|
|
var $form = $(this).closest('form');
|
|
setTimeout(function() {
|
|
$form.submit();
|
|
}, 200);
|
|
});
|
|
//changedate is not suitable as it triggered when showing datepicker. see #149
|
|
/*
|
|
this.$input.on('changeDate', function(e){
|
|
var $form = $(this).closest('form');
|
|
setTimeout(function() {
|
|
$form.submit();
|
|
}, 200);
|
|
});
|
|
*/
|
|
},
|
|
|
|
/*
|
|
For incorrect date bootstrap-datepicker returns current date that is not suitable
|
|
for datefield.
|
|
This function returns null for incorrect date.
|
|
*/
|
|
parseDate: function(str, format) {
|
|
var date = null, formattedBack;
|
|
if(str) {
|
|
date = this.dpg.parseDate(str, format, this.options.datepicker.language);
|
|
if(typeof str === 'string') {
|
|
formattedBack = this.dpg.formatDate(date, format, this.options.datepicker.language);
|
|
if(str !== formattedBack) {
|
|
date = null;
|
|
}
|
|
}
|
|
}
|
|
return date;
|
|
}
|
|
|
|
});
|
|
|
|
Date.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
|
|
/**
|
|
@property tpl
|
|
@default <div></div>
|
|
**/
|
|
tpl:'<div class="editable-date well"></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>
|
|
Possible tokens are: <code>d, dd, m, mm, yy, yyyy</code>
|
|
|
|
@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://bootstrap-datepicker.readthedocs.org/en/latest/options.html
|
|
|
|
@property datepicker
|
|
@type object
|
|
@default {
|
|
weekStart: 0,
|
|
startView: 0,
|
|
minViewMode: 0,
|
|
autoclose: false
|
|
}
|
|
**/
|
|
datepicker:{
|
|
weekStart: 0,
|
|
startView: 0,
|
|
minViewMode: 0,
|
|
autoclose: false
|
|
},
|
|
/**
|
|
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.date = Date;
|
|
|
|
}(window.jQuery));
|
|
|
|
/**
|
|
Bootstrap datefield input - modification for inline mode.
|
|
Shows normal <input type="text"> and binds popup datepicker.
|
|
Automatically shown in inline mode.
|
|
|
|
@class datefield
|
|
@extends date
|
|
|
|
@since 1.4.0
|
|
**/
|
|
(function ($) {
|
|
"use strict";
|
|
|
|
var DateField = function (options) {
|
|
this.init('datefield', options, DateField.defaults);
|
|
this.initPicker(options, DateField.defaults);
|
|
};
|
|
|
|
$.fn.editableutils.inherit(DateField, $.fn.editabletypes.date);
|
|
|
|
$.extend(DateField.prototype, {
|
|
render: function () {
|
|
this.$input = this.$tpl.find('input');
|
|
this.setClass();
|
|
this.setAttr('placeholder');
|
|
|
|
//bootstrap-datepicker is set `bdateicker` to exclude conflict with jQuery UI one. (in date.js)
|
|
this.$tpl.bdatepicker(this.options.datepicker);
|
|
|
|
//need to disable original event handlers
|
|
this.$input.off('focus keydown');
|
|
|
|
//update value of datepicker
|
|
this.$input.keyup($.proxy(function(){
|
|
this.$tpl.removeData('date');
|
|
this.$tpl.bdatepicker('update');
|
|
}, this));
|
|
|
|
},
|
|
|
|
value2input: function(value) {
|
|
this.$input.val(value ? this.dpg.formatDate(value, this.parsedViewFormat, this.options.datepicker.language) : '');
|
|
this.$tpl.bdatepicker('update');
|
|
},
|
|
|
|
input2value: function() {
|
|
return this.html2value(this.$input.val());
|
|
},
|
|
|
|
activate: function() {
|
|
$.fn.editabletypes.text.prototype.activate.call(this);
|
|
},
|
|
|
|
autosubmit: function() {
|
|
//reset autosubmit to empty
|
|
}
|
|
});
|
|
|
|
DateField.defaults = $.extend({}, $.fn.editabletypes.date.defaults, {
|
|
/**
|
|
@property tpl
|
|
**/
|
|
tpl:'<div class="input-append date"><input type="text"/><span class="add-on"><i class="icon-th"></i></span></div>',
|
|
/**
|
|
@property inputclass
|
|
@default 'input-small'
|
|
**/
|
|
inputclass: 'input-small',
|
|
|
|
/* datepicker config */
|
|
datepicker: {
|
|
weekStart: 0,
|
|
startView: 0,
|
|
minViewMode: 0,
|
|
autoclose: true
|
|
}
|
|
});
|
|
|
|
$.fn.editabletypes.datefield = DateField;
|
|
|
|
}(window.jQuery));
|
|
/**
|
|
Bootstrap-datetimepicker.
|
|
Based on [smalot bootstrap-datetimepicker plugin](https://github.com/smalot/bootstrap-datetimepicker).
|
|
Before usage you should manually include dependent js and css:
|
|
|
|
<link href="css/datetimepicker.css" rel="stylesheet" type="text/css"></link>
|
|
<script src="js/bootstrap-datetimepicker.js"></script>
|
|
|
|
For **i18n** you should include js file from here: https://github.com/smalot/bootstrap-datetimepicker/tree/master/js/locales
|
|
and set `language` option.
|
|
|
|
@class datetime
|
|
@extends abstractinput
|
|
@final
|
|
@since 1.4.4
|
|
@example
|
|
<a href="#" id="last_seen" data-type="datetime" data-pk="1" data-url="/post" title="Select date & time">15/03/2013 12:45</a>
|
|
<script>
|
|
$(function(){
|
|
$('#last_seen').editable({
|
|
format: 'yyyy-mm-dd hh:ii',
|
|
viewformat: 'dd/mm/yyyy hh:ii',
|
|
datetimepicker: {
|
|
weekStart: 1
|
|
}
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
**/
|
|
(function ($) {
|
|
"use strict";
|
|
|
|
var DateTime = function (options) {
|
|
this.init('datetime', options, DateTime.defaults);
|
|
this.initPicker(options, DateTime.defaults);
|
|
};
|
|
|
|
$.fn.editableutils.inherit(DateTime, $.fn.editabletypes.abstractinput);
|
|
|
|
$.extend(DateTime.prototype, {
|
|
initPicker: function(options, defaults) {
|
|
//'format' is set directly from settings or data-* attributes
|
|
|
|
//by default viewformat equals to format
|
|
if(!this.options.viewformat) {
|
|
this.options.viewformat = this.options.format;
|
|
}
|
|
|
|
//try parse datetimepicker config defined as json string in data-datetimepicker
|
|
options.datetimepicker = $.fn.editableutils.tryParseJson(options.datetimepicker, true);
|
|
|
|
//overriding datetimepicker config (as by default jQuery extend() is not recursive)
|
|
//since 1.4 datetimepicker internally uses viewformat instead of format. Format is for submit only
|
|
this.options.datetimepicker = $.extend({}, defaults.datetimepicker, options.datetimepicker, {
|
|
format: this.options.viewformat
|
|
});
|
|
|
|
//language
|
|
this.options.datetimepicker.language = this.options.datetimepicker.language || 'en';
|
|
|
|
//store DPglobal
|
|
this.dpg = $.fn.datetimepicker.DPGlobal;
|
|
|
|
//store parsed formats
|
|
this.parsedFormat = this.dpg.parseFormat(this.options.format, this.options.formatType);
|
|
this.parsedViewFormat = this.dpg.parseFormat(this.options.viewformat, this.options.formatType);
|
|
},
|
|
|
|
render: function () {
|
|
this.$input.datetimepicker(this.options.datetimepicker);
|
|
|
|
//adjust container position when viewMode changes
|
|
//see https://github.com/smalot/bootstrap-datetimepicker/pull/80
|
|
this.$input.on('changeMode', function(e) {
|
|
var f = $(this).closest('form').parent();
|
|
//timeout here, otherwise container changes position before form has new size
|
|
setTimeout(function(){
|
|
f.triggerHandler('resize');
|
|
}, 0);
|
|
});
|
|
|
|
//"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) {
|
|
//formatDate works with UTCDate!
|
|
var text = value ? this.dpg.formatDate(this.toUTC(value), this.parsedViewFormat, this.options.datetimepicker.language, this.options.formatType) : '';
|
|
if(element) {
|
|
DateTime.superclass.value2html.call(this, text, element);
|
|
} else {
|
|
return text;
|
|
}
|
|
},
|
|
|
|
html2value: function(html) {
|
|
//parseDate return utc date!
|
|
var value = this.parseDate(html, this.parsedViewFormat);
|
|
return value ? this.fromUTC(value) : null;
|
|
},
|
|
|
|
value2str: function(value) {
|
|
//formatDate works with UTCDate!
|
|
return value ? this.dpg.formatDate(this.toUTC(value), this.parsedFormat, this.options.datetimepicker.language, this.options.formatType) : '';
|
|
},
|
|
|
|
str2value: function(str) {
|
|
//parseDate return utc date!
|
|
var value = this.parseDate(str, this.parsedFormat);
|
|
return value ? this.fromUTC(value) : null;
|
|
},
|
|
|
|
value2submit: function(value) {
|
|
return this.value2str(value);
|
|
},
|
|
|
|
value2input: function(value) {
|
|
if(value) {
|
|
this.$input.data('datetimepicker').setDate(value);
|
|
}
|
|
},
|
|
|
|
input2value: function() {
|
|
//date may be cleared, in that case getDate() triggers error
|
|
var dt = this.$input.data('datetimepicker');
|
|
return dt.date ? dt.getDate() : null;
|
|
},
|
|
|
|
activate: function() {
|
|
},
|
|
|
|
clear: function() {
|
|
this.$input.data('datetimepicker').date = null;
|
|
this.$input.find('.active').removeClass('active');
|
|
if(!this.options.showbuttons) {
|
|
this.$input.closest('form').submit();
|
|
}
|
|
},
|
|
|
|
autosubmit: function() {
|
|
this.$input.on('mouseup', '.minute', function(e){
|
|
var $form = $(this).closest('form');
|
|
setTimeout(function() {
|
|
$form.submit();
|
|
}, 200);
|
|
});
|
|
},
|
|
|
|
//convert date from local to utc
|
|
toUTC: function(value) {
|
|
return value ? new Date(value.valueOf() - value.getTimezoneOffset() * 60000) : value;
|
|
},
|
|
|
|
//convert date from utc to local
|
|
fromUTC: function(value) {
|
|
return value ? new Date(value.valueOf() + value.getTimezoneOffset() * 60000) : value;
|
|
},
|
|
|
|
/*
|
|
For incorrect date bootstrap-datetimepicker returns current date that is not suitable
|
|
for datetimefield.
|
|
This function returns null for incorrect date.
|
|
*/
|
|
parseDate: function(str, format) {
|
|
var date = null, formattedBack;
|
|
if(str) {
|
|
date = this.dpg.parseDate(str, format, this.options.datetimepicker.language, this.options.formatType);
|
|
if(typeof str === 'string') {
|
|
formattedBack = this.dpg.formatDate(date, format, this.options.datetimepicker.language, this.options.formatType);
|
|
if(str !== formattedBack) {
|
|
date = null;
|
|
}
|
|
}
|
|
}
|
|
return date;
|
|
}
|
|
|
|
});
|
|
|
|
DateTime.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
|
|
/**
|
|
@property tpl
|
|
@default <div></div>
|
|
**/
|
|
tpl:'<div class="editable-date well"></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>
|
|
Possible tokens are: <code>d, dd, m, mm, yy, yyyy, h, i</code>
|
|
|
|
@property format
|
|
@type string
|
|
@default yyyy-mm-dd hh:ii
|
|
**/
|
|
format:'yyyy-mm-dd hh:ii',
|
|
formatType:'standard',
|
|
/**
|
|
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 datetimepicker.
|
|
Full list of options: https://github.com/smalot/bootstrap-datetimepicker
|
|
|
|
@property datetimepicker
|
|
@type object
|
|
@default { }
|
|
**/
|
|
datetimepicker:{
|
|
todayHighlight: false,
|
|
autoclose: false
|
|
},
|
|
/**
|
|
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.datetime = DateTime;
|
|
|
|
}(window.jQuery));
|
|
/**
|
|
Bootstrap datetimefield input - datetime input for inline mode.
|
|
Shows normal <input type="text"> and binds popup datetimepicker.
|
|
Automatically shown in inline mode.
|
|
|
|
@class datetimefield
|
|
@extends datetime
|
|
|
|
**/
|
|
(function ($) {
|
|
"use strict";
|
|
|
|
var DateTimeField = function (options) {
|
|
this.init('datetimefield', options, DateTimeField.defaults);
|
|
this.initPicker(options, DateTimeField.defaults);
|
|
};
|
|
|
|
$.fn.editableutils.inherit(DateTimeField, $.fn.editabletypes.datetime);
|
|
|
|
$.extend(DateTimeField.prototype, {
|
|
render: function () {
|
|
this.$input = this.$tpl.find('input');
|
|
this.setClass();
|
|
this.setAttr('placeholder');
|
|
|
|
this.$tpl.datetimepicker(this.options.datetimepicker);
|
|
|
|
//need to disable original event handlers
|
|
this.$input.off('focus keydown');
|
|
|
|
//update value of datepicker
|
|
this.$input.keyup($.proxy(function(){
|
|
this.$tpl.removeData('date');
|
|
this.$tpl.datetimepicker('update');
|
|
}, this));
|
|
|
|
},
|
|
|
|
value2input: function(value) {
|
|
this.$input.val(this.value2html(value));
|
|
this.$tpl.datetimepicker('update');
|
|
},
|
|
|
|
input2value: function() {
|
|
return this.html2value(this.$input.val());
|
|
},
|
|
|
|
activate: function() {
|
|
$.fn.editabletypes.text.prototype.activate.call(this);
|
|
},
|
|
|
|
autosubmit: function() {
|
|
//reset autosubmit to empty
|
|
}
|
|
});
|
|
|
|
DateTimeField.defaults = $.extend({}, $.fn.editabletypes.datetime.defaults, {
|
|
/**
|
|
@property tpl
|
|
**/
|
|
tpl:'<div class="input-append date"><input type="text"/><span class="add-on"><i class="icon-th"></i></span></div>',
|
|
/**
|
|
@property inputclass
|
|
@default 'input-medium'
|
|
**/
|
|
inputclass: 'input-medium',
|
|
|
|
/* datetimepicker config */
|
|
datetimepicker:{
|
|
todayHighlight: false,
|
|
autoclose: true
|
|
}
|
|
});
|
|
|
|
$.fn.editabletypes.datetimefield = DateTimeField;
|
|
|
|
}(window.jQuery));
|
|
/**
|
|
Typeahead input (bootstrap 2 only). Based on Twitter Bootstrap 2 [typeahead](http://getbootstrap.com/2.3.2/javascript.html#typeahead).
|
|
Depending on `source` format typeahead operates in two modes:
|
|
|
|
* **strings**:
|
|
When `source` defined as array of strings, e.g. `['text1', 'text2', 'text3' ...]`.
|
|
User can submit one of these strings or any text entered in input (even if it is not matching source).
|
|
|
|
* **objects**:
|
|
When `source` defined as array of objects, e.g. `[{value: 1, text: "text1"}, {value: 2, text: "text2"}, ...]`.
|
|
User can submit only values that are in source (otherwise `null` is submitted). This is more like *dropdown* behavior.
|
|
|
|
@class typeahead
|
|
@extends list
|
|
@since 1.4.1
|
|
@final
|
|
@example
|
|
<a href="#" id="country" data-type="typeahead" data-pk="1" data-url="/post" data-title="Input country"></a>
|
|
<script>
|
|
$(function(){
|
|
$('#country').editable({
|
|
value: 'ru',
|
|
source: [
|
|
{value: 'gb', text: 'Great Britain'},
|
|
{value: 'us', text: 'United States'},
|
|
{value: 'ru', text: 'Russia'}
|
|
]
|
|
});
|
|
});
|
|
</script>
|
|
**/
|
|
(function ($) {
|
|
"use strict";
|
|
|
|
var Constructor = function (options) {
|
|
this.init('typeahead', options, Constructor.defaults);
|
|
|
|
//overriding objects in config (as by default jQuery extend() is not recursive)
|
|
this.options.typeahead = $.extend({}, Constructor.defaults.typeahead, {
|
|
//set default methods for typeahead to work with objects
|
|
matcher: this.matcher,
|
|
sorter: this.sorter,
|
|
highlighter: this.highlighter,
|
|
updater: this.updater
|
|
}, options.typeahead);
|
|
};
|
|
|
|
$.fn.editableutils.inherit(Constructor, $.fn.editabletypes.list);
|
|
|
|
$.extend(Constructor.prototype, {
|
|
renderList: function() {
|
|
this.$input = this.$tpl.is('input') ? this.$tpl : this.$tpl.find('input[type="text"]');
|
|
|
|
//set source of typeahead
|
|
this.options.typeahead.source = this.sourceData;
|
|
|
|
//apply typeahead
|
|
this.$input.typeahead(this.options.typeahead);
|
|
|
|
//patch some methods in typeahead
|
|
var ta = this.$input.data('typeahead');
|
|
ta.render = $.proxy(this.typeaheadRender, ta);
|
|
ta.select = $.proxy(this.typeaheadSelect, ta);
|
|
ta.move = $.proxy(this.typeaheadMove, ta);
|
|
|
|
this.renderClear();
|
|
this.setClass();
|
|
this.setAttr('placeholder');
|
|
},
|
|
|
|
value2htmlFinal: function(value, element) {
|
|
if(this.getIsObjects()) {
|
|
var items = $.fn.editableutils.itemsByValue(value, this.sourceData);
|
|
value = items.length ? items[0].text : '';
|
|
}
|
|
$.fn.editabletypes.abstractinput.prototype.value2html.call(this, value, element);
|
|
},
|
|
|
|
html2value: function (html) {
|
|
return html ? html : null;
|
|
},
|
|
|
|
value2input: function(value) {
|
|
if(this.getIsObjects()) {
|
|
var items = $.fn.editableutils.itemsByValue(value, this.sourceData);
|
|
this.$input.data('value', value).val(items.length ? items[0].text : '');
|
|
} else {
|
|
this.$input.val(value);
|
|
}
|
|
},
|
|
|
|
input2value: function() {
|
|
if(this.getIsObjects()) {
|
|
var value = this.$input.data('value'),
|
|
items = $.fn.editableutils.itemsByValue(value, this.sourceData);
|
|
|
|
if(items.length && items[0].text.toLowerCase() === this.$input.val().toLowerCase()) {
|
|
return value;
|
|
} else {
|
|
return null; //entered string not found in source
|
|
}
|
|
} else {
|
|
return this.$input.val();
|
|
}
|
|
},
|
|
|
|
/*
|
|
if in sourceData values <> texts, typeahead in "objects" mode:
|
|
user must pick some value from list, otherwise `null` returned.
|
|
if all values == texts put typeahead in "strings" mode:
|
|
anything what entered is submited.
|
|
*/
|
|
getIsObjects: function() {
|
|
if(this.isObjects === undefined) {
|
|
this.isObjects = false;
|
|
for(var i=0; i<this.sourceData.length; i++) {
|
|
if(this.sourceData[i].value !== this.sourceData[i].text) {
|
|
this.isObjects = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return this.isObjects;
|
|
},
|
|
|
|
/*
|
|
Methods borrowed from text input
|
|
*/
|
|
activate: $.fn.editabletypes.text.prototype.activate,
|
|
renderClear: $.fn.editabletypes.text.prototype.renderClear,
|
|
postrender: $.fn.editabletypes.text.prototype.postrender,
|
|
toggleClear: $.fn.editabletypes.text.prototype.toggleClear,
|
|
clear: function() {
|
|
$.fn.editabletypes.text.prototype.clear.call(this);
|
|
this.$input.data('value', '');
|
|
},
|
|
|
|
|
|
/*
|
|
Typeahead option methods used as defaults
|
|
*/
|
|
/*jshint eqeqeq:false, curly: false, laxcomma: true, asi: true*/
|
|
matcher: function (item) {
|
|
return $.fn.typeahead.Constructor.prototype.matcher.call(this, item.text);
|
|
},
|
|
sorter: function (items) {
|
|
var beginswith = []
|
|
, caseSensitive = []
|
|
, caseInsensitive = []
|
|
, item
|
|
, text;
|
|
|
|
while (item = items.shift()) {
|
|
text = item.text;
|
|
if (!text.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item);
|
|
else if (~text.indexOf(this.query)) caseSensitive.push(item);
|
|
else caseInsensitive.push(item);
|
|
}
|
|
|
|
return beginswith.concat(caseSensitive, caseInsensitive);
|
|
},
|
|
highlighter: function (item) {
|
|
return $.fn.typeahead.Constructor.prototype.highlighter.call(this, item.text);
|
|
},
|
|
updater: function (item) {
|
|
this.$element.data('value', item.value);
|
|
return item.text;
|
|
},
|
|
|
|
|
|
/*
|
|
Overwrite typeahead's render method to store objects.
|
|
There are a lot of disscussion in bootstrap repo on this point and still no result.
|
|
See https://github.com/twitter/bootstrap/issues/5967
|
|
|
|
This function just store item via jQuery data() method instead of attr('data-value')
|
|
*/
|
|
typeaheadRender: function (items) {
|
|
var that = this;
|
|
|
|
items = $(items).map(function (i, item) {
|
|
// i = $(that.options.item).attr('data-value', item)
|
|
i = $(that.options.item).data('item', item);
|
|
i.find('a').html(that.highlighter(item));
|
|
return i[0];
|
|
});
|
|
|
|
//add option to disable autoselect of first line
|
|
//see https://github.com/twitter/bootstrap/pull/4164
|
|
if (this.options.autoSelect) {
|
|
items.first().addClass('active');
|
|
}
|
|
this.$menu.html(items);
|
|
return this;
|
|
},
|
|
|
|
//add option to disable autoselect of first line
|
|
//see https://github.com/twitter/bootstrap/pull/4164
|
|
typeaheadSelect: function () {
|
|
var val = this.$menu.find('.active').data('item')
|
|
if(this.options.autoSelect || val){
|
|
this.$element
|
|
.val(this.updater(val))
|
|
.change()
|
|
}
|
|
return this.hide()
|
|
},
|
|
|
|
/*
|
|
if autoSelect = false and nothing matched we need extra press onEnter that is not convinient.
|
|
This patch fixes it.
|
|
*/
|
|
typeaheadMove: function (e) {
|
|
if (!this.shown) return
|
|
|
|
switch(e.keyCode) {
|
|
case 9: // tab
|
|
case 13: // enter
|
|
case 27: // escape
|
|
if (!this.$menu.find('.active').length) return
|
|
e.preventDefault()
|
|
break
|
|
|
|
case 38: // up arrow
|
|
e.preventDefault()
|
|
this.prev()
|
|
break
|
|
|
|
case 40: // down arrow
|
|
e.preventDefault()
|
|
this.next()
|
|
break
|
|
}
|
|
|
|
e.stopPropagation()
|
|
}
|
|
|
|
/*jshint eqeqeq: true, curly: true, laxcomma: false, asi: false*/
|
|
|
|
});
|
|
|
|
Constructor.defaults = $.extend({}, $.fn.editabletypes.list.defaults, {
|
|
/**
|
|
@property tpl
|
|
@default <input type="text">
|
|
**/
|
|
tpl:'<input type="text">',
|
|
/**
|
|
Configuration of typeahead. [Full list of options](http://getbootstrap.com/2.3.2/javascript.html#typeahead).
|
|
|
|
@property typeahead
|
|
@type object
|
|
@default null
|
|
**/
|
|
typeahead: null,
|
|
/**
|
|
Whether to show `clear` button
|
|
|
|
@property clear
|
|
@type boolean
|
|
@default true
|
|
**/
|
|
clear: true
|
|
});
|
|
|
|
$.fn.editabletypes.typeahead = Constructor;
|
|
|
|
}(window.jQuery)); |