diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 183821c..4f9680d 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -2,6 +2,17 @@ X-editable changelog ============================= +Version 1.4.1 Jan 18, 2013 +---------------------------- +[enh #62] new option `selector` to work with delegated targets (vitalets) +[enh] new option `unsavedclass` to set css class when value was not sent to server (vitalets) +[enh] new option `emptyclass` to set css class when element is empty (vitalets) +[enh #59] select2 input (vitalets) +[enh #17] typeahead input (vitalets) +[enh] select: support of OPTGROUP via `children` key in source (vitalets) +[enh] checklist: set checked via prop instead of attr (vitalets) + + Version 1.4.0 Jan 11, 2013 ---------------------------- [enh] added new input type: combodate (vitalets) diff --git a/grunt.js b/grunt.js index 96c8095..373e532 100644 --- a/grunt.js +++ b/grunt.js @@ -14,7 +14,9 @@ function getFiles() { inputs: [ inputs+'date/date.js', inputs+'date/datefield.js', - inputs+'date/bootstrap-datepicker/js/bootstrap-datepicker.js'], + inputs+'date/bootstrap-datepicker/js/bootstrap-datepicker.js', + inputs+'typeahead.js' + ], css: [inputs+'date/bootstrap-datepicker/css/datepicker.css'] }, jqueryui: { @@ -52,6 +54,7 @@ function getFiles() { inputs+'select.js', inputs+'checklist.js', inputs+'html5types.js', + inputs+'select2/select2.js', inputs+'combodate/lib/combodate.js', inputs+'combodate/combodate.js' ]; @@ -162,6 +165,7 @@ module.exports = function(grunt) { 'src/inputs/date/*.js', 'src/inputs/dateui/*.js', 'src/inputs/combodate/*.js', + 'src/inputs/select2/*.js', 'src/inputs-ext/address/*.js', 'src/inputs-ext/wysihtml5/*.js' diff --git a/package.json b/package.json index 29f8ddb..1bae2fe 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.4.0", + "version": "1.4.1", "homepage": "http://github.com/vitalets/x-editable", "author": { "name": "Vitaliy Potapov", diff --git a/src/containers/editable-container.js b/src/containers/editable-container.js index 322d2d7..83cd56f 100644 --- a/src/containers/editable-container.js +++ b/src/containers/editable-container.js @@ -23,8 +23,8 @@ Applied as jQuery method. innerCss: null, //tbd in child class init: function(element, options) { this.$element = $(element); - //todo: what is in priority: data or js? - this.options = $.extend({}, $.fn.editableContainer.defaults, $.fn.editableutils.getConfigData(this.$element), options); + //since 1.4.1 container do not use data-* directly as they already merged into options. + this.options = $.extend({}, $.fn.editableContainer.defaults, options); this.splitOptions(); //set scope of form callbacks to element @@ -116,6 +116,7 @@ Applied as jQuery method. cancel: $.proxy(function(){ this.hide('cancel'); }, this), //click on calcel button 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 + resize: $.proxy(this.setPosition, this), //this allows to re-position container when form size is changed rendered: $.proxy(function(){ /** Fired when container is shown and form is rendered (for select will wait for loading dropdown options) @@ -242,7 +243,6 @@ Applied as jQuery method. }, save: function(e, params) { - this.hide('save'); /** Fired when new value was submitted. You can use $(this).data('editableContainer') inside handler to access to editableContainer instance @@ -263,6 +263,9 @@ Applied as jQuery method. }); **/ this.$element.triggerHandler('save', params); + + //hide must be after trigger, as saving value may require methods od plugin, applied to input + this.hide('save'); }, /** diff --git a/src/containers/editable-inline.js b/src/containers/editable-inline.js index cd1268e..3a75bbe 100644 --- a/src/containers/editable-inline.js +++ b/src/containers/editable-inline.js @@ -38,12 +38,14 @@ innerHide: function () { this.$tip.hide(this.options.anim, $.proxy(function() { this.$element.show(); - this.tip().empty().remove(); + this.innerDestroy(); }, this)); }, innerDestroy: function() { - this.tip().remove(); + if(this.tip()) { + this.tip().empty().remove(); + } } }); diff --git a/src/editable-form/editable-form-utils.js b/src/editable-form/editable-form-utils.js index af4689a..2fc0044 100644 --- a/src/editable-form/editable-form-utils.js +++ b/src/editable-form/editable-form-utils.js @@ -132,21 +132,34 @@ /* returns array items from sourceData having value property equal or inArray of 'value' */ - itemsByValue: function(value, sourceData) { + itemsByValue: function(value, sourceData, valueProp) { if(!sourceData || value === null) { return []; } - //convert to array - if(!$.isArray(value)) { - value = [value]; - } + valueProp = valueProp || 'value'; - /*jslint eqeq: true*/ - var result = $.grep(sourceData, function(o){ - return $.grep(value, function(v){ return v == o.value; }).length; + var isValArray = $.isArray(value), + result = [], + that = this; + + $.each(sourceData, function(i, o) { + if(o.children) { + result = result.concat(that.itemsByValue(value, o.children)); + } else { + /*jslint eqeq: true*/ + if(isValArray) { + if($.grep(value, function(v){ return v == (o && typeof o === 'object' ? o[valueProp] : o); }).length) { + result.push(o); + } + } else { + if(value == (o && typeof o === 'object' ? o[valueProp] : o)) { + result.push(o); + } + } + /*jslint eqeq: false*/ + } }); - /*jslint eqeq: false*/ return result; }, diff --git a/src/editable-form/editable-form.css b/src/editable-form/editable-form.css index be98f1f..83b43aa 100644 --- a/src/editable-form/editable-form.css +++ b/src/editable-form/editable-form.css @@ -110,18 +110,4 @@ .editable-clear-x:hover { opacity: 1; -} - -/* -.editable-clear-x1 { - background: url('../img/clear.png') center center no-repeat; - display: inline-block; - zoom: 1; - *display: inline; - width: 13px; - height: 13px; - vertical-align: middle; - position: relative; - margin-left: -20px; -} -*/ +} \ No newline at end of file diff --git a/src/editable-form/editable-form.js b/src/editable-form/editable-form.js index 3bff76d..f080a94 100644 --- a/src/editable-form/editable-form.js +++ b/src/editable-form/editable-form.js @@ -11,7 +11,7 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc. var EditableForm = function (div, options) { this.options = $.extend({}, $.fn.editableform.defaults, options); - this.$div = $(div); //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; } @@ -25,6 +25,7 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc. this.input = this.options.input; //set initial value + //todo: may be add check: typeof str === 'string' ? this.value = this.input.str2value(this.options.value); }, initTemplate: function() { @@ -227,6 +228,7 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc. } //if success callback returns object like {newValue: } --> use that value instead of submitted + //it is usefull if you want to chnage value in url-function if(res && typeof res === 'object' && res.hasOwnProperty('newValue')) { newValue = res.newValue; } @@ -382,18 +384,25 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc. type: 'text', /** Url for submit, e.g. '/post' - If function - it will be called instead of ajax. Function can return deferred object to run fail/done callbacks. + If function - it will be called instead of ajax. Function should return deferred object to run fail/done callbacks. @property url @type string|function @default null @example url: function(params) { + var d = new $.Deferred; if(params.value === 'abc') { - var d = new $.Deferred; return d.reject('error message'); //returning error via deferred object } else { - someModel.set(params.name, params.value); //save data in some js model + //async saving data in js model + someModel.asyncSaveMethod({ + ..., + success: function(){ + d.resolve(); + } + }); + return d.promise(); } } **/ diff --git a/src/element/editable-element.js b/src/element/editable-element.js index 5b37a47..0849836 100644 --- a/src/element/editable-element.js +++ b/src/element/editable-element.js @@ -8,8 +8,13 @@ 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.editableutils.getConfigData(this.$element), options); - this.init(); + //data-* has more priority over js options: because dynamically created elements may change data-* + this.options = $.extend({}, $.fn.editable.defaults, options, $.fn.editableutils.getConfigData(this.$element)); + if(this.options.selector) { + this.initLive(); + } else { + this.init(); + } }; Editable.prototype = { @@ -52,8 +57,10 @@ Makes editable any HTML element on the page. Applied as jQuery method. if(this.options.toggle !== 'manual') { this.$element.addClass('editable-click'); this.$element.on(this.options.toggle + '.editable', $.proxy(function(e){ + //prevent following link e.preventDefault(); - //stop propagation not required anymore because in document click handler it checks event target + + //stop propagation not required because in document click handler it checks event target //e.stopPropagation(); if(this.options.toggle === 'mouseenter') { @@ -95,6 +102,24 @@ Makes editable any HTML element on the page. Applied as jQuery method. }, this)); }, + /* + Initializes parent element for live editables + */ + initLive: function() { + //store selector + var selector = this.options.selector; + //modify options for child elements + this.options.selector = false; + this.options.autotext = 'never'; + //listen toggle events + this.$element.on(this.options.toggle + '.editable', selector, $.proxy(function(e){ + var $target = $(e.target); + if(!$target.data('editable')) { + $target.editable(this.options).trigger(e); + } + }, this)); + }, + /* Renders value into element's text. Can call custom display method from options. @@ -108,8 +133,8 @@ Makes editable any HTML element on the page. Applied as jQuery method. 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')) { + //if input has `value2htmlFinal` method, we pass callback in third param to be called when source is loaded + if(this.input.value2htmlFinal) { return this.input.value2html(this.value, this.$element[0], this.options.display, response); //if display method defined --> use it } else if(typeof this.options.display === 'function') { @@ -127,7 +152,7 @@ Makes editable any HTML element on the page. Applied as jQuery method. enable: function() { this.options.disabled = false; this.$element.removeClass('editable-disabled'); - this.handleEmpty(); + this.handleEmpty(this.isEmpty); if(this.options.toggle !== 'manual') { if(this.$element.attr('tabindex') === '-1') { this.$element.removeAttr('tabindex'); @@ -143,7 +168,7 @@ Makes editable any HTML element on the page. Applied as jQuery method. this.options.disabled = true; this.hide(); this.$element.addClass('editable-disabled'); - this.handleEmpty(); + this.handleEmpty(this.isEmpty); //do not stop focus on this element this.$element.attr('tabindex', -1); }, @@ -204,27 +229,33 @@ Makes editable any HTML element on the page. Applied as jQuery method. }, /* - * set emptytext if element is empty (reverse: remove emptytext if needed) + * set emptytext if element is empty */ - handleEmpty: function () { + handleEmpty: function (isEmpty) { //do not handle empty if we do not display anything if(this.options.display === false) { return; } - var emptyClass = 'editable-empty'; + this.isEmpty = isEmpty !== undefined ? isEmpty : $.trim(this.$element.text()) === ''; + //emptytext shown only for enabled if(!this.options.disabled) { - if ($.trim(this.$element.text()) === '') { - this.$element.addClass(emptyClass).text(this.options.emptytext); - } else { - this.$element.removeClass(emptyClass); + if (this.isEmpty) { + this.$element.text(this.options.emptytext); + if(this.options.emptyclass) { + this.$element.addClass(this.options.emptyclass); + } + } else if(this.options.emptyclass) { + this.$element.removeClass(this.options.emptyclass); } } else { //below required if element disable property was changed - if(this.$element.hasClass(emptyClass)) { + if(this.isEmpty) { this.$element.empty(); - this.$element.removeClass(emptyClass); + if(this.options.emptyclass) { + this.$element.removeClass(this.options.emptyclass); + } } } }, @@ -246,6 +277,7 @@ Makes editable any HTML element on the page. Applied as jQuery method. input: this.input //pass input to form (as it is already created) }); this.$element.editableContainer(containerOptions); + //listen `save` event this.$element.on("save.internal", $.proxy(this.save, this)); this.container = this.$element.data('editableContainer'); } else if(this.container.tip().is(':visible')) { @@ -283,13 +315,29 @@ Makes editable any HTML element on the page. Applied as jQuery method. * called when form was submitted */ 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' && 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'); + //mark element with unsaved class if needed + if(this.options.unsavedclass) { + /* + Add unsaved css to element if: + - url is not user's function + - value was not sent to server + - params.response === undefined, that means data was not sent + - value changed + */ + var sent = false; + sent = sent || typeof this.options.url === 'function'; + sent = sent || this.options.display === false; + sent = sent || params.response !== undefined; + sent = sent || (this.options.savenochange && this.input.value2str(this.value) !== this.input.value2str(params.newValue)); + + if(sent) { + this.$element.removeClass(this.options.unsavedclass); + } else { + this.$element.addClass(this.options.unsavedclass); + } } + //set new value this.setValue(params.newValue, false, params.response); /** @@ -381,7 +429,7 @@ Makes editable any HTML element on the page. Applied as jQuery method. url: '/post', pk: 1 }); - **/ + **/ $.fn.editable = function (option) { //special API methods returning non-jquery object var result = {}, args = arguments, datakey = 'editable'; @@ -398,7 +446,7 @@ Makes editable any HTML element on the page. Applied as jQuery method. username: "username is required", fullname: "fullname should be minimum 3 letters length" } - **/ + **/ case 'validate': this.each(function () { var $this = $(this), data = $this.data(datakey), error; @@ -419,7 +467,7 @@ Makes editable any HTML element on the page. Applied as jQuery method. username: "superuser", fullname: "John" } - **/ + **/ case 'getValue': this.each(function () { var $this = $(this), data = $this.data(datakey); @@ -438,11 +486,11 @@ 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 {object} options.ajaxOptions additional ajax options @param {function} options.error(obj) error handler @param {function} options.success(obj,config) success handler @returns {Object} jQuery object - **/ + **/ case 'submit': //collects value, validate and submit to server for creating new record var config = arguments[1] || {}, $elems = this, @@ -593,7 +641,51 @@ Makes editable any HTML element on the page. Applied as jQuery method. } } **/ - display: null + display: null, + /** + Css class applied when editable text is empty. + + @property emptyclass + @type string + @since 1.4.1 + @default editable-empty + **/ + emptyclass: 'editable-empty', + /** + Css class applied when value was stored but not sent to server (`pk` is empty or `send = 'never'`). + You may set it to `null` if you work with editables locally and submit them together. + + @property unsavedclass + @type string + @since 1.4.1 + @default editable-unsaved + **/ + unsavedclass: 'editable-unsaved', + /** + If a css selector is provided, editable will be delegated to the specified targets. + Usefull for dynamically generated DOM elements. + **Please note**, that delegated targets can't use `emptytext` and `autotext` options, + as they are initialized after first click. + + @property selector + @type string + @since 1.4.1 + @default null + @example +
+ awesome + Operator +
+ + + **/ + selector: null }; }(window.jQuery)); diff --git a/src/inputs/abstract.js b/src/inputs/abstract.js index e07189f..54245bf 100644 --- a/src/inputs/abstract.js +++ b/src/inputs/abstract.js @@ -186,15 +186,7 @@ To create your own input you can inherit from this class. @type string @default input-medium **/ - inputclass: 'input-medium', - /** - Name attribute of input - - @property name - @type string - @default null - **/ - name: null + inputclass: 'input-medium' }; $.extend($.fn.editabletypes, {abstractinput: AbstractInput}); diff --git a/src/inputs/checklist.js b/src/inputs/checklist.js index 682e9a1..320b02c 100644 --- a/src/inputs/checklist.js +++ b/src/inputs/checklist.js @@ -42,8 +42,7 @@ $(function(){ for(var i=0; i').append($('', { type: 'checkbox', - value: this.sourceData[i].value, - name: this.options.name + value: this.sourceData[i].value })) .append($('').text(' '+this.sourceData[i].text)); @@ -72,7 +71,7 @@ $(function(){ //set checked on required checkboxes value2input: function(value) { - this.$input.removeAttr('checked'); + this.$input.prop('checked', false); if($.isArray(value) && value.length) { this.$input.each(function(i, el) { var $el = $(el); @@ -81,7 +80,7 @@ $(function(){ /*jslint eqeq: true*/ if($el.val() == val) { /*jslint eqeq: false*/ - $el.attr('checked', 'checked'); + $el.prop('checked', true); } }); }); diff --git a/src/inputs/list.js b/src/inputs/list.js index 7ecc469..e5fcb00 100644 --- a/src/inputs/list.js +++ b/src/inputs/list.js @@ -144,7 +144,7 @@ List - abstract class for inputs that have source option loaded from js array or }, this) }); } else { //options as json/array/function - if (typeof this.options.source === 'function') { + if ($.isFunction(this.options.source)) { this.sourceData = this.makeArray(this.options.source()); } else { this.sourceData = this.makeArray(this.options.source); @@ -200,35 +200,45 @@ List - abstract class for inputs that have source option loaded from js array or * convert data to array suitable for sourceData, e.g. [{value: 1, text: 'abc'}, {...}] */ makeArray: function(data) { - var count, obj, result = [], iterateEl; + var count, obj, result = [], item, iterateItem; if(!data || typeof data === 'string') { return null; } if($.isArray(data)) { //array - iterateEl = function (k, v) { + /* + function to iterate inside item of array if item is object. + Caclulates count of keys in item and store in obj. + */ + iterateItem = function (k, v) { obj = {value: k, text: v}; if(count++ >= 2) { - return false;// exit each if object has more than one value + return false;// exit from `each` if item has more than one key. } }; for(var i = 0; i < data.length; i++) { - if(typeof data[i] === 'object') { - count = 0; - $.each(data[i], iterateEl); - if(count === 1) { + item = data[i]; + if(typeof item === 'object') { + count = 0; //count of keys inside item + $.each(item, iterateItem); + //case: [{val1: 'text1'}, {val2: 'text2} ...] + if(count === 1) { result.push(obj); - } else if(count > 1 && data[i].hasOwnProperty('value') && data[i].hasOwnProperty('text')) { - result.push(data[i]); - } else { - //data contains incorrect objects + //case: [{value: 1, text: 'text1'}, {value: 2, text: 'text2'}, ...] + } else if(count > 1) { + //removed check of existance: item.hasOwnProperty('value') && item.hasOwnProperty('text') + if(item.children) { + item.children = this.makeArray(item.children); + } + result.push(item); } } else { - result.push({value: data[i], text: data[i]}); + //case: ['text1', 'text2' ...] + result.push({value: item, text: item}); } } - } else { //object + } else { //case: {val1: 'text1', val2: 'text2, ...} $.each(data, function (k, v) { result.push({value: k, text: v}); }); @@ -251,12 +261,16 @@ List - abstract class for inputs that have source option loaded from js array or List.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { /** Source data for list. - If **array** - it should be in format: `[{value: 1, text: "text1"}, {...}]` + If **array** - it should be in format: `[{value: 1, text: "text1"}, {value: 2, text: "text2"}, ...]` For compability, object format is also supported: `{"1": "text1", "2": "text2" ...}` but it does not guarantee elements order. If **string** - considered ajax url to load items. In that case results will be cached for fields with the same source and name. See also `sourceCache` option. If **function**, it should return data in format above (since 1.4.0). + + Since 1.4.1 key `children` supported to render OPTGROUP (for **select** input only). + `[{text: "group1", children: [{value: 1, text: "text1"}, {value: 2, text: "text2"}]}, ...]` + @property source @type string | array | object | function @@ -280,8 +294,8 @@ List - abstract class for inputs that have source option loaded from js array or **/ sourceError: 'Error when loading list', /** - if true and source is **string url** - results will be cached for fields with the same source and name. - Usefull for editable grids. + if true and source is **string url** - results will be cached for fields with the same source. + Usefull for editable column in grid to prevent extra requests. @property sourceCache @type boolean diff --git a/src/inputs/select.js b/src/inputs/select.js index f08b512..0a0b4a7 100644 --- a/src/inputs/select.js +++ b/src/inputs/select.js @@ -31,14 +31,21 @@ $(function(){ $.extend(Select.prototype, { renderList: function() { this.$input.empty(); - - if(!$.isArray(this.sourceData)) { - return; - } - for(var i=0; i', {value: this.sourceData[i].value}).text(this.sourceData[i].text)); - } + var fillItems = function($el, data) { + if($.isArray(data)) { + for(var i=0; i', {label: data[i].text}), data[i].children)); + } else { + $el.append($('