Merge branch 'release-1.1.0'

This commit is contained in:
vitalets
2012-11-27 16:20:07 +04:00
30 changed files with 1103 additions and 545 deletions

@ -2,6 +2,17 @@ X-editable changelog
============================= =============================
Version 1.1.0 Nov 27, 2012
----------------------------
[enh #11] icon cancel changed to 'cross' (tarciozemel)
[enh] added support for IE7+ (vitalets)
[enh #9] 'name' or 'id' is not required anymore (vitalets)
[enh] 'clear' button added in date and dateui (vitalets)
[enh] form template changed: added DIV.editable-input, DIV.editable.buttons and $.fn.editableform.buttons (vitalets)
[enh] new input type: checklist (vitalets)
[enh] updated docs: inputs dropdown menu, global templates section (vitalets)
Version 1.0.1 Nov 22, 2012 Version 1.0.1 Nov 22, 2012
---------------------------- ----------------------------
[enh] contribution guide in README.md (vitalets) [enh] contribution guide in README.md (vitalets)

@ -13,7 +13,7 @@ Your feedback is very appreciated!
## Contribution ## Contribution
A few steps how to start contributing: A few steps how to start contributing:
1.[Fork X-editable](https://github.com/vitalets/x-editable/fork) 1.[Fork X-editable](https://github.com/vitalets/x-editable/fork) and pull the latest changes from <code>dev</code> branch
2.Arrange local directory structure. It should be: 2.Arrange local directory structure. It should be:
**x-editable** **x-editable**
@ -58,6 +58,7 @@ Or use grunt's _qunit_ task <code>grunt test</code>. For that you also need to [
* run <code>grunt build</code> in **lib** directory * run <code>grunt build</code> in **lib** directory
* run <code>build data-docs-dist</code> in **gh-pages** directory * run <code>build data-docs-dist</code> in **gh-pages** directory
You will get distributive in **lib/dist** and updated docs in **gh-pages/*.html**. You will get distributive in **lib/dist** and updated docs in **gh-pages/*.html**.
Do not edit **index.html** and **docs.html** directly! Instead look at [Handlebars](https://github.com/wycats/handlebars.js) templates in **generator/templates**.
6.Commit changes on <code>dev</code> branch and make pull request as usual. 6.Commit changes on <code>dev</code> branch and make pull request as usual.

@ -38,9 +38,11 @@ function getFiles() {
containers+'editable-container.js', containers+'editable-container.js',
lib+'element/editable-element.js', lib+'element/editable-element.js',
inputs+'abstract.js', inputs+'abstract.js',
inputs+'list.js',
inputs+'text.js', inputs+'text.js',
inputs+'textarea.js', inputs+'textarea.js',
inputs+'select.js' inputs+'select.js',
inputs+'checklist.js'
]; ];
//common css files //common css files

@ -2,7 +2,7 @@
"name": "X-editable", "name": "X-editable",
"title": "X-editable", "title": "X-editable",
"description": "In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery", "description": "In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery",
"version": "1.0.1", "version": "1.1.0",
"homepage": "http://github.com/vitalets/x-editable", "homepage": "http://github.com/vitalets/x-editable",
"author": { "author": {
"name": "Vitaliy Potapov", "name": "Vitaliy Potapov",

@ -1,6 +1,8 @@
.editable-container, .editable-container {
.editable-container.popover {
width: auto;
white-space: nowrap; /* without this rule buttons wrap input in non-static elements */
max-width: none; /* without this rule poshytip does not stretch */ max-width: none; /* without this rule poshytip does not stretch */
} }
.editable-container.popover {
/* width: 300px;*/ /* debug */
width: auto; /* without this rule popover does not stretch */
}

@ -3,11 +3,19 @@ Editableform based on Twitter Bootstrap
*/ */
(function ($) { (function ($) {
//form template $.extend($.fn.editableform.Constructor.prototype, {
$.fn.editableform.template = '<form class="form-inline editableform"><div class="control-group">' + initTemplate: function() {
'&nbsp;<button type="submit" class="btn btn-primary"><i class="icon-ok icon-white"></i></button>&nbsp;<button type="button" class="btn clearfix"><i class="icon-ban-circle"></i></button>' + this.$form = $($.fn.editableform.template);
'<div style="clear:both"><span class="help-block editable-error-block"></span></div>' + this.$form.find('.editable-error-block').addClass('help-block');
'</div></form>';
//buttons
this.$form.find('div.editable-buttons').append($.fn.editableform.buttons);
}
});
//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 //error classes
$.fn.editableform.errorGroupClass = 'error'; $.fn.editableform.errorGroupClass = 'error';

@ -4,27 +4,23 @@ Editableform based on jQuery UI
(function ($) { (function ($) {
$.extend($.fn.editableform.Constructor.prototype, { $.extend($.fn.editableform.Constructor.prototype, {
initTemplate: function() { initTemplate: function() {
this.$form = $($.fn.editableform.template); this.$form = $($.fn.editableform.template);
//init buttons //buttons
this.$form.find('button[type=submit]').button({ this.$form.find('.editable-buttons').append($.fn.editableform.buttons);
icons: { primary: "ui-icon-check" }, this.$form.find('.editable-submit').button({
text: false icons: { primary: "ui-icon-check" },
}); text: false
this.$form.find('button[type=button]').button({ }).removeAttr('title');
icons: { primary: "ui-icon-cancel" }, this.$form.find('.editable-cancel').button({
text: false icons: { primary: "ui-icon-closethick" },
}); text: false
} }).removeAttr('title');
}
}); });
//form template
$.fn.editableform.template = '<form class="editableform"><div class="control-group">' +
'&nbsp;<button type="submit" style="height: 24px">submit</button>&nbsp;<button type="button" style="height: 24px">cancel</button></div>' +
'<div class="editable-error-block"></div>' +
'</form>';
//error classes //error classes
$.fn.editableform.errorGroupClass = null; $.fn.editableform.errorGroupClass = null;
$.fn.editableform.errorBlockClass = 'ui-state-error'; $.fn.editableform.errorBlockClass = 'ui-state-error';

@ -2,104 +2,120 @@
* EditableForm utilites * EditableForm utilites
*/ */
(function ($) { (function ($) {
$.extend($.fn.editableform, { $.fn.editableform.utils = {
utils: { /**
/** * classic JS inheritance function
* classic JS inheritance function */
*/ inherit: function (Child, Parent) {
inherit: function (Child, Parent) { var F = function() { };
var F = function() { }; F.prototype = Parent.prototype;
F.prototype = Parent.prototype; Child.prototype = new F();
Child.prototype = new F(); Child.prototype.constructor = Child;
Child.prototype.constructor = Child; Child.superclass = Parent.prototype;
Child.superclass = Parent.prototype; },
},
/** /**
* set caret position in input * set caret position in input
* see http://stackoverflow.com/questions/499126/jquery-set-cursor-position-in-text-area * see http://stackoverflow.com/questions/499126/jquery-set-cursor-position-in-text-area
*/ */
setCursorPosition: function(elem, pos) { setCursorPosition: function(elem, pos) {
if (elem.setSelectionRange) { if (elem.setSelectionRange) {
elem.setSelectionRange(pos, pos); elem.setSelectionRange(pos, pos);
} else if (elem.createTextRange) { } else if (elem.createTextRange) {
var range = elem.createTextRange(); var range = elem.createTextRange();
range.collapse(true); range.collapse(true);
range.moveEnd('character', pos); range.moveEnd('character', pos);
range.moveStart('character', pos); range.moveStart('character', pos);
range.select(); range.select();
} }
}, },
/** /**
* function to parse JSON in *single* quotes. (jquery automatically parse only double quotes) * 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'}"> * That allows such code as: <a data-source="{'a': 'b', 'c': 'd'}">
* safe = true --> means no exception will be thrown * 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 * for details see http://stackoverflow.com/questions/7410348/how-to-set-json-format-to-html5-data-attributes-in-the-jquery
*/ */
tryParseJson: function(s, safe) { tryParseJson: function(s, safe) {
if (typeof s === 'string' && s.length && s.match(/^\{.*\}$/)) { if (typeof s === 'string' && s.length && s.match(/^[\{\[].*[\}\]]$/)) {
if (safe) { if (safe) {
try { try {
/*jslint evil: true*/
s = (new Function('return ' + s))();
/*jslint evil: false*/
} catch (e) {} finally {
return s;
}
} else {
/*jslint evil: true*/ /*jslint evil: true*/
s = (new Function('return ' + s))(); s = (new Function('return ' + s))();
/*jslint evil: false*/ /*jslint evil: false*/
} catch (e) {} finally {
return s;
} }
} else {
/*jslint evil: true*/
s = (new Function('return ' + s))();
/*jslint evil: false*/
} }
return s;
},
/**
* slice object by specified keys
*/
sliceObj: function(obj, keys, caseSensitive /* default: false */) {
var key, keyLower, newObj = {};
if (!$.isArray(keys) || !keys.length) {
return newObj;
}
for (var i = 0; i < keys.length; i++) {
key = keys[i];
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key];
}
if(caseSensitive === true) {
continue;
}
//when getting data-* attributes via $.data() it's converted to lowercase.
//details: http://stackoverflow.com/questions/7602565/using-data-attributes-with-jquery
//workaround is code below.
keyLower = key.toLowerCase();
if (obj.hasOwnProperty(keyLower)) {
newObj[key] = obj[keyLower];
}
}
return newObj;
},
/**
* exclude complex objects from $.data() before pass to config
*/
getConfigData: function($element) {
var data = {};
$.each($element.data(), function(k, v) {
if(typeof v !== 'object' || (v && typeof v === 'object' && v.constructor === Object)) {
data[k] = v;
}
});
return data;
} }
return s;
},
/**
* slice object by specified keys
*/
sliceObj: function(obj, keys, caseSensitive /* default: false */) {
var key, keyLower, newObj = {};
if (!$.isArray(keys) || !keys.length) {
return newObj;
}
for (var i = 0; i < keys.length; i++) {
key = keys[i];
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key];
}
if(caseSensitive === true) {
continue;
}
//when getting data-* attributes via $.data() it's converted to lowercase.
//details: http://stackoverflow.com/questions/7602565/using-data-attributes-with-jquery
//workaround is code below.
keyLower = key.toLowerCase();
if (obj.hasOwnProperty(keyLower)) {
newObj[key] = obj[keyLower];
}
}
return newObj;
},
/**
* exclude complex objects from $.data() before pass to config
*/
getConfigData: function($element) {
var data = {};
$.each($element.data(), function(k, v) {
if(typeof v !== 'object' || (v && typeof v === 'object' && v.constructor === Object)) {
data[k] = v;
}
});
return data;
},
objectKeys: function(o) {
if (Object.keys) {
return Object.keys(o);
} else {
if (o !== Object(o)) {
throw new TypeError('Object.keys called on a non-object');
}
var k=[], p;
for (p in o) {
if (Object.prototype.hasOwnProperty.call(o,p)) {
k.push(p);
}
}
return k;
}
} }
}); };
}(window.jQuery)); }(window.jQuery));

@ -1,11 +1,44 @@
.editableform, .editableform {
.editableform div.control-group { margin-bottom: 0; /* overwrites bootstrap margin */
margin-bottom: 0; }
.editableform .control-group {
margin-bottom: 0; /* overwrites bootstrap margin */
white-space: nowrap; /* prevent wrapping buttons on new line */
}
.editable-buttons {
display: inline-block; /* should be inline to take effect of parent's white-space: nowrap */
vertical-align: top;
margin-left: 7px;
/* display-inline emulation for IE7*/
zoom: 1;
*display: inline;
}
.editable-input {
vertical-align: top;
display: inline-block; /* should be inline to take effect of parent's white-space: nowrap */
width: auto; /* bootstrap-responsive has width: 100% that breakes layout */
white-space: normal; /* reset white-space decalred in parent*/
/* display-inline emulation for IE7*/
zoom: 1;
*display: inline;
}
.editable-buttons .editable-cancel {
margin-left: 7px;
}
/*for jquery-ui buttons need set height to look more pretty*/
.editable-buttons button.ui-button {
height: 24px;
} }
.editableform-loading { .editableform-loading {
background: url('img/loading.gif') center center no-repeat; background: url('img/loading.gif') center center no-repeat;
height: 25px; height: 25px;
width: auto;
} }
.editable-inline .editableform-loading { .editable-inline .editableform-loading {
@ -14,29 +47,40 @@
.editable-error-block { .editable-error-block {
max-width: 300px; max-width: 300px;
margin-top: 3px; margin: 5px 0 0 0;
margin-bottom: 0; width: auto;
clear: both; }
/*add padding for jquery ui*/
.editable-error-block.ui-state-error {
padding: 3px;
} }
.editable-error { .editable-error {
color: red; color: red;
} }
.editableform input,
.editableform select,
.editableform textarea {
vertical-align: top;
display: inline-block;
width: auto; /* bootstrap-responsive has width: 100% that breakes layout */
}
.editableform textarea { .editableform textarea {
height: 150px; /*default height for textarea*/ height: 150px; /*default height for textarea*/
} }
.editableform .editable-date { .editableform .editable-date {
float: left;
padding: 0; padding: 0;
margin: 0 0 9px 0; margin: 0;
float: left;
}
/* checklist vertical alignment */
.editable-checklist label input[type="checkbox"],
.editable-checklist label span {
vertical-align: middle;
margin: 0;
}
.editable-clear {
clear: both;
font-size: 0.9em;
text-decoration: none;
text-align: right;
} }

@ -17,13 +17,13 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'.
EditableForm.prototype = { EditableForm.prototype = {
constructor: EditableForm, constructor: EditableForm,
initInput: function() { initInput: function() { //called once
var TypeConstructor, typeOptions; var TypeConstructor, typeOptions;
//create input of specified type //create input of specified type
if(typeof $.fn.editableform.types[this.options.type] === 'function') { if(typeof $.fn.editableform.types[this.options.type] === 'function') {
TypeConstructor = $.fn.editableform.types[this.options.type]; TypeConstructor = $.fn.editableform.types[this.options.type];
typeOptions = $.fn.editableform.utils.sliceObj(this.options, Object.keys(TypeConstructor.defaults)); typeOptions = $.fn.editableform.utils.sliceObj(this.options, $.fn.editableform.utils.objectKeys(TypeConstructor.defaults));
this.input = new TypeConstructor(typeOptions); this.input = new TypeConstructor(typeOptions);
} else { } else {
$.error('Unknown type: '+ this.options.type); $.error('Unknown type: '+ this.options.type);
@ -34,6 +34,9 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'.
}, },
initTemplate: function() { initTemplate: function() {
this.$form = $($.fn.editableform.template); this.$form = $($.fn.editableform.template);
//buttons
this.$form.find('div.editable-buttons').append($.fn.editableform.buttons);
}, },
/** /**
Renders editableform Renders editableform
@ -57,20 +60,29 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'.
//render input //render input
$.when(this.input.render()) $.when(this.input.render())
.then($.proxy(function () { .then($.proxy(function () {
//place input //input
this.$form.find('div.control-group').prepend(this.input.$input); this.$form.find('div.editable-input').append(this.input.$input);
//attach 'cancel' handler
this.$form.find('button[type=button]').click($.proxy(this.cancel, this)); //"clear" link
if(this.input.$clear) {
this.$form.find('div.editable-input').append($('<div class="editable-clear">').append(this.input.$clear));
}
//append form to container //append form to container
this.$element.append(this.$form); this.$element.append(this.$form);
//attach 'cancel' handler
this.$form.find('.editable-cancel').click($.proxy(this.cancel, this));
// this.$form.find('.editable-buttons button').eq(1).click($.proxy(this.cancel, this));
if(this.input.error) { if(this.input.error) {
this.error(this.input.error); this.error(this.input.error);
this.$form.find('button[type=submit]').attr('disabled', true); this.$form.find('.editable-submit').attr('disabled', true);
this.input.$input.attr('disabled', true); this.input.$input.attr('disabled', true);
} else { } else {
this.error(false); this.error(false);
this.input.$input.removeAttr('disabled'); this.input.$input.removeAttr('disabled');
this.$form.find('button[type=submit]').removeAttr('disabled'); this.$form.find('.editable-submit').removeAttr('disabled');
this.input.value2input(this.value); this.input.value2input(this.value);
this.$form.submit($.proxy(this.submit, this)); this.$form.submit($.proxy(this.submit, this));
} }
@ -94,20 +106,18 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'.
this.$element.triggerHandler('cancel'); this.$element.triggerHandler('cancel');
}, },
showLoading: function() { showLoading: function() {
var fw, fh, iw, ih; var w;
//set loading size equal to form
if(this.$form) { if(this.$form) {
fh = this.$form.outerHeight() || 0; //set loading size equal to form
fw = this.$form.outerWidth() || 0; this.$loading.width(this.$form.outerWidth());
ih = (this.input && this.input.$input.outerHeight()) || 0; this.$loading.height(this.$form.outerHeight());
iw = (this.input && this.input.$input.outerWidth()) || 0;
if(fh || ih) {
this.$loading.height(fh > ih ? fh : ih);
}
if(fw || iw) {
this.$loading.width(fw > iw ? fw : iw);
}
this.$form.hide(); this.$form.hide();
} else {
//stretch loading to fill container width
w = this.$loading.parent().width();
if(w) {
this.$loading.width(w);
}
} }
this.$loading.show(); this.$loading.show();
}, },
@ -126,7 +136,7 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'.
error: function(msg) { error: function(msg) {
var $group = this.$form.find('.control-group'), var $group = this.$form.find('.control-group'),
$block = this.$form.find('.editable-error-block'); $block = this.$form.find('.editable-error-block');
if(msg === false) { if(msg === false) {
$group.removeClass($.fn.editableform.errorGroupClass); $group.removeClass($.fn.editableform.errorGroupClass);
@ -142,9 +152,9 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'.
e.preventDefault(); e.preventDefault();
var error, var error,
//get value from input //get value from input
newValue = this.input.input2value(), newValue = this.input.input2value(),
newValueStr; newValueStr;
//validation //validation
if (error = this.validate(newValue)) { if (error = this.validate(newValue)) {
@ -159,7 +169,7 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'.
//if value not changed --> cancel //if value not changed --> cancel
/*jslint eqeq: true*/ /*jslint eqeq: true*/
if (newValueStr == this.input.value2str(this.value)) { if (newValueStr == this.input.value2str(this.value)) {
/*jslint eqeq: false*/ /*jslint eqeq: false*/
this.cancel(); this.cancel();
return; return;
} }
@ -175,34 +185,34 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'.
return; return;
} }
//clear error message //clear error message
this.error(false); this.error(false);
this.value = newValue; this.value = newValue;
/** /**
Fired when form is submitted Fired when form is submitted
@event save @event save
@param {Object} event event object @param {Object} event event object
@param {Object} params additional params @param {Object} params additional params
@param {mixed} params.newValue submitted value @param {mixed} params.newValue submitted value
@param {Object} params.response ajax response @param {Object} params.response ajax response
@example @example
$('#form-div').on('save'), function(e, params){ $('#form-div').on('save'), function(e, params){
if(params.newValue === 'username') {...} if(params.newValue === 'username') {...}
}); });
**/ **/
this.$element.triggerHandler('save', {newValue: newValue, response: response}); this.$element.triggerHandler('save', {newValue: newValue, response: response});
}, this)) }, this))
.fail($.proxy(function(xhr) { .fail($.proxy(function(xhr) {
this.error(typeof xhr === 'string' ? xhr : xhr.responseText || xhr.statusText || 'Unknown error!'); this.error(typeof xhr === 'string' ? xhr : xhr.responseText || xhr.statusText || 'Unknown error!');
this.showForm(); this.showForm();
}, this)); }, this));
}, },
save: function(value) { save: function(value) {
var pk = (typeof this.options.pk === 'function') ? this.options.pk.call(this) : this.options.pk, var pk = (typeof this.options.pk === 'function') ? this.options.pk.call(this) : this.options.pk,
send = !!(typeof this.options.url === 'function' || (this.options.url && ((this.options.send === 'always') || (this.options.send === 'auto' && pk)))), send = !!(typeof this.options.url === 'function' || (this.options.url && ((this.options.send === 'always') || (this.options.send === 'auto' && pk)))),
params; params;
if (send) { //send to server if (send) { //send to server
this.showLoading(); this.showLoading();
@ -245,9 +255,20 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'.
} }
}, },
option: function(key, value) { option: function(key, value) {
this.options[key] = value; this.options[key] = value;
} if(key === 'value') {
this.setValue(value);
}
},
setValue: function(value, convertStr) {
if(convertStr) {
this.value = this.input.str2value(value);
} else {
this.value = value;
}
}
}; };
/* /*
@ -256,15 +277,15 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'.
@method $().editableform(options) @method $().editableform(options)
@params {Object} options @params {Object} options
@example @example
var $form = $('&lt;div&gt;').editableform({ var $form = $('&lt;div&gt;').editableform({
type: 'text', type: 'text',
name: 'username', name: 'username',
url: '/post', url: '/post',
value: 'vitaliy' value: 'vitaliy'
}); });
//to display form you should call 'render' method //to display form you should call 'render' method
$form.editableform('render'); $form.editableform('render');
*/ */
$.fn.editableform = function (option) { $.fn.editableform = function (option) {
var args = arguments; var args = arguments;
@ -290,7 +311,7 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'.
/* see also defaults for input */ /* see also defaults for input */
/** /**
Type of input. Can be <code>text|textarea|select|date</code> Type of input. Can be <code>text|textarea|select|date|checklist</code>
@property type @property type
@type string @type string
@ -306,12 +327,12 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'.
@default null @default null
@example @example
url: function(params) { url: function(params) {
if(params.value === 'abc') { if(params.value === 'abc') {
var d = new $.Deferred; var d = new $.Deferred;
return d.reject('field cannot be "abc"'); //returning error via deferred object return d.reject('field cannot be "abc"'); //returning error via deferred object
} else { } else {
someModel.set(params.name, params.value); //save data in some js model someModel.set(params.name, params.value); //save data in some js model
} }
} }
**/ **/
url:null, url:null,
@ -319,7 +340,7 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'.
Additional params for submit. Function can be used to calculate params dynamically Additional params for submit. Function can be used to calculate params dynamically
@example @example
params: function() { params: function() {
return { a: 1 }; return { a: 1 };
} }
@property params @property params
@ -392,26 +413,32 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'.
}; };
/* /*
Note: following params could redefined in engine: bootstrap or jqueryui: Note: following params could redefined in engine: bootstrap or jqueryui:
Classes 'control-group' and 'editable-error-block' must always present! Classes 'control-group' and 'editable-error-block' must always present!
*/ */
$.fn.editableform.template = '<form class="form-inline editableform"><div class="control-group">' + $.fn.editableform.template = '<form class="form-inline editableform">'+
'&nbsp;<button type="submit">Ok</button>&nbsp;<button type="button">Cancel</button></div>' + '<div class="control-group">' +
'<div><div class="editable-input"></div><div class="editable-buttons"></div></div>'+
'<div class="editable-error-block"></div>' + '<div class="editable-error-block"></div>' +
'</div>' +
'</form>'; '</form>';
//loading div //loading div
$.fn.editableform.loading = '<div class="editableform-loading"></div>'; $.fn.editableform.loading = '<div class="editableform-loading"></div>';
//error class attahced to control-group //buttons
$.fn.editableform.errorGroupClass = null; $.fn.editableform.buttons = '<button type="submit" class="editable-submit">ok</button>'+
'<button type="button" class="editable-cancel">cancel</button>';
//error class attahced to editable-error-block //error class attahced to control-group
$.fn.editableform.errorBlockClass = 'editable-error'; $.fn.editableform.errorGroupClass = null;
//input types //error class attahced to editable-error-block
$.fn.editableform.types = {}; $.fn.editableform.errorBlockClass = 'editable-error';
//utils
$.fn.editableform.utils = {}; //input types
$.fn.editableform.types = {};
//utils
$.fn.editableform.utils = {};
}(window.jQuery)); }(window.jQuery));

