gridftp: multiple fixes in handling and presenting errors
[qcg-portal.git] / filex / static / filex / filex.js
1 var Filex = Filex || {};
2
3 $(function(){
4         'use strict';
5
6     // ------------------------------------------------------------------------
7     // Models
8     // ------------------------------------------------------------------------
9
10     var OfflineModel = Backbone.Model.extend({
11         // prevent syncing with server
12         sync: function() {
13             return false;
14         }
15     });
16
17     Filex.File = OfflineModel.extend({
18         defaults: {
19             checked: false
20         },
21
22         isDir: function() {
23             return false;
24         },
25
26         isFile: function() {
27             return true;
28         },
29
30         isHidden: function() {
31             return this.get('name')[0] == '.';
32         },
33
34         toggle: function() {
35             this.set('checked', !this.get('checked'));
36         }
37     });
38
39     Filex.Directory = Filex.File.extend({
40         isDir: function() {
41             return true;
42         },
43
44         isFile: function() {
45             return false;
46         }
47     });
48
49     Filex.PathBit = OfflineModel.extend({
50         defaults: {
51             'active': false
52         }
53     });
54
55
56     // ------------------------------------------------------------------------
57     // Collections
58     // ------------------------------------------------------------------------
59
60     Filex.FileList = Backbone.Collection.extend({
61         url: '/filex/list/',
62
63         model: function(attrs, options) {
64             switch (attrs['type']) {
65                 case 'directory':
66                     return new Filex.Directory(attrs, options);
67                 case 'file':
68                     return new Filex.File(attrs, options);
69                 default:
70                     console.error('Unknown model type:', attrs['type']);
71             }
72         },
73
74         comparator: function(a, b) {
75             if (a.isFile() == b.isFile())
76                 return a.get('name').toLowerCase().localeCompare(b.get('name').toLowerCase());
77
78             return (a.isFile() && b.isDir()) ? 1 : -1;
79         },
80
81         hidden: function() {
82             return this.filter(function (item) {
83                 return item.isHidden();
84             })
85         },
86
87         visible: function () {
88             return this.filter(function (item) {
89                 return !item.isHidden();
90             })
91         }
92     });
93
94     Filex.Path = Backbone.Collection.extend({
95         model: Filex.PathBit,
96
97         initialize: function() {
98             this.listenTo(this, 'reset add', this.setActive);
99         },
100
101         setActive: function() {
102             _.each(this.initial(), function(bit) {
103                 bit.set('active', false);
104             });
105
106             this.last().set('active', true);
107         },
108
109         full: function() {
110             return this.pluck('name').join('/').replace(/^\/+/, '/');
111         }
112     });
113
114
115     // ------------------------------------------------------------------------
116     // Views
117     // ------------------------------------------------------------------------
118
119     Filex.ListingItem = Backbone.View.extend({
120         tagName:  'tr',
121
122         events: {
123             'click': 'click'
124         },
125
126         initialize: function(options) {
127             this.view = options.view;
128
129             this.listenTo(this.model, 'change:checked', this.toggleChecked);
130             this.listenTo(this.model, 'remove', this.remove);
131             this.listenTo(this.model, 'hidden', this.toggleHidden);
132         },
133
134         template: function() {
135             var templateSelector = this.model.isDir() ? '#dir-template' : '#file-template';
136
137             return _.template($(templateSelector).html());
138         },
139
140         render: function() {
141             var data = this.model.toJSON();
142             data['url_params'] = $.param({
143                 host: this.view.host,
144                 path: this.view.path.full() + '/' + this.model.get('name')
145             });
146             data['cid'] = this.model.cid;
147
148             this.$el.html(this.template()(data));
149             this.toggleHidden();
150
151             return this;
152         },
153
154         toggleHidden: function() {
155             var isHidden = this.model.isHidden() && !this.view.showHidden();
156             this.$el.toggleClass('hidden', isHidden);
157
158             if (isHidden && this.model.get('checked'))
159                 this.model.toggle();
160         },
161
162         toggleChecked: function(obj, value) {
163             this.$el.toggleClass('active', value);
164             this.$el.find('input[type="checkbox"]').prop('checked', value);
165         },
166
167         click: function(e) {
168             if (e.target.className == 'link') {
169                 if (this.model.isDir()) {
170                     e.preventDefault();
171                     this.model.trigger('selected:dir', this.model);
172                 }
173
174                 return;
175             }
176
177             this.model.toggle();
178         }
179     });
180
181     Filex.Breadcrumb = Backbone.View.extend({
182         tagName: 'li',
183
184         events: {
185             'click a': 'selected'
186         },
187
188         initialize: function() {
189             this.listenTo(this.model, 'change:active', this.render);
190             this.listenTo(this.model, 'remove', this.remove);
191         },
192
193         render: function() {
194             if (this.model.get('active')) {
195                 this.$el.text(this.model.get('name'));
196             }
197             else {
198                 this.$el.html($('<a/>', {
199                     href: '#',
200                     text: this.model.get('name')
201                 }));
202             }
203             this.$el.toggleClass('active', this.model.get('active'));
204
205             return this;
206         },
207
208         selected: function(e) {
209             e.preventDefault();
210             this.model.trigger('selected', this.model);
211         }
212     });
213
214     Filex.FilexView = Backbone.View.extend({
215         el: $('#filex'),
216
217         events: {
218             'change #show-hidden': 'toggleHidden',
219             'click #select-all': 'selectAll',
220             'click #btn-refresh': 'reloadFiles',
221             'click #btn-favorites': 'toggleFavorites',
222             'click #btn-host': 'editHost'
223         },
224
225         initialize: function(options) {
226             this.$noItems = $('#no-items');
227             this.$error = $('#error');
228             this.$showHidden = $('#show-hidden');
229             this.$selectAll = $('#select-all');
230             this.$favorites = $('#btn-favorites');
231             this.$host = $('#btn-host');
232
233             this.path = new Filex.Path();
234             this.files = new Filex.FileList();
235
236             this.listenTo(this.path, 'reset', this.resetPath);
237             this.listenTo(this.path, 'add', this.addPath);
238             this.listenTo(this.path, 'add reset', this.changedPath);
239             this.listenTo(this.path, 'selected', this.selectedPath);
240             this.listenTo(this.files, 'reset', this.resetFiles);
241             this.listenTo(this.files, 'selected:dir', this.selectedDir);
242             this.listenTo(this.files, 'change:checked', this.updateSelectAll);
243
244             // used in selectize callbacks
245             var view = this,
246                 optionTemplate = _.template('<div><div><%= host %></div><div class="small text-muted"><%= path %></div></div>');
247
248             this.$('#host-selector').selectize({
249                 optgroupField: 'group',
250                 labelField: 'host',
251                 searchField: ['host', 'path'],
252                 sortField: [
253                     {field: 'host', direction: 'asc'},
254                     {field: 'path', direction: 'asc'}
255                 ],
256                 options: options.locations,
257                 optgroups: [
258                     {value: 'sys', label: 'Podstawowe'},
259                     {value: 'usr', label: 'Użytkownika'}
260                 ],
261                 lockOptgroupOrder: true,
262                 create: function(input, callback) {
263                     var $form = $('#favorite-form'),
264                         parts = input.split('/'),
265                         callback_called = false;
266
267                     $form[0].reset();
268                     $form.find('#id_host').val(parts.shift());
269
270                     if (parts.length)
271                         $form.find('#id_path').val(parts.join('/'));
272
273                     $form.off();
274                     $form.on('submit', function(e) {
275                         var $this = $(this),
276                             $btn = $this.find('[type="submit"]');
277
278                         e.preventDefault();
279                         $btn.button('loading');
280
281                         $.post($this.attr('action'), $this.serialize(), function(data) {
282                             callback(data);
283                             callback_called = true;
284
285                             $this.modal('hide');
286                             $btn.button('reset');
287                         }, 'json').fail(function(xhr) {
288                             console.error(arguments);
289                             var error = (xhr.responseJSON || {}).error || undefined;
290
291                             if (typeof error === 'string') {
292                                 $('<div>', {
293                                     'class': 'alert alert-danger',
294                                     html: '<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> ' + error
295                                 }).prependTo($this.find('.modal-body'));
296                                 $btn.button('reset');
297                             }
298                             else {
299                                 $btn.button('error');
300                             }
301                         });
302                     });
303
304                     $form.one('hide.bs.modal', function() {
305                         if (!callback_called)
306                             callback();
307                         $form.off();
308                     });
309
310                     $form.modal();
311                 },
312                 render: {
313                     option: function(item) {
314                         return optionTemplate(item);
315                     },
316                     option_create : function(data, escape) {
317                         return '<div class="create">Dodaj <em>' + escape(data.input) + '</em>&hellip;</div>';
318                     }
319                 },
320                 onItemAdd: function(value) {
321                     view.load(value);
322                     this.blur();
323                 },
324                 onBlur: function() {
325                     $('#host').removeClass('edit');
326                     this.clear();
327                 }
328             });
329             this.hostSelectize = this.$('#host-selector')[0].selectize;
330
331             this.render();
332         },
333
334         render: function() {
335             this.updateSelectAll();
336             this.updateFavorites();
337             this.$noItems.toggle(!Boolean(this.visibleFiles().length));
338             this.$error.hide();
339         },
340
341         load: function(location) {
342             var hostRootPath = location.split(/\/(\/|~)(.*)/),
343                 pathBits = [new Filex.PathBit({'name': hostRootPath[1]})].concat(
344                     _.chain(hostRootPath[2].split('/'))
345                         .filter(_.identity)
346                         .map(function(name) { return new Filex.PathBit({'name': name}) })
347                         .value()
348                 );
349
350             this.host = hostRootPath[0];
351             this.path.reset(pathBits);
352
353             this.$host.text(this.host);
354         },
355
356         reloadFiles: function() {
357             this.busy();
358
359             var view = this;
360
361             this.files.fetch({
362                 reset: true,
363                 data: {host: this.host, path: this.path.full()},
364                 success: function() {
365                     view.render();
366                     view.idle();
367                 },
368                 error: function(collection, response) {
369                     view.files.reset();
370
371                     var msg = (response.responseJSON || {}).error || 'Błąd serwera';
372
373                     view.$noItems.hide();
374                     view.$error.find('.msg').text(msg);
375                     view.$error.show();
376                     view.idle();
377                 }
378             });
379         },
380
381         addPath: function(bit) {
382             var view = new Filex.Breadcrumb({model: bit});
383             this.$('.path').append(view.render().el);
384         },
385
386         resetPath: function(models, options) {
387             _.each(options.previousModels, function(model) {
388                 model.trigger('remove');
389             });
390
391             this.path.each(this.addPath, this);
392         },
393
394         resetFiles: function(models, options) {
395             _.each(options.previousModels, function(model) {
396                 model.trigger('remove');
397             });
398
399             this.files.each(function(file) {
400                 var view = new Filex.ListingItem({model: file, view: this});
401                 this.$('tbody').append(view.render().el);
402             }, this);
403         },
404
405         toggleHidden: function() {
406             this.files.each(function(item) { item.trigger('hidden'); }, this);
407             this.render();
408         },
409
410         selectedDir: function(dir) {
411             this.path.add({'name': dir.get('name')});
412         },
413
414         selectedPath: function(bit) {
415             this.path.reset(this.path.slice(0, this.path.indexOf(bit) + 1));
416         },
417
418         showHidden: function() {
419             return this.$showHidden[0].checked;
420         },
421
422         busy: function() {
423             this.$el.addClass('busy');
424         },
425
426         idle: function() {
427             this.$el.removeClass('busy');
428         },
429
430         visibleFiles: function() {
431             return this.showHidden() ? this.files.models : this.files.visible();
432         },
433
434         selectedFiles: function() {
435             return _.filter(this.visibleFiles(), function(item) {
436                 return item.get('checked');
437             });
438         },
439
440         selectAll: function() {
441             var checked = this.$selectAll[0].checked;
442
443             _.each(this.visibleFiles(), function(item) {
444                 item.set('checked', checked);
445             })
446         },
447
448         updateSelectAll: function() {
449             if (this.visibleFiles().length) {
450                 this.$selectAll.prop('disabled', false);
451                 this.$selectAll.prop('checked', this.selectedFiles().length == this.visibleFiles().length);
452             }
453             else {
454                 this.$selectAll.prop('disabled', true);
455                 this.$selectAll.prop('checked', false);
456             }
457         },
458
459         clearSelection: function() {
460             _.each(this.visibleFiles(), function(item) {
461                 item.set('checked', false);
462             });
463         },
464
465         toggleFavorites: function() {
466             var $btn = this.$favorites,
467                 locations = this.hostSelectize,
468                 is_active = $btn.hasClass('active'),
469                 url = is_active ? '/filex/fav/delete/' : '/filex/fav/add/',
470                 data = {
471                     host: this.host,
472                     path: this.path.full()
473                 };
474
475             $btn.button('loading');
476
477             $.post(url, data, 'json').done(function (response) {
478                 $btn.button('reset');
479
480                 if (is_active)
481                     locations.removeOption(data.host + '/' + data.path);
482                 else
483                     locations.addOption(response);
484             }).fail(function() {
485                 $btn.button('reset');
486                 $btn.button('toggle');
487
488                 console.error(arguments);
489             });
490         },
491
492         initialLoad: function() {
493             if (!this.host) {
494                 var opts = this.hostSelectize.options;
495
496                 this.load(opts[Object.keys(opts)[0]].value);
497             }
498         },
499
500         changedPath: function () {
501             this.reloadFiles();
502             this.updateFavorites();
503         },
504
505         updateFavorites: function() {
506             var loc = this.host + '/' + this.path.full(),
507                 favorites = this.hostSelectize.options;
508
509             if (favorites.hasOwnProperty(loc)) {
510                 if (favorites[loc].group == 'sys') {
511                     this.$favorites.addClass('disabled').prop('disabled', true);
512                     if (this.$favorites.hasClass('active'))
513                         this.$favorites.button('toggle');
514                 }
515                 else {
516                     this.$favorites.removeClass('disabled').prop('disabled', false);
517                     if (!this.$favorites.hasClass('active'))
518                         this.$favorites.button('toggle');
519                 }
520             }
521             else {
522                 this.$favorites.removeClass('disabled').prop('disabled', false);
523                 if (this.$favorites.hasClass('active'))
524                     this.$favorites.button('toggle');
525             }
526         },
527
528         editHost: function() {
529             $('#host').addClass('edit');
530             this.hostSelectize.focus();
531         }
532     });
533 });