diff --git a/CHANGELOG.txt b/CHANGELOG.txt index c1a6fcb..3dc6bcc 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -3,10 +3,13 @@ X-editable changelog Version 1.2.0 wip ----------------------------- +---------------------------- +[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 'cancelnochange' to cancel or submit value when it was not changed in form (vitalets) +[enh] new option: 'cancelnochange' to cancel or submit 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) diff --git a/src/containers/editable-container.js b/src/containers/editable-container.js index d20aed9..55fab88 100644 --- a/src/containers/editable-container.js +++ b/src/containers/editable-container.js @@ -75,6 +75,7 @@ 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({ @@ -263,7 +264,7 @@ Applied as jQuery method. closeOthers: function(element) { $('.editable-open').each(function(i, el){ //do nothing with passed element - if(el === element) { + if(el === element || $(el).find(element).length) { return; } diff --git a/src/editable-form/editable-form-utils.js b/src/editable-form/editable-form-utils.js index 29981fe..aa22f64 100644 --- a/src/editable-form/editable-form-utils.js +++ b/src/editable-form/editable-form-utils.js @@ -117,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.js b/src/editable-form/editable-form.js index da8e452..f7f7dcc 100644 --- a/src/editable-form/editable-form.js +++ b/src/editable-form/editable-form.js @@ -168,8 +168,7 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc. 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)) { @@ -178,19 +177,16 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc. return; } - //value as string - newValueStr = this.input.value2str(newValue); - //if value not changed --> cancel /*jslint eqeq: true*/ - if (this.options.cancelnochange && newValueStr == this.input.value2str(this.value)) { + if (this.options.cancelnochange && 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.options.scope, response, newValue) : null; @@ -238,13 +234,16 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc. }, this)); }, - save: function(value) { + 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(); @@ -252,7 +251,7 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc. //standard params params = { name: this.options.name || '', - value: value, + value: submitValue, pk: pk }; @@ -267,15 +266,14 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc. if(typeof this.options.url === 'function') { //user's function return this.options.url.call(this.options.scope, params); - } else { //send ajax to server and return deferred object - ajaxOptions = $.extend({ + } 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)); } } }, @@ -455,6 +453,7 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc. @property ajaxOptions @type object @default null + @since 1.1.1 **/ ajaxOptions: null, /** diff --git a/src/element/editable-element.js b/src/element/editable-element.js index 2df7d75..3beb7d2 100644 --- a/src/element/editable-element.js +++ b/src/element/editable-element.js @@ -47,13 +47,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) @@ -64,6 +71,13 @@ Makes editable any HTML element on the page. Applied as jQuery method. //stop propagation not required anymore because in document click handler it checks event target //e.stopPropagation(); + //mark event with special flag: it will not be processed in document click handler + /* + if(e.type === 'click' && e.target !== e.currentTarget) { + $(e.target).data('editable-element', e.currentTarget); + } + */ + if(this.options.toggle === 'mouseenter') { //for hover only show container this.show(); @@ -81,7 +95,7 @@ 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 { @@ -104,6 +118,25 @@ Makes editable any HTML element on the page. Applied as jQuery method. this.isInit = false; }, this)); }, + + /* + Renders value into element's text. + Can call custom display method from options. + Can return deferred object. + @method render() + */ + render: function() { + //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 @@ -222,8 +255,7 @@ Makes editable any HTML element on the page. Applied as jQuery method. if(!this.container) { var containerOptions = $.extend({}, this.options, { value: this.value, - autohide: false, //element will take care to show/hide container. Otherwise hide() will be called twice - scope: this.$element[0] //set scope to element + autohide: false //element will take care to show/hide container. Otherwise hide() will be called twice }); this.$element.editableContainer(containerOptions); this.$element.on({ @@ -326,7 +358,7 @@ 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); @@ -401,7 +433,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; @@ -544,7 +576,23 @@ Makes editable any HTML element on the page. Applied as jQuery method. @type mixed @default element's text **/ - value: null + value: null, + /** + Callback to perform custom displaying of value in element's text. + If not defined, default input's value2html() will be called. + Runs under element's scope. + Second parameter __sourceData__ is passed for inputs with source (select, checklist). + + @property display + @type function + @default null + @since 1.2.0 + @example + display: function(value, sourceData) { + $(this).html('<b>'+value+'</b>'); + } + **/ + display: null }; }(window.jQuery)); \ No newline at end of file diff --git a/src/inputs/abstract.js b/src/inputs/abstract.js index 02eb07e..40fcb0c 100644 --- a/src/inputs/abstract.js +++ b/src/inputs/abstract.js @@ -49,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); }, /** @@ -65,7 +64,7 @@ To create your own input you should inherit from this class. }, /** - Converts value to string (for submitting to server) + Converts value to string (for comparering) @method value2str(value) @param {mixed} value @@ -86,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. @@ -121,7 +131,7 @@ To create your own input you should inherit from this class. @method clear() **/ - clear: function() { + clear: function() { this.$input.val(null); }, diff --git a/src/inputs/checklist.js b/src/inputs/checklist.js index d36d603..243b008 100644 --- a/src/inputs/checklist.js +++ b/src/inputs/checklist.js @@ -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,16 @@ $(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 = [], + checked = $.grep(this.sourceData, function(o){ + return $.grep(value, function(v){return v == o.value;}).length; + }); + 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() { @@ -138,39 +133,13 @@ $(function(){ 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.editabletypes.checklist = Checklist; diff --git a/src/inputs/date/date.js b/src/inputs/date/date.js index 3cc974a..1de781f 100644 --- a/src/inputs/date/date.js +++ b/src/inputs/date/date.js @@ -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); diff --git a/src/inputs/dateui/dateui.js b/src/inputs/dateui/dateui.js index 356cc41..baa8afc 100644 --- a/src/inputs/dateui/dateui.js +++ b/src/inputs/dateui/dateui.js @@ -97,7 +97,11 @@ $(function(){ } catch(e) {} return d; - }, + }, + + value2submit: function(value) { + return this.value2str(value); + }, value2input: function(value) { this.$input.datepicker('setDate', value); diff --git a/src/inputs/list.js b/src/inputs/list.js index 2fe54fb..9570cb8 100644 --- a/src/inputs/list.js +++ b/src/inputs/list.js @@ -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); 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/select.js b/test/unit/select.js index ea90852..1da2835 100644 --- a/test/unit/select.js +++ b/test/unit/select.js @@ -523,6 +523,34 @@ $(function () { e.remove(); start(); }, timeout); - }); + }); + + 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); + }); }); \ No newline at end of file diff --git a/test/unit/text.js b/test/unit/text.js index e9ad7d0..ad500ff 100644 --- a/test/unit/text.js +++ b/test/unit/text.js @@ -454,6 +454,31 @@ $(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); + + }); }); \ No newline at end of file diff --git a/test/unit/textarea.js b/test/unit/textarea.js index 6a76813..a24b225 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'); } });