@ -29,17 +29,13 @@ Makes editable any HTML element on the page. Applied as jQuery method.
return; return;
} }
//name must be defined //name
this.options.name = this.options.name || this.$element.attr('id'); this.options.name = this.options.name || this.$element.attr('id');
if (!this.options.name) {
$.error('You must define name (or id) for Editable element');
return;
}
//create input of specified type. Input will be used for converting value, not in form //create input of specified type. Input will be used for converting value, not in form
if(typeof $.fn.editableform.types[this.options.type] === 'function') { if(typeof $.fn.editableform.types[this.options.type] === 'function') {
TypeConstructor = $.fn.editableform.types[this.options.type]; TypeConstructor = $.fn.editableform.types[this.options.type];
this.typeOptions = $.fn.editableform.utils.sliceObj(this.options, Object.keys(TypeConstructor.defaults)); this.typeOptions = $.fn.editableform.utils.sliceObj(this.options, $.fn.editableform.utils.objectKeys(TypeConstructor.defaults));
this.input = new TypeConstructor(this.typeOptions); this.input = new TypeConstructor(this.typeOptions);
} else { } else {
$.error('Unknown type: '+ this.options.type); $.error('Unknown type: '+ this.options.type);
@ -57,7 +53,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
//attach handler to close any container on escape //attach handler to close any container on escape
$(document).off('keyup.editable').on('keyup.editable', function (e) { $(document).off('keyup.editable').on('keyup.editable', function (e) {
if (e.which === 27) { if (e.which === 27) {
$('.editable-container').find('button[type=button]').click(); $('.editable-container').find('.editable-cancel').click();
} }
}); });
@ -69,7 +65,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
return; return;
} }
//close all other containers //close all other containers
$('.editable-container').find('button[type=button]').click(); $('.editable-container').find('.editable-cancel').click();
}); });
//add 'editable' class //add 'editable' class
@ -233,7 +229,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
} }
//hide all other editable containers. Required to work correctly with toggle = manual //hide all other editable containers. Required to work correctly with toggle = manual
$('.editable-container').find('button[type=button]').click(); $('.editable-container').find('.editable-cancel').click();
//show container //show container
this.container.show(); this.container.show();

