Merge branch 'release-1.1.1'

This commit is contained in:
vitalets 2012-11-30 19:47:20 +04:00
commit b2009d7c45
26 changed files with 832 additions and 197 deletions

@ -1,5 +1,16 @@
X-editable changelog
=============================
Version 1.1.1 Nov 30, 2012
----------------------------
[enh] 'showbuttons' option to hide buttons in form (vitalets)
[enh] object can be passed in 'option' method to set several options at once (vitalets)
[enh #20] toggle editable by 'dblclick' and 'mouseenter' (vitalets)
[enh] added 'inputs-ext' directory with sample input 'address'. They will not be concatenated to main files (vitalets)
[enh #13] 'onblur' option: to cancel, submit or ignore when user clicks outside the form (vitalets)
[enh] 'ajaxOptions' parameter for advanced ajax configuration (vitalets)
[enh] 'success' callback can return object to overwrite submitted value (vitalets)
Version 1.1.0 Nov 27, 2012

@ -7,7 +7,7 @@ It is a new life of [bootstrap-editable plugin](http://github.com/vitalets/boots
See **http://vitalets.github.com/x-editable**
## Reporting issues
When creating issues please provide jsFiddle example. You can just fork [this fiddle](http://jsfiddle.net/xBB5x/1/) as starting point.
When creating issues please provide jsFiddle example. You can just fork [this fiddle](http://jsfiddle.net/xBB5x/5/) as starting point.
Your feedback is very appreciated!
## Contribution
@ -60,7 +60,7 @@ Or use grunt's _qunit_ task <code>grunt test</code>. For that you also need to [
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> / <code>gh-pages-dev</code> branch and make pull request as usual.
Thanks for your support!

@ -151,7 +151,8 @@ module.exports = function(grunt) {
'src/element/*.js',
'src/inputs/*.js',
'src/inputs/date/date.js',
'src/inputs/dateui/dateui.js'
'src/inputs/dateui/dateui.js',
'src/inputs-ext/**/*.js'
]
},
/*
@ -192,29 +193,20 @@ module.exports = function(grunt) {
flatten: true
}
},
inputs_ext: {
files: {
'<%= dist %>/inputs-ext/': 'src/inputs-ext/**'
},
options: {
basePath: 'inputs-ext'
}
},
ui_datepicker: {
files: {
//copy jquery ui datepicker
'<%= dist %>/jquery-editable/jquery-ui-datepicker/' : 'src/inputs/dateui/jquery-ui-datepicker/**'
}
}
},
yuidoc: {
compile: {
name: '<%= pkg.title || pkg.name %>',
description: '<%= pkg.description %>',
version: '<%= pkg.version %>',
url: "<%= pkg.homepage %>",
// logo: 'src/editable-form/img/loading.gif',
options: {
paths: "src/",
ignorePaths: ['src/inputs/date/locales'],
outdir: "../docs/",
// theme: "simple",
themedir: "../yuidoc-theme"
//themedir: "../yuidoc-bootstrap-theme-master"
}
}
}
},
//compress does not work properly for MAC OS (see https://github.com/vitalets/bootstrap-editable/issues/19)

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

@ -5,4 +5,9 @@
.editable-container.popover {
/* width: 300px;*/ /* debug */
width: auto; /* without this rule popover does not stretch */
}
.editable-container.editable-inline {
display: inline;
vertical-align: middle;
}

@ -27,7 +27,33 @@ Applied as jQuery method.
//bind 'destroyed' listener to destroy container when element is removed from dom
this.$element.on('destroyed', $.proxy(function(){
this.destroy();
}, this));
}, this));
//attach document handlers (once)
if(!$(document).data('editable-handlers-attached')) {
//close all on escape
$(document).on('keyup.editable', function (e) {
if (e.which === 27) {
$('.editable-open').editableContainer('hide');
//todo: return focus on element
}
});
//close containers when click outside
$(document).on('click.editable', function(e) {
var $target = $(e.target);
//if click inside some editableContainer --> no nothing
if($target.is('.editable-container') || $target.parents('.editable-container').length || $target.parents('.ui-datepicker-header').length) {
return;
} else {
//close all open containers (except one)
EditableContainer.prototype.closeOthers(e.target);
}
});
$(document).data('editable-handlers-attached', true);
}
},
//split options on containerOptions and formOptions
@ -93,11 +119,22 @@ Applied as jQuery method.
/**
Shows container with form
@method show()
@param {boolean} closeAll Wether to close all other editable containers when showing this one. Default true.
**/
show: function () {
show: function (closeAll) {
this.$element.addClass('editable-open');
if(closeAll !== false) {
//close all open containers (except this)
this.closeOthers(this.$element[0]);
}
this.innerShow();
},
/* internal show method. To be overwritten in child classes */
innerShow: function () {
this.call('show');
this.tip().addClass('editable-container');
this.initForm();
this.tip().find(this.innerCss).empty().append(this.$form);
this.$form.editableform('render');
@ -108,10 +145,11 @@ Applied as jQuery method.
@method hide()
**/
hide: function() {
if(!this.tip() || !this.tip().is(':visible')) {
if(!this.tip() || !this.tip().is(':visible') || !this.$element.hasClass('editable-open')) {
return;
}
this.call('hide');
this.$element.removeClass('editable-open');
this.innerHide();
/**
Fired when container was hidden. It occurs on both save or cancel.
@ -121,15 +159,21 @@ Applied as jQuery method.
this.$element.triggerHandler('hidden');
},
/* internal hide method. To be overwritten in child classes */
innerHide: function () {
this.call('hide');
},
/**
Toggles container visibility (show / hide)
@method toggle()
@param {boolean} closeAll Wether to close all other editable containers when showing this one. Default true.
**/
toggle: function() {
toggle: function(closeAll) {
if(this.tip && this.tip().is(':visible')) {
this.hide();
} else {
this.show();
this.show(closeAll);
}
},
@ -210,6 +254,44 @@ Applied as jQuery method.
**/
destroy: function() {
this.call('destroy');
},
/*
Closes other containers except one related to passed element.
Other containers can be cancelled or submitted (depends on onblur option)
*/
closeOthers: function(element) {
$('.editable-open').each(function(i, el){
//do nothing with passed element
if(el === element) {
return;
}
//otherwise cancel or submit all open containers
var $el = $(el),
ec = $el.data('editableContainer');
if(!ec) {
return;
}
if(ec.options.onblur === 'cancel') {
$el.data('editableContainer').hide();
} else if(ec.options.onblur === 'submit') {
$el.data('editableContainer').tip().find('form').submit();
}
});
},
/**
Activates input of visible container (e.g. set focus)
@method activate()
**/
activate: function() {
if(this.tip && this.tip().is(':visible') && this.$form) {
this.$form.data('editableform').input.activate();
}
}
};
@ -248,7 +330,7 @@ Applied as jQuery method.
//store constructor
$.fn.editableContainer.Constructor = EditableContainer;
//defaults - must be redefined!
//defaults
$.fn.editableContainer.defaults = {
/**
Initial value of form input
@ -275,7 +357,16 @@ Applied as jQuery method.
@default true
@private
**/
autohide: true
autohide: true,
/**
Action when user clicks outside the container. Can be <code>cancel|submit|ignore</code>.
Setting <code>ignore</code> allows to have several containers open.
@property onblur
@type string
@default 'cancel'
**/
onblur: 'cancel'
};
/*
@ -290,4 +381,4 @@ Applied as jQuery method.
}
};
}(window.jQuery));
}(window.jQuery));

@ -26,7 +26,7 @@
return this.$form;
},
show: function () {
innerShow: function () {
this.$element.hide();
if(this.$form) {
@ -40,19 +40,13 @@
this.$form.editableform('render');
},
hide: function () {
if(!this.tip() || !this.tip().is(':visible')) {
return;
}
innerHide: function () {
this.$form.hide(this.options.anim, $.proxy(function() {
this.$element.show();
//return focus on element
if (this.options.enablefocus) {
this.$element.focus();
}
//trigger event
this.$element.triggerHandler('hidden');
}, this));
},

@ -28,11 +28,10 @@
this.call('update', $content);
},
show: function () {
innerShow: function () {
this.$form.editableform('render');
this.tip().addClass('editable-container');
this.call('show');
this.tip().addClass('editable-container');
this.$form.data('editableform').input.activate();
},

@ -50,7 +50,7 @@
return this.container()._find(this.container().element);
},
show: function() {
innerShow: function() {
this.call('open');
this.tip().addClass('editable-container');
@ -62,12 +62,8 @@
this.$form.editableform('render');
},
hide: function() {
if(!this.tip() || !this.tip().is(':visible')) {
return;
}
innerHide: function() {
this.call('close');
this.$element.triggerHandler('hidden');
},
setPosition: function() {

@ -5,11 +5,8 @@ Editableform based on Twitter Bootstrap
$.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);
this.$form = $($.fn.editableform.template);
this.$form.find('.editable-error-block').addClass('help-block');
}
});

@ -4,10 +4,7 @@ Editableform based on jQuery UI
(function ($) {
$.extend($.fn.editableform.Constructor.prototype, {
initTemplate: function() {
this.$form = $($.fn.editableform.template);
//buttons
initButtons: function() {
this.$form.find('.editable-buttons').append($.fn.editableform.buttons);
this.$form.find('.editable-submit').button({
icons: { primary: "ui-icon-check" },
@ -17,7 +14,6 @@ Editableform based on jQuery UI
icons: { primary: "ui-icon-closethick" },
text: false
}).removeAttr('title');
}
});

@ -38,7 +38,8 @@
.editableform-loading {
background: url('img/loading.gif') center center no-repeat;
height: 25px;
width: auto;
width: auto;
min-width: 25px;
}
.editable-inline .editableform-loading {

@ -11,7 +11,7 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'.
var EditableForm = function (element, options) {
this.options = $.extend({}, $.fn.editableform.defaults, options);
this.$element = $(element); //div (usually), containing form. not form tag!
this.$element = $(element); //div, containing form. Not form tag! Not editable-element.
this.initInput();
};
@ -34,9 +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);
},
initButtons: function() {
this.$form.find('.editable-buttons').append($.fn.editableform.buttons);
},
/**
Renders editableform
@ -47,8 +47,14 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'.
this.$loading = $($.fn.editableform.loading);
this.$element.empty().append(this.$loading);
this.showLoading();
//init form template and buttons
this.initTemplate();
if(this.options.showbuttons) {
this.initButtons();
} else {
this.$form.find('.editable-buttons').remove();
}
/**
Fired when rendering starts
@ -63,6 +69,11 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'.
//input
this.$form.find('div.editable-input').append(this.input.$input);
//automatically submit inputs when no buttons shown
if(!this.options.showbuttons) {
this.input.autosubmit();
}
//"clear" link
if(this.input.$clear) {
this.$form.find('div.editable-input').append($('<div class="editable-clear">').append(this.input.$clear));
@ -150,11 +161,10 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'.
submit: function(e) {
e.stopPropagation();
e.preventDefault();
var error,
//get value from input
newValue = this.input.input2value(),
newValueStr;
newValue = this.input.input2value(), //get new value from input
newValueStr;
//validation
if (error = this.validate(newValue)) {
@ -162,14 +172,14 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'.
this.showForm();
return;
}
//value as string
newValueStr = this.input.value2str(newValue);
//if value not changed --> cancel
/*jslint eqeq: true*/
if (newValueStr == this.input.value2str(this.value)) {
/*jslint eqeq: false*/
/*jslint eqeq: false*/
this.cancel();
return;
}
@ -177,13 +187,20 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'.
//sending data to server
$.when(this.save(newValueStr))
.done($.proxy(function(response) {
var error;
//call success callback. if it returns string --> show error
if(error = this.options.success.call(this, response, newValue)) {
this.error(error);
//run success callback
var res = typeof this.options.success === 'function' ? this.options.success.call(this, response, newValue) : null;
//if success callback returns string --> show error
if(res && typeof res === 'string') {
this.error(res);
this.showForm();
return;
}
}
//if success callback returns object like {newValue: <something>} --> use that value instead of submitted
if(res && typeof res === 'object' && res.hasOwnProperty('newValue')) {
newValue = res.newValue;
}
//clear error message
this.error(false);
@ -212,7 +229,7 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'.
save: function(value) {
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)))),
params;
params, ajaxOptions;
if (send) { //send to server
this.showLoading();
@ -236,12 +253,14 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'.
if(typeof this.options.url === 'function') { //user's function
return this.options.url.call(this, params);
} else { //send ajax to server and return deferred object
return $.ajax({
ajaxOptions = $.extend({
url : this.options.url,
data : params,
type : 'post',
dataType: 'json'
});
}, this.options.ajaxOptions);
return $.ajax(ajaxOptions);
}
}
},
@ -339,7 +358,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
@example
params: function() {
params: function(params) {
return { a: 1 };
}
@ -398,9 +417,13 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'.
**/
validate: null,
/**
Success callback. Called when value successfully sent on server and response status = 200.
Can be used to process json response. If this function returns string - means error occured and string is shown as error message.
Success callback. Called when value successfully sent on server and **response status = 200**.
Usefull to work with json response. For example, if your backend response can be <code>{success: true}</code>
or <code>{success: false, msg: "server error"}</code> you can check it inside this callback.
If it returns **string** - means error occured and string is shown as error message.
If it returns **object like** <code>{newValue: &lt;something&gt;}</code> - it overwrites value, submitted by user.
Otherwise newValue simply rendered into element.
@property success
@type function
@default null
@ -409,7 +432,36 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'.
if(!response.success) return response.msg;
}
**/
success: function(response, newValue) {}
success: function(response, newValue) {},
/**
Additional options for ajax request.
List of values: http://api.jquery.com/jQuery.ajax
@property ajaxOptions
@type object
@default null
**/
ajaxOptions: null,
/**
Wether to show buttons or not.
Form without buttons can be auto-submitted by input or by onblur = 'submit'.
@property showbuttons
@type boolean
@default true
**/
showbuttons: true
/*todo:
Submit strategy. Can be <code>normal|never</code>
<code>submitmode='never'</code> usefull for turning into classic form several inputs and submitting them together manually.
Works pretty with <code>showbuttons=false</code>
@property submitmode
@type string
@default normal
*/
// submitmode: 'normal'
};
/*

@ -47,34 +47,32 @@ Makes editable any HTML element on the page. Applied as jQuery method.
this.value = this.input.html2value($.trim(this.$element.html()));
isValueByText = true;
} else {
this.value = this.input.str2value($.trim(this.options.value));
if(typeof this.options.value === 'string') {
this.options.value = $.trim(this.options.value);
}
this.value = this.input.str2value(this.options.value);
}
//attach handler to close any container on escape
$(document).off('keyup.editable').on('keyup.editable', function (e) {
if (e.which === 27) {
$('.editable-container').find('.editable-cancel').click();
}
});
//attach handler to close container when click outside
$(document).off('click.editable').on('click.editable', function(e) {
var $target = $(e.target);
//if click inside container --> do nothing
if($target.is('.editable-container') || $target.parents('.editable-container').length || $target.parents('.ui-datepicker-header').length) {
return;
}
//close all other containers
$('.editable-container').find('.editable-cancel').click();
});
//add 'editable' class
this.$element.addClass('editable');
//attach click handler. In disabled mode it just prevent default action (useful for links)
if(this.options.toggle === 'click') {
//attach handler activating editable. In disabled mode it just prevent default action (useful for links)
if(this.options.toggle !== 'manual') {
this.$element.addClass('editable-click');
this.$element.on('click.editable', $.proxy(this.click, this));
this.$element.on(this.options.toggle + '.editable', $.proxy(function(e){
e.preventDefault();
//stop propagation not required anymore because in document click handler it checks event target
//e.stopPropagation();
if(this.options.toggle === 'mouseenter') {
//for hover only show container
this.show();
} else {
//when toggle='click' we should not close all other containers as they will be closed automatically in document click listener
var closeAll = (this.options.toggle !== 'click');
this.toggle(closeAll);
}
}, this));
} else {
this.$element.attr('tabindex', -1); //do not stop focus on element when toggled manually
}
@ -115,7 +113,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
this.options.disabled = false;
this.$element.removeClass('editable-disabled');
this.handleEmpty();
if(this.options.toggle === 'click') {
if(this.options.toggle !== 'manual') {
if(this.$element.attr('tabindex') === '-1') {
this.$element.removeAttr('tabindex');
}
@ -151,10 +149,24 @@ Makes editable any HTML element on the page. Applied as jQuery method.
Sets new option
@method option(key, value)
@param {string} key
@param {mixed} value
@param {string|object} key option name or object with several options
@param {mixed} value option new value
@example
$('.editable').editable('option', 'pk', 2);
**/
option: function(key, value) {
//set option(s) by object
if(key && typeof key === 'object') {
$.each(key, $.proxy(function(k, v){
this.option($.trim(k), v);
}, this));
return;
}
//set option by string
this.options[key] = value;
//disabled
if(key === 'disabled') {
if(value) {
this.disable();
@ -163,12 +175,15 @@ Makes editable any HTML element on the page. Applied as jQuery method.
}
return;
}
this.options[key] = value;
//value
if(key === 'value') {
this.setValue(value);
}
//transfer new option to container!
if(this.container) {
this.container.option(key, value);
this.container.option(key, value);
}
},
@ -193,21 +208,12 @@ Makes editable any HTML element on the page. Applied as jQuery method.
}
},
click: function (e) {
e.preventDefault();
if(this.options.disabled) {
return;
}
//stop propagation bacause document listen any click to hide all editableContainers
e.stopPropagation();
this.toggle();
},
/**
Shows container with form
@method show()
@param {boolean} closeAll Wether to close all other editable containers when showing this one. Default true.
**/
show: function () {
show: function (closeAll) {
if(this.options.disabled) {
return;
}
@ -216,7 +222,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
if(!this.container) {
var containerOptions = $.extend({}, this.options, {
value: this.value,
autohide: false //element itsef will show/hide container
autohide: false //element will take care to show/hide container
});
this.$element.editableContainer(containerOptions);
this.$element.on({
@ -227,12 +233,9 @@ Makes editable any HTML element on the page. Applied as jQuery method.
} else if(this.container.tip().is(':visible')) {
return;
}
//hide all other editable containers. Required to work correctly with toggle = manual
$('.editable-container').find('.editable-cancel').click();
//show container
this.container.show();
this.container.show(closeAll);
},
/**
@ -247,18 +250,19 @@ Makes editable any HTML element on the page. Applied as jQuery method.
//return focus on element
if (this.options.enablefocus && this.options.toggle === 'click') {
this.$element.focus();
}
}
},
/**
Toggles container visibility (show / hide)
@method toggle()
@param {boolean} closeAll Wether to close all other editable containers when showing this one. Default true.
**/
toggle: function() {
toggle: function(closeAll) {
if(this.container && this.container.tip().is(':visible')) {
this.hide();
} else {
this.show();
this.show(closeAll);
}
},
@ -324,7 +328,17 @@ Makes editable any HTML element on the page. Applied as jQuery method.
this.handleEmpty();
this.$element.triggerHandler('render', this);
}, this));
}
},
/**
Activates input of visible container (e.g. set focus)
@method activate()
**/
activate: function() {
if(this.container) {
this.container.activate();
}
}
};
/* EDITABLE PLUGIN DEFINITION
@ -459,7 +473,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
$.fn.editable.defaults = {
/**
Type of input. Can be <code>text|textarea|select|date</code>
Type of input. Can be <code>text|textarea|select|date|checklist</code> and more
@property type
@type string
@ -475,9 +489,10 @@ Makes editable any HTML element on the page. Applied as jQuery method.
**/
disabled: false,
/**
How to toggle editable. Can be <code>click|manual</code>.
When set to <code>manual</code> you should manually call <code>show/hide</code> methods of editable.
Note: if you are calling <code>show</code> on **click** event you need to apply <code>e.stopPropagation()</code> because container has behavior to hide on any click outside.
How to toggle editable. Can be <code>click|dblclick|mouseenter|manual</code>.
When set to <code>manual</code> you should manually call <code>show/hide</code> methods of editable.
**Note**: if you call <code>show</code> or <code>toggle</code> inside **click** handler of some DOM element,
you need to apply <code>e.stopPropagation()</code> because containers are being closed on any click on document.
@example
$('#edit-button').click(function(e) {
@ -490,6 +505,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
@default 'click'
**/
toggle: 'click',
/**
Text shown when element is empty.
@ -519,7 +535,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
**/
enablefocus: false,
/**
Initial value of input
Initial value of input. Taken from <code>data-value</code> or element's text.
@property value
@type mixed

@ -0,0 +1,9 @@
.editable-address {
display: block;
margin-bottom: 5px;
}
.editable-address span {
width: 70px;
display: inline-block;
}

@ -0,0 +1,107 @@
/**
Address editable input.
Internally value stored as {city: "Moscow", street: "Lenina", building: "15"}
@class address
@extends abstract
@final
@example
<a href="#" id="address" data-type="address" data-pk="1">awesome</a>
<script>
$(function(){
$('#address').editable({
url: '/post',
title: 'Enter city, street and building #',
value: {
city: "Moscow",
street: "Lenina",
building: "15"
}
});
});
</script>
**/
(function ($) {
var Address = function (options) {
this.init('address', options, Address.defaults);
};
$.fn.editableform.utils.inherit(Address, $.fn.editableform.types.abstract);
$.extend(Address.prototype, {
render: function() {
Address.superclass.render.call(this);
// this.$input.
},
value2html: function(value, element) {
var html = value.city + ', ' + value.street + ' st., bld. ' + value.building;
$(element).text(html);
},
html2value: function(html) {
/*
you may write parsing method to get value by element's html
e.g. "Moscow, st. Lenina, bld. 15" => {city: "Moscow", street: "Lenina", building: "15"}
but for complex structures I do not recommend do that.
Better always set value directly via javascript, e.g.
editable({
value: {
city: "Moscow",
street: "Lenina",
building: "15"
}
});
*/
return null;
},
/*
method for converting data before sent on server.
As jQuery correctly sends objects via ajax, you can just return value
*/
value2str: function(value) {
return value;
},
/*
this is mainly for parsing value defined in data-value attribute.
If you will always set value by javascript, no need to overwrite it
*/
str2value: function(str) {
return str;
},
value2input: function(value) {
this.$input.find('input[name="city"]').val(value.city);
this.$input.find('input[name="street"]').val(value.street);
this.$input.find('input[name="building"]').val(value.building);
},
input2value: function() {
return {
city: this.$input.find('input[name="city"]').val(),
street: this.$input.find('input[name="street"]').val(),
building: this.$input.find('input[name="building"]').val()
};
},
activate: function() {
//set focus on city
this.$input.find('input[name="city"]').focus();
}
});
Address.defaults = $.extend({}, $.fn.editableform.types.abstract.defaults, {
tpl: '<div><label><span>City: </span><input type="text" name="city" class="span2"></label></div>'+
'<div><label><span>Street: </span><input type="text" name="street" class="span2"></label></div>'+
'<div><label><span>Building: </span><input type="text" name="building" class="span1"></label></div>',
inputclass: 'editable-address'
});
$.fn.editableform.types.address = Address;
}(window.jQuery));

