xref: /plugin/tagging/script/editable.js (revision 8ed4f4c425b5570b43f6a78b2b790fd057acae70)
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