@ -18,6 +18,7 @@ To create your own input you should inherit from this class.
this.type = type; this.type = type;
this.options = $.extend({}, defaults, options); this.options = $.extend({}, defaults, options);
this.$input = null; this.$input = null;
this.$clear = null;
this.error = null; this.error = null;
}, },
@ -110,6 +111,15 @@ To create your own input you should inherit from this class.
if(this.$input.is(':visible')) { if(this.$input.is(':visible')) {
this.$input.focus(); this.$input.focus();
} }
},
/**
Creares input.
@method clear()
**/
clear: function() {
this.$input.val(null);
} }
}; };

157
src/inputs/checklist.js Normal file

@ -0,0 +1,157 @@
/**
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-original-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 ($) {
var Checklist = function (options) {
this.init('checklist', options, Checklist.defaults);
};
$.fn.editableform.utils.inherit(Checklist, $.fn.editableform.types.list);
$.extend(Checklist.prototype, {
renderList: function() {
var $label, $div;
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,
name: this.options.name
}))
.append($('<span>').text(' '+this.sourceData[i].text));
$('<div>').append($label).appendTo(this.$input);
}
},
value2str: function(value) {
return $.isArray(value) ? value.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;
}
return value;
},
//set checked on required checkboxes
value2input: function(value) {
var $checks = this.$input.find('input[type="checkbox"]');
$checks.removeAttr('checked');
if($.isArray(value) && value.length) {
$checks.each(function(i, el) {
if($.inArray($(el).val(), value) !== -1) {
$(el).attr('checked', 'checked');
}
});
}
},
input2value: function() {
var checked = [];
this.$input.find('input:checked').each(function(i, el) {
checked.push($(el).val());
});
return checked;
},
//collect text of checked boxes
value2htmlFinal: function(value, element) {
var selected = [], item, i, html = '';
if($.isArray(value) && value.length <= this.options.limit) {
for(i=0; i<value.length; i++){
item = this.itemByVal(value[i]);
if(item) {
selected.push($('<div>').text(item.text).html());
}
}
html = selected.join(this.options.viewseparator);
} else {
html = this.options.limitText.replace('{checked}', $.isArray(value) ? value.length : 0).replace('{count}', this.sourceData.length);
}
$(element).html(html);
}
});
Checklist.defaults = $.extend({}, $.fn.editableform.types.list.defaults, {
/**
@property tpl
@default <div></div>
**/
tpl:'<div></div>',
/**
@property inputclass
@type string
@default span2 editable-checklist
**/
inputclass: 'span2 editable-checklist',
/**
Separator of values in string when sending to server
@property separator
@type string
@default ', '
**/
separator: ',',
/**
Separator of text when display as element content.
@property viewseparator
@type string
@default '<br>'
**/
viewseparator: '<br>',
/**
Maximum number of items shown as element content.
If checked more items - <code>limitText</code> will be shown.
@property limit
@type integer
@default 4
**/
limit: 4,
/**
Text shown when count of checked items is greater than <code>limit</code> parameter.
You can use <code>{checked}</code> and <code>{count}</code> placeholders.
@property limitText
@type string
@default 'Selected {checked} of {count}'
**/
limitText: 'Selected {checked} of {count}'
});
$.fn.editableform.types.checklist = Checklist;
}(window.jQuery));

