diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 8257188..6498081 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -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 diff --git a/README.md b/README.md index ca3354d..aa774c1 100644 --- a/README.md +++ b/README.md @@ -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! diff --git a/grunt.js b/grunt.js index 3b66d5b..4282da2 100644 --- a/grunt.js +++ b/grunt.js @@ -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) diff --git a/package.json b/package.json index f7cf0e1..e0700a2 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/containers/editable-container.css b/src/containers/editable-container.css index 9098275..1a91e98 100644 --- a/src/containers/editable-container.css +++ b/src/containers/editable-container.css @@ -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; } \ No newline at end of file diff --git a/src/containers/editable-container.js b/src/containers/editable-container.js index 8cc64c2..0029eba 100644 --- a/src/containers/editable-container.js +++ b/src/containers/editable-container.js @@ -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)); \ No newline at end of file +}(window.jQuery)); diff --git a/src/containers/editable-inline.js b/src/containers/editable-inline.js index 92c5455..df585a6 100644 --- a/src/containers/editable-inline.js +++ b/src/containers/editable-inline.js @@ -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)); }, diff --git a/src/containers/editable-poshytip.js b/src/containers/editable-poshytip.js index 5cbd427..3488ec9 100644 --- a/src/containers/editable-poshytip.js +++ b/src/containers/editable-poshytip.js @@ -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(); }, diff --git a/src/containers/editable-tooltip.js b/src/containers/editable-tooltip.js index dcfa261..4fbc842 100644 --- a/src/containers/editable-tooltip.js +++ b/src/containers/editable-tooltip.js @@ -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() { diff --git a/src/editable-form/editable-form-bootstrap.js b/src/editable-form/editable-form-bootstrap.js index 50f72bb..3388544 100644 --- a/src/editable-form/editable-form-bootstrap.js +++ b/src/editable-form/editable-form-bootstrap.js @@ -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'); } }); diff --git a/src/editable-form/editable-form-jqueryui.js b/src/editable-form/editable-form-jqueryui.js index 804b6d2..9f0ea9e 100644 --- a/src/editable-form/editable-form-jqueryui.js +++ b/src/editable-form/editable-form-jqueryui.js @@ -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'); - } }); diff --git a/src/editable-form/editable-form.css b/src/editable-form/editable-form.css index 1ff18fe..874705a 100644 --- a/src/editable-form/editable-form.css +++ b/src/editable-form/editable-form.css @@ -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 { diff --git a/src/editable-form/editable-form.js b/src/editable-form/editable-form.js index b6f8018..a2db423 100644 --- a/src/editable-form/editable-form.js +++ b/src/editable-form/editable-form.js @@ -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: <something>}</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' }; /* diff --git a/src/element/editable-element.js b/src/element/editable-element.js index f51afec..bd23548 100644 --- a/src/element/editable-element.js +++ b/src/element/editable-element.js @@ -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 diff --git a/src/inputs-ext/address/address.css b/src/inputs-ext/address/address.css new file mode 100644 index 0000000..f37a43b --- /dev/null +++ b/src/inputs-ext/address/address.css @@ -0,0 +1,9 @@ +.editable-address { + display: block; + margin-bottom: 5px; +} + +.editable-address span { + width: 70px; + display: inline-block; +} \ No newline at end of file diff --git a/src/inputs-ext/address/address.js b/src/inputs-ext/address/address.js new file mode 100644 index 0000000..ec144b8 --- /dev/null +++ b/src/inputs-ext/address/address.js @@ -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)); \ No newline at end of file diff --git a/src/inputs/abstract.js b/src/inputs/abstract.js index a40d742..0da5ddb 100644 --- a/src/inputs/abstract.js +++ b/src/inputs/abstract.js @@ -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 = { diff --git a/src/inputs/checklist.js b/src/inputs/checklist.js index 1a5b149..60af4ce 100644 --- a/src/inputs/checklist.js +++ b/src/inputs/checklist.js @@ -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, { diff --git a/src/inputs/date/date.js b/src/inputs/date/date.js index 6490021..2fb8eab 100644 --- a/src/inputs/date/date.js +++ b/src/inputs/date/date.js @@ -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); + }); + } }); diff --git a/src/inputs/dateui/dateui.js b/src/inputs/dateui/dateui.js index 5b39f27..a8978dc 100644 --- a/src/inputs/dateui/dateui.js +++ b/src/inputs/dateui/dateui.js @@ -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); + }); + } }); diff --git a/src/inputs/select.js b/src/inputs/select.js index 9497ae1..42ffc77 100644 --- a/src/inputs/select.js +++ b/src/inputs/select.js @@ -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, { diff --git a/test/loader.js b/test/loader.js index ba91063..b1b4a6b 100644 --- a/test/loader.js +++ b/test/loader.js @@ -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'); diff --git a/test/unit/api.js b/test/unit/api.js index 313e682..47f6ca2 100644 --- a/test/unit/api.js +++ b/test/unit/api.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 () { diff --git a/test/unit/common.js b/test/unit/common.js index 468c48e..a5e6eb6 100644 --- a/test/unit/common.js +++ b/test/unit/common.js @@ -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. /* diff --git a/test/unit/select.js b/test/unit/select.js index b4b969f..05d37b3 100644 --- a/test/unit/select.js +++ b/test/unit/select.js @@ -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); + }); }); \ No newline at end of file diff --git a/test/unit/text.js b/test/unit/text.js index 79f1b90..f7fc3e9 100644 --- a/test/unit/text.js +++ b/test/unit/text.js @@ -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);