diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 6498081..131c27b 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -2,6 +2,34 @@ X-editable changelog ============================= +Version 1.2.0 Dec 6, 2012 +---------------------------- +[enh #36] 'submit' method: added 'ajaxOptions' property to modify ajax request (vitalets) +[enh] inputs now internally use 'value2submit' method instead of previous 'value2str' (vitalets) +[enh] editableContainer removed from docs (vitalets) +[enh] editableContainer: removed 'autohide' option and 'cancel' event. Use 'hidden' event instead (vitalets) +[enh] 'hidden' event: added param 'reason' that points to reason caused hiding (vitalets) +[enh] 'select' submit by enter (vitalets) +[bug #37] fix incorrectly shown datepicker in jquery 1.7.1 + webkit (vitalets) +[enh] added url param 'jquery' to run tests in different versions of jquery, e.g. '&jquery=1.7.2' (vitalets) +[enh] 'enablefocus' option removed. More efficient to use 'save/hide' events to set focus to any element (vitalets) +[enh] 'init' event was added due to removal of render event (vitalets) +[enh] 'render' event was removed, use 'display' callback instead (vitalets) +[enh] 'checklist' submit value as array, not comma separated string (vitalets) +[enh] 'checklist' was refactored: options 'viewseparator', 'limit', 'limitText' are supressed by 'display' callback (vitalets) +[enh] new option: 'display' callback. Makes far more flexible rendering value into element's text. (vitalets) +[bug] fix typos (atrophic) +[enh] all callbacks scope changed to element (vitalets) +[enh] new option: 'savenochange' to save or cancel value when it was not changed in form (vitalets) +[enh] composite pk can be defined as JSON in data-pk attribute (vitalets) +[enh #30] new option 'sourceCache' true|false to disable cache for select (vitalets) +[bug #34] inputclass span* broken with fluid bootstrap layout. Classes changed to 'input-*'. (vitalets) +[enh] utils now added to $.fn.editableutils instead of $.fn.editableform.utils (vitalets) +[enh] input types now added to $.fn.editabletypes instead of $.fn.editableform.types (vitalets) +[enh] playground and tests now use requirejs (vitalets) +[bug #27] 'today' button toggle bug in bootstrap-datepicker (vitalets) + + Version 1.1.1 Nov 30, 2012 ---------------------------- [enh] 'showbuttons' option to hide buttons in form (vitalets) @@ -29,7 +57,7 @@ Version 1.0.1 Nov 22, 2012 [enh] contribution guide in README.md (vitalets) [enh #7] 'shown', 'hidden' events added (vitalets) [enh #1] params can be a function to calculate it dynamically (vitalets) -[enh #6] do not preventDetault() in click when toggle='manual'. This allows to have clickable links (vitalets) +[enh #6] do not preventDefault() in click when toggle='manual'. This allows to have clickable links (vitalets) [bug #3] should not mark element with unsave css if url is user's function (vitalets) @@ -48,13 +76,13 @@ Here list of differences to help you to upgrade your application: [change] 'toggle' option value can be only click|manual (not toggling element id). In case of 'manual' you should write handler calling 'show' method. [change] 'validate' option cannot be defined as object anymore. [change] events 'init', 'update', 'shown', 'hidden' removed. Events 'save', 'cancel' added. Event 'render' remains. -[change] input's optiom 'template' renamed to 'tpl' (to exclude conflict with container's template). -[change] value can be stored internaly as object (previously was always string). Usefull for date inupt. +[change] input's option 'template' renamed to 'tpl' (to exclude conflict with container's template). +[change] value can be stored internally as object (previously was always string). Useful for date input. [change] 'error' callback option is removed. 'success' callback remained. -[enh] 'source' option in select can be array of structure [{value: 1, text: 'abc'}, {...}]. This allows to keep ordering of items in dropdown list. Previous format is supported for compability. +[enh] 'source' option in select can be array of structure [{value: 1, text: 'abc'}, {...}]. This allows to keep ordering of items in dropdown list. Previous format is supported for compatibility. [enh] api method 'setValue' to set manually value of editable. [change] locales directory is excluded from bootstrap-datepicker input. If you need localization you should jus download corresponding file from github. [change] date and dateui specific options can be set only via 'datepicker' option in first level of config (previously it was possible to set some options directly in config, e.g. weekStart). -[change] if 'url' option defined as function - it is used as submit method instead of ajax (previously it was dynamically return url string and ajax occured anyway) +[change] if 'url' option defined as function - it is used as submit method instead of ajax (previously it was dynamically return url string and ajax occurred anyway) -Also all known bugs of bootstrap-editable were closed. \ No newline at end of file +Also all known bugs of bootstrap-editable were closed. diff --git a/README.md b/README.md index aa774c1..1f3320b 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/5/) as starting point. +When creating issues please provide jsFiddle example. You can easily fork one of templates: [bootstrap](http://jsfiddle.net/xBB5x/25/), [jqueryui](http://jsfiddle.net/xBB5x/24/), [plain](http://jsfiddle.net/xBB5x/23/). Your feedback is very appreciated! ## Contribution @@ -19,7 +19,7 @@ A few steps how to start contributing: **x-editable** | -- **lib** (repo related to <code>dev</code> and <code>master</code> branches) | -- **gh-pages** (repo related to <code>gh-pages</code> branch for docs & demo) - | -- **playground** (simple node-server and html page for testing, [playground.zip](https://github.com/downloads/vitalets/x-editable/playground.zip)) + | -- **playground** (simple node-server and html page for testing, [playground_1.2.zip](https://github.com/downloads/vitalets/x-editable/playground_1.2.zip), **updated in 1.2.0!**) To make it easy follow this script ( _assuming you have [nodejs](http://nodejs.org) installed_ ). Please replace <code><your-github-name></code> with your name: @@ -43,7 +43,7 @@ npm install cd .. #playground -#download playground.zip from https://github.com/downloads/vitalets/x-editable/playground.zip +#download playground.zip from https://github.com/downloads/vitalets/x-editable/playground_1.2.zip unzip playground.zip cd playground npm install diff --git a/grunt.js b/grunt.js index 4282da2..e7427c9 100644 --- a/grunt.js +++ b/grunt.js @@ -100,10 +100,19 @@ module.exports = function(grunt) { //module for testing var module = ''; -//module = '&module=textarea'; +// module = '&module=textarea'; //module = '&module=select'; //module = '&module=text'; + var qunit_testover = []; + ['bootstrap', 'jqueryui', 'plain'].forEach(function(f){ + ['popup', 'inline'].forEach(function(c){ + ['1.6.4', '1.7.1', '1.7.2', '1.8.2', '1.8.3'].forEach(function(jqver) { + qunit_testover.push('http://localhost:8000/test/index.html?f='+f+'&c='+c+'&jquery='+jqver+module); + }); + }); + }); + //get js and css for different builds var files = getFiles(); @@ -133,8 +142,9 @@ module.exports = function(grunt) { plain: [ 'http://localhost:8000/test/index.html?f=plain&c=popup'+module, 'http://localhost:8000/test/index.html?f=plain&c=inline'+module - ] -// files: ['test/index.html'] + ], + //test all builds under several versions of jquery + testover: qunit_testover }, server: { port: 8000, @@ -208,38 +218,14 @@ module.exports = function(grunt) { } } }, - - //compress does not work properly for MAC OS (see https://github.com/vitalets/bootstrap-editable/issues/19) - //zip will be created manually - /* - compress: { - zip: { - options: { - mode: "zip", - //TODO: unfortunatly here <%= dist_source %> and <config:dist_source> does not work - basePath: "dist" - }, - files: { - "<%= dist %>/bootstrap-editable-v<%= pkg.version %>.zip": ["<%= dist_source %>/ **", "<%= dist %>/libs/ **"] - } - }, - tgz: { - options: { - mode: "tgz", - basePath: "dist" - }, - files: { - "<%= dist %>/bootstrap-editable-v<%= pkg.version %>.tar.gz": ["<%= dist_source %>/ **", "<%= dist %>/libs/ **"] - } - } - }, - */ + uglify: {} }); //test task grunt.registerTask('test', 'lint server qunit:bootstrap'); - grunt.registerTask('testall', 'lint server qunit'); + grunt.registerTask('testall', 'lint server qunit:bootstrap qunit:jqueryui qunit:plain'); + grunt.registerTask('testover', 'lint server qunit:testover'); // Default task. // grunt.registerTask('default', 'lint qunit'); diff --git a/package.json b/package.json index e0700a2..5f92913 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.1", + "version": "1.2.0", "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 1a91e98..bebf690 100644 --- a/src/containers/editable-container.css +++ b/src/containers/editable-container.css @@ -1,5 +1,5 @@ .editable-container { - max-width: none; /* without this rule poshytip does not stretch */ + max-width: none !important; /* without this rule poshytip/tooltip does not stretch */ } .editable-container.popover { @@ -10,4 +10,8 @@ .editable-container.editable-inline { display: inline; vertical-align: middle; +} + +.editable-container.ui-widget { + font-size: inherit; /* jqueryui widget font 1.1em too big, overwrite it */ } \ No newline at end of file diff --git a/src/containers/editable-container.js b/src/containers/editable-container.js index 0029eba..1094c46 100644 --- a/src/containers/editable-container.js +++ b/src/containers/editable-container.js @@ -20,7 +20,7 @@ Applied as jQuery method. init: function(element, options) { this.$element = $(element); //todo: what is in priority: data or js? - this.options = $.extend({}, $.fn.editableContainer.defaults, $.fn.editableform.utils.getConfigData(this.$element), options); + this.options = $.extend({}, $.fn.editableContainer.defaults, $.fn.editableutils.getConfigData(this.$element), options); this.splitOptions(); this.initContainer(); @@ -75,12 +75,15 @@ Applied as jQuery method. }, initForm: function() { + this.formOptions.scope = this.$element[0]; //set scope of form callbacks to element this.$form = $('<div>') .editableform(this.formOptions) .on({ save: $.proxy(this.save, this), - cancel: $.proxy(this.cancel, this), - show: $.proxy(this.setPosition, this), //re-position container every time form is shown (after loading state) + cancel: $.proxy(function(){ + this.hide('cancel'); + }, this), + show: $.proxy(this.setPosition, this), //re-position container every time form is shown (occurs each time after loading state) rendering: $.proxy(this.setPosition, this), //this allows to place container correctly when loading shown rendered: $.proxy(function(){ /** @@ -119,7 +122,7 @@ 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. + @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. **/ show: function (closeAll) { this.$element.addClass('editable-open'); @@ -143,8 +146,9 @@ Applied as jQuery method. /** Hides container with form @method hide() + @param {string} reason Reason caused hiding. Can be <code>save|cancel|onblur|undefined (=manual)</code> **/ - hide: function() { + hide: function(reason) { if(!this.tip() || !this.tip().is(':visible') || !this.$element.hasClass('editable-open')) { return; } @@ -154,9 +158,17 @@ Applied as jQuery method. Fired when container was hidden. It occurs on both save or cancel. @event hidden - @param {Object} event event object + @param {object} event event object + @param {string} reason Reason caused hiding. Can be <code>save|cancel|onblur|undefined (=manual)</code> + @example + $('#username').on('hidden', function(e, reason) { + if(reason === 'save' || reason === 'cancel') { + //auto-open next editable + $(this).closest('tr').next().find('.editable').editable('show'); + } + }); **/ - this.$element.triggerHandler('hidden'); + this.$element.triggerHandler('hidden', reason); }, /* internal hide method. To be overwritten in child classes */ @@ -167,7 +179,7 @@ Applied as jQuery method. /** Toggles container visibility (show / hide) @method toggle() - @param {boolean} closeAll Wether to close all other editable containers when showing this one. Default true. + @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. **/ toggle: function(closeAll) { if(this.tip && this.tip().is(':visible')) { @@ -185,23 +197,8 @@ Applied as jQuery method. //tbd in child class }, - cancel: function() { - if(this.options.autohide) { - this.hide(); - } - /** - Fired when form was cancelled by user - - @event cancel - @param {Object} event event object - **/ - this.$element.triggerHandler('cancel'); - }, - save: function(e, params) { - if(this.options.autohide) { - this.hide(); - } + this.hide('save'); /** Fired when new value was submitted. You can use <code>$(this).data('editableContainer')</code> inside handler to access to editableContainer instance @@ -262,8 +259,8 @@ Applied as jQuery method. */ closeOthers: function(element) { $('.editable-open').each(function(i, el){ - //do nothing with passed element - if(el === element) { + //do nothing with passed element and it's children + if(el === element || $(el).find(element).length) { return; } @@ -276,7 +273,7 @@ Applied as jQuery method. } if(ec.options.onblur === 'cancel') { - $el.data('editableContainer').hide(); + $el.data('editableContainer').hide('onblur'); } else if(ec.options.onblur === 'submit') { $el.data('editableContainer').tip().find('form').submit(); } @@ -350,7 +347,7 @@ Applied as jQuery method. **/ placement: 'top', /** - Wether to hide container on save/cancel. + Whether to hide container on save/cancel. @property autohide @type boolean @@ -365,6 +362,7 @@ Applied as jQuery method. @property onblur @type string @default 'cancel' + @since 1.1.1 **/ onblur: 'cancel' }; diff --git a/src/containers/editable-inline.js b/src/containers/editable-inline.js index df585a6..05800dc 100644 --- a/src/containers/editable-inline.js +++ b/src/containers/editable-inline.js @@ -43,10 +43,6 @@ innerHide: function () { this.$form.hide(this.options.anim, $.proxy(function() { this.$element.show(); - //return focus on element - if (this.options.enablefocus) { - this.$element.focus(); - } }, this)); }, @@ -57,8 +53,7 @@ //defaults $.fn.editableContainer.defaults = $.extend({}, $.fn.editableContainer.defaults, { - anim: 'fast', - enablefocus: false + anim: 'fast' }); diff --git a/src/containers/editable-popover.js b/src/containers/editable-popover.js index e369b6f..863148b 100644 --- a/src/containers/editable-popover.js +++ b/src/containers/editable-popover.js @@ -14,7 +14,7 @@ $.extend(this.containerOptions, { trigger: 'manual', selector: false, - content: 'dfgh' + content: ' ' }); this.call(this.containerOptions); }, diff --git a/src/editable-form/editable-form-utils.js b/src/editable-form/editable-form-utils.js index 4d8169d..aa22f64 100644 --- a/src/editable-form/editable-form-utils.js +++ b/src/editable-form/editable-form-utils.js @@ -2,10 +2,11 @@ * EditableForm utilites */ (function ($) { - $.fn.editableform.utils = { + //utils + $.fn.editableutils = { /** * classic JS inheritance function - */ + */ inherit: function (Child, Parent) { var F = function() { }; F.prototype = Parent.prototype; @@ -116,6 +117,13 @@ return k; } - } + }, + + /** + method to escape html. + **/ + escape: function(str) { + return $('<div>').text(str).html(); + } }; }(window.jQuery)); \ No newline at end of file diff --git a/src/editable-form/editable-form.css b/src/editable-form/editable-form.css index 874705a..c7bc226 100644 --- a/src/editable-form/editable-form.css +++ b/src/editable-form/editable-form.css @@ -16,6 +16,8 @@ *display: inline; } + + .editable-input { vertical-align: top; display: inline-block; /* should be inline to take effect of parent's white-space: nowrap */ @@ -31,8 +33,9 @@ } /*for jquery-ui buttons need set height to look more pretty*/ -.editable-buttons button.ui-button { +.editable-buttons button.ui-button-icon-only { height: 24px; + width: 30px; } .editableform-loading { @@ -79,6 +82,10 @@ margin: 0; } +.editable-checklist label { + white-space: nowrap; +} + .editable-clear { clear: both; font-size: 0.9em; diff --git a/src/editable-form/editable-form.js b/src/editable-form/editable-form.js index a2db423..f85e170 100644 --- a/src/editable-form/editable-form.js +++ b/src/editable-form/editable-form.js @@ -1,7 +1,7 @@ /** Form with single input element, two buttons and two states: normal/loading. -Applied as jQuery method to DIV tag (not to form tag!) -Editableform is linked with one of input types, e.g. 'text' or 'select'. +Applied as jQuery method to DIV tag (not to form tag!). This is because form can be in loading state when spinner shown. +Editableform is linked with one of input types, e.g. 'text', 'select' etc. @class editableform @uses text @@ -9,9 +9,12 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'. **/ (function ($) { - var EditableForm = function (element, options) { + var EditableForm = function (div, options) { this.options = $.extend({}, $.fn.editableform.defaults, options); - this.$element = $(element); //div, containing form. Not form tag! Not editable-element. + this.$div = $(div); //div, containing form. Not form tag! Not editable-element. + if(!this.options.scope) { + this.options.scope = this; + } this.initInput(); }; @@ -21,9 +24,9 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'. var TypeConstructor, typeOptions; //create input of specified type - if(typeof $.fn.editableform.types[this.options.type] === 'function') { - TypeConstructor = $.fn.editableform.types[this.options.type]; - typeOptions = $.fn.editableform.utils.sliceObj(this.options, $.fn.editableform.utils.objectKeys(TypeConstructor.defaults)); + if(typeof $.fn.editabletypes[this.options.type] === 'function') { + TypeConstructor = $.fn.editabletypes[this.options.type]; + typeOptions = $.fn.editableutils.sliceObj(this.options, $.fn.editableutils.objectKeys(TypeConstructor.defaults)); this.input = new TypeConstructor(typeOptions); } else { $.error('Unknown type: '+ this.options.type); @@ -45,7 +48,7 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'. **/ render: function() { this.$loading = $($.fn.editableform.loading); - this.$element.empty().append(this.$loading); + this.$div.empty().append(this.$loading); this.showLoading(); //init form template and buttons @@ -61,7 +64,7 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'. @event rendering @param {Object} event event object **/ - this.$element.triggerHandler('rendering'); + this.$div.triggerHandler('rendering'); //render input $.when(this.input.render()) @@ -80,21 +83,23 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'. } //append form to container - this.$element.append(this.$form); - + this.$div.append(this.$form); + //attach 'cancel' handler this.$form.find('.editable-cancel').click($.proxy(this.cancel, this)); - // this.$form.find('.editable-buttons button').eq(1).click($.proxy(this.cancel, this)); if(this.input.error) { this.error(this.input.error); this.$form.find('.editable-submit').attr('disabled', true); this.input.$input.attr('disabled', true); + //prevent form from submitting + this.$form.submit(function(e){ e.preventDefault(); }); } else { this.error(false); this.input.$input.removeAttr('disabled'); this.$form.find('.editable-submit').removeAttr('disabled'); this.input.value2input(this.value); + //attach submit handler this.$form.submit($.proxy(this.submit, this)); } @@ -103,7 +108,7 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'. @event rendered @param {Object} event event object **/ - this.$element.triggerHandler('rendered'); + this.$div.triggerHandler('rendered'); this.showForm(); }, this)); @@ -114,7 +119,7 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'. @event cancel @param {Object} event event object **/ - this.$element.triggerHandler('cancel'); + this.$div.triggerHandler('cancel'); }, showLoading: function() { var w; @@ -133,16 +138,18 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'. this.$loading.show(); }, - showForm: function() { + showForm: function(activate) { this.$loading.hide(); this.$form.show(); - this.input.activate(); + if(activate !== false) { + this.input.activate(); + } /** Fired when form is shown @event show @param {Object} event event object **/ - this.$element.triggerHandler('show'); + this.$div.triggerHandler('show'); }, error: function(msg) { @@ -163,8 +170,7 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'. e.preventDefault(); var error, - newValue = this.input.input2value(), //get new value from input - newValueStr; + newValue = this.input.input2value(); //get new value from input //validation if (error = this.validate(newValue)) { @@ -173,25 +179,29 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'. return; } - //value as string - newValueStr = this.input.value2str(newValue); - //if value not changed --> cancel /*jslint eqeq: true*/ - if (newValueStr == this.input.value2str(this.value)) { + if (!this.options.savenochange && this.input.value2str(newValue) == this.input.value2str(this.value)) { /*jslint eqeq: false*/ this.cancel(); return; } //sending data to server - $.when(this.save(newValueStr)) + $.when(this.save(newValue)) .done($.proxy(function(response) { //run success callback - var res = typeof this.options.success === 'function' ? this.options.success.call(this, response, newValue) : null; + var res = typeof this.options.success === 'function' ? this.options.success.call(this.options.scope, response, newValue) : null; - //if success callback returns string --> show error - if(res && typeof res === 'string') { + //if success callback returns false --> keep form open and do not activate input + if(res === false) { + this.error(false); + this.showForm(false); + return; + } + + //if success callback returns string --> keep form open, show error and activate input + if(typeof res === 'string') { this.error(res); this.showForm(); return; @@ -218,7 +228,7 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'. if(params.newValue === 'username') {...} }); **/ - this.$element.triggerHandler('save', {newValue: newValue, response: response}); + this.$div.triggerHandler('save', {newValue: newValue, response: response}); }, this)) .fail($.proxy(function(xhr) { this.error(typeof xhr === 'string' ? xhr : xhr.responseText || xhr.statusText || 'Unknown error!'); @@ -226,10 +236,16 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'. }, this)); }, - save: function(value) { - var pk = (typeof this.options.pk === 'function') ? this.options.pk.call(this) : this.options.pk, + save: function(newValue) { + //convert value for submitting to server + var submitValue = this.input.value2submit(newValue); + + //try parse composite pk defined as json string in data-pk + this.options.pk = $.fn.editableutils.tryParseJson(this.options.pk, true); + + var pk = (typeof this.options.pk === 'function') ? this.options.pk.call(this.options.scope) : this.options.pk, send = !!(typeof this.options.url === 'function' || (this.options.url && ((this.options.send === 'always') || (this.options.send === 'auto' && pk)))), - params, ajaxOptions; + params; if (send) { //send to server this.showLoading(); @@ -237,30 +253,29 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'. //standard params params = { name: this.options.name || '', - value: value, + value: submitValue, pk: pk }; //additional params if(typeof this.options.params === 'function') { - $.extend(params, this.options.params.call(this, params)); + $.extend(params, this.options.params.call(this.options.scope, params)); } else { //try parse json in single quotes (from data-params attribute) - this.options.params = $.fn.editableform.utils.tryParseJson(this.options.params, true); + this.options.params = $.fn.editableutils.tryParseJson(this.options.params, true); $.extend(params, this.options.params); } if(typeof this.options.url === 'function') { //user's function - return this.options.url.call(this, params); - } else { //send ajax to server and return deferred object - ajaxOptions = $.extend({ + return this.options.url.call(this.options.scope, params); + } else { + //send ajax to server and return deferred object + return $.ajax($.extend({ url : this.options.url, data : params, type : 'post', dataType: 'json' - }, this.options.ajaxOptions); - - return $.ajax(ajaxOptions); + }, this.options.ajaxOptions)); } } }, @@ -270,7 +285,7 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'. value = this.value; } if (typeof this.options.validate === 'function') { - return this.options.validate.call(this, value); + return this.options.validate.call(this.options.scope, value); } }, @@ -377,7 +392,7 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'. name: null, /** Primary key of editable object (e.g. record id in database). For composite keys use object, e.g. <code>{id: 1, lang: 'en'}</code>. - Can be calculated dinamically via function. + Can be calculated dynamically via function. @property pk @type string|object|function @@ -418,7 +433,7 @@ 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**. - Usefull to work with json response. For example, if your backend response can be <code>{success: true}</code> + Useful 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. @@ -440,28 +455,44 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'. @property ajaxOptions @type object @default null + @since 1.1.1 **/ ajaxOptions: null, /** - Wether to show buttons or not. + Whether to show buttons or not. Form without buttons can be auto-submitted by input or by onblur = 'submit'. + @example + ajaxOptions: { + method: 'PUT', + dataType: 'xml' + } @property showbuttons @type boolean @default true + @since 1.1.1 **/ - 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> + showbuttons: true, + /** + Scope for callback methods (success, validate). + If <code>null</code> means editableform instance itself. - @property submitmode - @type string - @default normal - */ -// submitmode: 'normal' + @property scope + @type DOMElement|object + @default null + @since 1.2.0 + @private + **/ + scope: null, + /** + Whether to save or cancel value when it was not changed but form was submitted + + @property savenochange + @type boolean + @default false + @since 1.2.0 + **/ + savenochange: false }; /* @@ -482,15 +513,9 @@ Editableform is linked with one of input types, e.g. 'text' or 'select'. $.fn.editableform.buttons = '<button type="submit" class="editable-submit">ok</button>'+ '<button type="button" class="editable-cancel">cancel</button>'; - //error class attahced to control-group + //error class attached to control-group $.fn.editableform.errorGroupClass = null; - //error class attahced to editable-error-block + //error class attached to editable-error-block $.fn.editableform.errorBlockClass = 'editable-error'; - - //input types - $.fn.editableform.types = {}; - //utils - $.fn.editableform.utils = {}; - }(window.jQuery)); \ No newline at end of file diff --git a/src/element/editable-element.js b/src/element/editable-element.js index bd23548..1a20c7b 100644 --- a/src/element/editable-element.js +++ b/src/element/editable-element.js @@ -8,7 +8,7 @@ Makes editable any HTML element on the page. Applied as jQuery method. var Editable = function (element, options) { this.$element = $(element); - this.options = $.extend({}, $.fn.editable.defaults, $.fn.editableform.utils.getConfigData(this.$element), options); + this.options = $.extend({}, $.fn.editable.defaults, $.fn.editableutils.getConfigData(this.$element), options); this.init(); }; @@ -20,9 +20,6 @@ Makes editable any HTML element on the page. Applied as jQuery method. doAutotext, finalize; - //initialization flag - this.isInit = true; - //editableContainer must be defined if(!$.fn.editableContainer) { $.error('You must define $.fn.editableContainer via including corresponding file (e.g. editable-popover.js)'); @@ -33,9 +30,9 @@ Makes editable any HTML element on the page. Applied as jQuery method. this.options.name = this.options.name || this.$element.attr('id'); //create input of specified type. Input will be used for converting value, not in form - if(typeof $.fn.editableform.types[this.options.type] === 'function') { - TypeConstructor = $.fn.editableform.types[this.options.type]; - this.typeOptions = $.fn.editableform.utils.sliceObj(this.options, $.fn.editableform.utils.objectKeys(TypeConstructor.defaults)); + if(typeof $.fn.editabletypes[this.options.type] === 'function') { + TypeConstructor = $.fn.editabletypes[this.options.type]; + this.typeOptions = $.fn.editableutils.sliceObj(this.options, $.fn.editableutils.objectKeys(TypeConstructor.defaults)); this.input = new TypeConstructor(this.typeOptions); } else { $.error('Unknown type: '+ this.options.type); @@ -47,13 +44,20 @@ Makes editable any HTML element on the page. Applied as jQuery method. this.value = this.input.html2value($.trim(this.$element.html())); isValueByText = true; } else { + /* + value can be string when received from 'data-value' attribute + for complext objects value can be set as json string in data-value attribute, + e.g. data-value="{city: 'Moscow', street: 'Lenina'}" + */ + this.options.value = $.fn.editableutils.tryParseJson(this.options.value, true); if(typeof this.options.value === 'string') { - this.options.value = $.trim(this.options.value); + this.value = this.input.str2value(this.options.value); + } else { + this.value = this.options.value; } - this.value = this.input.str2value(this.options.value); } - //add 'editable' class + //add 'editable' class to every editable element this.$element.addClass('editable'); //attach handler activating editable. In disabled mode it just prevent default action (useful for links) @@ -81,29 +85,46 @@ Makes editable any HTML element on the page. Applied as jQuery method. //if value was generated by text or value is empty, no sense to run autotext doAutotext = !isValueByText && this.value !== null && this.value !== undefined; doAutotext &= (this.options.autotext === 'always') || (this.options.autotext === 'auto' && !this.$element.text().length); - $.when(doAutotext ? this.input.value2html(this.value, this.$element) : true).then($.proxy(function() { + $.when(doAutotext ? this.render() : true).then($.proxy(function() { if(this.options.disabled) { this.disable(); } else { this.enable(); } /** - Fired each time when element's text is rendered. Occurs on initialization and on each update of value. - Can be used for display customization. + Fired when element was initialized by editable method. - @event render + @event init @param {Object} event event object @param {Object} editable editable instance - @example - $('#action').on('render', function(e, editable) { - var colors = {0: "gray", 1: "green", 2: "blue", 3: "red"}; - $(this).css("color", colors[editable.value]); - }); + @since 1.2.0 **/ - this.$element.triggerHandler('render', this); - this.isInit = false; + this.$element.triggerHandler('init', this); }, this)); }, + + /* + Renders value into element's text. + Can call custom display method from options. + Can return deferred object. + @method render() + */ + render: function() { + //do not display anything + if(this.options.display === false) { + return; + } + //if it is input with source, we pass callback in third param to be called when source is loaded + if(this.input.options.hasOwnProperty('source')) { + return this.input.value2html(this.value, this.$element[0], this.options.display); + //if display method defined --> use it + } else if(typeof this.options.display === 'function') { + return this.options.display.call(this.$element[0], this.value); + //else use input's original value2html() method + } else { + return this.input.value2html(this.value, this.$element[0]); + } + }, /** Enables editable @@ -191,6 +212,11 @@ Makes editable any HTML element on the page. Applied as jQuery method. * set emptytext if element is empty (reverse: remove emptytext if needed) */ handleEmpty: function () { + //do not handle empty if we do not display anything + if(this.options.display === false) { + return; + } + var emptyClass = 'editable-empty'; //emptytext shown only for enabled if(!this.options.disabled) { @@ -211,7 +237,7 @@ Makes editable any HTML element on the page. 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. + @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. **/ show: function (closeAll) { if(this.options.disabled) { @@ -221,13 +247,11 @@ Makes editable any HTML element on the page. Applied as jQuery method. //init editableContainer: popover, tooltip, inline, etc.. if(!this.container) { var containerOptions = $.extend({}, this.options, { - value: this.value, - autohide: false //element will take care to show/hide container + value: this.value }); this.$element.editableContainer(containerOptions); this.$element.on({ - save: $.proxy(this.save, this), - cancel: $.proxy(this.hide, this) + save: $.proxy(this.save, this) }); this.container = this.$element.data('editableContainer'); } else if(this.container.tip().is(':visible')) { @@ -246,17 +270,12 @@ Makes editable any HTML element on the page. Applied as jQuery method. if(this.container) { this.container.hide(); } - - //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. + @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. **/ toggle: function(closeAll) { if(this.container && this.container.tip().is(':visible')) { @@ -271,13 +290,13 @@ Makes editable any HTML element on the page. Applied as jQuery method. */ save: function(e, params) { //if url is not user's function and value was not sent to server and value changed --> mark element with unsaved css. - if(typeof this.options.url !== 'function' && params.response === undefined && this.input.value2str(this.value) !== this.input.value2str(params.newValue)) { + if(typeof this.options.url !== 'function' && this.options.display !== false && params.response === undefined && this.input.value2str(this.value) !== this.input.value2str(params.newValue)) { this.$element.addClass('editable-unsaved'); } else { this.$element.removeClass('editable-unsaved'); } - this.hide(); + // this.hide(); this.setValue(params.newValue); /** @@ -312,7 +331,7 @@ Makes editable any HTML element on the page. Applied as jQuery method. Sets new value of editable @method setValue(value, convertStr) @param {mixed} value new value - @param {boolean} convertStr wether to convert value from string to internal format + @param {boolean} convertStr whether to convert value from string to internal format **/ setValue: function(value, convertStr) { if(convertStr) { @@ -323,10 +342,9 @@ Makes editable any HTML element on the page. Applied as jQuery method. if(this.container) { this.container.option('value', this.value); } - $.when(this.input.value2html(this.value, this.$element)) + $.when(this.render()) .then($.proxy(function() { this.handleEmpty(); - this.$element.triggerHandler('render', this); }, this)); }, @@ -338,7 +356,7 @@ Makes editable any HTML element on the page. Applied as jQuery method. if(this.container) { this.container.activate(); } - } + } }; /* EDITABLE PLUGIN DEFINITION @@ -369,7 +387,7 @@ Makes editable any HTML element on the page. Applied as jQuery method. $('#username, #fullname').editable('validate'); // possible result: { - username: "username is requied", + username: "username is required", fullname: "fullname should be minimum 3 letters length" } **/ @@ -398,7 +416,7 @@ Makes editable any HTML element on the page. Applied as jQuery method. this.each(function () { var $this = $(this), data = $this.data(datakey); if (data && data.value !== undefined && data.value !== null) { - result[data.options.name] = data.input.value2str(data.value); + result[data.options.name] = data.input.value2submit(data.value); } }); return result; @@ -411,6 +429,7 @@ Makes editable any HTML element on the page. Applied as jQuery method. @param {object} options @param {object} options.url url to submit data @param {object} options.data additional data to submit + @param {object} options.ajaxOptions additional ajax options @param {function} options.error(obj) error handler (called on both client-side and server-side validation errors) @param {function} options.success(obj) success handler @returns {Object} jQuery object @@ -421,21 +440,20 @@ Makes editable any HTML element on the page. Applied as jQuery method. errors = this.editable('validate'), values; - if(typeof config.error !== 'function') { - config.error = function() {}; - } - if($.isEmptyObject(errors)) { values = this.editable('getValue'); if(config.data) { $.extend(values, config.data); - } - $.ajax({ - type: 'POST', + } + + $.ajax($.extend({ url: config.url, data: values, + type: 'POST', dataType: 'json' - }).success(function(response) { + }, config.ajaxOptions)) + .success(function(response) { + //successful response if(typeof response === 'object' && response.id) { $elems.editable('option', 'pk', response.id); $elems.removeClass('editable-unsaved'); @@ -443,13 +461,20 @@ Makes editable any HTML element on the page. Applied as jQuery method. config.success.apply($elems, arguments); } } else { //server-side validation error + if(typeof config.error === 'function') { + config.error.apply($elems, arguments); + } + } + }) + .error(function(){ //ajax error + if(typeof config.error === 'function') { config.error.apply($elems, arguments); } - }).error(function(){ //ajax error - config.error.apply($elems, arguments); }); } else { //client-side validation error - config.error.call($elems, {errors: errors}); + if(typeof config.error === 'function') { + config.error.call($elems, {errors: errors}); + } } return this; } @@ -505,7 +530,6 @@ Makes editable any HTML element on the page. Applied as jQuery method. @default 'click' **/ toggle: 'click', - /** Text shown when element is empty. @@ -515,7 +539,7 @@ Makes editable any HTML element on the page. Applied as jQuery method. **/ emptytext: 'Empty', /** - Allows to automatically set element's text based on it's value. Can be <code>auto|always|never</code>. Usefull for select and date. + Allows to automatically set element's text based on it's value. Can be <code>auto|always|never</code>. Useful for select and date. For example, if dropdown list is <code>{1: 'a', 2: 'b'}</code> and element's value set to <code>1</code>, it's html will be automatically set to <code>'a'</code>. <code>auto</code> - text will be automatically set only if element is empty. <code>always|never</code> - always(never) try to set element's text. @@ -526,22 +550,31 @@ Makes editable any HTML element on the page. Applied as jQuery method. **/ autotext: 'auto', /** - Wether to return focus on element after form is closed. - This allows fully keyboard input. - - @property enablefocus - @type boolean - @default false - **/ - enablefocus: false, - /** Initial value of input. Taken from <code>data-value</code> or element's text. @property value @type mixed @default element's text **/ - value: null + value: null, + /** + Callback to perform custom displaying of value in element's text. + If <code>null</code>, default input's value2html() will be called. + If <code>false</code>, no displaying methods will be called, element's text will no change. + Runs under element's scope. + Second parameter __sourceData__ is passed for inputs with source (select, checklist). + + @property display + @type function|boolean + @default null + @since 1.2.0 + @example + display: function(value, sourceData) { + var escapedValue = $('<div>').text(value).html(); + $(this).html('<b>'+escapedValue+'</b>'); + } + **/ + display: null }; }(window.jQuery)); \ No newline at end of file diff --git a/src/inputs-ext/address/address.js b/src/inputs-ext/address/address.js index ec144b8..fa62147 100644 --- a/src/inputs-ext/address/address.js +++ b/src/inputs-ext/address/address.js @@ -26,19 +26,21 @@ $(function(){ this.init('address', options, Address.defaults); }; - $.fn.editableform.utils.inherit(Address, $.fn.editableform.types.abstract); + $.fn.editableutils.inherit(Address, $.fn.editabletypes.abstract); $.extend(Address.prototype, { render: function() { Address.superclass.render.call(this); - - // this.$input. }, - + //standard way to show value in element. Used only if display option not defined. value2html: function(value, element) { - var html = value.city + ', ' + value.street + ' st., bld. ' + value.building; - $(element).text(html); + if(!value) { + $(element).empty(); + return; + } + var html = $('<div>').text(value.city).html() + ', ' + $('<div>').text(value.street).html() + ' st., bld. ' + $('<div>').text(value.building).html(); + $(element).html(html); }, html2value: function(html) { @@ -59,11 +61,17 @@ $(function(){ }, /* - method for converting data before sent on server. - As jQuery correctly sends objects via ajax, you can just return value + converts value to string. + It is used in internal comparing (not for sending to server). */ value2str: function(value) { - return value; + var str = ''; + if(value) { + for(var k in value) { + str = str + k + ':' + value[k] + ';'; + } + } + return str; }, /* @@ -94,14 +102,14 @@ $(function(){ } }); - 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>', + Address.defaults = $.extend({}, $.fn.editabletypes.abstract.defaults, { + tpl: '<div><label><span>City: </span><input type="text" name="city" class="input-small"></label></div>'+ + '<div><label><span>Street: </span><input type="text" name="street" class="input-small"></label></div>'+ + '<div><label><span>Building: </span><input type="text" name="building" class="input-mini"></label></div>', inputclass: 'editable-address' }); - $.fn.editableform.types.address = Address; + $.fn.editabletypes.address = Address; }(window.jQuery)); \ No newline at end of file diff --git a/src/inputs/abstract.js b/src/inputs/abstract.js index 0da5ddb..40fcb0c 100644 --- a/src/inputs/abstract.js +++ b/src/inputs/abstract.js @@ -6,11 +6,14 @@ To create your own input you should inherit from this class. **/ (function ($) { + //types + $.fn.editabletypes = {}; + var Abstract = function () { }; Abstract.prototype = { /** - Iinitializes input + Initializes input @method init() **/ @@ -46,8 +49,7 @@ To create your own input you should inherit from this class. @param {DOMElement} element **/ value2html: function(value, element) { - var html = this.escape(value); - $(element).html(html); + $(element).text(value); }, /** @@ -62,7 +64,7 @@ To create your own input you should inherit from this class. }, /** - Converts value to string (for submiting to server) + Converts value to string (for comparering) @method value2str(value) @param {mixed} value @@ -83,6 +85,17 @@ To create your own input you should inherit from this class. return str; }, + /** + Converts value for submitting to server + + @method value2submit(value) + @param {mixed} value + @returns {mixed} + **/ + value2submit: function(value) { + return value; + }, + /** Sets value of input. @@ -114,11 +127,11 @@ To create your own input you should inherit from this class. }, /** - Creares input. + Creates input. @method clear() **/ - clear: function() { + clear: function() { this.$input.val(null); }, @@ -130,7 +143,7 @@ To create your own input you should inherit from this class. }, /** - attach handler to automatically submit form when value changed (usefull when buttons not shown) + attach handler to automatically submit form when value changed (useful when buttons not shown) **/ autosubmit: function() { @@ -148,12 +161,12 @@ To create your own input you should inherit from this class. tpl: '', /** CSS class automatically applied to input - + @property inputclass @type string - @default span2 + @default input-medium **/ - inputclass: 'span2', + inputclass: 'input-medium', /** Name attribute of input @@ -164,6 +177,6 @@ To create your own input you should inherit from this class. name: null }; - $.extend($.fn.editableform.types, {abstract: Abstract}); + $.extend($.fn.editabletypes, {abstract: Abstract}); }(window.jQuery)); \ No newline at end of file diff --git a/src/inputs/checklist.js b/src/inputs/checklist.js index 60af4ce..ea61ac3 100644 --- a/src/inputs/checklist.js +++ b/src/inputs/checklist.js @@ -27,7 +27,7 @@ $(function(){ this.init('checklist', options, Checklist.defaults); }; - $.fn.editableform.utils.inherit(Checklist, $.fn.editableform.types.list); + $.fn.editableutils.inherit(Checklist, $.fn.editabletypes.list); $.extend(Checklist.prototype, { renderList: function() { @@ -49,10 +49,8 @@ $(function(){ }, value2str: function(value) { - return $.isArray(value) ? value.join($.trim(this.options.separator)) : ''; - //it is also possible to sent as array - //return value; - }, + return $.isArray(value) ? value.sort().join($.trim(this.options.separator)) : ''; + }, //parse separated string str2value: function(str) { @@ -95,19 +93,18 @@ $(function(){ //collect text of checked boxes value2htmlFinal: function(value, element) { - var selected = [], item, i, html = ''; - if($.isArray(value) && value.length <= this.options.limit) { - for(i=0; i<value.length; i++){ - item = this.itemByVal(value[i]); - if(item) { - selected.push($('<div>').text(item.text).html()); - } - } - html = selected.join(this.options.viewseparator); - } else { - html = this.options.limitText.replace('{checked}', $.isArray(value) ? value.length : 0).replace('{count}', this.sourceData.length); + var html = [], + /*jslint eqeq: true*/ + checked = $.grep(this.sourceData, function(o){ + return $.grep(value, function(v){ return v == o.value; }).length; + }); + /*jslint eqeq: false*/ + if(checked.length) { + $.each(checked, function(i, v) { html.push($.fn.editableutils.escape(v.text)); }); + $(element).html(html.join('<br>')); + } else { + $(element).empty(); } - $(element).html(html); }, activate: function() { @@ -123,7 +120,7 @@ $(function(){ } }); - Checklist.defaults = $.extend({}, $.fn.editableform.types.list.defaults, { + Checklist.defaults = $.extend({}, $.fn.editabletypes.list.defaults, { /** @property tpl @default <div></div> @@ -133,46 +130,20 @@ $(function(){ /** @property inputclass @type string - @default span2 editable-checklist + @default editable-checklist **/ - inputclass: 'span2 editable-checklist', + inputclass: 'editable-checklist', /** - Separator of values in string when sending to server + Separator of values when reading from 'data-value' string @property separator @type string @default ', ' **/ - separator: ',', - /** - Separator of text when display as element content. - - @property viewseparator - @type string - @default '<br>' - **/ - viewseparator: '<br>', - /** - Maximum number of items shown as element content. - If checked more items - <code>limitText</code> will be shown. - - @property limit - @type integer - @default 4 - **/ - limit: 4, - /** - Text shown when count of checked items is greater than <code>limit</code> parameter. - You can use <code>{checked}</code> and <code>{count}</code> placeholders. - - @property limitText - @type string - @default 'Selected {checked} of {count}' - **/ - limitText: 'Selected {checked} of {count}' + separator: ',' }); - $.fn.editableform.types.checklist = Checklist; + $.fn.editabletypes.checklist = Checklist; }(window.jQuery)); diff --git a/src/inputs/date/bootstrap-datepicker/js/bootstrap-datepicker.js b/src/inputs/date/bootstrap-datepicker/js/bootstrap-datepicker.js index f236209..fae1165 100644 --- a/src/inputs/date/bootstrap-datepicker/js/bootstrap-datepicker.js +++ b/src/inputs/date/bootstrap-datepicker/js/bootstrap-datepicker.js @@ -292,7 +292,7 @@ .text(dates[this.language].months[month]+' '+year); this.picker.find('tfoot th.today') .text(dates[this.language].today) - .toggle(this.todayBtn); + .toggle(this.todayBtn !== false); this.updateNavArrows(); this.fillMonths(); var prevMonth = UTCDate(year, month-1, 28,0,0,0,0), @@ -429,10 +429,7 @@ break; case 'today': var date = new Date(); - date.setUTCHours(0); - date.setUTCMinutes(0); - date.setUTCSeconds(0); - date.setUTCMilliseconds(0); + date = UTCDate(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0); this.showMode(-2); var which = this.todayBtn == 'linked' ? null : 'view'; @@ -655,7 +652,17 @@ if (dir) { this.viewMode = Math.max(0, Math.min(2, this.viewMode + dir)); } - this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).show(); + /* + vitalets: fixing bug of very special conditions: + jquery 1.7.1 + webkit + show inline datepicker in bootstrap popover. + Method show() does not set display css correctly and datepicker is not shown. + Changed to .css('display', 'block') solve the problem. + See https://github.com/vitalets/x-editable/issues/37 + + In jquery 1.7.2+ everything works fine. + */ + //this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).show(); + this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).css('display', 'block'); this.updateNavArrows(); } }; @@ -773,7 +780,7 @@ val, filtered, part; setters_map['M'] = setters_map['MM'] = setters_map['mm'] = setters_map['m']; setters_map['dd'] = setters_map['d']; - date = UTCDate(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0); + date = UTCDate(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0); if (parts.length == format.parts.length) { for (var i=0, cnt = format.parts.length; i < cnt; i++) { val = parseInt(parts[i], 10); diff --git a/src/inputs/date/date.js b/src/inputs/date/date.js index 2fb8eab..1de781f 100644 --- a/src/inputs/date/date.js +++ b/src/inputs/date/date.js @@ -27,7 +27,7 @@ $(function(){ this.init('date', options, Date.defaults); //set popular options directly from settings or data-* attributes - var directOptions = $.fn.editableform.utils.sliceObj(this.options, ['format']); + var directOptions = $.fn.editableutils.sliceObj(this.options, ['format']); //overriding datepicker config (as by default jQuery extend() is not recursive) this.options.datepicker = $.extend({}, Date.defaults.datepicker, directOptions, options.datepicker); @@ -48,7 +48,7 @@ $(function(){ this.parsedViewFormat = this.dpg.parseFormat(this.options.viewformat); }; - $.fn.editableform.utils.inherit(Date, $.fn.editableform.types.abstract); + $.fn.editableutils.inherit(Date, $.fn.editabletypes.abstract); $.extend(Date.prototype, { render: function () { @@ -79,7 +79,11 @@ $(function(){ str2value: function(str) { return str ? this.dpg.parseDate(str, this.parsedFormat, this.options.datepicker.language) : null; - }, + }, + + value2submit: function(value) { + return this.value2str(value); + }, value2input: function(value) { this.$input.datepicker('update', value); @@ -108,7 +112,7 @@ $(function(){ }); - Date.defaults = $.extend({}, $.fn.editableform.types.abstract.defaults, { + Date.defaults = $.extend({}, $.fn.editabletypes.abstract.defaults, { /** @property tpl @default <div></div> @@ -165,6 +169,6 @@ $(function(){ clear: '× clear' }); - $.fn.editableform.types.date = Date; + $.fn.editabletypes.date = Date; }(window.jQuery)); diff --git a/src/inputs/dateui/dateui.js b/src/inputs/dateui/dateui.js index a8978dc..baa8afc 100644 --- a/src/inputs/dateui/dateui.js +++ b/src/inputs/dateui/dateui.js @@ -27,7 +27,7 @@ $(function(){ this.init('dateui', options, DateUI.defaults); //set popular options directly from settings or data-* attributes - var directOptions = $.fn.editableform.utils.sliceObj(this.options, ['format']); + var directOptions = $.fn.editableutils.sliceObj(this.options, ['format']); //overriding datepicker config (as by default jQuery extend() is not recursive) this.options.datepicker = $.extend({}, DateUI.defaults.datepicker, directOptions, options.datepicker); @@ -46,7 +46,7 @@ $(function(){ this.options.datepicker.dateFormat = this.options.datepicker.format; }; - $.fn.editableform.utils.inherit(DateUI, $.fn.editableform.types.abstract); + $.fn.editableutils.inherit(DateUI, $.fn.editabletypes.abstract); $.extend(DateUI.prototype, { render: function () { @@ -97,7 +97,11 @@ $(function(){ } catch(e) {} return d; - }, + }, + + value2submit: function(value) { + return this.value2str(value); + }, value2input: function(value) { this.$input.datepicker('setDate', value); @@ -125,7 +129,7 @@ $(function(){ }); - DateUI.defaults = $.extend({}, $.fn.editableform.types.abstract.defaults, { + DateUI.defaults = $.extend({}, $.fn.editabletypes.abstract.defaults, { /** @property tpl @default <div></div> @@ -183,7 +187,7 @@ $(function(){ clear: '× clear' }); - $.fn.editableform.types.dateui = DateUI; - $.fn.editableform.types.date = DateUI; + $.fn.editabletypes.dateui = DateUI; + $.fn.editabletypes.date = DateUI; }(window.jQuery)); diff --git a/src/inputs/list.js b/src/inputs/list.js index ad53e68..7af00b0 100644 --- a/src/inputs/list.js +++ b/src/inputs/list.js @@ -10,7 +10,7 @@ List - abstract class for inputs that have source option loaded from js array or }; - $.fn.editableform.utils.inherit(List, $.fn.editableform.types.abstract); + $.fn.editableutils.inherit(List, $.fn.editabletypes.abstract); $.extend(List.prototype, { render: function () { @@ -34,10 +34,15 @@ List - abstract class for inputs that have source option loaded from js array or return null; //can't set value by text }, - value2html: function (value, element) { + value2html: function (value, element, display) { var deferred = $.Deferred(); this.onSourceReady(function () { - this.value2htmlFinal(value, element); + if(typeof display === 'function') { + //custom display method + display.call(element, value, this.sourceData); + } else { + this.value2htmlFinal(value, element); + } deferred.resolve(); }, function () { List.superclass.value2html(this.options.sourceError, element); @@ -58,7 +63,7 @@ List - abstract class for inputs that have source option loaded from js array or // try parse json in single quotes (for double quotes jquery does automatically) try { - this.options.source = $.fn.editableform.utils.tryParseJson(this.options.source, false); + this.options.source = $.fn.editableutils.tryParseJson(this.options.source, false); } catch (e) { error.call(this); return; @@ -66,32 +71,35 @@ List - abstract class for inputs that have source option loaded from js array or //loading from url if (typeof this.options.source === 'string') { - var cacheID = this.options.source + (this.options.name ? '-' + this.options.name : ''), - cache; + //try to get from cache + if(this.options.sourceCache) { + var cacheID = this.options.source + (this.options.name ? '-' + this.options.name : ''), + cache; - if (!$(document).data(cacheID)) { - $(document).data(cacheID, {}); - } - cache = $(document).data(cacheID); + if (!$(document).data(cacheID)) { + $(document).data(cacheID, {}); + } + cache = $(document).data(cacheID); - //check for cached data - if (cache.loading === false && cache.sourceData) { //take source from cache - this.sourceData = cache.sourceData; - success.call(this); - return; - } else if (cache.loading === true) { //cache is loading, put callback in stack to be called later - cache.callbacks.push($.proxy(function () { + //check for cached data + if (cache.loading === false && cache.sourceData) { //take source from cache this.sourceData = cache.sourceData; success.call(this); - }, this)); + return; + } else if (cache.loading === true) { //cache is loading, put callback in stack to be called later + cache.callbacks.push($.proxy(function () { + this.sourceData = cache.sourceData; + success.call(this); + }, this)); - //also collecting error callbacks - cache.err_callbacks.push($.proxy(error, this)); - return; - } else { //no cache yet, activate it - cache.loading = true; - cache.callbacks = []; - cache.err_callbacks = []; + //also collecting error callbacks + cache.err_callbacks.push($.proxy(error, this)); + return; + } else { //no cache yet, activate it + cache.loading = true; + cache.callbacks = []; + cache.err_callbacks = []; + } } //loading sourceData from server @@ -102,23 +110,32 @@ List - abstract class for inputs that have source option loaded from js array or data: this.options.name ? {name: this.options.name} : {}, dataType: 'json', success: $.proxy(function (data) { - cache.loading = false; + if(cache) { + cache.loading = false; + } this.sourceData = this.makeArray(data); if($.isArray(this.sourceData)) { this.doPrepend(); - //store result in cache - cache.sourceData = this.sourceData; success.call(this); - $.each(cache.callbacks, function () { this.call(); }); //run success callbacks for other fields + if(cache) { + //store result in cache + cache.sourceData = this.sourceData; + $.each(cache.callbacks, function () { this.call(); }); //run success callbacks for other fields + } } else { error.call(this); - $.each(cache.err_callbacks, function () { this.call(); }); //run error callbacks for other fields + if(cache) { + $.each(cache.err_callbacks, function () { this.call(); }); //run error callbacks for other fields + } } }, this), error: $.proxy(function () { - cache.loading = false; error.call(this); - $.each(cache.err_callbacks, function () { this.call(); }); //run error callbacks for other fields + if(cache) { + cache.loading = false; + //run error callbacks for other fields + $.each(cache.err_callbacks, function () { this.call(); }); + } }, this) }); } else { //options as json/array @@ -139,7 +156,7 @@ List - abstract class for inputs that have source option loaded from js array or if(!$.isArray(this.prependData)) { //try parse json in single quotes - this.options.prepend = $.fn.editableform.utils.tryParseJson(this.options.prepend, true); + this.options.prepend = $.fn.editableutils.tryParseJson(this.options.prepend, true); if (typeof this.options.prepend === 'string') { this.options.prepend = {'': this.options.prepend}; } @@ -220,19 +237,20 @@ List - abstract class for inputs that have source option loaded from js array or }); - List.defaults = $.extend({}, $.fn.editableform.types.abstract.defaults, { + List.defaults = $.extend({}, $.fn.editabletypes.abstract.defaults, { /** Source data for list. If string - considered ajax url to load items. Otherwise should be an array. Array format is: <code>[{value: 1, text: "text"}, {...}]</code><br> For compability it also supports format <code>{value1: "text1", value2: "text2" ...}</code> but it does not guarantee elements order. - + If source is **string**, results will be cached for fields with the same source and name. See also <code>sourceCache</code> option. + @property source @type string|array|object @default null **/ source:null, /** - Data automatically prepended to the begining of dropdown list. + Data automatically prepended to the beginning of dropdown list. @property prepend @type string|array|object @@ -246,9 +264,19 @@ List - abstract class for inputs that have source option loaded from js array or @type string @default Error when loading list **/ - sourceError: 'Error when loading list' + sourceError: 'Error when loading list', + /** + if <code>true</code> and source is **string url** - results will be cached for fields with the same source and name. + Usefull for editable grids. + + @property sourceCache + @type boolean + @default true + @since 1.2.0 + **/ + sourceCache: true }); - $.fn.editableform.types.list = List; + $.fn.editabletypes.list = List; }(window.jQuery)); \ No newline at end of file diff --git a/src/inputs/select.js b/src/inputs/select.js index 42ffc77..60afb6b 100644 --- a/src/inputs/select.js +++ b/src/inputs/select.js @@ -26,7 +26,7 @@ $(function(){ this.init('select', options, Select.defaults); }; - $.fn.editableform.utils.inherit(Select, $.fn.editableform.types.list); + $.fn.editableutils.inherit(Select, $.fn.editabletypes.list); $.extend(Select.prototype, { renderList: function() { @@ -37,6 +37,13 @@ $(function(){ for(var i=0; i<this.sourceData.length; i++) { this.$input.append($('<option>', {value: this.sourceData[i].value}).text(this.sourceData[i].text)); } + + //enter submit + this.$input.on('keydown.editable', function (e) { + if (e.which === 13) { + $(this).closest('form').submit(); + } + }); }, value2htmlFinal: function(value, element) { @@ -48,13 +55,13 @@ $(function(){ }, autosubmit: function() { - this.$input.on('change', function(){ + this.$input.off('keydown.editable').on('change.editable', function(){ $(this).closest('form').submit(); }); } }); - Select.defaults = $.extend({}, $.fn.editableform.types.list.defaults, { + Select.defaults = $.extend({}, $.fn.editabletypes.list.defaults, { /** @property tpl @default <select></select> @@ -62,6 +69,6 @@ $(function(){ tpl:'<select></select>' }); - $.fn.editableform.types.select = Select; + $.fn.editabletypes.select = Select; }(window.jQuery)); \ No newline at end of file diff --git a/src/inputs/text.js b/src/inputs/text.js index 8557c2a..ce5872e 100644 --- a/src/inputs/text.js +++ b/src/inputs/text.js @@ -20,18 +20,18 @@ $(function(){ this.init('text', options, Text.defaults); }; - $.fn.editableform.utils.inherit(Text, $.fn.editableform.types.abstract); + $.fn.editableutils.inherit(Text, $.fn.editabletypes.abstract); $.extend(Text.prototype, { activate: function() { if(this.$input.is(':visible')) { this.$input.focus(); - $.fn.editableform.utils.setCursorPosition(this.$input.get(0), this.$input.val().length); + $.fn.editableutils.setCursorPosition(this.$input.get(0), this.$input.val().length); } } }); - Text.defaults = $.extend({}, $.fn.editableform.types.abstract.defaults, { + Text.defaults = $.extend({}, $.fn.editabletypes.abstract.defaults, { /** @property tpl @default <input type="text"> @@ -47,6 +47,6 @@ $(function(){ placeholder: null }); - $.fn.editableform.types.text = Text; + $.fn.editabletypes.text = Text; }(window.jQuery)); diff --git a/src/inputs/textarea.js b/src/inputs/textarea.js index eb4cd3c..0a8918e 100644 --- a/src/inputs/textarea.js +++ b/src/inputs/textarea.js @@ -21,7 +21,7 @@ $(function(){ this.init('textarea', options, Textarea.defaults); }; - $.fn.editableform.utils.inherit(Textarea, $.fn.editableform.types.abstract); + $.fn.editableutils.inherit(Textarea, $.fn.editabletypes.abstract); $.extend(Textarea.prototype, { render: function () { @@ -60,13 +60,13 @@ $(function(){ activate: function() { if(this.$input.is(':visible')) { - $.fn.editableform.utils.setCursorPosition(this.$input.get(0), this.$input.val().length); + $.fn.editableutils.setCursorPosition(this.$input.get(0), this.$input.val().length); this.$input.focus(); } } }); - Textarea.defaults = $.extend({}, $.fn.editableform.types.abstract.defaults, { + Textarea.defaults = $.extend({}, $.fn.editabletypes.abstract.defaults, { /** @property tpl @default <textarea></textarea> @@ -74,9 +74,9 @@ $(function(){ tpl:'<textarea></textarea>', /** @property inputclass - @default span3 + @default input-large **/ - inputclass:'span3', + inputclass: 'input-large', /** Placeholder attribute of input. Shown when input is empty. @@ -87,6 +87,6 @@ $(function(){ placeholder: null }); - $.fn.editableform.types.textarea = Textarea; + $.fn.editabletypes.textarea = Textarea; -}(window.jQuery)); \ No newline at end of file +}(window.jQuery)); diff --git a/test/index.html b/test/index.html index fe1e4df..233b5e7 100644 --- a/test/index.html +++ b/test/index.html @@ -3,55 +3,25 @@ <head> <meta charset="utf-8"> <title>Test X-editable</title> - - <!-- jquery --> - <script src="libs/jquery/jquery-1.8.2.js"></script> - - <!-- qunit --> - <link rel="stylesheet" href="libs/qunit/qunit-1.10.0.css" type="text/css" media="screen" /> - <script src="libs/qunit/qunit-1.10.0.js"></script> - - <!-- mockjax --> - <script src="libs/mockjax/jquery.mockjax.js"></script> - <script src="mocks.js"></script> - - <script src="loader.js"></script> <script> - var fc = getFC(), - assets = getAssets(fc.f, fc.c, '../src/', 'libs/'); - loadAssets(assets.css, assets.js); - - var fx, sfx; - $(function () { - $.fx.off = true; - $.support.transition = false; - }); - </script> + window.onload = function() { + QUnit.config.autostart = false; + }; + </script> + +<!-- <script data-main="main.js" src="require-jquery.js"></script>--> + <script data-main="main.js" src="require.js"></script> + + <!-- qunit (should be included here, otherwise phantomjs hangs-up on several tests )--> + <link rel="stylesheet" href="libs/qunit/qunit-1.10.0.css" type="text/css" media="screen" /> + <script src="libs/qunit/qunit-1.10.0.js"></script> </head> <body> - <div> + <div> <div id="qunit"></div> <div id="qunit-fixture"></div> <div id="async-fixture"></div> </div> - - <!-- unit tests --> - <!-- You can also add "?module=[module]" URL to run only tests within that module--> - <script src="unit/common.js"></script> - <script src="unit/text.js"></script> - <script src="unit/select.js"></script> - <script src="unit/textarea.js"></script> - <script src="unit/api.js"></script> - <script src="unit/checklist.js"></script> - <script> - if(fc.f === 'bootstrap') { - loadJs('unit/date.js'); - } else { - loadJs('unit/dateui.js'); - } - </script> - - </body> </html> diff --git a/test/loader.js b/test/loader.js index b1b4a6b..3ccb962 100644 --- a/test/loader.js +++ b/test/loader.js @@ -1,128 +1,170 @@ -/** -* load requred js and css according to f (form) and c (container) parameters -*/ +define(function () { + + function loadCss(url) { + var link = document.createElement("link"); + link.type = "text/css"; + link.rel = "stylesheet"; + link.href = url; + document.getElementsByTagName("head")[0].appendChild(link); + }; + + return { + loadCss: loadCss, + getConfig: function (baseUrl) { + var params = this.getParams(), + f = params.f, + c = params.c, + shim = { + 'containers/editable-container': { + deps: ['require', 'editable-form/editable-form-utils', 'editable-form/editable-form'], + init: function(require) { + loadCss(require.toUrl("./editable-container.css")); + } + }, + 'element/editable-element': { + deps: ['require'], //here should be dynamically added container + init: function(require) { + loadCss(require.toUrl("./editable-element.css")); + } + }, + 'editable-form/editable-form': { + deps: ['require', + 'inputs/text', + 'inputs/textarea', + 'inputs/select', + 'inputs/checklist', + 'inputs-ext/address/address'], + init: function(require) { + loadCss(require.toUrl("./editable-form.css")); + } + }, + 'inputs/select': ['inputs/list'], + 'inputs/checklist': ['inputs/list'], + 'inputs/list': ['inputs/abstract'], + 'inputs/text': ['inputs/abstract'], + 'inputs/textarea': ['inputs/abstract'], + 'inputs/abstract': ['editable-form/editable-form-utils'], -function getFC() { - var url = window.location.href, f, c; - if(url.match(/f=jqueryui/i)) { - f = 'jqueryui'; - } else if(url.match(/f=plain/i)) { - f = 'plain'; - } else { //bootstrap - f = 'bootstrap'; + //bootstrap + 'bootstrap/js/bootstrap': { + deps: ['require'], + init: function(require) { + loadCss(require.toUrl("../css/bootstrap.css")); + } + }, + 'editable-form/editable-form-bootstrap': [ + 'editable-form/editable-form', + 'bootstrap/js/bootstrap' + ], + 'containers/editable-popover': ['containers/editable-container', + 'bootstrap/js/bootstrap' + ], + 'inputs/date/date': { + deps: ['require', + 'bootstrap/js/bootstrap', + 'inputs/abstract', + 'inputs/date/bootstrap-datepicker/js/bootstrap-datepicker'], + init: function(require) { + loadCss(require.toUrl("./bootstrap-datepicker/css/datepicker.css")); + } + }, + + //jqueryui + 'jqueryui/js/jquery-ui-1.9.1.custom': { + deps: ['require'], + init: function(require) { + loadCss(require.toUrl("../css/redmond/jquery-ui-1.9.1.custom.css")); + } + }, + 'editable-form/editable-form-jqueryui': [ + 'editable-form/editable-form', + 'jqueryui/js/jquery-ui-1.9.1.custom' + ], + 'containers/editable-tooltip': ['containers/editable-container', + 'jqueryui/js/jquery-ui-1.9.1.custom' + ], + 'inputs/dateui/dateui': ['inputs/abstract'], + + //plain + //'inputs/dateui/dateui': ['inputs/abstract', 'inputs/date/bootstrap-datepicker/js/bootstrap-datepicker'], + 'containers/editable-poshytip': [ + 'containers/editable-container', + 'poshytip/jquery.poshytip' + ], + 'poshytip/jquery.poshytip': { + deps: ['require'], + init: function(require) { + loadCss(require.toUrl("./tip-yellowsimple/tip-yellowsimple.css")); + } + }, + 'inputs/dateui/jquery-ui-datepicker/js/jquery-ui-1.9.1.custom': { + deps: ['require'], + init: function(require) { + loadCss(require.toUrl("../css/redmond/jquery-ui-1.9.1.custom.css")); + } + }, + + //inline container + 'containers/editable-inline': ['containers/editable-container'], + + //inputs-ext + 'inputs-ext/address/address': { + deps: ['require', 'inputs/abstract'], + init: function(require) { + loadCss(require.toUrl("./address.css")); + } + } + }; + + /* + modify shim for bootstrap, jqueryui or plain + */ + + if(f === 'bootstrap') { + //bootstrap + shim['editable-form/editable-form'].deps.push('inputs/date/date'); + shim['element/editable-element'].deps.push('editable-form/editable-form-bootstrap'); + shim['element/editable-element'].deps.push(c === 'popup' ? 'containers/editable-popover' : 'containers/editable-inline'); + } else if(f === 'jqueryui') { + //jqueryui + shim['editable-form/editable-form'].deps.push('inputs/dateui/dateui'); + shim['element/editable-element'].deps.push('editable-form/editable-form-jqueryui'); + shim['element/editable-element'].deps.push(c === 'popup' ? 'containers/editable-tooltip' : 'containers/editable-inline'); + } else { + //plain + shim['editable-form/editable-form'].deps.push('inputs/dateui/dateui'); + shim['inputs/dateui/dateui'].push('inputs/dateui/jquery-ui-datepicker/js/jquery-ui-1.9.1.custom'); + shim['element/editable-element'].deps.push(c === 'popup' ? 'containers/editable-poshytip' : 'containers/editable-inline'); + } + + + /* + return requirejs config + */ + + return { + baseUrl: baseUrl, + paths: { + "bootstrap": "../test/libs/bootstrap221", + "jqueryui": "../test/libs/jquery-ui-1.9.1.custom", + "poshytip": "../test/libs/poshytip", + "test": "../test" + }, + shim: shim + }; + }, + + getParams: function() { + var url = window.location.href, f, c; + if(url.match(/f=jqueryui/i)) { + f = 'jqueryui'; + } else if(url.match(/f=plain/i)) { + f = 'plain'; + } else { + f = 'bootstrap'; + } + c = url.match(/c=inline/i) ? 'inline' : 'popup'; + return {f: f, c: c}; + } } - c = url.match(/c=inline/i) ? 'inline' : 'popup'; - return {f: f, c: c}; -} - -function getAssets(f, c, src, libs) { - var - forms = src+'editable-form/', - inputs = src+'inputs/', - containers = src+'containers/', - element = src+'element/', - - bootstrap = libs+'bootstrap221/', - jqueryui = libs+'jquery-ui-1.9.1.custom/', - - js = [ - forms+'editable-form.js', - forms+'editable-form-utils.js', - containers+'editable-container.js', - element+'editable-element.js', - inputs+'abstract.js', - inputs+'list.js', - inputs+'text.js', - inputs+'textarea.js', - inputs+'select.js', - inputs+'checklist.js' - ], - - css = [ - forms+'editable-form.css', - containers+'editable-container.css', - element+'editable-element.css' - ]; - - - //tune js and css - if(f==='jqueryui') { - //core - js.unshift(jqueryui+'js/jquery-ui-1.9.1.custom.js') - css.unshift(jqueryui+'css/redmond/jquery-ui-1.9.1.custom.css'); - - //editable - js.push(forms+'editable-form-jqueryui.js'); - js.push(getContainer('editable-tooltip.js')); - - //date - js.push(inputs+'dateui/dateui.js'); - - //style - css.push('style.css'); - } else if(f==='plain') { - //core - js.unshift(libs+'poshytip/jquery.poshytip.js'); - css.unshift(libs+'poshytip/tip-yellowsimple/tip-yellowsimple.css'); - - //editable - js.push(getContainer('editable-poshytip.js')); - - //date - js.push(inputs+'dateui/dateui.js'); - js.push(inputs+'dateui/jquery-ui-datepicker/js/jquery-ui-1.9.1.custom.js'); - css.unshift(inputs+'dateui/jquery-ui-datepicker/css/redmond/jquery-ui-1.9.1.custom.css'); - - //style - css.push('style.css'); - /* bootstrap */ - } else { - //core - js.unshift(bootstrap+'js/bootstrap.js') - css.unshift(bootstrap+'css/bootstrap.css'); - css.unshift(bootstrap+'css/bootstrap-responsive.css'); - - //editable - js.push(forms+'editable-form-bootstrap.js'); - js.push(getContainer('editable-popover.js')); - - //date - js.push(inputs+'date/bootstrap-datepicker/js/bootstrap-datepicker.js'); - js.push(inputs+'date/date.js'); - css.push(inputs+'date/bootstrap-datepicker/css/datepicker.css'); - } - - function getContainer(container) { - return (c === 'inline') ? containers+'/editable-inline.js' : containers + container; - } - - //js.push('main.js'); - - return {css: css, js: js}; -} - - -function loadAssets(css, js) { - for(var i = 0; i < css.length; i++) { - loadCss(css[i]); - } - - for(i = 0; i < js.length; i++) { - loadJs(js[i]); - } -} - -function loadCss(url) { - var link = document.createElement("link"); - link.type = "text/css"; - link.rel = "stylesheet"; - link.href = url; - document.getElementsByTagName("head")[0].appendChild(link); -} - -function loadJs(url) { - if(!url) return; - var script = document.createElement("script"); - script.src = url; - document.getElementsByTagName("head")[0].appendChild(script); -} \ No newline at end of file +}); \ No newline at end of file diff --git a/test/main.js b/test/main.js new file mode 100644 index 0000000..6264575 --- /dev/null +++ b/test/main.js @@ -0,0 +1,33 @@ +//detect version of jquery from url param, e.g. 'jquery=1.7.2' +var jqver = decodeURIComponent((new RegExp('[?|&]' + 'jquery' + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search)||[,""])[1].replace(/\+/g, '%20'))||null; + jqurl = jqver ? "http://ajax.googleapis.com/ajax/libs/jquery/"+jqver+"/jquery.min.js" : "libs/jquery/jquery-1.8.2.min.js"; + +require(["loader", jqurl], function(loader) { + + requirejs.config(loader.getConfig("../src")); + + require(['element/editable-element', + 'test/libs/mockjax/jquery.mockjax' + ], + function() { + //disable effects + $.fx.off = true; + $.support.transition = false; + + var params = loader.getParams(); + + require([ + 'test/mocks', + 'test/unit/common', + 'test/unit/text', + 'test/unit/textarea', + 'test/unit/select', + 'test/unit/checklist', + 'test/unit/api', + (params.f === 'bootstrap') ? 'test/unit/date' : 'test/unit/dateui' + ], function() { + QUnit.load(); + QUnit.start(); + }); + }); +}); \ No newline at end of file diff --git a/test/mocks.js b/test/mocks.js index 46d7a4f..446c901 100644 --- a/test/mocks.js +++ b/test/mocks.js @@ -57,7 +57,7 @@ $(function () { }); -// usefull functions +// useful functions function tip(e) { return e.data('editableContainer').tip(); diff --git a/test/require.js b/test/require.js new file mode 100644 index 0000000..8de013d --- /dev/null +++ b/test/require.js @@ -0,0 +1,35 @@ +/* + RequireJS 2.1.2 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved. + Available via the MIT or new BSD license. + see: http://github.com/jrburke/requirejs for details +*/ +var requirejs,require,define; +(function(Y){function H(b){return"[object Function]"===L.call(b)}function I(b){return"[object Array]"===L.call(b)}function x(b,c){if(b){var d;for(d=0;d<b.length&&(!b[d]||!c(b[d],d,b));d+=1);}}function M(b,c){if(b){var d;for(d=b.length-1;-1<d&&(!b[d]||!c(b[d],d,b));d-=1);}}function r(b,c){return da.call(b,c)}function i(b,c){return r(b,c)&&b[c]}function E(b,c){for(var d in b)if(r(b,d)&&c(b[d],d))break}function Q(b,c,d,i){c&&E(c,function(c,h){if(d||!r(b,h))i&&"string"!==typeof c?(b[h]||(b[h]={}),Q(b[h], +c,d,i)):b[h]=c});return b}function t(b,c){return function(){return c.apply(b,arguments)}}function Z(b){if(!b)return b;var c=Y;x(b.split("."),function(b){c=c[b]});return c}function J(b,c,d,i){c=Error(c+"\nhttp://requirejs.org/docs/errors.html#"+b);c.requireType=b;c.requireModules=i;d&&(c.originalError=d);return c}function ea(b){function c(a,g,v){var e,n,b,c,d,j,f,h=g&&g.split("/");e=h;var l=m.map,k=l&&l["*"];if(a&&"."===a.charAt(0))if(g){e=i(m.pkgs,g)?h=[g]:h.slice(0,h.length-1);g=a=e.concat(a.split("/")); +for(e=0;g[e];e+=1)if(n=g[e],"."===n)g.splice(e,1),e-=1;else if(".."===n)if(1===e&&(".."===g[2]||".."===g[0]))break;else 0<e&&(g.splice(e-1,2),e-=2);e=i(m.pkgs,g=a[0]);a=a.join("/");e&&a===g+"/"+e.main&&(a=g)}else 0===a.indexOf("./")&&(a=a.substring(2));if(v&&(h||k)&&l){g=a.split("/");for(e=g.length;0<e;e-=1){b=g.slice(0,e).join("/");if(h)for(n=h.length;0<n;n-=1)if(v=i(l,h.slice(0,n).join("/")))if(v=i(v,b)){c=v;d=e;break}if(c)break;!j&&(k&&i(k,b))&&(j=i(k,b),f=e)}!c&&j&&(c=j,d=f);c&&(g.splice(0,d, +c),a=g.join("/"))}return a}function d(a){z&&x(document.getElementsByTagName("script"),function(g){if(g.getAttribute("data-requiremodule")===a&&g.getAttribute("data-requirecontext")===j.contextName)return g.parentNode.removeChild(g),!0})}function y(a){var g=i(m.paths,a);if(g&&I(g)&&1<g.length)return d(a),g.shift(),j.require.undef(a),j.require([a]),!0}function f(a){var g,b=a?a.indexOf("!"):-1;-1<b&&(g=a.substring(0,b),a=a.substring(b+1,a.length));return[g,a]}function h(a,g,b,e){var n,u,d=null,h=g?g.name: +null,l=a,m=!0,k="";a||(m=!1,a="_@r"+(L+=1));a=f(a);d=a[0];a=a[1];d&&(d=c(d,h,e),u=i(p,d));a&&(d?k=u&&u.normalize?u.normalize(a,function(a){return c(a,h,e)}):c(a,h,e):(k=c(a,h,e),a=f(k),d=a[0],k=a[1],b=!0,n=j.nameToUrl(k)));b=d&&!u&&!b?"_unnormalized"+(M+=1):"";return{prefix:d,name:k,parentMap:g,unnormalized:!!b,url:n,originalName:l,isDefine:m,id:(d?d+"!"+k:k)+b}}function q(a){var g=a.id,b=i(k,g);b||(b=k[g]=new j.Module(a));return b}function s(a,g,b){var e=a.id,n=i(k,e);if(r(p,e)&&(!n||n.defineEmitComplete))"defined"=== +g&&b(p[e]);else q(a).on(g,b)}function C(a,g){var b=a.requireModules,e=!1;if(g)g(a);else if(x(b,function(g){if(g=i(k,g))g.error=a,g.events.error&&(e=!0,g.emit("error",a))}),!e)l.onError(a)}function w(){R.length&&(fa.apply(F,[F.length-1,0].concat(R)),R=[])}function A(a,g,b){var e=a.map.id;a.error?a.emit("error",a.error):(g[e]=!0,x(a.depMaps,function(e,c){var d=e.id,h=i(k,d);h&&(!a.depMatched[c]&&!b[d])&&(i(g,d)?(a.defineDep(c,p[d]),a.check()):A(h,g,b))}),b[e]=!0)}function B(){var a,g,b,e,n=(b=1E3*m.waitSeconds)&& +j.startTime+b<(new Date).getTime(),c=[],h=[],f=!1,l=!0;if(!T){T=!0;E(k,function(b){a=b.map;g=a.id;if(b.enabled&&(a.isDefine||h.push(b),!b.error))if(!b.inited&&n)y(g)?f=e=!0:(c.push(g),d(g));else if(!b.inited&&(b.fetched&&a.isDefine)&&(f=!0,!a.prefix))return l=!1});if(n&&c.length)return b=J("timeout","Load timeout for modules: "+c,null,c),b.contextName=j.contextName,C(b);l&&x(h,function(a){A(a,{},{})});if((!n||e)&&f)if((z||$)&&!U)U=setTimeout(function(){U=0;B()},50);T=!1}}function D(a){r(p,a[0])|| +q(h(a[0],null,!0)).init(a[1],a[2])}function G(a){var a=a.currentTarget||a.srcElement,b=j.onScriptLoad;a.detachEvent&&!V?a.detachEvent("onreadystatechange",b):a.removeEventListener("load",b,!1);b=j.onScriptError;(!a.detachEvent||V)&&a.removeEventListener("error",b,!1);return{node:a,id:a&&a.getAttribute("data-requiremodule")}}function K(){var a;for(w();F.length;){a=F.shift();if(null===a[0])return C(J("mismatch","Mismatched anonymous define() module: "+a[a.length-1]));D(a)}}var T,W,j,N,U,m={waitSeconds:7, +baseUrl:"./",paths:{},pkgs:{},shim:{},map:{},config:{}},k={},X={},F=[],p={},S={},L=1,M=1;N={require:function(a){return a.require?a.require:a.require=j.makeRequire(a.map)},exports:function(a){a.usingExports=!0;if(a.map.isDefine)return a.exports?a.exports:a.exports=p[a.map.id]={}},module:function(a){return a.module?a.module:a.module={id:a.map.id,uri:a.map.url,config:function(){return m.config&&i(m.config,a.map.id)||{}},exports:p[a.map.id]}}};W=function(a){this.events=i(X,a.id)||{};this.map=a;this.shim= +i(m.shim,a.id);this.depExports=[];this.depMaps=[];this.depMatched=[];this.pluginMaps={};this.depCount=0};W.prototype={init:function(a,b,c,e){e=e||{};if(!this.inited){this.factory=b;if(c)this.on("error",c);else this.events.error&&(c=t(this,function(a){this.emit("error",a)}));this.depMaps=a&&a.slice(0);this.errback=c;this.inited=!0;this.ignore=e.ignore;e.enabled||this.enabled?this.enable():this.check()}},defineDep:function(a,b){this.depMatched[a]||(this.depMatched[a]=!0,this.depCount-=1,this.depExports[a]= +b)},fetch:function(){if(!this.fetched){this.fetched=!0;j.startTime=(new Date).getTime();var a=this.map;if(this.shim)j.makeRequire(this.map,{enableBuildCallback:!0})(this.shim.deps||[],t(this,function(){return a.prefix?this.callPlugin():this.load()}));else return a.prefix?this.callPlugin():this.load()}},load:function(){var a=this.map.url;S[a]||(S[a]=!0,j.load(this.map.id,a))},check:function(){if(this.enabled&&!this.enabling){var a,b,c=this.map.id;b=this.depExports;var e=this.exports,n=this.factory; +if(this.inited)if(this.error)this.emit("error",this.error);else{if(!this.defining){this.defining=!0;if(1>this.depCount&&!this.defined){if(H(n)){if(this.events.error)try{e=j.execCb(c,n,b,e)}catch(d){a=d}else e=j.execCb(c,n,b,e);this.map.isDefine&&((b=this.module)&&void 0!==b.exports&&b.exports!==this.exports?e=b.exports:void 0===e&&this.usingExports&&(e=this.exports));if(a)return a.requireMap=this.map,a.requireModules=[this.map.id],a.requireType="define",C(this.error=a)}else e=n;this.exports=e;if(this.map.isDefine&& +!this.ignore&&(p[c]=e,l.onResourceLoad))l.onResourceLoad(j,this.map,this.depMaps);delete k[c];this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}else this.fetch()}},callPlugin:function(){var a=this.map,b=a.id,d=h(a.prefix);this.depMaps.push(d);s(d,"defined",t(this,function(e){var n,d;d=this.map.name;var v=this.map.parentMap?this.map.parentMap.name:null,f=j.makeRequire(a.parentMap,{enableBuildCallback:!0, +skipMap:!0});if(this.map.unnormalized){if(e.normalize&&(d=e.normalize(d,function(a){return c(a,v,!0)})||""),e=h(a.prefix+"!"+d,this.map.parentMap),s(e,"defined",t(this,function(a){this.init([],function(){return a},null,{enabled:!0,ignore:!0})})),d=i(k,e.id)){this.depMaps.push(e);if(this.events.error)d.on("error",t(this,function(a){this.emit("error",a)}));d.enable()}}else n=t(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),n.error=t(this,function(a){this.inited=!0;this.error= +a;a.requireModules=[b];E(k,function(a){0===a.map.id.indexOf(b+"_unnormalized")&&delete k[a.map.id]});C(a)}),n.fromText=t(this,function(e,c){var d=a.name,u=h(d),v=O;c&&(e=c);v&&(O=!1);q(u);r(m.config,b)&&(m.config[d]=m.config[b]);try{l.exec(e)}catch(k){throw Error("fromText eval for "+d+" failed: "+k);}v&&(O=!0);this.depMaps.push(u);j.completeLoad(d);f([d],n)}),e.load(a.name,f,n,m)}));j.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){this.enabling=this.enabled=!0;x(this.depMaps,t(this,function(a, +b){var c,e;if("string"===typeof a){a=h(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=i(N,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;s(a,"defined",t(this,function(a){this.defineDep(b,a);this.check()}));this.errback&&s(a,"error",this.errback)}c=a.id;e=k[c];!r(N,c)&&(e&&!e.enabled)&&j.enable(a,this)}));E(this.pluginMaps,t(this,function(a){var b=i(k,a.id);b&&!b.enabled&&j.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c= +this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){x(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};j={config:m,contextName:b,registry:k,defined:p,urlFetched:S,defQueue:F,Module:W,makeModuleMap:h,nextTick:l.nextTick,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");var b=m.pkgs,c=m.shim,e={paths:!0,config:!0,map:!0};E(a,function(a,b){e[b]?"map"===b?Q(m[b],a,!0,!0):Q(m[b],a,!0):m[b]=a});a.shim&&(E(a.shim,function(a, +b){I(a)&&(a={deps:a});if((a.exports||a.init)&&!a.exportsFn)a.exportsFn=j.makeShimExports(a);c[b]=a}),m.shim=c);a.packages&&(x(a.packages,function(a){a="string"===typeof a?{name:a}:a;b[a.name]={name:a.name,location:a.location||a.name,main:(a.main||"main").replace(ga,"").replace(aa,"")}}),m.pkgs=b);E(k,function(a,b){!a.inited&&!a.map.unnormalized&&(a.map=h(b))});if(a.deps||a.callback)j.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(Y,arguments)); +return b||a.exports&&Z(a.exports)}},makeRequire:function(a,d){function f(e,c,u){var i,m;d.enableBuildCallback&&(c&&H(c))&&(c.__requireJsBuild=!0);if("string"===typeof e){if(H(c))return C(J("requireargs","Invalid require call"),u);if(a&&r(N,e))return N[e](k[a.id]);if(l.get)return l.get(j,e,a);i=h(e,a,!1,!0);i=i.id;return!r(p,i)?C(J("notloaded",'Module name "'+i+'" has not been loaded yet for context: '+b+(a?"":". Use require([])"))):p[i]}K();j.nextTick(function(){K();m=q(h(null,a));m.skipMap=d.skipMap; +m.init(e,c,u,{enabled:!0});B()});return f}d=d||{};Q(f,{isBrowser:z,toUrl:function(b){var d=b.lastIndexOf("."),g=null;-1!==d&&(g=b.substring(d,b.length),b=b.substring(0,d));return j.nameToUrl(c(b,a&&a.id,!0),g)},defined:function(b){return r(p,h(b,a,!1,!0).id)},specified:function(b){b=h(b,a,!1,!0).id;return r(p,b)||r(k,b)}});a||(f.undef=function(b){w();var c=h(b,a,!0),d=i(k,b);delete p[b];delete S[c.url];delete X[b];d&&(d.events.defined&&(X[b]=d.events),delete k[b])});return f},enable:function(a){i(k, +a.id)&&q(a).enable()},completeLoad:function(a){var b,c,d=i(m.shim,a)||{},h=d.exports;for(w();F.length;){c=F.shift();if(null===c[0]){c[0]=a;if(b)break;b=!0}else c[0]===a&&(b=!0);D(c)}c=i(k,a);if(!b&&!r(p,a)&&c&&!c.inited){if(m.enforceDefine&&(!h||!Z(h)))return y(a)?void 0:C(J("nodefine","No define call for "+a,null,[a]));D([a,d.deps||[],d.exportsFn])}B()},nameToUrl:function(a,b){var c,d,h,f,j,k;if(l.jsExtRegExp.test(a))f=a+(b||"");else{c=m.paths;d=m.pkgs;f=a.split("/");for(j=f.length;0<j;j-=1)if(k= +f.slice(0,j).join("/"),h=i(d,k),k=i(c,k)){I(k)&&(k=k[0]);f.splice(0,j,k);break}else if(h){c=a===h.name?h.location+"/"+h.main:h.location;f.splice(0,j,c);break}f=f.join("/");f+=b||(/\?/.test(f)?"":".js");f=("/"===f.charAt(0)||f.match(/^[\w\+\.\-]+:/)?"":m.baseUrl)+f}return m.urlArgs?f+((-1===f.indexOf("?")?"?":"&")+m.urlArgs):f},load:function(a,b){l.load(j,a,b)},execCb:function(a,b,c,d){return b.apply(d,c)},onScriptLoad:function(a){if("load"===a.type||ha.test((a.currentTarget||a.srcElement).readyState))P= +null,a=G(a),j.completeLoad(a.id)},onScriptError:function(a){var b=G(a);if(!y(b.id))return C(J("scripterror","Script error",a,[b.id]))}};j.require=j.makeRequire();return j}var l,w,A,D,s,G,P,K,ba,ca,ia=/(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg,ja=/[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,aa=/\.js$/,ga=/^\.\//;w=Object.prototype;var L=w.toString,da=w.hasOwnProperty,fa=Array.prototype.splice,z=!!("undefined"!==typeof window&&navigator&&document),$=!z&&"undefined"!==typeof importScripts,ha=z&& +"PLAYSTATION 3"===navigator.platform?/^complete$/:/^(complete|loaded)$/,V="undefined"!==typeof opera&&"[object Opera]"===opera.toString(),B={},q={},R=[],O=!1;if("undefined"===typeof define){if("undefined"!==typeof requirejs){if(H(requirejs))return;q=requirejs;requirejs=void 0}"undefined"!==typeof require&&!H(require)&&(q=require,require=void 0);l=requirejs=function(b,c,d,y){var f,h="_";!I(b)&&"string"!==typeof b&&(f=b,I(c)?(b=c,c=d,d=y):b=[]);f&&f.context&&(h=f.context);(y=i(B,h))||(y=B[h]=l.s.newContext(h)); +f&&y.configure(f);return y.require(b,c,d)};l.config=function(b){return l(b)};l.nextTick="undefined"!==typeof setTimeout?function(b){setTimeout(b,4)}:function(b){b()};require||(require=l);l.version="2.1.2";l.jsExtRegExp=/^\/|:|\?|\.js$/;l.isBrowser=z;w=l.s={contexts:B,newContext:ea};l({});x(["toUrl","undef","defined","specified"],function(b){l[b]=function(){var c=B._;return c.require[b].apply(c,arguments)}});if(z&&(A=w.head=document.getElementsByTagName("head")[0],D=document.getElementsByTagName("base")[0]))A= +w.head=D.parentNode;l.onError=function(b){throw b;};l.load=function(b,c,d){var i=b&&b.config||{},f;if(z)return f=i.xhtml?document.createElementNS("http://www.w3.org/1999/xhtml","html:script"):document.createElement("script"),f.type=i.scriptType||"text/javascript",f.charset="utf-8",f.async=!0,f.setAttribute("data-requirecontext",b.contextName),f.setAttribute("data-requiremodule",c),f.attachEvent&&!(f.attachEvent.toString&&0>f.attachEvent.toString().indexOf("[native code"))&&!V?(O=!0,f.attachEvent("onreadystatechange", +b.onScriptLoad)):(f.addEventListener("load",b.onScriptLoad,!1),f.addEventListener("error",b.onScriptError,!1)),f.src=d,K=f,D?A.insertBefore(f,D):A.appendChild(f),K=null,f;$&&(importScripts(d),b.completeLoad(c))};z&&M(document.getElementsByTagName("script"),function(b){A||(A=b.parentNode);if(s=b.getAttribute("data-main"))return q.baseUrl||(G=s.split("/"),ba=G.pop(),ca=G.length?G.join("/")+"/":"./",q.baseUrl=ca,s=ba),s=s.replace(aa,""),q.deps=q.deps?q.deps.concat(s):[s],!0});define=function(b,c,d){var i, +f;"string"!==typeof b&&(d=c,c=b,b=null);I(c)||(d=c,c=[]);!c.length&&H(d)&&d.length&&(d.toString().replace(ia,"").replace(ja,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c));if(O){if(!(i=K))P&&"interactive"===P.readyState||M(document.getElementsByTagName("script"),function(b){if("interactive"===b.readyState)return P=b}),i=P;i&&(b||(b=i.getAttribute("data-requiremodule")),f=B[i.getAttribute("data-requirecontext")])}(f?f.defQueue:R).push([b,c,d])};define.amd= +{jQuery:!0};l.exec=function(b){return eval(b)};l(q)}})(this); diff --git a/test/unit/api.js b/test/unit/api.js index 47f6ca2..ca7a1d9 100644 --- a/test/unit/api.js +++ b/test/unit/api.js @@ -68,43 +68,7 @@ $(function () { ok(!('dob' in values), 'date value not present') ; }); - /* - //deprecated in 2.0 - asyncTest("'update' event", function () { - expect(2); - var e = $('<a href="#" data-pk="1" data-url="post.php" data-name="text1">abc</a>').appendTo(fx).editable(), - e_nopk = $('<a href="#" data-url="post.php" data-name="text1">abc</a>').appendTo(fx).editable(), - newVal = 'xyt'; - - e.on('update', function() { - equal($(this).data('editable').value, newVal, 'triggered update after submit'); - }); - - e_nopk.on('update', function() { - equal($(this).data('editable').value, newVal, 'triggered update after no-submit'); - }); - - e_nopk.click(); - var p = e_nopk.data('popover').$tip; - p.find('input').val(newVal); - p.find('form').submit(); - - e.click(); - p = tip(e); - p.find('input').val(newVal); - p.find('form').submit(); - - setTimeout(function() { - e.remove(); - e_nopk.remove(); - start(); - }, timeout); - }); - */ - - /* - //deprecated in 2.0 - test("'init' event", function () { + test("'init' event", function () { expect(1); var e = $('<a href="#" data-pk="1" data-url="post.php" data-name="text1">abc</a>').appendTo('#qunit-fixture'); @@ -113,74 +77,11 @@ $(function () { }); e.editable(); - }); - */ - - asyncTest("'render' event for text", function () { - expect(4); - var val = 'afas', - e = $('<a href="#" data-pk="1" data-type="text" data-url="post.php" data-name="text1">'+val+'</a>').appendTo(fx), - isInit = true; - - e.on('render', function(e, editable) { - equal(editable.isInit, isInit, 'isInit flag correct'); - equal(editable.value, val, 'value correct'); - }); - - e.editable(); - - isInit = false; - val = '123'; - - e.click(); - var p = tip(e); - p.find('input[type=text]').val(val); - p.find('form').submit(); - - setTimeout(function() { - e.remove(); - start(); - }, timeout); - - }); - - asyncTest("'render' event for select", function () { - expect(4); - var val = '1', - e = $('<a href="#" data-pk="1" data-type="select" data-url="post.php" data-name="text1" data-value="'+val+'"></a>').appendTo(fx), - isInit = true; - - e.on('render', function(e, editable) { - equal(editable.isInit, isInit, 'isInit flag correct'); - equal(editable.value, val, 'init triggered, value correct'); - }); - - e.editable({ - source: 'groups.php', - autotext: 'always' - }); - - setTimeout(function() { - isInit = false; - val = '3'; - - e.click(); - var p = tip(e); - p.find('select').val(val); - p.find('form').submit(); - - setTimeout(function() { - e.remove(); - start(); - }, timeout); - }, timeout); - - }); - - - asyncTest("events: shown / cancel / hidden", function () { - expect(3); - var val = '1', + }); + + asyncTest("events: shown / hidden (reason: cancel, onblur, manual)", function () { + expect(11); + var val = '1', test_reason, e = $('<a href="#" data-pk="1" data-type="select" data-url="post.php" data-name="text" data-value="'+val+'"></a>').appendTo(fx); e.on('shown', function(event) { @@ -188,14 +89,8 @@ $(function () { equal(editable.value, val, 'shown triggered, value correct'); }); - e.on('cancel', function(event) { - var editable = $(this).data('editable'); - ok(true, 'cancel triggered'); - }); - - e.on('hidden', function(event) { - var editable = $(this).data('editable'); - ok(true, 'hidden triggered'); + e.on('hidden', function(event, reason) { + ok((reason === test_reason) || (test_reason === 'manual' && reason === undefined), 'hidden triggered, reason ok'); }); e.editable({ @@ -206,41 +101,54 @@ $(function () { setTimeout(function() { var p = tip(e); - p.find('button[type=button]').click(); - setTimeout(function() { - e.remove(); - start(); - }, timeout); + + test_reason = 'cancel' + p.find('button[type=button]').click(); //cancel + ok(!p.is(':visible'), 'popover closed'); + + test_reason = 'onblur' + e.click(); + p = tip(e); + ok(p.is(':visible'), 'popover shown'); + e.parent().click(); + ok(!p.is(':visible'), 'popover closed'); + + test_reason = 'manual' + e.click(); + p = tip(e); + ok(p.is(':visible'), 'popover shown'); + e.editable('hide'); + ok(!p.is(':visible'), 'popover closed'); + + e.remove(); + start(); }, timeout); }); - asyncTest("event: save / hidden", function () { + asyncTest("event: save / hidden (reason: save)", function () { expect(2); var val = '1', e = $('<a href="#" data-pk="1" data-type="select" data-url="post.php" data-name="text" data-value="'+val+'"></a>').appendTo(fx); e.on('save', function(event, params) { - var editable = $(this).data('editable'); equal(params.newValue, 2, 'save triggered, value correct'); }); - e.on('hidden', function(event) { - var editable = $(this).data('editable'); - ok(true, 'hidden triggered'); + e.on('hidden', function(event, reason) { + equal(reason, 'save', 'hidden triggered, reason ok'); }); e.editable({ - source: 'groups.php', + source: groups, }); e.click(); var p = tip(e); p.find('select').val(2); p.find('form').submit(); - + setTimeout(function() { - p.find('button[type=button]').click(); e.remove(); start(); }, timeout); @@ -305,7 +213,6 @@ $(function () { }); asyncTest("'submit' method: client and server validation", function () { - expect(6); var ev1 = 'ev1', ev2 = 'ev2', e1v = 'e1v', @@ -322,6 +229,7 @@ $(function () { equal(settings.data.text, ev2, 'first value ok'); equal(settings.data.text1, e1v, 'second value ok'); equal(settings.data.a, 123, 'custom data ok'); + equal(settings.type, 'PUT', 'ajaxOptions ok'); this.responseText = {errors: { text1: 'server-invalid' } @@ -348,11 +256,13 @@ $(function () { data: {a: 123}, error: function(data) { equal(data.errors.text1, 'server-invalid', 'server validation error ok'); - e.remove(); e1.remove(); start(); - } + }, + ajaxOptions: { + type: 'PUT' + } }); }); @@ -431,4 +341,4 @@ $(function () { equal(e.text(), groups[2], 'new text shown correctly'); }); -}); \ No newline at end of file +}); diff --git a/test/unit/checklist.js b/test/unit/checklist.js index 294d2a5..f94604a 100644 --- a/test/unit/checklist.js +++ b/test/unit/checklist.js @@ -4,21 +4,28 @@ $(function () { setup: function(){ sfx = $('#qunit-fixture'), fx = $('#async-fixture'); - $.support.transition = false; } }); asyncTest("should load options, set correct value and save new value", function () { - var sep = '-', + var sep = '<br>', newValue, - e = $('<a href="#" data-type="checklist" data-url="post.php"></a>').appendTo(fx).editable({ + e = $('<a href="#" data-type="checklist" data-url="post-checklist.php"></a>').appendTo(fx).editable({ pk: 1, source: groupsArr, - value: [2, 3], - viewseparator: sep + value: [2, 3] }); - equal(e.text(), groups[2]+sep+groups[3], 'autotext ok'); + equal(e.html(), groups[2]+sep+groups[3], 'autotext ok'); + + $.mockjax({ + url: 'post-checklist.php', + response: function(settings) { + ok($.isArray(settings.data.value), 'value submitted as array'); + equal(settings.data.value.sort().join(''), [newValue, 3].join(''), 'submitted array correct'); + } + }); + e.click(); var p = tip(e); @@ -28,8 +35,8 @@ $(function () { equal(p.find('input[type="checkbox"]:checked').eq(1).val(), 3, '2nd checked'); //set new value - p.find('input[type="checkbox"]:checked').eq(0).click(); - p.find('input[type="checkbox"]').first().click(); + p.find('input[type="checkbox"]:checked').eq(0).click(); //uncheck 2 + p.find('input[type="checkbox"]').first().click(); //check first newValue = p.find('input[type="checkbox"]').first().val(); //submit @@ -39,7 +46,7 @@ $(function () { ok(!p.is(':visible'), 'popup closed'); equal(e.data('editable').value.join(''), [newValue, 3].join(''), 'new value ok') - equal(e.text(), groups[newValue]+sep+groups[3], 'new text ok'); + equal(e.html(), groups[newValue]+'<br>'+groups[3], 'new text ok'); // open container again to see what checked e.click() @@ -54,40 +61,5 @@ $(function () { start(); }, timeout); }); - - asyncTest("limit option", function () { - var e = $('<a href="#" data-type="checklist" data-value="2,3" data-url="post.php"></a>').appendTo(fx).editable({ - pk: 1, - source: groupsArr, - limit: 1, - limitText: '{checked} of {count}' - }); - - equal(e.text(), '2 of '+groupsArr.length, 'autotext ok'); - - e.click(); - var p = tip(e); - - equal(p.find('input[type="checkbox"]:checked').length, 2, 'checked count ok'); - equal(p.find('input[type="checkbox"]:checked').eq(0).val(), 2, '1st checked'); - equal(p.find('input[type="checkbox"]:checked').eq(1).val(), 3, '2nd checked'); - - //set new value - p.find('input[type="checkbox"]').first().click(); - newValue = p.find('input[type="checkbox"]').first().val(); - - //submit - p.find('form').submit(); - - setTimeout(function() { - ok(!p.is(':visible'), 'popup closed'); - - equal(e.text(), '3 of '+groupsArr.length, 'autotext ok'); - - e.remove(); - start(); - }, timeout); - }); - }); \ No newline at end of file diff --git a/test/unit/common.js b/test/unit/common.js index a5e6eb6..d0a5bee 100644 --- a/test/unit/common.js +++ b/test/unit/common.js @@ -62,9 +62,9 @@ // equal(e3.data('editable').lastSavedValue, v, 'lastSavedValue taken from text correctly (escaped)'); }); - test("should take container's title from json options", function () { + test("container's title and placement from json options", function () { //do not test inline - if(fc.c === 'inline') { + if($.fn.editableContainer.Constructor.prototype.containerName === 'editableform') { expect(0); return; } @@ -81,7 +81,7 @@ ok(p.is(':visible'), 'popover shown'); //todo: for jqueryui phantomjs calcs wrong position. Need investigation - if(!$.browser.webkit && fc.f !== 'jqueryui') { + if(!$.browser.webkit && $.fn.editableContainer.Constructor.prototype.containerName !== 'tooltip') { ok(p.offset().top > e.offset().top, 'placement ok'); } @@ -362,36 +362,88 @@ ok(!p.find('.editable-buttons').length, '.editable-buttons block not rendered'); }); - //unfortunatly, testing this feature does not always work in browsers. Tested manually. - /* - test("enablefocus option", function () { - // focusing not passed in phantomjs - if($.browser.webkit) { - ok(true, 'skipped in PhantomJS'); - return; - } - - var e = $('<a href="#">abc</a>').appendTo('#qunit-fixture').editable({ - enablefocus: true - }), - e1 = $('<a href="#">abcd</a>').appendTo('#qunit-fixture').editable({ - enablefocus: false - }); - - e.click() - var p = tip(e); - ok(p.is(':visible'), 'popover 1 visible'); - p.find('button[type=button]').click(); - ok(!p.is(':visible'), 'popover closed'); - ok(e.is(':focus'), 'element 1 is focused'); - - e1.click() - p = tip(e1); - ok(p.is(':visible'), 'popover 2 visible'); - p.find('button[type=button]').click(); - ok(!p.is(':visible'), 'popover closed'); - ok(!e1.is(':focus'), 'element 2 is not focused'); - }); - */ + asyncTest("composite pk defined as json in data-pk attribute", function () { + var e = $('<a href="#" data-pk="{a: 1, b: 2}" data-url="post-pk.php">abc</a>').appendTo(fx).editable({ + name: 'username' + }), + newText = 'cd<e>;"' + + $.mockjax({ + url: 'post-pk.php', + response: function(settings) { + equal(settings.data.pk.a, 1, 'first part ok'); + equal(settings.data.pk.b, 2, 'second part 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("savenochange: false", function () { + var v = 'abc', + e = $('<a href="#" data-type="text" data-pk="1" data-url="post-no.php" data-name="text1">'+v+'</a>').appendTo(fx).editable({ + savenochange: false + }), + req = 0; + + $.mockjax({ + url: 'post-no.php', + response: function() { + req++; + } + }); + + e.click(); + var p = tip(e); + ok(p.is(':visible'), 'popover visible'); + p.find('input[type="text"]').val(v); + p.find('form').submit(); + + setTimeout(function() { + ok(!p.is(':visible'), 'popover closed'); + equal(req, 0, 'request was not performed'); + e.remove(); + start(); + }, timeout); + }); + + asyncTest("savenochange: true", function () { + var v = 'abc', + e = $('<a href="#" data-type="text" data-pk="1" data-url="post-yes.php" data-name="text1">'+v+'</a>').appendTo(fx).editable({ + savenochange: true + }), + req = 0; + + $.mockjax({ + url: 'post-yes.php', + response: function() { + req++; + } + }); + + e.click(); + var p = tip(e); + ok(p.is(':visible'), 'popover visible'); + p.find('input[type="text"]').val(v); + p.find('form').submit(); + + setTimeout(function() { + ok(!p.is(':visible'), 'popover closed'); + equal(req, 1, 'request was performed'); + e.remove(); + start(); + }, timeout); + }); }(jQuery)); \ No newline at end of file diff --git a/test/unit/date.js b/test/unit/date.js index 16ca25a..ac25878 100644 --- a/test/unit/date.js +++ b/test/unit/date.js @@ -14,9 +14,7 @@ $(function () { } asyncTest("container should contain datepicker with value and save new entered date", function () { - expect(9); - - $.fn.editableform.types.date.defaults.datepicker.weekStart = 1; + $.fn.editabletypes.date.defaults.datepicker.weekStart = 1; var d = '15.05.1984', e = $('<a href="#" data-type="date" data-pk="1" data-url="post-date.php">'+d+'</a>').appendTo(fx).editable({ @@ -39,8 +37,10 @@ $(function () { e.click(); var p = tip(e); ok(p.find('.datepicker').is(':visible'), 'datepicker exists'); + ok(p.find('.datepicker').find('.datepicker-days').is(':visible'), 'datepicker days visible'); equal(frmt(e.data('editable').value, f), d, 'day set correct'); + ok(p.find('td.day.active').is(':visible'), 'active day is visible'); equal(p.find('td.day.active').text(), 15, 'day shown correct'); equal(p.find('th.dow').eq(0).text(), 'Mo', 'weekStart correct'); @@ -60,7 +60,7 @@ $(function () { asyncTest("viewformat, init by text", function () { - $.fn.editableform.types.date.defaults.datepicker.weekStart = 1; + $.fn.editabletypes.date.defaults.datepicker.weekStart = 1; var dview = '15/05/1984', d = '1984-05-15', diff --git a/test/unit/select.js b/test/unit/select.js index 05d37b3..a0e7e06 100644 --- a/test/unit/select.js +++ b/test/unit/select.js @@ -345,7 +345,7 @@ $(function () { var e = $('<a href="#" data-type="select" data-pk="1" data-name="name1" data-value="1" data-autotext="always" data-url="post.php" data-source="groups-cache-sim-err.php">35</a>').appendTo(fx).editable(), e1 = $('<a href="#" data-type="select" data-pk="1" data-name="name1" data-value="2" data-autotext="always" data-url="post.php" data-source="groups-cache-sim-err.php">35</a>').appendTo(fx).editable(), e2 = $('<a href="#" data-type="select" data-pk="1" data-name="name1" data-value="3" data-autotext="always" data-url="post.php" data-source="groups-cache-sim-err.php">6456</a>').appendTo(fx).editable(), - errText = $.fn.editableform.types.select.defaults.sourceError; + errText = $.fn.editabletypes.select.defaults.sourceError; setTimeout(function() { @@ -363,6 +363,55 @@ $(function () { }); + asyncTest("sourceCache: false", function () { + var e = $('<a href="#" data-type="select" data-pk="1" data-name="name1" data-value="2" data-url="post.php" data-source="groups-cache-false.php">customer</a>').appendTo(fx).editable({ + sourceCache: false + }), + e1 = $('<a href="#" data-type="select" data-pk="1" id="name1" data-value="2" data-url="post.php" data-source="groups-cache-false.php">customer</a>').appendTo(fx).editable({ + sourceCache: false + }), + req = 0; + + $.mockjax({ + url: 'groups-cache-false.php', + response: function() { + req++; + this.responseText = groups; + } + }); + + //click first + e.click(); + var p = tip(e); + + setTimeout(function() { + ok(p.is(':visible'), 'popover visible'); + equal(p.find('select').find('option').length, size, 'options loaded'); + equal(req, 1, 'one request performed'); + + p.find('button[type=button]').click(); + ok(!p.is(':visible'), 'popover was removed'); + + //click second + e1.click(); + p = tip(e1); + + setTimeout(function() { + ok(p.is(':visible'), 'popover2 visible'); + equal(p.find('select').find('option').length, size, 'options loaded'); + equal(req, 2, 'second request performed'); + + p.find('button[type=button]').click(); + ok(!p.is(':visible'), 'popover was removed'); + + e.remove(); + e1.remove(); + start(); + }, timeout); + }, timeout); + + }); + asyncTest("autotext: auto", function () { expect(3); @@ -474,6 +523,60 @@ $(function () { e.remove(); start(); }, timeout); - }); + }); -}); \ No newline at end of file + asyncTest("'display' callback", function () { + var e = $('<a href="#" data-type="select" data-value="2" data-url="post.php"></a>').appendTo(fx).editable({ + pk: 1, + source: groups, + display: function(value, sourceData) { + var els = $.grep(sourceData, function(o) {return o.value == value;}); + $(this).text('qq' + els[0].text); + } + }), + selected = 3; + + equal(e.text(), 'qq'+groups[2], 'autotext display ok'); + + e.click(); + var p = tip(e); + + p.find('select').val(selected); + p.find('form').submit(); + + setTimeout(function() { + ok(!p.is(':visible'), 'popover closed'); + equal(e.data('editable').value, selected, 'new value saved') + equal(e.text(), 'qq'+groups[selected], 'text shown correctly') + e.remove(); + start(); + }, timeout); + }); + + asyncTest("submit by enter", function () { + var e = $('<a href="#" data-type="select" data-value="2" data-url="post.php"></a>').appendTo(fx).editable({ + pk: 1, + source: groups + }), + selected = 3; + + e.click(); + var p = tip(e); + p.find('select').val(selected); + + var event = jQuery.Event("keydown"); + event.which = 13; + + p.find('select').trigger(event); + + 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); + }) + + +}); diff --git a/test/unit/text.js b/test/unit/text.js index f7fc3e9..fa236ec 100644 --- a/test/unit/text.js +++ b/test/unit/text.js @@ -43,16 +43,16 @@ $(function () { asyncTest("should load correct value and save new entered text (and value)", function () { var v = 'ab<b>"', esc_v = $('<div>').text(v).html(), - e = $('<a href="#" data-pk="1" data-name="text1" data-url="post-text.php" data-params="{\'q\': \'w\'}">'+esc_v+'</a>').appendTo(fx).editable({ - success: function(data) { - return false; + e = $('<a href="#" data-pk="1" data-name="text1" data-url="post-text-main.php" data-params="{\'q\': \'w\'}">'+esc_v+'</a>').appendTo(fx).editable({ + success: function(response, newValue) { + equal(newValue, newText, 'new value in success correct'); } }), data, - newText = 'cd<e>;"'; + newText = 'cd>e>;"'; $.mockjax({ - url: 'post-text.php', + url: 'post-text-main.php', response: function(settings) { data = settings.data; } @@ -61,13 +61,13 @@ $(function () { e.click() var p = tip(e); - ok(p.is(':visible'), 'popover visible') - ok(p.find('.editableform-loading').length, 'loading class exists') - ok(!p.find('.editableform-loading').is(':visible'), 'loading class is hidden') - ok(p.find('input[type=text]').length, 'input exists') - equal(p.find('input[type=text]').val(), v, 'input contain correct value') + ok(p.is(':visible'), 'popover visible'); + ok(p.find('.editableform-loading').length, 'loading class exists'); + ok(!p.find('.editableform-loading').is(':visible'), 'loading class is hidden'); + ok(p.find('input[type=text]').length, 'input exists'); + equal(p.find('input[type=text]').val(), v, 'input contain correct value'); p.find('input').val(newText); - p.find('button[type=submit]').click(); + p.find('form').submit(); ok(p.find('.editableform-loading').is(':visible'), 'loading class is visible'); setTimeout(function() { @@ -88,7 +88,10 @@ $(function () { asyncTest("should show error on server validation", function () { var msg = 'required', e = $('<a href="#" data-name="text1">abc</a>').appendTo(fx).editable({ - validate: function(value) { if(value == '') return msg; } + validate: function(value) { + ok(this === e[0], 'scope is ok'); + if(value == '') return msg; + } }), newText = ''; @@ -149,34 +152,11 @@ $(function () { }); */ - asyncTest("should not perform request if value not changed", function () { - var e = $('<a href="#" data-pk="1" data-url="post-no.php" data-name="text1">abc</a>').appendTo(fx).editable(), - req = 0; - - $.mockjax({ - url: 'post-no.php', - response: function() { - req++; - } - }); - - e.click(); - var p = tip(e); - ok(p.is(':visible'), 'popover visible'); - p.find('button[type=submit]').click(); - - setTimeout(function() { - ok(!p.is(':visible'), 'popover closed'); - equal(req, 0, 'request was not performed'); - e.remove(); - start(); - }, timeout); - }); - asyncTest("should show error if success callback returns string", 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) { + ok(this === e[0], 'scope is ok'); equal(newValue, newText, 'value in success passed correctly'); return 'error'; } @@ -266,6 +246,7 @@ $(function () { var e = $('<a href="#" data-pk="1" data-url="post-resp.php">abc</a>').appendTo(fx).editable({ name: 'username', params: function(params) { + ok(this === e[0], 'scope is ok'); equal(params.pk, 1, 'params in func already have values (pk)'); return { q: 2, pk: 3 }; }, @@ -328,11 +309,12 @@ $(function () { asyncTest("submit to url defined as function", function () { - expect(3); + expect(4); var newText = 'qwe', //should be called even without pk! e = $('<a href="#" data-pk1="1" id="a"></a>').appendTo(fx).editable({ url: function(params) { + ok(this === e[0], 'scope is ok'); ok(params.value, newText, 'new text passed in users function'); var d = new $.Deferred; return d.reject('my error'); @@ -472,6 +454,55 @@ $(function () { delete $.fn.editable.defaults.name; var e = $('<a href="#" id="cde">abc</a>').appendTo('#qunit-fixture').editable(); equal(e.data('editable').options.name, 'cde', 'name is taken from id'); - }); + }); + + asyncTest("'display' callback", function () { + var newText = 'cd<e>;"', + e = $('<a href="#" data-pk="1" data-url="post.php" data-name="text1">abc</a>').appendTo(fx).editable({ + display: function(value) { + ok(this === e[0], 'scope is ok'); + $(this).text('qq'+value); + } + }); + + 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 was removed'); + equal(e.text(), 'qq'+newText, 'custom display ok'); + e.remove(); + start(); + }, timeout); + + }); + + asyncTest("display: false", function () { + var newText = 'cd<e>;"', + e = $('<a href="#" data-pk="1" data-url="post.php" data-name="text1" data-value="abc"></a>').appendTo(fx).editable({ + display: false + }); + + ok(!e.text().length, 'element still empty, autotext did not display value'); + + e.click() + var p = tip(e); + + p.find('input').val(newText); + p.find('form').submit(); + + setTimeout(function() { + ok(!p.is(':visible'), 'popover was removed'); + ok(!e.text().length, 'element still empty, new value was not displayed'); + equal(e.data('editable').value, newText, 'new text saved to value'); + e.remove(); + start(); + }, timeout); + + }); }); \ No newline at end of file diff --git a/test/unit/textarea.js b/test/unit/textarea.js index 6a76813..5784189 100644 --- a/test/unit/textarea.js +++ b/test/unit/textarea.js @@ -34,8 +34,8 @@ $(function () { var e = $('<a href="#" data-pk="1" data-url="post.php">'+v1+'</a>').appendTo(fx).editable({ type: 'textarea', send: 'ifpk', - success: function(data) { - return false; + success: function(response, newvalue) { + equal(newvalue, v2, 'value in success ok'); } }); @@ -93,7 +93,7 @@ $(function () { var p = tip(e); p.find('textarea').val(vnew); - event = jQuery.Event("keydown"); + var event = jQuery.Event("keydown"); event.ctrlKey = true; event.which = 13;