@ -5,6 +5,7 @@ For localization you can include js file from here: https://github.com/eternicod
@class date @class date
@extends abstract @extends abstract
@final
@example @example
<a href="#" id="dob" data-type="date" data-pk="1" data-url="/post" data-original-title="Select date">15/05/1984</a> <a href="#" id="dob" data-type="date" data-pk="1" data-url="/post" data-original-title="Select date">15/05/1984</a>
<script> <script>
@ -53,6 +54,14 @@ $(function(){
render: function () { render: function () {
Date.superclass.render.call(this); Date.superclass.render.call(this);
this.$input.datepicker(this.options.datepicker); this.$input.datepicker(this.options.datepicker);
if(this.options.clear) {
this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){
e.preventDefault();
e.stopPropagation();
this.clear();
}, this));
}
}, },
value2html: function(value, element) { value2html: function(value, element) {
@ -81,6 +90,11 @@ $(function(){
}, },
activate: function() { activate: function() {
},
clear: function() {
this.$input.data('datepicker').date = null;
this.$input.find('.active').removeClass('active');
} }
}); });
@ -130,7 +144,16 @@ $(function(){
weekStart: 0, weekStart: 0,
startView: 0, startView: 0,
autoclose: 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: '&times; clear'
}); });
$.fn.editableform.types.date = Date; $.fn.editableform.types.date = Date;