@ -46,7 +46,7 @@ To create your own input you should inherit from this class.
@param {DOMElement} element
**/
value2html: function(value, element) {
var html = $('<div>').text(value).html();
var html = this.escape(value);
$(element).html(html);
},
@ -120,7 +120,21 @@ To create your own input you should inherit from this class.
**/
clear: function() {
this.$input.val(null);
}
},
/**
method to escape html.
**/
escape: function(str) {
return $('<div>').text(str).html();
},
/**
attach handler to automatically submit form when value changed (usefull when buttons not shown)
**/
autosubmit: function() {
}
};
Abstract.defaults = {

@ -1,5 +1,6 @@
/**
List of checkboxes. Internally value stored as javascript array of values.
List of checkboxes.
Internally value stored as javascript array of values.
@class checklist
@extends list
@ -49,6 +50,8 @@ $(function(){
value2str: function(value) {
return $.isArray(value) ? value.join($.trim(this.options.separator)) : '';
//it is also possible to sent as array
//return value;
},
//parse separated string
@ -68,11 +71,17 @@ $(function(){
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');
}
});
$checks.each(function(i, el) {
var $el = $(el);
// cannot use $.inArray as it performs strict comparison
$.each(value, function(j, val){
/*jslint eqeq: true*/
if($el.val() == val) {
/*jslint eqeq: false*/
$el.attr('checked', 'checked');
}
});
});
}
},
@ -99,7 +108,19 @@ $(function(){
html = this.options.limitText.replace('{checked}', $.isArray(value) ? value.length : 0).replace('{count}', this.sourceData.length);
}
$(element).html(html);
}
},
activate: function() {
this.$input.find('input[type="checkbox"]').first().focus();
},
autosubmit: function() {
this.$input.find('input[type="checkbox"]').on('keydown', function(e){
if (e.which === 13) {
$(this).closest('form').submit();
}
});
}
});
Checklist.defaults = $.extend({}, $.fn.editableform.types.list.defaults, {

@ -95,7 +95,16 @@ $(function(){
clear: function() {
this.$input.data('datepicker').date = null;
this.$input.find('.active').removeClass('active');
}
},
autosubmit: function() {
this.$input.on('changeDate', function(e){
var $form = $(this).closest('form');
setTimeout(function() {
$form.submit();
}, 200);
});
}
});

@ -112,7 +112,16 @@ $(function(){
clear: function() {
this.$input.datepicker('setDate', null);
}
},
autosubmit: function() {
this.$input.on('mouseup', 'table.ui-datepicker-calendar a.ui-state-default', function(e){
var $form = $(this).closest('form');
setTimeout(function() {
$form.submit();
}, 200);
});
}
});

