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 Fired when form is shown 166 **/ 167 this.$div.triggerHandler('show'); 168 }, 169 170 error: function(msg) { 171 if(msg === false) { 172 this.$form.find('.editable-error-block').removeClass('ui-state-error').empty().hide(); 173 } else { 174 this.$form.find('.editable-error-block').addClass('ui-state-error').html(msg).show(); 175 } 176 }, 177 178 submit: function(e) { 179 e.stopPropagation(); 180 e.preventDefault(); 181 182 //get new value from input 183 var newValue = this.input.$input.val(); 184 185 if (newValue === this.options.value) { 186 this.$div.triggerHandler('nochange'); 187 return; 188 } 189 190 this.showLoading(); 191 192 this.isSaving = true; 193 194 //sending data to server 195 var params = $.extend({}, this.options.params, {oldValue: this.options.value, newValue: newValue}); 196 return $.ajax({ 197 url : this.options.url, 198 data : params, 199 type : 'POST' 200 }) 201 .done($.proxy(function(response) { 202 this.isSaving = false; 203 204 if (response.status === 'error') { 205 this.showForm(); 206 return; 207 } 208 this.error(false); 209 this.options.value = newValue; 210 if (typeof this.options.success === 'function') { 211 this.options.success.call(this.options.scope, response, newValue); 212 } 213 214 this.$div.triggerHandler('save', {newValue: newValue, response: response}); 215 }, this)) 216 .fail($.proxy(function(xhr) { 217 this.isSaving = false; 218 219 var msg = typeof xhr === 'string' ? xhr : xhr.responseText || xhr.statusText || 'Unknown error'; 220 221 this.error(msg); 222 this.showForm(); 223 224 }, this)); 225 } 226 }; 227 228 229 var Editable = function (element, options) { 230 this.$element = jQuery(element); 231 this.options = jQuery.extend({}, jQuery.fn.editable.defaults, options); 232 this.init(); 233 }; 234 235 Editable.prototype = { 236 isVisible: function() { 237 return this.$element.hasClass('editable-open'); 238 }, 239 240 init: function () { 241 this.value = this.$element.text(); 242 //add 'editable' class to every editable element 243 this.$element.addClass('editable'); 244 245 if (!this.options.disabled) { 246 this.$element.addClass('editable-click'); 247 } 248 249 this.$popup = null; 250 251 this.$element.on('click.editable', $.proxy(function(e){ 252 //prevent following link if editable enabled 253 if(!this.options.disabled) { 254 e.preventDefault(); 255 } 256 257 this.toggle(); 258 }, this)); 259 260 if(!$(document).data('editable-handlers-attached')) { 261 //close all on escape 262 $(document).on('keyup.editable', $.proxy(function (e) { 263 if (e.keyCode === jQuery.ui.keyCode.ESCAPE) { 264 this.closeOthers(null); 265 } 266 }, this)); 267 268 //close containers when click outside 269 //(mousedown could be better than click, it closes everything also on drag drop) 270 $(document).on('click.editable', function(e) { 271 var $target = $(e.target); 272 273 if($target.is('.editable-popup') || $target.parents('.editable-popup').length) { 274 return; 275 } 276 277 //close all open containers 278 Editable.prototype.closeOthers(e.target); 279 }); 280 281 $(document).data('editable-handlers-attached', true); 282 } 283 }, 284 285 /* 286 Closes other containers except one related to passed element. 287 Other containers are canceled 288 */ 289 closeOthers: function(element) { 290 $('.editable-open').each(function(i, el){ 291 //do nothing with passed element and it's children 292 if(el === element || $(el).find(element).length) { 293 return; 294 } 295 296 $(el).data('editable').hide(); 297 }); 298 299 }, 300 301 /** 302 Enables editable 303 @method enable() 304 **/ 305 enable: function() { 306 this.options.disabled = false; 307 this.$element.addClass('editable-click'); 308 }, 309 310 /** 311 Disables editable 312 @method disable() 313 **/ 314 disable: function() { 315 this.options.disabled = true; 316 this.hide(); 317 this.$element.removeClass('editable-click'); 318 }, 319 320 /** 321 Toggles enabled / disabled state of editable element 322 @method toggleDisabled() 323 **/ 324 toggleDisabled: function() { 325 if(this.options.disabled) { 326 this.enable(); 327 } else { 328 this.disable(); 329 } 330 }, 331 332 /** 333 Shows container with form 334 @method show() 335 **/ 336 show: function () { 337 if(this.options.disabled) { 338 return; 339 } 340 341 this.$element.addClass('editable-open'); 342 343 //redraw element 344 this.$popup = $('<div>') 345 .addClass('ui-tooltip ui-corner-all ui-widget-shadow ui-widget ui-widget-content editable-popup') 346 .css('max-width', 'none') //remove ui-tooltip max-width property 347 .prependTo('body') 348 .hide() 349 .fadeIn(); 350 351 this.$popup.append($('<label>').text(this.options.label)); 352 353 this.$form_container = $('<div>'); 354 this.$popup.append(this.$form_container); 355 356 //firstly bind the events 357 this.$form_container.on({ 358 save: $.proxy(function(){ this.hide(); }, this), //click on submit button (value changed) 359 nochange: $.proxy(function(){ this.hide(); }, this), //click on submit button (value NOT changed) 360 cancel: $.proxy(function(){ this.hide(); }, this), //click on cancel button 361 show: $.proxy(function() { 362 this.$popup.position({ 363 of: this.$element, 364 my: 'center bottom-5', 365 at: 'center top', 366 collision: 'flipfit' 367 }); 368 }, this) 369 }); 370 371 //render form 372 this.form = new EditableForm(this.$form_container, { 373 value : this.value, 374 url : this.options.url, 375 success : this.options.success, 376 scope : this.options.scope, 377 params : this.options.params 378 }); 379 }, 380 381 /** 382 Hides container with form 383 @method hide() 384 **/ 385 hide: function () { 386 if (!this.isVisible()) { 387 return; 388 } 389 390 //if form is saving value, schedule hide 391 if(this.form.isSaving) { 392 return; 393 } 394 395 this.$popup.fadeOut({ 396 complete: $.proxy(function() { 397 this.$popup.remove(); 398 this.$popup = null; 399 this.$element.removeClass('editable-open'); 400 }, this) 401 }); 402 }, 403 /** 404 Toggles container visibility (show / hide) 405 @method toggle() 406 **/ 407 toggle: function() { 408 if(this.isVisible()) { 409 this.hide(); 410 } else { 411 this.show(); 412 } 413 } 414 }; 415 416 $.fn.editable = function (option) { 417 var datakey = 'editable'; 418 //return jquery object 419 return this.each(function () { 420 var $this = $(this), 421 data = $this.data(datakey), 422 options = typeof option === 'object' && option; 423 424 if (!data) { 425 $this.data(datakey, (data = new Editable(this, options))); 426 } 427 428 if (typeof option === 'string') { //call method 429 data[option].apply(data, Array.prototype.slice.call(arguments, 1)); 430 } 431 }); 432 }; 433 434 $.fn.editable.defaults = { 435 disabled : false, 436 label : 'Enter value', 437 success : null, //success callback 438 scope : null, //success calback scope 439 params : {} //additional params passed to ajax post request 440 }; 441}(window.jQuery)); 442