@ -1,10 +1,11 @@
/** /**
jQuery UI Datepicker. jQuery UI Datepicker.
Description and examples: http://jqueryui.com/datepicker. Description and examples: http://jqueryui.com/datepicker.
Do not use it together with bootstrap-datepicker. This input is also accessible as **date** type. Do not use it together with __bootstrap-datepicker__ as both apply <code>$().datepicker()</code> method.
@class dateui @class dateui
@extends abstract @extends abstract
@final
@example @example
<a href="#" id="dob" data-type="date" data-pk="1" data-url="/post" data-original-title="Select date">15/05/1984</a> <a href="#" id="dob" data-type="date" data-pk="1" data-url="/post" data-original-title="Select date">15/05/1984</a>
<script> <script>
@ -51,6 +52,14 @@ $(function(){
render: function () { render: function () {
DateUI.superclass.render.call(this); DateUI.superclass.render.call(this);
this.$input.datepicker(this.options.datepicker); this.$input.datepicker(this.options.datepicker);
if(this.options.clear) {
this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){
e.preventDefault();
e.stopPropagation();
this.clear();
}, this));
}
}, },
value2html: function(value, element) { value2html: function(value, element) {
@ -99,6 +108,10 @@ $(function(){
}, },
activate: function() { activate: function() {
},
clear: function() {
this.$input.datepicker('setDate', null);
} }
}); });
@ -149,7 +162,16 @@ $(function(){
firstDay: 0, firstDay: 0,
changeYear: true, changeYear: true,
changeMonth: true changeMonth: true
} },
/**
Text shown as clear date button.
If <code>false</code> clear button will not be rendered.
@property clear
@type boolean|string
@default 'x clear'
**/
clear: '&times; clear'
}); });
$.fn.editableform.types.dateui = DateUI; $.fn.editableform.types.dateui = DateUI;

254
src/inputs/list.js Normal file

@ -0,0 +1,254 @@
/**
List - abstract class for inputs that have source option loaded from js array or via ajax
@class list
@extends abstract
**/
(function ($) {
var List = function (options) {
};
$.fn.editableform.utils.inherit(List, $.fn.editableform.types.abstract);
$.extend(List.prototype, {
render: function () {
List.superclass.render.call(this);
var deferred = $.Deferred();
this.error = null;
this.sourceData = null;
this.prependData = 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) {
var deferred = $.Deferred();
this.onSourceReady(function () {
this.value2htmlFinal(value, element);
deferred.resolve();
}, function () {
List.superclass.value2html(this.options.sourceError, element);
deferred.resolve();
});
return deferred.promise();
},
// ------------- additional functions ------------
onSourceReady: function (success, error) {
//if allready loaded just call success
if($.isArray(this.sourceData)) {
success.call(this);
return;
}
// try parse json in single quotes (for double quotes jquery does automatically)
try {
this.options.source = $.fn.editableform.utils.tryParseJson(this.options.source, false);
} catch (e) {
error.call(this);
return;
}
//loading from url
if (typeof this.options.source === 'string') {
var cacheID = this.options.source + (this.options.name ? '-' + this.options.name : ''),
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;
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;
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 = [];
}
//loading sourceData from server
$.ajax({
url: this.options.source,
type: 'get',
cache: false,
data: this.options.name ? {name: this.options.name} : {},
dataType: 'json',
success: $.proxy(function (data) {
cache.loading = false;
this.sourceData = this.makeArray(data);
if($.isArray(this.sourceData)) {
this.doPrepend();
//store result in cache
cache.sourceData = this.sourceData;
success.call(this);
$.each(cache.callbacks, function () { this.call(); }); //run success callbacks for other fields
} else {
error.call(this);
$.each(cache.err_callbacks, function () { this.call(); }); //run error callbacks for other fields
}
}, this),
error: $.proxy(function () {
cache.loading = false;
error.call(this);
$.each(cache.err_callbacks, function () { this.call(); }); //run error callbacks for other fields
}, this)
});
} else { //options as json/array
this.sourceData = this.makeArray(this.options.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)) {
//try parse json in single quotes
this.options.prepend = $.fn.editableform.utils.tryParseJson(this.options.prepend, true);
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 = [], iterateEl;
if(!data || typeof data === 'string') {
return null;
}
if($.isArray(data)) { //array
iterateEl = function (k, v) {
obj = {value: k, text: v};
if(count++ >= 2) {
return false;// exit each if object has more than one value
}
};
for(var i = 0; i < data.length; i++) {
if(typeof data[i] === 'object') {
count = 0;
$.each(data[i], iterateEl);
if(count === 1) {
result.push(obj);
} else if(count > 1 && data[i].hasOwnProperty('value') && data[i].hasOwnProperty('text')) {
result.push(data[i]);
} else {
//data contains incorrect objects
}
} else {
result.push({value: data[i], text: data[i]});
}
}
} else { //object
$.each(data, function (k, v) {
result.push({value: k, text: v});
});
}
return result;
},
//search for item by particular value
itemByVal: function(val) {
if($.isArray(this.sourceData)) {
for(var i=0; i<this.sourceData.length; i++){
/*jshint eqeqeq: false*/
if(this.sourceData[i].value == val) {
/*jshint eqeqeq: true*/
return this.sourceData[i];
}
}
}
}
});
List.defaults = $.extend({}, $.fn.editableform.types.abstract.defaults, {
/**
Source data for list. If string - considered ajax url to load items. Otherwise should be an array.
Array format is: <code>[{value: 1, text: "text"}, {...}]</code><br>
For compability it also supports format <code>{value1: "text1", value2: "text2" ...}</code> but it does not guarantee elements order.
@property source
@type string|array|object
@default null
**/
source:null,
/**
Data automatically prepended to the begining of dropdown list.
@property prepend
@type string|array|object
@default false
**/
prepend:false,
/**
Error message when list cannot be loaded (e.g. ajax error)
@property sourceError
@type string
@default Error when loading list
**/
sourceError: 'Error when loading list'
});
$.fn.editableform.types.list = List;
}(window.jQuery));

