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