validate user data in gridftp
[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('path').join('/') || '/';
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             this.$el.toggleClass('hidden', this.model.isHidden() && !this.view.showHidden());
156         },
157
158         toggleChecked: function(obj, value) {
159             this.$el.toggleClass('active', value);
160             this.$el.find('input[type="checkbox"]').prop('checked', value);
161         },
162
163         click: function(e) {
164             if (e.target.className == 'link') {
165                 if (this.model.isDir()) {
166                     e.preventDefault();
167                     this.model.trigger('selected:dir', this.model);
168                 }
169
170                 return;
171             }
172
173             this.model.toggle();
174         }
175     });
176
177     Filex.Breadcrumb = Backbone.View.extend({
178         tagName: 'li',
179
180         events: {
181             'click a': 'selected'
182         },
183
184         initialize: function() {
185             this.listenTo(this.model, 'change:active', this.render);
186             this.listenTo(this.model, 'remove', this.remove);
187         },
188
189         render: function() {
190             if (this.model.get('active')) {
191                 this.$el.text(this.model.get('text'));
192             }
193             else {
194                 this.$el.html($('<a/>', {
195                     href: '#',
196                     text: this.model.get('text')
197                 }));
198             }
199             this.$el.toggleClass('active', this.model.get('active'));
200
201             return this;
202         },
203
204         selected: function(e) {
205             e.preventDefault();
206             this.model.trigger('selected', this.model);
207         }
208     });
209
210     Filex.FilexView = Backbone.View.extend({
211         el: $('#filex'),
212
213         events: {
214             'change #show-hidden': 'toggleHidden',
215             'click #select-all': 'selectAll',
216             'click #btn-refresh': 'reloadFiles',
217             'click #btn-favorites': 'toggleFavorites',
218             'click #btn-host': 'editHost'
219         },
220
221         initialize: function(options) {
222             this.$noItems = $('#no-items');
223             this.$error = $('#error');
224             this.$showHidden = $('#show-hidden');
225             this.$selectAll = $('#select-all');
226             this.$favorites = $('#btn-favorites');
227             this.$host = $('#btn-host');
228
229             this.path = new Filex.Path();
230             this.files = new Filex.FileList();
231
232             this.listenTo(this.path, 'reset', this.resetPath);
233             this.listenTo(this.path, 'add', this.addPath);
234             this.listenTo(this.path, 'add reset', this.changedPath);
235             this.listenTo(this.path, 'selected', this.selectedPath);
236             this.listenTo(this.files, 'reset', this.resetFiles);
237             this.listenTo(this.files, 'selected:dir', this.selectedDir);
238             this.listenTo(this.files, 'change:checked', this.updateSelectAll);
239
240             // used in selectize callbacks
241             var view = this,
242                 optionTemplate = _.template('<div><div><%= host %></div><div class="small text-muted"><%= path %></div></div>');
243
244             this.$('#host-selector').selectize({
245                 optgroupField: 'group',
246                 labelField: 'host',
247                 searchField: ['host', 'path'],
248                 sortField: [
249                     {field: 'host', direction: 'asc'},
250                     {field: 'path', direction: 'asc'}
251                 ],
252                 options: options.locations,
253                 optgroups: [
254                     {value: 'sys', label: 'Podstawowe'},
255                     {value: 'usr', label: 'Użytkownika'}
256                 ],
257                 lockOptgroupOrder: true,
258                 create: function(input, callback) {
259                     var $form = $('#favorite-form'),
260                         parts = input.split('/', 1),
261                         callback_called = false;
262
263                     $form.find('#id_host').val(parts[0]);
264
265                     if (parts.length > 1)
266                         $form.find('#id_path').val(parts[1]);
267
268                     $form.on('submit', function(e) {
269                         var $this = $(this),
270                             $btn = $this.find('[type="submit"]');
271
272                         e.preventDefault();
273                         $btn.button('loading');
274
275                         $.post($this.attr('action'), $this.serialize(), function(data) {
276                             callback(data);
277                             callback_called = true;
278
279                             $this.modal('hide');
280                             $btn.button('reset');
281                         }, 'json').fail(function() {
282                             console.error(arguments);
283                             $btn.button('error');
284                         });
285                     });
286
287                     $form.one('hide.bs.modal', function() {
288                         if (!callback_called)
289                             callback();
290                         $form.off();
291                     });
292
293                     $form.modal();
294                 },
295                 render: {
296                     option: function(item) {
297                         return optionTemplate(item);
298                     },
299                     option_create : function(data, escape) {
300                         return '<div class="create">Dodaj <em>' + escape(data.input) + '</em>&hellip;</div>';
301                     }
302                 },
303                 onItemAdd: function(value) {
304                     view.load(value);
305                     this.blur();
306                 },
307                 onBlur: function() {
308                     $('#host').removeClass('edit');
309                     this.clear();
310                 }
311             });
312             this.hostSelectize = this.$('#host-selector')[0].selectize;
313
314             this.render();
315         },
316
317         render: function() {
318             this.updateSelectAll();
319             this.updateFavorites();
320             this.$noItems.toggle(!Boolean(this.visibleFiles().length));
321             this.$error.hide();
322         },
323
324         load: function(location) {
325             var path = location.replace(/(^\/+|\/+$)/g, '').split('/'),
326                 host = path.shift(),
327                 pathBits = [new Filex.PathBit({'text': '/', 'path': ''})].concat(_.map(path, function(name) {
328                     return new Filex.PathBit({'text': name, 'path': name});
329                 }));
330
331             this.host = host;
332             this.path.reset(pathBits);
333
334             this.$host.text(this.host);
335         },
336
337         reloadFiles: function() {
338             this.busy();
339
340             var view = this;
341
342             this.files.fetch({
343                 reset: true,
344                 data: {host: this.host, path: this.path.full()},
345                 success: function() {
346                     view.render();
347                     view.idle();
348                 },
349                 error: function(collection, response) {
350                     view.files.reset();
351
352                     var msg = (response.responseJSON || {}).error || 'Błąd serwera';
353
354                     view.$noItems.hide();
355                     view.$error.find('.msg').text(msg);
356                     view.$error.show();
357                     view.idle();
358                 }
359             });
360         },
361
362         addPath: function(bit) {
363             var view = new Filex.Breadcrumb({model: bit});
364             this.$('.path').append(view.render().el);
365         },
366
367         resetPath: function(models, options) {
368             _.each(options.previousModels, function(model) {
369                 model.trigger('remove');
370             });
371
372             this.path.each(this.addPath, this);
373         },
374
375         resetFiles: function(models, options) {
376             _.each(options.previousModels, function(model) {
377                 model.trigger('remove');
378             });
379
380             this.files.each(function(file) {
381                 var view = new Filex.ListingItem({model: file, view: this});
382                 this.$('tbody').append(view.render().el);
383             }, this);
384         },
385
386         toggleHidden: function() {
387             this.files.each(function(item) { item.trigger('hidden'); }, this);
388             this.render();
389         },
390
391         selectedDir: function(dir) {
392             this.path.add({'text': dir.get('name'), 'path': dir.get('name')});
393         },
394
395         selectedPath: function(bit) {
396             this.path.reset(this.path.slice(0, this.path.indexOf(bit) + 1));
397         },
398
399         showHidden: function() {
400             return this.$showHidden[0].checked;
401         },
402
403         busy: function() {
404             this.$el.addClass('busy');
405         },
406
407         idle: function() {
408             this.$el.removeClass('busy');
409         },
410
411         visibleFiles: function() {
412             return this.showHidden() ? this.files.models : this.files.visible();
413         },
414
415         selectedFiles: function() {
416             return _.filter(this.visibleFiles(), function(item) {
417                 return item.get('checked');
418             });
419         },
420
421         selectAll: function() {
422             var checked = this.$selectAll[0].checked;
423
424             _.each(this.visibleFiles(), function(item) {
425                 item.set('checked', checked);
426             })
427         },
428
429         updateSelectAll: function() {
430             if (this.visibleFiles().length) {
431                 this.$selectAll.prop('disabled', false);
432                 this.$selectAll.prop('checked', this.selectedFiles().length == this.visibleFiles().length);
433             }
434             else {
435                 this.$selectAll.prop('disabled', true);
436                 this.$selectAll.prop('checked', false);
437             }
438         },
439
440         clearSelection: function() {
441             _.each(this.visibleFiles(), function(item) {
442                 item.set('checked', false);
443             });
444         },
445
446         toggleFavorites: function() {
447             var $btn = this.$favorites,
448                 locations = this.hostSelectize,
449                 is_active = $btn.hasClass('active'),
450                 url = is_active ? '/filex/fav/delete/' : '/filex/fav/add/',
451                 data = {
452                     host: this.host,
453                     path: this.path.full()
454                 };
455
456             $btn.button('loading');
457
458             $.post(url, data, 'json').done(function () {
459                 $btn.button('reset');
460
461                 if (is_active) {
462                     locations.removeOption(data.host + data.path);
463                 }
464                 else {
465                     locations.addOption({
466                         group: 'usr',
467                         host: data.host,
468                         path: data.path,
469                         value: data.host + data.path
470                     });
471                 }
472             }).fail(function() {
473                 $btn.button('reset');
474                 $btn.button('toggle');
475
476                 console.error(arguments);
477             });
478         },
479
480         initialLoad: function() {
481             if (!this.host) {
482                 var opts = this.hostSelectize.options;
483
484                 this.load(opts[Object.keys(opts)[0]].value);
485             }
486         },
487
488         changedPath: function () {
489             this.reloadFiles();
490             this.updateFavorites();
491         },
492
493         updateFavorites: function() {
494             var loc = this.host + this.path.full(),
495                 favorites = this.hostSelectize.options;
496
497             if (favorites.hasOwnProperty(loc)) {
498                 if (favorites[loc].group == 'sys') {
499                     this.$favorites.addClass('disabled').prop('disabled', true);
500                     if (this.$favorites.hasClass('active'))
501                         this.$favorites.button('toggle');
502                 }
503                 else {
504                     this.$favorites.removeClass('disabled').prop('disabled', false);
505                     if (!this.$favorites.hasClass('active'))
506                         this.$favorites.button('toggle');
507                 }
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
516         editHost: function() {
517             $('#host').addClass('edit');
518             this.hostSelectize.focus();
519         }
520     });
521 });