@ -1,8 +1,9 @@
/** /**
Select (dropdown) input Select (dropdown)
@class select @class select
@extends abstract @extends list
@final
@example @example
<a href="#" id="status" data-type="select" data-pk="1" data-url="/post" data-original-title="Select status"></a> <a href="#" id="status" data-type="select" data-pk="1" data-url="/post" data-original-title="Select status"></a>
<script> <script>
@ -25,160 +26,10 @@ $(function(){
this.init('select', options, Select.defaults); this.init('select', options, Select.defaults);
}; };
$.fn.editableform.utils.inherit(Select, $.fn.editableform.types.abstract); $.fn.editableform.utils.inherit(Select, $.fn.editableform.types.list);
$.extend(Select.prototype, { $.extend(Select.prototype, {
render: function () { renderList: function() {
Select.superclass.render.call(this);
var deferred = $.Deferred();
this.error = null;
this.sourceData = null;
this.prependData = null;
this.onSourceReady(function () {
this.renderOptions();
deferred.resolve();
}, function () {
this.error = this.options.sourceError;
deferred.resolve();
});
return deferred.promise();
},
html2value: function (html) {
return null; //it's not good idea to set value by text for SELECT. Better set NULL
},
value2html: function (value, element) {
var deferred = $.Deferred();
this.onSourceReady(function () {
var i, text = '';
if($.isArray(this.sourceData)) {
for(i=0; i<this.sourceData.length; i++){
/*jshint eqeqeq: false*/
if(this.sourceData[i].value == value) {
/*jshint eqeqeq: true*/
text = this.sourceData[i].text;
break;
}
}
}
Select.superclass.value2html(text, element);
deferred.resolve();
}, function () {
Select.superclass.value2html(this.options.sourceError, element);
deferred.resolve();
});
return deferred.promise();
},
// ------------- additional functions ------------
onSourceReady: function (success, error) {
//if allready loaded just call success
if($.isArray(this.sourceData)) {
success.call(this);
return;
}
// try parse json in single quotes (for double quotes jquery does automatically)
try {
this.options.source = $.fn.editableform.utils.tryParseJson(this.options.source, false);
} catch (e) {
error.call(this);
return;
}
//loading from url
if (typeof this.options.source === 'string') {
var cacheID = this.options.source + (this.options.name ? '-' + this.options.name : ''),
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;
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;
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 = [];
}
//loading sourceData from server
$.ajax({
url: this.options.source,
type: 'get',
cache: false,
data: {name: this.options.name},
dataType: 'json',
success: $.proxy(function (data) {
cache.loading = false;
// this.options.source = data;
this.sourceData = this.makeArray(data);
if($.isArray(this.sourceData)) {
this.doPrepend();
//store result in cache
cache.sourceData = this.sourceData;
success.call(this);
$.each(cache.callbacks, function () { this.call(); }); //run success callbacks for other fields
} else {
error.call(this);
$.each(cache.err_callbacks, function () { this.call(); }); //run error callbacks for other fields
}
}, this),
error: $.proxy(function () {
cache.loading = false;
error.call(this);
$.each(cache.err_callbacks, function () { this.call(); }); //run error callbacks for other fields
}, this)
});
} else { //options as json/array
this.sourceData = this.makeArray(this.options.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)) {
//try parse json in single quotes
this.options.prepend = $.fn.editableform.utils.tryParseJson(this.options.prepend, true);
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);
}
},
renderOptions: function() {
if(!$.isArray(this.sourceData)) { if(!$.isArray(this.sourceData)) {
return; return;
} }
@ -188,80 +39,21 @@ $(function(){
} }
}, },
/** value2htmlFinal: function(value, element) {
* convert data to array suitable for sourceData, e.g. [{value: 1, text: 'abc'}, {...}] var text = '', item = this.itemByVal(value);
*/ if(item) {
makeArray: function(data) { text = item.text;
var count, obj, result = [], iterateEl;
if(!data || typeof data === 'string') {
return null;
} }
Select.superclass.constructor.superclass.value2html(text, element);
if($.isArray(data)) { //array
iterateEl = function (k, v) {
obj = {value: k, text: v};
if(count++ >= 2) {
return false;// exit each if object has more than one value
}
};
for(var i = 0; i < data.length; i++) {
if(typeof data[i] === 'object') {
count = 0;
$.each(data[i], iterateEl);
if(count === 1) {
result.push(obj);
} else if(count > 1 && data[i].hasOwnProperty('value') && data[i].hasOwnProperty('text')) {
result.push(data[i]);
} else {
//data contains incorrect objects
}
} else {
result.push({value: i, text: data[i]});
}
}
} else { //object
$.each(data, function (k, v) {
result.push({value: k, text: v});
});
}
return result;
} }
}); });
Select.defaults = $.extend({}, $.fn.editableform.types.abstract.defaults, { Select.defaults = $.extend({}, $.fn.editableform.types.list.defaults, {
/** /**
@property tpl @property tpl
@default <select></select> @default <select></select>
**/ **/
tpl:'<select></select>', tpl:'<select></select>'
/**
Source data for dropdown list. If string - considered ajax url to load items. Otherwise should be an array.
Array format is: <code>[{value: 1, text: "text"}, {...}]</code><br>
For compability it also supports format <code>{value1: text1, value2: text2 ...}</code> but it does not guarantee elements order.
@property source
@type string|array|object
@default null
**/
source:null,
/**
Data automatically prepended to the begining of dropdown list.
@property prepend
@type string|array|object
@default false
**/
prepend:false,
/**
Error message shown when list cannot be loaded (e.g. ajax error)
@property sourceError
@type string
@default Error when loading options
**/
sourceError: 'Error when loading options'
}); });
$.fn.editableform.types.select = Select; $.fn.editableform.types.select = Select;

