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