@ -45,7 +45,13 @@ $(function(){
text = item.text;
}
Select.superclass.constructor.superclass.value2html(text, element);
}
},
autosubmit: function() {
this.$input.on('change', function(){
$(this).closest('form').submit();
});
}
});
Select.defaults = $.extend({}, $.fn.editableform.types.list.defaults, {

@ -80,8 +80,7 @@ function getAssets(f, c, src, libs) {
//core
js.unshift(bootstrap+'js/bootstrap.js')
css.unshift(bootstrap+'css/bootstrap.css');
// css.push(bootstrap+'css/bootstrap.css');
//css.unshift(bootstrap+'css/bootstrap-responsive.css');
css.unshift(bootstrap+'css/bootstrap-responsive.css');
//editable
js.push(forms+'editable-form-bootstrap.js');

@ -287,7 +287,7 @@ $(function () {
});
test("option method", function () {
test("option method (string and object)", function () {
var e = $('<a href="#" data-url="post.php" data-name="text">abc</a>').appendTo('#qunit-fixture').editable(),
e1 = $('<a href="#" data-pk="1" data-name="text1">abc</a>').appendTo('#qunit-fixture').editable(),
url = 'abc';
@ -296,6 +296,12 @@ $(function () {
equal(e.data('editable').options.pk, 2, 'pk set correctly');
equal(e1.data('editable').options.pk, 2, 'pk2 set correctly');
$('#qunit-fixture a').editable('option', {pk: 3, value: 'abcd'});
equal(e.data('editable').options.pk, 3, 'pk set correctly (by object)');
equal(e.data('editable').value, 'abcd', 'value set correctly (by object)');
equal(e.text(), 'abcd', 'text set correctly (by object)');
});
asyncTest("'submit' method: client and server validation", function () {

@ -78,7 +78,7 @@
e.click();
var p = tip(e);
ok(p.is(':visible'), 'popover shown');
ok(p.is(':visible'), 'popover shown');
//todo: for jqueryui phantomjs calcs wrong position. Need investigation
if(!$.browser.webkit && fc.f !== 'jqueryui') {
@ -90,41 +90,195 @@
e.remove();
});
test("should close all other containers on click on editable", function () {
var e1 = $('<a href="#" data-pk="1" data-url="post.php" id="a">abc</a>').appendTo('#qunit-fixture').editable(),
e2 = $('<a href="#" data-pk="1" data-url="post.php" id="b">abcd</a>').appendTo('#qunit-fixture').editable();
e1.click()
var p1 = tip(e1);
ok(p1.is(':visible'), 'popover1 visible');
e2.click()
var p2 = tip(e2);
ok(p2.is(':visible'), 'popover2 visible');
ok(!p1.is(':visible'), 'popover1 closed');
p2.find('button[type=button]').click();
ok(!p2.is(':visible'), 'popover2 closed');
});
test("click outside container should hide it", function () {
var e = $('<a href="#" data-pk="1" data-url="post.php" data-name="text1">abc</a>').appendTo('#qunit-fixture').editable(),
e1 = $('<div>').appendTo('body');
test("onblur: cancel", function () {
var oldValue = 'abc',
newValue = 'cde',
e = $('<a href="#" data-type="text" data-pk="1" data-url="post.php" id="a">'+oldValue+'</a>').appendTo('#qunit-fixture').editable({
onblur: 'cancel',
url: function() {}
}),
e2 = $('<a href="#" data-type="text" data-pk="1" data-url="post.php" id="b">abcd</a>').appendTo('#qunit-fixture').editable();
//click inside
e.click();
var p = tip(e);
ok(p.is(':visible'), 'popover shown');
ok(p.is(':visible'), 'popover1 visible');
p.find('input').val(newValue);
p.click();
ok(p.is(':visible'), 'popover still shown');
p.find('input').click();
ok(p.is(':visible'), 'popover1 still visible');
//click outside
p.find('input').val(newValue);
$('#qunit-fixture').click();
ok(!p.is(':visible'), 'popover1 closed');
equal(e.data('editable').value, oldValue, 'old value exists');
e1.click();
ok(!p.is(':visible'), 'popover closed');
});
//click on another editable
e.click();
p = tip(e);
ok(p.is(':visible'), 'popover1 visible');
p.find('input').val(newValue);
e2.click();
var p2 = tip(e2);
ok(!p.is(':visible'), 'popover1 closed');
ok(p2.is(':visible'), 'popover2 visible');
equal(e.data('editable').value, oldValue, 'old value exists');
e2.editable('hide');
ok(!p2.is(':visible'), 'popover2 closed');
//call show method of another editable, closeAll = true (default)
e.click();
p = tip(e);
ok(p.is(':visible'), 'popover1 visible');
p.find('input').val(newValue);
e2.editable('show');
p2 = tip(e2);
ok(!p.is(':visible'), 'popover1 closed');
ok(p2.is(':visible'), 'popover2 visible');
equal(e.data('editable').value, oldValue, 'old value exists');
e2.editable('hide');
ok(!p2.is(':visible'), 'popover2 closed');
//call show method of another editable, closeAll = false
e.click();
p = tip(e);
ok(p.is(':visible'), 'popover1 visible');
p.find('input').val(newValue);
e2.editable('show', false);
p2 = tip(e2);
ok(p.is(':visible'), 'popover1 visible');
ok(p2.is(':visible'), 'popover2 visible');
e.editable('hide');
e2.editable('hide');
ok(!p.is(':visible'), 'popover1 closed');
ok(!p2.is(':visible'), 'popover2 closed');
});
test("onblur: submit", function () {
var oldValue = 'abc',
newValue = 'cde',
e = $('<a href="#" data-type="text" data-pk="1" data-url="post.php" id="a">'+oldValue+'</a>').appendTo('#qunit-fixture').editable({
onblur: 'submit',
url: function() {}
}),
e2 = $('<a href="#" data-type="text" data-pk="1" data-url="post.php" id="b">abcd</a>').appendTo('#qunit-fixture').editable();
//click inside
e.click();
var p = tip(e);
ok(p.is(':visible'), 'popover1 visible');
p.find('input').val(newValue);
p.click();
p.find('input').click();
ok(p.is(':visible'), 'popover1 still visible');
//click outside
p.find('input').val(newValue);
$('#qunit-fixture').click();
ok(!p.is(':visible'), 'popover1 closed');
equal(e.data('editable').value, newValue, 'new value saved');
//click on another editable
e.click();
p = tip(e);
ok(p.is(':visible'), 'popover1 visible');
p.find('input').val(oldValue);
e2.click();
var p2 = tip(e2);
ok(!p.is(':visible'), 'popover1 closed');
ok(p2.is(':visible'), 'popover2 visible');
equal(e.data('editable').value, oldValue, 'old value re-saved');
e2.editable('hide');
ok(!p2.is(':visible'), 'popover2 closed');
//call show method of another editable, closeAll = true (default)
e.click();
p = tip(e);
ok(p.is(':visible'), 'popover1 visible');
p.find('input').val(newValue);
e2.editable('show');
p2 = tip(e2);
ok(!p.is(':visible'), 'popover1 closed');
ok(p2.is(':visible'), 'popover2 visible');
equal(e.data('editable').value, newValue, 'new value saved');
e2.editable('hide');
ok(!p2.is(':visible'), 'popover2 closed');
//call show method of another editable, closeAll = false
e.click();
p = tip(e);
ok(p.is(':visible'), 'popover1 visible');
p.find('input').val(oldValue);
e2.editable('show', false);
p2 = tip(e2);
ok(p.is(':visible'), 'popover1 visible');
ok(p2.is(':visible'), 'popover2 visible');
e.editable('hide');
e2.editable('hide');
ok(!p.is(':visible'), 'popover1 closed');
ok(!p2.is(':visible'), 'popover2 closed');
});
test("onblur: ignore", function () {
var oldValue = 'abc',
newValue = 'cde',
e = $('<a href="#" data-type="text" data-pk="1" data-url="post.php" id="a">'+oldValue+'</a>').appendTo('#qunit-fixture').editable({
onblur: 'ignore',
url: function() {}
}),
e2 = $('<a href="#" data-type="text" data-pk="1" data-url="post.php" id="b">abcd</a>').appendTo('#qunit-fixture').editable();
//click inside
e.click();
var p = tip(e);
ok(p.is(':visible'), 'popover1 visible');
p.find('input').val(newValue);
p.click();
p.find('input').click();
ok(p.is(':visible'), 'popover1 still visible');
//click outside
p.find('input').val(newValue);
$('#qunit-fixture').click();
ok(p.is(':visible'), 'popover1 still visible');
//click on another editable
e2.click();
var p2 = tip(e2);
ok(p.is(':visible'), 'popover1 still visible');
ok(p2.is(':visible'), 'popover2 visible');
e2.editable('hide');
ok(!p2.is(':visible'), 'popover2 closed');
//call show method of another editable, closeAll = true (default)
e2.editable('show');
p2 = tip(e2);
ok(p.is(':visible'), 'popover1 still visible');
ok(p2.is(':visible'), 'popover2 visible');
e2.editable('hide');
ok(!p2.is(':visible'), 'popover2 closed');
//call show method of another editable, closeAll = false
e2.editable('show', false);
p2 = tip(e2);
ok(p.is(':visible'), 'popover1 still visible');
ok(p2.is(':visible'), 'popover2 visible');
e2.editable('hide');
ok(!p2.is(':visible'), 'popover2 closed');
e.editable('hide');
ok(!p.is(':visible'), 'popover1 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 = $('<a href="#" data-pk="1" data-url="post.php" data-name="text1">abc</a>').appendTo(d).editable({
showbuttons: true
});
e.click();
var p = tip(e);
@ -132,7 +286,81 @@
ok(p.find('button').offset().left > p.find('.editable-input').offset().left + p.find('.editable-input').width(), 'buttons left ok');
d.remove();
});
});
test("toggle: manual", function () {
var e = $('<a href="#" id="a"></a>').appendTo('#qunit-fixture').editable({
toggle: 'manual'
});
e.click();
ok(!e.data('editableContainer'), 'popover not visible after click');
e.editable('show');
var p = tip(e);
ok(p.is(':visible'), 'shown manually');
});
test("toggle: dblclick", function () {
var e = $('<a href="#" id="a"></a>').appendTo('#qunit-fixture').editable({
toggle: 'dblclick'
}),
p, p2,
e2 = $('<a href="#" data-type="text" data-pk="1" data-url="post.php" id="b">abcd</a>').appendTo('#qunit-fixture').editable();
e.click();
ok(!e.data('editableContainer'), 'popover not visible after click');
e2.click();
p2 = tip(e2);
ok(p2.is(':visible'), 'popover2 visible');
e.dblclick();
p = tip(e);
ok(p.is(':visible'), 'popover1 visible');
ok(!p2.is(':visible'), 'popover2 closed');
});
test("toggle: mouseenter", function () {
var e = $('<a href="#" id="a"></a>').appendTo('#qunit-fixture').editable({
toggle: 'mouseenter'
}),
p, p2,
e2 = $('<a href="#" data-type="text" data-pk="1" data-url="post.php" id="b">abcd</a>').appendTo('#qunit-fixture').editable();
e.click();
ok(!e.data('editableContainer'), 'popover not visible after click');
e.dblclick();
ok(!e.data('editableContainer'), 'popover not visible after dblclick');
e2.click();
p2 = tip(e2);
ok(p2.is(':visible'), 'popover2 visible');
e.mouseenter();
ok(e.data('editableContainer'), 'container defined');
p = tip(e);
ok(p.is(':visible'), 'popover1 visible');
ok(!p2.is(':visible'), 'popover2 closed');
//hover once again --> container should stay open
e.hover();
p = tip(e);
ok(p.is(':visible'), 'popover1 visible after second hover');
});
test("showbuttons: false", function () {
var e = $('<a href="#" id="a" data-type="text"></a>').appendTo('#qunit-fixture').editable({
showbuttons: false
});
e.click();
var p = tip(e);
ok(p.is(':visible'), 'popover visible');
ok(!p.find('.editable-submit').length, 'submit not rendered');
ok(!p.find('.editable-cancel').length, 'cancel not rendered');
ok(!p.find('.editable-buttons').length, '.editable-buttons block not rendered');
});
//unfortunatly, testing this feature does not always work in browsers. Tested manually.
/*

@ -450,5 +450,30 @@ $(function () {
start();
}, timeout);
});
asyncTest("autosubmit when showbuttons=false", function () {
expect(4);
var e = $('<a href="#" data-type="select" data-value="2" data-url="post.php">customer</a>').appendTo(fx).editable({
pk: 1,
source: groups,
showbuttons: false
}),
selected = 3;
e.click();
var p = tip(e);
equal(p.find('select').val(), e.data('editable').value, 'selected value correct');
p.find('select').val(selected);
p.find('select').trigger('change');
setTimeout(function() {
ok(!p.is(':visible'), 'popover closed');
equal(e.data('editable').value, selected, 'new value saved')
equal(e.text(), groups[selected], 'text shown correctly')
e.remove();
start();
}, timeout);
});
});

@ -39,18 +39,6 @@ $(function () {
p.find('button[type=button]').click();
ok(!p.is(':visible'), 'popover was removed');
});
test("option 'toggle' = manual", function () {
var e = $('<a href="#" id="a"></a>').appendTo('#qunit-fixture').editable({
toggle: 'manual'
});
e.click();
ok(!e.data().editableContainer, 'popover not visible after click');
e.editable('show');
var p = tip(e);
ok(p.is(':visible'), 'shown manually');
});
asyncTest("should load correct value and save new entered text (and value)", function () {
var v = 'ab<b>"',
@ -211,7 +199,38 @@ $(function () {
start();
}, timeout);
});
});
asyncTest("should show new value if success callback returns object", function () {
var newText = 'cd<e>;"',
e = $('<a href="#" data-pk="1" data-url="post.php" data-name="text1">abc</a>').appendTo(fx).editable({
success: function(response, newValue) {
equal(newValue, newText, 'value in success passed correctly');
return {newValue: 'xyz'};
}
});
e.click()
var p = tip(e);
ok(p.find('input[type=text]').length, 'input exists')
p.find('input').val(newText);
p.find('form').submit();
setTimeout(function() {
ok(!p.is(':visible'), 'popover closed');
equal(p.find('.editable-error-block').text(), '', 'no error msg');
equal(e.data('editable').value, 'xyz', 'value ok');
equal(e.text(), 'xyz', 'text ok');
p.find('button[type=button]').click();
ok(!p.is(':visible'), 'popover was removed');
e.remove();
start();
}, timeout);
});
asyncTest("should submit all required params", function () {
var e = $('<a href="#" data-pk="1" data-url="post-resp.php">abc</a>').appendTo(fx).editable({
@ -256,6 +275,9 @@ $(function () {
equal(resp.data.name, 'username', 'name ok');
equal(resp.data.value, newText, 'value ok');
equal(resp.data.q, 2, 'additional params ok');
},
ajaxOptions: {
headers: {"myHeader": "123"}
}
}),
newText = 'cd<e>;"'
@ -272,8 +294,38 @@ $(function () {
start();
}, timeout);
});
});
asyncTest("ajaxOptions", function () {
var e = $('<a href="#" data-pk="1" data-url="post-options.php">abc</a>').appendTo(fx).editable({
name: 'username',
ajaxOptions: {
dataType: 'html'
}
}),
newText = 'cd<e>;"'
$.mockjax({
url: 'post-options.php',
response: function(settings) {
equal(settings.dataType, 'html', 'dataType key ok');
}
});
e.click()
var p = tip(e);
ok(p.find('input[type=text]').length, 'input exists')
p.find('input').val(newText);
p.find('form').submit();
setTimeout(function() {
e.remove();
start();
}, timeout);
});
asyncTest("submit to url defined as function", function () {
expect(3);