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
----------------------------
[enh] contribution guide in README.md (vitalets)

@ -13,7 +13,7 @@ Your feedback is very appreciated!
## Contribution
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:
**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>build data-docs-dist</code> in **gh-pages** directory
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.

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

@ -2,7 +2,7 @@
"name": "X-editable",
"title": "X-editable",
"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",
"author": {
"name": "Vitaliy Potapov",

@ -1,6 +1,8 @@
.editable-container,
.editable-container.popover {
width: auto;
white-space: nowrap; /* without this rule buttons wrap input in non-static elements */
.editable-container {
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 ($) {
//form template
$.fn.editableform.template = '<form class="form-inline editableform"><div class="control-group">' +
'&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>' +
'<div style="clear:both"><span class="help-block editable-error-block"></span></div>' +
'</div></form>';
$.extend($.fn.editableform.Constructor.prototype, {
initTemplate: function() {
this.$form = $($.fn.editableform.template);
this.$form.find('.editable-error-block').addClass('help-block');
//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
$.fn.editableform.errorGroupClass = 'error';

@ -7,24 +7,20 @@ Editableform based on jQuery UI
initTemplate: function() {
this.$form = $($.fn.editableform.template);
//init buttons
this.$form.find('button[type=submit]').button({
//buttons
this.$form.find('.editable-buttons').append($.fn.editableform.buttons);
this.$form.find('.editable-submit').button({
icons: { primary: "ui-icon-check" },
text: false
});
this.$form.find('button[type=button]').button({
icons: { primary: "ui-icon-cancel" },
}).removeAttr('title');
this.$form.find('.editable-cancel').button({
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
$.fn.editableform.errorGroupClass = null;
$.fn.editableform.errorBlockClass = 'ui-state-error';

@ -2,8 +2,7 @@
* EditableForm utilites
*/
(function ($) {
$.extend($.fn.editableform, {
utils: {
$.fn.editableform.utils = {
/**
* classic JS inheritance function
*/
@ -38,7 +37,7 @@
* 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 (typeof s === 'string' && s.length && s.match(/^[\{\[].*[\}\]]$/)) {
if (safe) {
try {
/*jslint evil: true*/
@ -99,7 +98,24 @@
}
});
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));

@ -1,11 +1,44 @@
.editableform,
.editableform div.control-group {
margin-bottom: 0;
.editableform {
margin-bottom: 0; /* overwrites bootstrap margin */
}
.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 {
background: url('img/loading.gif') center center no-repeat;
height: 25px;
width: auto;
}
.editable-inline .editableform-loading {
@ -14,29 +47,40 @@
.editable-error-block {
max-width: 300px;
margin-top: 3px;
margin-bottom: 0;
clear: both;
margin: 5px 0 0 0;
width: auto;
}
/*add padding for jquery ui*/
.editable-error-block.ui-state-error {
padding: 3px;
}
.editable-error {
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 {
height: 150px; /*default height for textarea*/
}
.editableform .editable-date {
float: left;
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 = {
constructor: EditableForm,
initInput: function() {
initInput: function() { //called once
var TypeConstructor, typeOptions;
//create input of specified type
if(typeof $.fn.editableform.types[this.options.type] === 'function') {
TypeConstructor = $.fn.editableform.types[this.options.type];
typeOptions = $.fn.editableform.utils.sliceObj(this.options, Object.keys(TypeConstructor.defaults));
typeOptions = $.fn.editableform.utils.sliceObj(this.options, $.fn.editableform.utils.objectKeys(TypeConstructor.defaults));
this.input = new TypeConstructor(typeOptions);
} else {
$.error('Unknown type: '+ this.options.type);
@ -34,6 +34,9 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'.
},
initTemplate: function() {
this.$form = $($.fn.editableform.template);
//buttons
this.$form.find('div.editable-buttons').append($.fn.editableform.buttons);
},
/**
Renders editableform
@ -57,20 +60,29 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'.
//render input
$.when(this.input.render())
.then($.proxy(function () {
//place input
this.$form.find('div.control-group').prepend(this.input.$input);
//attach 'cancel' handler
this.$form.find('button[type=button]').click($.proxy(this.cancel, this));
//input
this.$form.find('div.editable-input').append(this.input.$input);
//"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
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) {
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);
} else {
this.error(false);
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.$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');
},
showLoading: function() {
var fw, fh, iw, ih;
//set loading size equal to form
var w;
if(this.$form) {
fh = this.$form.outerHeight() || 0;
fw = this.$form.outerWidth() || 0;
ih = (this.input && this.input.$input.outerHeight()) || 0;
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);
}
//set loading size equal to form
this.$loading.width(this.$form.outerWidth());
this.$loading.height(this.$form.outerHeight());
this.$form.hide();
} else {
//stretch loading to fill container width
w = this.$loading.parent().width();
if(w) {
this.$loading.width(w);
}
}
this.$loading.show();
},
@ -247,6 +257,17 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'.
option: function(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;
}
}
};
@ -290,7 +311,7 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'.
/* 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
@type string
@ -395,14 +416,20 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'.
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">' +
'&nbsp;<button type="submit">Ok</button>&nbsp;<button type="button">Cancel</button></div>' +
$.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 attahced to control-group
$.fn.editableform.errorGroupClass = null;

@ -29,17 +29,13 @@ Makes editable any HTML element on the page. Applied as jQuery method.
return;
}
//name must be defined
//name
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
if(typeof $.fn.editableform.types[this.options.type] === 'function') {
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);
} else {
$.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
$(document).off('keyup.editable').on('keyup.editable', function (e) {
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;
}
//close all other containers
$('.editable-container').find('button[type=button]').click();
$('.editable-container').find('.editable-cancel').click();
});
//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
$('.editable-container').find('button[type=button]').click();
$('.editable-container').find('.editable-cancel').click();
//show container
this.container.show();

@ -18,6 +18,7 @@ To create your own input you should inherit from this class.
this.type = type;
this.options = $.extend({}, defaults, options);
this.$input = null;
this.$clear = null;
this.error = null;
},
@ -110,6 +111,15 @@ To create your own input you should inherit from this class.
if(this.$input.is(':visible')) {
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
@extends abstract
@final
@example
<a href="#" id="dob" data-type="date" data-pk="1" data-url="/post" data-original-title="Select date">15/05/1984</a>
<script>
@ -53,6 +54,14 @@ $(function(){
render: function () {
Date.superclass.render.call(this);
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) {
@ -81,6 +90,11 @@ $(function(){
},
activate: function() {
},
clear: function() {
this.$input.data('datepicker').date = null;
this.$input.find('.active').removeClass('active');
}
});
@ -130,7 +144,16 @@ $(function(){
weekStart: 0,
startView: 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: '&times; clear'
});
$.fn.editableform.types.date = Date;

@ -1,10 +1,11 @@
/**
jQuery UI 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
@extends abstract
@final
@example
<a href="#" id="dob" data-type="date" data-pk="1" data-url="/post" data-original-title="Select date">15/05/1984</a>
<script>
@ -51,6 +52,14 @@ $(function(){
render: function () {
DateUI.superclass.render.call(this);
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) {
@ -99,6 +108,10 @@ $(function(){
},
activate: function() {
},
clear: function() {
this.$input.datepicker('setDate', null);
}
});
@ -149,7 +162,16 @@ $(function(){
firstDay: 0,
changeYear: 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;

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
@extends abstract
@extends list
@final
@example
<a href="#" id="status" data-type="select" data-pk="1" data-url="/post" data-original-title="Select status"></a>
<script>
@ -25,160 +26,10 @@ $(function(){
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, {
render: 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() {
renderList: function() {
if(!$.isArray(this.sourceData)) {
return;
}
@ -188,80 +39,21 @@ $(function(){
}
},
/**
* 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;
value2htmlFinal: function(value, element) {
var text = '', item = this.itemByVal(value);
if(item) {
text = item.text;
}
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
Select.superclass.constructor.superclass.value2html(text, element);
}
};
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
@default <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'
tpl:'<select></select>'
});
$.fn.editableform.types.select = Select;

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

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

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

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

@ -23,6 +23,38 @@ $(function () {
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

@ -3,7 +3,6 @@ $(function () {
module("api", {
setup: function(){
fx = $('#async-fixture');
delete $.fn.editable.defaults.name;
$.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');
});
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.
/*

@ -5,7 +5,6 @@ $(function () {
module("date", {
setup: function(){
fx = $('#async-fixture');
$.fn.editable.defaults.name = 'name1';
dpg = $.fn.datepicker.DPGlobal;
}
});
@ -14,7 +13,7 @@ $(function () {
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);
$.fn.editableform.types.date.defaults.datepicker.weekStart = 1;
@ -131,4 +130,46 @@ $(function () {
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", {
setup: function(){
fx = $('#async-fixture');
$.fn.editable.defaults.name = 'name1';
}
});
@ -82,4 +81,45 @@ $(function () {
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 () {
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", {
setup: function(){
sfx = $('#qunit-fixture'),
fx = $('#async-fixture');
$.fn.editable.defaults.name = 'name1';
//clear cache
$(document).removeData('groups.php-'+$.fn.editable.defaults.name);
$.support.transition = false;
}
});
@ -111,7 +78,7 @@ $(function () {
test("load options from simple array", function () {
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,
autotext: true,
source: arr
@ -122,7 +89,7 @@ $(function () {
ok(p.is(':visible'), 'popover visible')
ok(p.find('select').length, 'select exists')
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();
ok(!p.is(':visible'), 'popover was removed');
})
@ -197,7 +164,7 @@ $(function () {
}, 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({
pk: 1,
source: groups
@ -278,8 +245,11 @@ $(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(),
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(),
//clear cache
$(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;
$.mockjax({
@ -323,6 +293,10 @@ $(function () {
asyncTest("cache simultaneous requests", function () {
expect(4);
//clear cache
$(document).removeData('groups.php-name1');
var req = 0;
$.mockjax({
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(),
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(),
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();
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-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-name="name1" data-value="3" data-url="post.php" data-source="groups-cache-sim.php"></a>').appendTo(fx).editable();
setTimeout(function() {
@ -354,6 +328,10 @@ $(function () {
asyncTest("cache simultaneous requests (loading error)", function () {
expect(4);
//clear cache
$(document).removeData('groups.php-name1');
var req = 0;
$.mockjax({
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(),
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(),
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(),
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-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-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;
setTimeout(function() {

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

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