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