gridftp: fixed height file list
[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         full: function() {
98             return this.pluck('name').join('/').replace(/^\/+/, '/');
99         },
100
101         append: function(name) {
102             return this.pluck('name').concat(name).join('/').replace(/^\/+/, '/');
103         }
104     });
105
106
107     // ------------------------------------------------------------------------
108     // Routers
109     // ------------------------------------------------------------------------
110
111     Filex.Router = Backbone.Router.extend({
112                 routes: {
113                         '*url': 'load'
114                 }
115         });
116
117
118     // ------------------------------------------------------------------------
119     // Views
120     // ------------------------------------------------------------------------
121
122     Filex.ListingItem = Backbone.View.extend({
123         tagName:  'tr',
124
125         events: {
126             'click': 'click'
127         },
128
129         initialize: function(options) {
130             this.view = options.view;
131
132             this.listenTo(this.model, 'change:checked', this.toggleChecked);
133             this.listenTo(this.model, 'remove', this.remove);
134             this.listenTo(this.model, 'hidden', this.toggleHidden);
135         },
136
137         template: function() {
138             var templateSelector = this.model.isDir() ? '#dir-template' : '#file-template';
139
140             return _.template($(templateSelector).html());
141         },
142
143         render: function() {
144             var data = this.model.toJSON();
145
146             if (this.model.isDir()) {
147                 data['url'] = this.view.host + '/' + this.view.path.append(this.model.get('name'))
148             }
149             else {
150                 data['params'] = $.param({
151                     host: this.view.host,
152                     path: this.view.path.append(this.model.get('name'))
153                 });
154             }
155             data['cid'] = this.model.cid;
156
157             this.$el.html(this.template()(data));
158             this.toggleHidden();
159
160             return this;
161         },
162
163         toggleHidden: function() {
164             var isHidden = this.model.isHidden() && !this.view.showHidden();
165             this.$el.toggleClass('hidden', isHidden);
166
167             if (isHidden && this.model.get('checked'))
168                 this.model.toggle();
169         },
170
171         toggleChecked: function(obj, value) {
172             this.$el.toggleClass('active', value);
173             this.$el.find('input[type="checkbox"]').prop('checked', value);
174         },
175
176         click: function(e) {
177             if (e.target.className != 'link')
178                 this.model.toggle();
179         }
180     });
181
182     Filex.Breadcrumb = Backbone.View.extend({
183         tagName: 'li',
184
185         initialize: function() {
186             this.listenTo(this.model, 'change:active', this.render);
187             this.listenTo(this.model, 'remove', this.remove);
188         },
189
190         render: function() {
191             if (this.model.get('active')) {
192                 this.$el.addClass('active');
193                 this.$el.text(this.model.get('name'));
194             }
195             else {
196                 this.$el.html($('<a/>', {
197                     href: '#' + this.model.get('url'),
198                     text: this.model.get('name')
199                 }));
200             }
201
202             return this;
203         }
204     });
205
206     Filex.FilexView = Backbone.View.extend({
207         el: $('#filex'),
208
209         events: {
210             'change #show-hidden': 'toggleHidden',
211             'click #select-all': 'selectAll',
212             'click #btn-refresh': 'reloadFiles',
213             'click #btn-favorites': 'toggleFavorites',
214             'click #btn-host': 'editHost'
215         },
216
217         initialize: function(options) {
218             this.$noItems = $('#no-items');
219             this.$error = $('#error');
220             this.$showHidden = $('#show-hidden');
221             this.$selectAll = $('#select-all');
222             this.$favorites = $('#btn-favorites');
223             this.$host = $('#btn-host');
224
225             this.path = new Filex.Path();
226             this.files = new Filex.FileList();
227             this.router = new Filex.Router();
228
229             this.listenTo(this.path, 'add', this.addPath);
230             this.listenTo(this.path, 'reset', this.resetPath);
231             this.listenTo(this.files, 'reset', this.resetFiles);
232             this.listenTo(this.files, 'change:checked', this.updateSelectAll);
233             this.listenTo(this.router, 'route:load', this.load);
234
235             // used in selectize callbacks
236             var view = this,
237                 optionTemplate = _.template('<div><div><%= host %></div><div class="small text-muted"><%= path %></div></div>');
238
239             this.$('#host-selector').selectize({
240                 optgroupField: 'group',
241                 labelField: 'host',
242                 searchField: ['host', 'path'],
243                 sortField: [
244                     {field: 'host', direction: 'asc'},
245                     {field: 'path', direction: 'asc'}
246                 ],
247                 options: options.locations,
248                 optgroups: [
249                     {value: 'sys', label: 'Podstawowe'},
250                     {value: 'usr', label: 'Użytkownika'}
251                 ],
252                 lockOptgroupOrder: true,
253                 create: function(input, callback) {
254                     var $form = $('#favorite-form'),
255                         parts = input.split('/'),
256                         callback_called = false;
257
258                     $form[0].reset();
259                     $form.find('.alert-danger').remove();
260                     $form.find('#id_host').val(parts.shift());
261
262                     if (parts.length)
263                         $form.find('#id_path').val(parts.join('/'));
264
265                     $form.off();
266                     $form.on('submit', function(e) {
267                         var $this = $(this),
268                             $btn = $this.find('[type="submit"]');
269
270                         e.preventDefault();
271                         $btn.button('loading');
272                         $this.find('.alert-danger').remove();
273
274                         $.post($this.attr('action'), $this.serialize(), function(data) {
275                             callback(data);
276                             callback_called = true;
277
278                             $this.modal('hide');
279                             $btn.button('reset');
280                         }, 'json').fail(function(xhr) {
281                             console.error(arguments);
282                             var error = (xhr.responseJSON || {}).error || undefined;
283
284                             if (error.__all__) {
285                                 error = 'Podana lokalizacja jest już zapisana'
286                             }
287                             else if (xhr.status == 400) {
288                                 error = 'Niepoprawna nazwa hosta lub ścieżka'
289                             }
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.router.navigate('#' + value, {trigger: true});
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                 Backbone.history.start();
333         },
334
335         render: function() {
336             this.updateSelectAll();
337             this.updateFavorites();
338             this.$noItems.toggle(!Boolean(this.visibleFiles().length));
339             this.$error.hide();
340         },
341
342         load: function(location) {
343             if (!location)
344                 return;
345
346             var hostRootPath = location.split(/\/(\/|~)(.*)/), self = this;
347
348             this.host = hostRootPath[0];
349             this.$host.text(this.host);
350
351             this.path.reset([new Filex.PathBit({'name': hostRootPath[1], url: this.host + '/' + hostRootPath[1]})]);
352             _.each(hostRootPath[2].split('/'), function(item) {
353                 if (item)
354                     self.path.add(new Filex.PathBit({name: item, url: self.host + '/' + self.path.append(item)}));
355             });
356             this.path.last().set('active', true);
357
358             this.reloadFiles();
359             this.updateFavorites();
360
361             localStorage.last_location = location;
362         },
363
364         reloadFiles: function() {
365             this.busy();
366
367             var view = this;
368
369             this.files.fetch({
370                 reset: true,
371                 data: {host: this.host, path: this.path.full()},
372                 success: function() {
373                     view.render();
374                     view.idle();
375                 },
376                 error: function(collection, response) {
377                     view.files.reset();
378
379                     var msg = (response.responseJSON || {}).error || 'Błąd serwera';
380
381                     view.$noItems.hide();
382                     view.$error.find('.msg').text(msg);
383                     view.$error.show();
384                     view.idle();
385                 }
386             });
387         },
388
389         addPath: function(bit) {
390             var view = new Filex.Breadcrumb({model: bit});
391             this.$('.path').append(view.render().el);
392         },
393
394         resetPath: function(models, options) {
395             _.each(options.previousModels, function(model) {
396                 model.trigger('remove');
397             });
398
399             this.path.each(this.addPath, this);
400         },
401
402         resetFiles: function(models, options) {
403             _.each(options.previousModels, function(model) {
404                 model.trigger('remove');
405             });
406
407             this.files.each(function(file) {
408                 var view = new Filex.ListingItem({model: file, view: this});
409                 this.$('tbody').append(view.render().el);
410             }, this);
411         },
412
413         toggleHidden: function() {
414             this.files.each(function(item) { item.trigger('hidden'); }, this);
415             this.render();
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             var opts = this.hostSelectize.options,
494                 location = localStorage.last_location || opts[Object.keys(opts)[0]].value;
495
496             this.router.navigate('#' + location, {trigger: true, replace: true});
497         },
498
499         updateFavorites: function() {
500             var loc = this.host + '/' + this.path.full(),
501                 favorites = this.hostSelectize.options;
502
503             if (favorites.hasOwnProperty(loc)) {
504                 if (favorites[loc].group == 'sys') {
505                     this.$favorites.addClass('disabled').prop('disabled', true);
506                     if (this.$favorites.hasClass('active'))
507                         this.$favorites.button('toggle');
508                 }
509                 else {
510                     this.$favorites.removeClass('disabled').prop('disabled', false);
511                     if (!this.$favorites.hasClass('active'))
512                         this.$favorites.button('toggle');
513                 }
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
522         editHost: function() {
523             $('#host').addClass('edit');
524             this.hostSelectize.focus();
525         }
526     });
527 });