/** Makes editable any HTML element on the page. Applied as jQuery method. @class editable @uses editableContainer **/ (function ($) { var Editable = function (element, options) { this.$element = $(element); this.options = $.extend({}, $.fn.editable.defaults, $.fn.editableutils.getConfigData(this.$element), options); this.init(); }; Editable.prototype = { constructor: Editable, init: function () { var TypeConstructor, isValueByText = false, doAutotext, finalize; //editableContainer must be defined if(!$.fn.editableContainer) { $.error('You must define $.fn.editableContainer via including corresponding file (e.g. editable-popover.js)'); return; } //name 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.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); return; } //set value from settings or by element's text if (this.options.value === undefined || this.options.value === null) { 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.value = this.input.str2value(this.options.value); } else { this.value = this.options.value; } } //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) if(this.options.toggle !== 'manual') { this.$element.addClass('editable-click'); this.$element.on(this.options.toggle + '.editable', $.proxy(function(e){ e.preventDefault(); //stop propagation not required anymore because in document click handler it checks event target //e.stopPropagation(); if(this.options.toggle === 'mouseenter') { //for hover only show container this.show(); } else { //when toggle='click' we should not close all other containers as they will be closed automatically in document click listener var closeAll = (this.options.toggle !== 'click'); this.toggle(closeAll); } }, this)); } else { this.$element.attr('tabindex', -1); //do not stop focus on element when toggled manually } //check conditions for autotext: //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.render() : true).then($.proxy(function() { if(this.options.disabled) { this.disable(); } else { this.enable(); } /** Fired when element was initialized by editable method. @event init @param {Object} event event object @param {Object} editable editable instance @since 1.2.0 **/ 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 @method enable() **/ enable: function() { this.options.disabled = false; this.$element.removeClass('editable-disabled'); this.handleEmpty(); if(this.options.toggle !== 'manual') { if(this.$element.attr('tabindex') === '-1') { this.$element.removeAttr('tabindex'); } } }, /** Disables editable @method disable() **/ disable: function() { this.options.disabled = true; this.hide(); this.$element.addClass('editable-disabled'); this.handleEmpty(); //do not stop focus on this element this.$element.attr('tabindex', -1); }, /** Toggles enabled / disabled state of editable element @method toggleDisabled() **/ toggleDisabled: function() { if(this.options.disabled) { this.enable(); } else { this.disable(); } }, /** Sets new option @method option(key, value) @param {string|object} key option name or object with several options @param {mixed} value option new value @example $('.editable').editable('option', 'pk', 2); **/ option: function(key, value) { //set option(s) by object if(key && typeof key === 'object') { $.each(key, $.proxy(function(k, v){ this.option($.trim(k), v); }, this)); return; } //set option by string this.options[key] = value; //disabled if(key === 'disabled') { if(value) { this.disable(); } else { this.enable(); } return; } //value if(key === 'value') { this.setValue(value); } //transfer new option to container! if(this.container) { this.container.option(key, value); } }, /* * 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) { if ($.trim(this.$element.text()) === '') { this.$element.addClass(emptyClass).text(this.options.emptytext); } else { this.$element.removeClass(emptyClass); } } else { //below required if element disable property was changed if(this.$element.hasClass(emptyClass)) { this.$element.empty(); this.$element.removeClass(emptyClass); } } }, /** Shows container with form @method show() @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. **/ show: function (closeAll) { if(this.options.disabled) { return; } //init editableContainer: popover, tooltip, inline, etc.. if(!this.container) { var containerOptions = $.extend({}, this.options, { value: this.value }); this.$element.editableContainer(containerOptions); this.$element.on({ save: $.proxy(this.save, this) }); this.container = this.$element.data('editableContainer'); } else if(this.container.tip().is(':visible')) { return; } //show container this.container.show(closeAll); }, /** Hides container with form @method hide() **/ hide: function () { if(this.container) { this.container.hide(); } }, /** Toggles container visibility (show / hide) @method toggle() @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')) { this.hide(); } else { this.show(closeAll); } }, /* * 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'); } // this.hide(); this.setValue(params.newValue); /** Fired when new value was submitted. You can use $(this).data('editable') to access to editable instance @event save @param {Object} event event object @param {Object} params additional params @param {mixed} params.newValue submitted value @param {Object} params.response ajax response @example $('#username').on('save', function(e, params) { //assuming server response: '{success: true}' var pk = $(this).data('editable').options.pk; if(params.response && params.response.success) { alert('value: ' + params.newValue + ' with pk: ' + pk + ' saved!'); } else { alert('error!'); } }); **/ //event itself is triggered by editableContainer. Description here is only for documentation }, validate: function () { if (typeof this.options.validate === 'function') { return this.options.validate.call(this, this.value); } }, /** Sets new value of editable @method setValue(value, convertStr) @param {mixed} value new value @param {boolean} convertStr whether to convert value from string to internal format **/ setValue: function(value, convertStr) { if(convertStr) { this.value = this.input.str2value(value); } else { this.value = value; } if(this.container) { this.container.option('value', this.value); } $.when(this.render()) .then($.proxy(function() { this.handleEmpty(); }, this)); }, /** Activates input of visible container (e.g. set focus) @method activate() **/ activate: function() { if(this.container) { this.container.activate(); } } }; /* EDITABLE PLUGIN DEFINITION * ======================= */ /** jQuery method to initialize editable element. @method $().editable(options) @params {Object} options @example $('#username').editable({ type: 'text', url: '/post', pk: 1 }); **/ $.fn.editable = function (option) { //special API methods returning non-jquery object var result = {}, args = arguments, datakey = 'editable'; switch (option) { /** Runs client-side validation for all matched editables @method validate() @returns {Object} validation errors map @example $('#username, #fullname').editable('validate'); // possible result: { 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; if (data && (error = data.validate())) { result[data.options.name] = error; } }); return result; /** Returns current values of editable elements. If value is null or undefined it will not be returned @method getValue() @returns {Object} object of element names and values @example $('#username, #fullname').editable('validate'); // possible result: { username: "superuser", fullname: "John" } **/ case 'getValue': this.each(function () { var $this = $(this), data = $this.data(datakey); if (data && data.value !== undefined && data.value !== null) { result[data.options.name] = data.input.value2submit(data.value); } }); return result; /** This method collects values from several editable elements and submit them all to server. It is designed mainly for creating new records. @method submit(options) @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 **/ case 'submit': //collects value, validate and submit to server for creating new record var config = arguments[1] || {}, $elems = this, errors = this.editable('validate'), values; if($.isEmptyObject(errors)) { values = this.editable('getValue'); if(config.data) { $.extend(values, config.data); } $.ajax($.extend({ url: config.url, data: values, type: 'POST', dataType: 'json' }, config.ajaxOptions)) .success(function(response) { //successful response if(typeof response === 'object' && response.id) { $elems.editable('option', 'pk', response.id); $elems.removeClass('editable-unsaved'); if(typeof config.success === 'function') { 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); } }); } else { //client-side validation error if(typeof config.error === 'function') { config.error.call($elems, {errors: errors}); } } return this; } //return jquery object return this.each(function () { var $this = $(this), data = $this.data(datakey), options = typeof option === 'object' && option; if (!data) { $this.data(datakey, (data = new Editable(this, options))); } if (typeof option === 'string') { //call method data[option].apply(data, Array.prototype.slice.call(args, 1)); } }); }; $.fn.editable.defaults = { /** Type of input. Can be text|textarea|select|date|checklist and more @property type @type string @default 'text' **/ type: 'text', /** Sets disabled state of editable @property disabled @type boolean @default false **/ disabled: false, /** How to toggle editable. Can be click|dblclick|mouseenter|manual. When set to manual you should manually call show/hide methods of editable. **Note**: if you call show or toggle inside **click** handler of some DOM element, you need to apply e.stopPropagation() because containers are being closed on any click on document. @example $('#edit-button').click(function(e) { e.stopPropagation(); $('#username').editable('toggle'); }); @property toggle @type string @default 'click' **/ toggle: 'click', /** Text shown when element is empty. @property emptytext @type string @default 'Empty' **/ emptytext: 'Empty', /** Allows to automatically set element's text based on it's value. Can be auto|always|never. Useful for select and date. For example, if dropdown list is {1: 'a', 2: 'b'} and element's value set to 1, it's html will be automatically set to 'a'. auto - text will be automatically set only if element is empty. always|never - always(never) try to set element's text. @property autotext @type string @default 'auto' **/ autotext: 'auto', /** Initial value of input. Taken from data-value or element's text. @property value @type mixed @default element's text **/ value: null, /** Callback to perform custom displaying of value in element's text. If null, default input's value2html() will be called. If false, 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 = $('
').text(value).html(); $(this).html(''+escapedValue+''); } **/ display: null }; }(window.jQuery));