@ -3,6 +3,7 @@ Text input
@class text @class text
@extends abstract @extends abstract
@final
@example @example
<a href="#" id="username" data-type="text" data-pk="1">awesome</a> <a href="#" id="username" data-type="text" data-pk="1">awesome</a>
<script> <script>
@ -24,8 +25,8 @@ $(function(){
$.extend(Text.prototype, { $.extend(Text.prototype, {
activate: function() { activate: function() {
if(this.$input.is(':visible')) { if(this.$input.is(':visible')) {
$.fn.editableform.utils.setCursorPosition(this.$input.get(0), this.$input.val().length);
this.$input.focus(); this.$input.focus();
$.fn.editableform.utils.setCursorPosition(this.$input.get(0), this.$input.val().length);
} }
} }
}); });

@ -3,6 +3,7 @@ Textarea input
@class textarea @class textarea
@extends abstract @extends abstract
@final
@example @example
<a href="#" id="comments" data-type="textarea" data-pk="1">awesome comment!</a> <a href="#" id="comments" data-type="textarea" data-pk="1">awesome comment!</a>
<script> <script>

@ -43,6 +43,7 @@
<script src="unit/select.js"></script> <script src="unit/select.js"></script>
<script src="unit/textarea.js"></script> <script src="unit/textarea.js"></script>
<script src="unit/api.js"></script> <script src="unit/api.js"></script>
<script src="unit/checklist.js"></script>
<script> <script>
if(fc.f === 'bootstrap') { if(fc.f === 'bootstrap') {
loadJs('unit/date.js'); loadJs('unit/date.js');

@ -31,9 +31,11 @@ function getAssets(f, c, src, libs) {
containers+'editable-container.js', containers+'editable-container.js',
element+'editable-element.js', element+'editable-element.js',
inputs+'abstract.js', inputs+'abstract.js',
inputs+'list.js',
inputs+'text.js', inputs+'text.js',
inputs+'textarea.js', inputs+'textarea.js',
inputs+'select.js' inputs+'select.js',
inputs+'checklist.js'
], ],
css = [ css = [

@ -1,7 +1,7 @@
var timeout = 200; var timeout = 200;
$(function () { $(function () {
$.mockjaxSettings.responseTime = 50; $.mockjaxSettings.responseTime = 50;
$.mockjax({ $.mockjax({
url: 'post.php', url: 'post.php',
@ -23,6 +23,38 @@ $(function () {
this.responseText = settings; this.responseText = settings;
} }
}); });
window.groups = {
0: 'Guest',
1: 'Service',
2: 'Customer',
3: 'Operator',
4: 'Support',
5: 'Admin',
6: '',
'': 'Nothing'
};
//groups as array
window.groupsArr = [];
for(var i in groups) {
groupsArr.push({value: i, text: groups[i]});
}
window.size = groupsArr.length;
$.mockjax({
url: 'groups.php',
responseText: groups
});
$.mockjax({
url: 'groups-error.php',
status: 500,
responseText: 'Internal Server Error'
});
}); });
// usefull functions // usefull functions

@ -3,7 +3,6 @@ $(function () {
module("api", { module("api", {
setup: function(){ setup: function(){
fx = $('#async-fixture'); fx = $('#async-fixture');
delete $.fn.editable.defaults.name;
$.support.transition = false; $.support.transition = false;
} }
}); });

93
test/unit/checklist.js Normal file

@ -0,0 +1,93 @@
$(function () {
module("checklist", {
setup: function(){
sfx = $('#qunit-fixture'),
fx = $('#async-fixture');
$.support.transition = false;
}
});
asyncTest("should load options, set correct value and save new value", function () {
var sep = '-',
newValue,
e = $('<a href="#" data-type="checklist" data-url="post.php"></a>').appendTo(fx).editable({
pk: 1,
source: groupsArr,
value: [2, 3],
viewseparator: sep
});
equal(e.text(), groups[2]+sep+groups[3], 'autotext ok');
e.click();
var p = tip(e);
equal(p.find('input[type="checkbox"]').length, groupsArr.length, 'checkboxes rendered');
equal(p.find('input[type="checkbox"]:checked').length, 2, 'checked count ok');
equal(p.find('input[type="checkbox"]:checked').eq(0).val(), 2, '1st checked');
equal(p.find('input[type="checkbox"]:checked').eq(1).val(), 3, '2nd checked');
//set new value
p.find('input[type="checkbox"]:checked').eq(0).click();
p.find('input[type="checkbox"]').first().click();
newValue = p.find('input[type="checkbox"]').first().val();
//submit
p.find('form').submit();
setTimeout(function() {
ok(!p.is(':visible'), 'popup closed');
equal(e.data('editable').value.join(''), [newValue, 3].join(''), 'new value ok')
equal(e.text(), groups[newValue]+sep+groups[3], 'new text ok');
// open container again to see what checked
e.click()
p = tip(e);
equal(p.find('input[type="checkbox"]').length, groupsArr.length, 'checkboxes rendered');
equal(p.find('input[type="checkbox"]:checked').length, 2, 'checked count ok');
equal(p.find('input[type="checkbox"]:checked').eq(0).val(), newValue, '1st checked');
equal(p.find('input[type="checkbox"]:checked').eq(1).val(), 3, '2nd checked');
e.remove();
start();
}, timeout);
});
asyncTest("limit option", function () {
var e = $('<a href="#" data-type="checklist" data-value="2,3" data-url="post.php"></a>').appendTo(fx).editable({
pk: 1,
source: groupsArr,
limit: 1,
limitText: '{checked} of {count}'
});
equal(e.text(), '2 of '+groupsArr.length, 'autotext ok');
e.click();
var p = tip(e);
equal(p.find('input[type="checkbox"]:checked').length, 2, 'checked count ok');
equal(p.find('input[type="checkbox"]:checked').eq(0).val(), 2, '1st checked');
equal(p.find('input[type="checkbox"]:checked').eq(1).val(), 3, '2nd checked');
//set new value
p.find('input[type="checkbox"]').first().click();
newValue = p.find('input[type="checkbox"]').first().val();
//submit
p.find('form').submit();
setTimeout(function() {
ok(!p.is(':visible'), 'popup closed');
equal(e.text(), '3 of '+groupsArr.length, 'autotext ok');
e.remove();
start();
}, timeout);
});
});

@ -122,7 +122,17 @@
ok(!p.is(':visible'), 'popover closed'); ok(!p.is(':visible'), 'popover closed');
}); });
test("should not wrap buttons when parent has position:absolute", function () {
var d = $('<div style="position: absolute; top: 200px">').appendTo(fx),
e = $('<a href="#" data-pk="1" data-url="post.php" data-name="text1">abc</a>').appendTo(d).editable();
e.click();
var p = tip(e);
ok(p.find('button').offset().top < p.find('.editable-input').offset().top + p.find('.editable-input').height(), 'buttons top ok');
ok(p.find('button').offset().left > p.find('.editable-input').offset().left + p.find('.editable-input').width(), 'buttons left ok');
d.remove();
});
//unfortunatly, testing this feature does not always work in browsers. Tested manually. //unfortunatly, testing this feature does not always work in browsers. Tested manually.
/* /*

@ -5,7 +5,6 @@ $(function () {
module("date", { module("date", {
setup: function(){ setup: function(){
fx = $('#async-fixture'); fx = $('#async-fixture');
$.fn.editable.defaults.name = 'name1';
dpg = $.fn.datepicker.DPGlobal; dpg = $.fn.datepicker.DPGlobal;
} }
}); });
@ -14,7 +13,7 @@ $(function () {
return dpg.formatDate(date, dpg.parseFormat(format), 'en'); return dpg.formatDate(date, dpg.parseFormat(format), 'en');
} }
asyncTest("popover should contain datepicker with value and save new entered date", function () { asyncTest("container should contain datepicker with value and save new entered date", function () {
expect(9); expect(9);
$.fn.editableform.types.date.defaults.datepicker.weekStart = 1; $.fn.editableform.types.date.defaults.datepicker.weekStart = 1;
@ -131,4 +130,46 @@ $(function () {
ok(!p.is(':visible'), 'popover closed'); ok(!p.is(':visible'), 'popover closed');
}); });
asyncTest("clear button", function () {
var d = '15.05.1984',
e = $('<a href="#" data-type="date" data-pk="1" data-url="post-date-clear.php">'+d+'</a>').appendTo(fx).editable({
format: f,
clear: 'abc'
});
$.mockjax({
url: 'post-date-clear.php',
response: function(settings) {
equal(settings.data.value, '', 'submitted value correct');
}
});
equal(frmt(e.data('editable').value, 'dd.mm.yyyy'), d, 'value correct');
e.click();
var p = tip(e);
ok(p.find('.datepicker').is(':visible'), 'datepicker exists');
equal(frmt(e.data('editable').value, f), d, 'day set correct');
equal(p.find('td.day.active').text(), 15, 'day shown correct');
var clear = p.find('.editable-clear a');
equal(clear.text(), 'abc', 'clear link shown');
//click clear
clear.click();
ok(!p.find('td.day.active').length, 'no active day');
p.find('form').submit();
setTimeout(function() {
ok(!p.is(':visible'), 'popover closed');
equal(e.data('editable').value, null, 'null saved to value');
equal(e.text(), e.data('editable').options.emptytext, 'empty text shown');
e.remove();
start();
}, timeout);
});
}); });

@ -5,7 +5,6 @@ $(function () {
module("dateui", { module("dateui", {
setup: function(){ setup: function(){
fx = $('#async-fixture'); fx = $('#async-fixture');
$.fn.editable.defaults.name = 'name1';
} }
}); });
@ -82,4 +81,45 @@ $(function () {
ok(!p.is(':visible'), 'popover closed'); ok(!p.is(':visible'), 'popover closed');
}); });
asyncTest("clear button", function () {
var d = '15.05.1984',
f = 'dd.mm.yyyy',
e = $('<a href="#" data-type="date" data-pk="1" data-url="post-date-clear.php">'+d+'</a>').appendTo(fx).editable({
format: f,
clear: 'abc'
});
$.mockjax({
url: 'post-date-clear.php',
response: function(settings) {
equal(settings.data.value, '', 'submitted value correct');
}
});
equal(frmt(e.data('editable').value, 'dd.mm.yyyy'), d, 'value correct');
e.click();
var p = tip(e);
ok(p.find('.ui-datepicker').is(':visible'), 'datepicker exists');
equal(frmt(e.data('editable').value, f), d, 'day set correct');
equal(p.find('a.ui-state-active').text(), 15, 'day shown correct');
var clear = p.find('.editable-clear a');
equal(clear.text(), 'abc', 'clear link shown');
//click clear
clear.click();
p.find('form').submit();
setTimeout(function() {
ok(!p.is(':visible'), 'popover closed');
equal(e.data('editable').value, null, 'null saved to value');
equal(e.text(), e.data('editable').options.emptytext, 'empty text shown');
e.remove();
start();
}, 500);
});
}); });

@ -1,42 +1,9 @@
$(function () { $(function () {
window.groups = {
0: 'Guest',
1: 'Service',
2: 'Customer',
3: 'Operator',
4: 'Support',
5: 'Admin',
6: '',
'': 'Nothing'
};
//groups as array
window.groupsArr = [];
for(var i in groups) {
groupsArr.push({value: i, text: groups[i]});
}
window.size = groupsArr.length;
$.mockjax({
url: 'groups.php',
responseText: groups
});
$.mockjax({
url: 'groups-error.php',
status: 500,
responseText: 'Internal Server Error'
});
module("select", { module("select", {
setup: function(){ setup: function(){
sfx = $('#qunit-fixture'), sfx = $('#qunit-fixture'),
fx = $('#async-fixture'); fx = $('#async-fixture');
$.fn.editable.defaults.name = 'name1';
//clear cache
$(document).removeData('groups.php-'+$.fn.editable.defaults.name);
$.support.transition = false; $.support.transition = false;
} }
}); });
@ -111,7 +78,7 @@ $(function () {
test("load options from simple array", function () { test("load options from simple array", function () {
var arr = ['q', 'w', 'x'], var arr = ['q', 'w', 'x'],
e = $('<a href="#" data-type="select" data-value="2" data-url="post.php">customer</a>').appendTo('#qunit-fixture').editable({ e = $('<a href="#" data-type="select" data-value="x" data-url="post.php">customer</a>').appendTo('#qunit-fixture').editable({
pk: 1, pk: 1,
autotext: true, autotext: true,
source: arr source: arr
@ -122,7 +89,7 @@ $(function () {
ok(p.is(':visible'), 'popover visible') ok(p.is(':visible'), 'popover visible')
ok(p.find('select').length, 'select exists') ok(p.find('select').length, 'select exists')
equal(p.find('select').find('option').length, arr.length, 'options loaded') equal(p.find('select').find('option').length, arr.length, 'options loaded')
equal(p.find('select').val(), 2, 'selected value correct') equal(p.find('select').val(), 'x', 'selected value correct')
p.find('button[type=button]').click(); p.find('button[type=button]').click();
ok(!p.is(':visible'), 'popover was removed'); ok(!p.is(':visible'), 'popover was removed');
}) })
@ -197,7 +164,7 @@ $(function () {
}, timeout); }, timeout);
}) })
asyncTest("popover should save new selected value", function () { asyncTest("should save new selected value", function () {
var e = $('<a href="#" data-type="select" data-value="2" data-url="post.php">customer</a>').appendTo(fx).editable({ var e = $('<a href="#" data-type="select" data-value="2" data-url="post.php">customer</a>').appendTo(fx).editable({
pk: 1, pk: 1,
source: groups source: groups
@ -278,8 +245,11 @@ $(function () {
}); });
asyncTest("cache request for same selects", function () { asyncTest("cache request for same selects", function () {
var e = $('<a href="#" data-type="select" data-pk="1" data-value="2" data-url="post.php" data-source="groups-cache.php">customer</a>').appendTo(fx).editable(), //clear cache
e1 = $('<a href="#" data-type="select" data-pk="1" data-value="2" data-url="post.php" data-source="groups-cache.php">customer</a>').appendTo(fx).editable(), $(document).removeData('groups.php-name1');
var e = $('<a href="#" data-type="select" data-pk="1" data-name="name1" data-value="2" data-url="post.php" data-source="groups-cache.php">customer</a>').appendTo(fx).editable(),
e1 = $('<a href="#" data-type="select" data-pk="1" id="name1" data-value="2" data-url="post.php" data-source="groups-cache.php">customer</a>').appendTo(fx).editable(),
req = 0; req = 0;
$.mockjax({ $.mockjax({
@ -323,6 +293,10 @@ $(function () {
asyncTest("cache simultaneous requests", function () { asyncTest("cache simultaneous requests", function () {
expect(4); expect(4);
//clear cache
$(document).removeData('groups.php-name1');
var req = 0; var req = 0;
$.mockjax({ $.mockjax({
url: 'groups-cache-sim.php', url: 'groups-cache-sim.php',
@ -333,9 +307,9 @@ $(function () {
} }
}); });
var e = $('<a href="#" data-type="select" data-pk="1" data-value="1" data-url="post.php" data-source="groups-cache-sim.php"></a>').appendTo(fx).editable(), var e = $('<a href="#" data-type="select" data-pk="1" data-name="name1" data-value="1" data-url="post.php" data-source="groups-cache-sim.php"></a>').appendTo(fx).editable(),
e1 = $('<a href="#" data-type="select" data-pk="1" data-value="2" data-url="post.php" data-source="groups-cache-sim.php"></a>').appendTo(fx).editable(), e1 = $('<a href="#" data-type="select" data-pk="1" data-name="name1" data-value="2" data-url="post.php" data-source="groups-cache-sim.php"></a>').appendTo(fx).editable(),
e2 = $('<a href="#" data-type="select" data-pk="1" data-value="3" data-url="post.php" data-source="groups-cache-sim.php"></a>').appendTo(fx).editable(); e2 = $('<a href="#" data-type="select" data-pk="1" data-name="name1" data-value="3" data-url="post.php" data-source="groups-cache-sim.php"></a>').appendTo(fx).editable();
setTimeout(function() { setTimeout(function() {
@ -354,6 +328,10 @@ $(function () {
asyncTest("cache simultaneous requests (loading error)", function () { asyncTest("cache simultaneous requests (loading error)", function () {
expect(4); expect(4);
//clear cache
$(document).removeData('groups.php-name1');
var req = 0; var req = 0;
$.mockjax({ $.mockjax({
url: 'groups-cache-sim-err.php', url: 'groups-cache-sim-err.php',
@ -364,9 +342,9 @@ $(function () {
} }
}); });
var e = $('<a href="#" data-type="select" data-pk="1" data-value="1" data-autotext="always" data-url="post.php" data-source="groups-cache-sim-err.php">35</a>').appendTo(fx).editable(), var e = $('<a href="#" data-type="select" data-pk="1" data-name="name1" data-value="1" data-autotext="always" data-url="post.php" data-source="groups-cache-sim-err.php">35</a>').appendTo(fx).editable(),
e1 = $('<a href="#" data-type="select" data-pk="1" data-value="2" data-autotext="always" data-url="post.php" data-source="groups-cache-sim-err.php">35</a>').appendTo(fx).editable(), e1 = $('<a href="#" data-type="select" data-pk="1" data-name="name1" data-value="2" data-autotext="always" data-url="post.php" data-source="groups-cache-sim-err.php">35</a>').appendTo(fx).editable(),
e2 = $('<a href="#" data-type="select" data-pk="1" data-value="3" data-autotext="always" data-url="post.php" data-source="groups-cache-sim-err.php">6456</a>').appendTo(fx).editable(), e2 = $('<a href="#" data-type="select" data-pk="1" data-name="name1" data-value="3" data-autotext="always" data-url="post.php" data-source="groups-cache-sim-err.php">6456</a>').appendTo(fx).editable(),
errText = $.fn.editableform.types.select.defaults.sourceError; errText = $.fn.editableform.types.select.defaults.sourceError;
setTimeout(function() { setTimeout(function() {

@ -3,7 +3,6 @@ $(function () {
module("text", { module("text", {
setup: function() { setup: function() {
fx = $('#async-fixture'); fx = $('#async-fixture');
$.fn.editable.defaults.name = 'name1';
$.support.transition = false; $.support.transition = false;
} }
}); });

@ -5,8 +5,8 @@ $(function () {
module("textarea", { module("textarea", {
setup: function(){ setup: function(){
fx = $('#async-fixture'), fx = $('#async-fixture');
$.fn.editable.defaults.name = 'name1'; $.support.transition = false;
} }
}); });