1(function ($) { 2 "use strict"; 3 var TextInput = function (div, options) { 4 this.$div = $(div); 5 this.options = options; 6 7 this.init(); 8 }; 9 10 TextInput.prototype = { 11 init: function() { 12 this.$input = $('<input type="text">').val(this.options.defaultValue).appendTo(this.$div); 13 this.renderClear(); 14 }, 15 16 activate: function() { 17 if(this.$input.is(':visible')) { 18 this.$input.focus(); 19 20 var pos = this.$input.val().length; 21 this.$input.get(0).setSelectionRange(pos, pos); 22 this.toggleClear(); 23 } 24 }, 25 26 //render clear button 27 renderClear: function() { 28 this.$clear = $('<span class="editable-clear-x"></span>'); 29 this.$input.after(this.$clear) 30 .css('padding-right', 24) 31 .keyup($.proxy(function(e) { 32 //arrows, enter, tab, etc 33 if(~$.inArray(e.keyCode, [40,38,9,13,27])) { 34 return; 35 } 36 37 clearTimeout(this.t); 38 var that = this; 39 this.t = setTimeout(function() { 40 that.toggleClear(e); 41 }, 100); 42 43 }, this)) 44 .parent().css('position', 'relative'); 45 46 this.$clear.click($.proxy(this.clear, this)); 47 48 }, 49 50 //show / hide clear button 51 toggleClear: function() { 52 var len = this.$input.val().length, 53 visible = this.$clear.is(':visible'); 54 55 if(len && !visible) { 56 this.$clear.show(); 57 } 58 59 if(!len && visible) { 60 this.$clear.hide(); 61 } 62 }, 63 64 clear: function() { 65 this.$clear.hide(); 66 this.$input.val('').focus(); 67 } 68 }; 69 70 var EditableForm = function (div, options) { 71 this.$div = $(div); //div, containing form. Not form tag. Not editable-element. 72 this.options = options; 73 this.init(); 74 }; 75 76 EditableForm.prototype = { 77 template : '<form class="form-inline editableform">'+ 78 '<div class="control-group">' + 79 '<div><div class="editable-input"></div><div class="editable-buttons"></div></div>'+ 80 '<div class="editable-error-block"></div>' + 81 '</div>' + 82 '</form>', 83 //loading div 84 loading: '<div class="editableform-loading"></div>', 85 86 //buttons 87 buttons: '<button type="submit" class="editable-submit">ok</button>'+ 88 '<button type="button" class="editable-cancel">cancel</button>', 89 90 /** 91 Renders editableform 92 @method render 93 **/ 94 init: function() { 95 //init loader 96 this.$loading = $(this.loading); 97 this.$div.empty().append(this.$loading); 98 99 //init form template and buttons 100 this.$form = $(this.template); 101 102 var $btn = this.$form.find('.editable-buttons'); 103 $btn.append(this.buttons); 104 105 this.$form.find('.editable-submit').button({ 106 icons: { primary: "ui-icon-check" }, 107 text: false 108 }); 109 this.$form.find('.editable-cancel').button({ 110 icons: { primary: "ui-icon-closethick" }, 111 text: false 112 }); 113 114 //render: get input.$input 115 //append input to form 116 this.input = new TextInput(this.$form.find('div.editable-input'), {defaultValue: this.options.value}); 117 118 //flag showing is form now saving value to server. 119 //It is needed to wait when closing form. 120 this.isSaving = false; 121 122 //append form to container 123 this.$div.append(this.$form); 124 125 this.$form.find('.editable-cancel').click($.proxy(this.cancel, this)); 126 127 //attach submit handler 128 this.$form.submit($.proxy(this.submit, this)); 129 130 //show form 131 this.showForm(); 132 }, 133 cancel: function() { 134 /** 135 Fired when form was cancelled by user 136 @event cancel 137 @param {Object} event event object 138 **/ 139 this.$div.triggerHandler('cancel'); 140 }, 141 showLoading: function() { 142 var w, h; 143 //set loading size equal to form 144 w = this.$form.outerWidth(); 145 h = this.$form.outerHeight(); 146 147 if(w) { 148 this.$loading.width(w); 149 } 150 if(h) { 151 this.$loading.height(h); 152 } 153 this.$form.hide(); 154 155 this.$loading.show(); 156 }, 157 158 showForm: function() { 159 this.$loading.hide(); 160 this.$form.show(); 161 162 this.input.activate(); 163 }, 164 165 error: function(msg) { 166 if(msg === false) { 167 this.$form.find('.editable-error-block').removeClass('ui-state-error').empty().hide(); 168 } else { 169 this.$form.find('.editable-error-block').addClass('ui-state-error').html(msg).show(); 170 } 171 }, 172 173 submit: function(e) { 174 e.stopPropagation(); 175 e.preventDefault(); 176 177 //get new value from input 178 var newValue = this.input.$input.val(); 179 180 if (newValue === this.options.value) { 181 this.$div.triggerHandler('nochange'); 182 return; 183 } 184 185 this.showLoading(); 186 187 this.isSaving = true; 188 189 //sending data to server 190 var params = $.extend({}, this.options.params, {oldValue: this.options.value, newValue: newValue}); 191 return $.ajax({ 192 url : this.options.url, 193 data : params, 194 type : 'POST' 195 }) 196 .done($.proxy(function(response) { 197 this.isSaving = false; 198 199 if (response.status === 'error') { 200 this.showForm(); 201 return; 202 } 203 this.error(false); 204 this.options.value = newValue; 205 if (typeof this.options.success === 'function') { 206 this.options.success.call(this.options.scope, response, newValue); 207 } 208 209 this.$div.triggerHandler('save', {newValue: newValue, response: response}); 210 }, this)) 211 .fail($.proxy(function(xhr) { 212 this.isSaving = false; 213 214 var msg = typeof xhr === 'string' ? xhr : xhr.responseText || xhr.statusText || 'Unknown error'; 215 216 this.error(msg); 217 this.showForm(); 218 219 }, this)); 220 } 221 }; 222 223 224 var Editable = function (element, options) { 225 this.$element = jQuery(element); 226 this.options = jQuery.extend({}, jQuery.fn.editable.defaults, options); 227 this.init(); 228 }; 229 230 Editable.prototype = { 231 isVisible: function() { 232 return this.$element.hasClass('editable-open'); 233 }, 234 235 init: function () { 236 this.value = this.$element.text(); 237 //add 'editable' class to every editable element 238 this.$element.addClass('editable'); 239 240 if (!this.options.disabled) { 241 this.$element.addClass('editable-click'); 242 } 243 244 this.$element.tooltip({ 245 items: '*', 246 content: ' ', 247 track: false, 248 open: $.proxy(function() { 249 this.$element.off('mouseleave focusout'); 250 }, this) 251 }); 252 //disable standart triggering tooltip events 253 this.$element.off('mouseover focusin'); 254 255 this.$element.on('click.editable', $.proxy(function(e){ 256 //prevent following link if editable enabled 257 if(!this.options.disabled) { 258 e.preventDefault(); 259 } 260 261 this.toggle(); 262 }, this)); 263 264 if(!$(document).data('editable-handlers-attached')) { 265 //close all on escape 266 $(document).on('keyup.editable', $.proxy(function (e) { 267 if (e.keyCode === jQuery.ui.keyCode.ESCAPE) { 268 this.closeOthers(null); 269 } 270 }, this)); 271 272 //close containers when click outside 273 //(mousedown could be better than click, it closes everything also on drag drop) 274 $(document).on('click.editable', function(e) { 275 var $target = $(e.target); 276 277 if($target.is('.ui-tooltip') || $target.parents('.ui-tooltip').length) { 278 return; 279 } 280 //close all open containers (except one - target) 281 Editable.prototype.closeOthers(e.target); 282 }); 283 284 $(document).data('editable-handlers-attached', true); 285 } 286 }, 287 288 /* 289 Closes other containers except one related to passed element. 290 Other containers are canceled 291 */ 292 closeOthers: function(element) { 293 $('.editable-open').each(function(i, el){ 294 //do nothing with passed element and it's children 295 if(el === element || $(el).find(element).length) { 296 return; 297 } 298 299 $(el).data('editable').hide(); 300 }); 301 302 }, 303 304 /** 305 Enables editable 306 @method enable() 307 **/ 308 enable: function() { 309 this.options.disabled = false; 310 this.$element.addClass('editable-click'); 311 }, 312 313 /** 314 Disables editable 315 @method disable() 316 **/ 317 disable: function() { 318 this.options.disabled = true; 319 this.hide(); 320 this.$element.removeClass('editable-click'); 321 }, 322 323 /** 324 Toggles enabled / disabled state of editable element 325 @method toggleDisabled() 326 **/ 327 toggleDisabled: function() { 328 if(this.options.disabled) { 329 this.enable(); 330 } else { 331 this.disable(); 332 } 333 }, 334 335 /** 336 Shows container with form 337 @method show() 338 **/ 339 show: function () { 340 if(this.options.disabled) { 341 return; 342 } 343 this.$element.addClass('editable-open'); 344 345 //redraw element 346 var $content = $('<div>'); 347 348 //append elements to dom so they are visible 349 this.$element.tooltip('option', 'content', $content); 350 351 //open tooltip 352 this.$element.tooltip('open'); 353 354 $content.append($('<label>').text(this.options.label)); 355 356 this.$form_container = $('<div>'); 357 $content.append(this.$form_container); 358 359 //render form 360 this.form = new EditableForm(this.$form_container, { 361 value : this.value, 362 url : this.options.url, 363 success : this.options.success, 364 scope : this.options.scope, 365 params : this.options.params 366 }); 367 368 this.$form_container.on({ 369 save: $.proxy(function(){ this.hide(); }, this), //click on submit button (value changed) 370 nochange: $.proxy(function(){ this.hide(); }, this), //click on submit button (value NOT changed) 371 cancel: $.proxy(function(){ this.hide(); }, this) //click on cancel button 372 }); 373 }, 374 375 /** 376 Hides container with form 377 @method hide() 378 **/ 379 hide: function () { 380 if (!this.isVisible()) { 381 return; 382 } 383 384 //if form is saving value, schedule hide 385 if(this.form.isSaving) { 386 return; 387 } 388 389 this.$element.removeClass('editable-open'); 390 this.$element.tooltip('close'); 391 }, 392 /** 393 Toggles container visibility (show / hide) 394 @method toggle() 395 **/ 396 toggle: function() { 397 if(this.isVisible()) { 398 this.hide(); 399 } else { 400 this.show(); 401 } 402 } 403 }; 404 405 $.fn.editable = function (option) { 406 var datakey = 'editable'; 407 //return jquery object 408 return this.each(function () { 409 var $this = $(this), 410 data = $this.data(datakey), 411 options = typeof option === 'object' && option; 412 413 if (!data) { 414 $this.data(datakey, (data = new Editable(this, options))); 415 } 416 417 if (typeof option === 'string') { //call method 418 data[option].apply(data, Array.prototype.slice.call(arguments, 1)); 419 } 420 }); 421 }; 422 423 $.fn.editable.defaults = { 424 disabled : false, 425 label : 'Enter value', 426 success : null, //success callback 427 scope : null, //success calback scope 428 params : {} //additional params passed to ajax post request 429 }; 430}(window.jQuery)); 431