ac71ea549224049788ed7575b13ea3ab48430b88
[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('.alert-danger').remove();
269                     $form.find('#id_host').val(parts.shift());
270
271                     if (parts.length)
272                         $form.find('#id_path').val(parts.join('/'));
273
274                     $form.off();
275                     $form.on('submit', function(e) {
276                         var $this = $(this),
277                             $btn = $this.find('[type="submit"]');
278
279                         e.preventDefault();
280                         $btn.button('loading');
281                         $this.find('.alert-danger').remove();
282
283                         $.post($this.attr('action'), $this.serialize(), function(data) {
284                             callback(data);
285                             callback_called = true;
286
287                             $this.modal('hide');
288                             $btn.button('reset');
289                         }, 'json').fail(function(xhr) {
290                             console.error(arguments);
291                             var error = (xhr.responseJSON || {}).error || undefined;
292
293                             if (error.__all__) {
294                                 error = 'Podana lokalizacja jest już zapisana'
295                             }
296                             else if (xhr.status == 400) {
297                                 error = 'Niepoprawna nazwa hosta lub ścieżka'
298                             }
299
300                             if (typeof error === 'string') {
301                                 $('<div>', {
302                                     'class': 'alert alert-danger',
303                                     html: '<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> ' + error
304                                 }).prependTo($this.find('.modal-body'));
305                                 $btn.button('reset');
306                             }
307                             else {
308                                 $btn.button('error');
309                             }
310                         });
311                     });
312
313                     $form.one('hide.bs.modal', function() {
314                         if (!callback_called)
315                             callback();
316                         $form.off();
317                     });
318
319                     $form.modal();
320                 },
321                 render: {
322                     option: function(item) {
323                         return optionTemplate(item);
324                     },
325                     option_create : function(data, escape) {
326                         return '<div class="create">Dodaj <em>' + escape(data.input) + '</em>&hellip;</div>';
327                     }
328                 },
329                 onItemAdd: function(value) {
330                     view.load(value);
331                     this.blur();
332                 },
333                 onBlur: function() {
334                     $('#host').removeClass('edit');
335                     this.clear();
336                 }
337             });
338             this.hostSelectize = this.$('#host-selector')[0].selectize;
339
340             this.render();
341         },
342
343         render: function() {
344             this.updateSelectAll();
345             this.updateFavorites();
346             this.$noItems.toggle(!Boolean(this.visibleFiles().length));
347             this.$error.hide();
348         },
349
350         load: function(location) {
351             var hostRootPath = location.split(/\/(\/|~)(.*)/),
352                 pathBits = [new Filex.PathBit({'name': hostRootPath[1]})].concat(
353                     _.chain(hostRootPath[2].split('/'))
354                         .filter(_.identity)
355                         .map(function(name) { return new Filex.PathBit({'name': name}) })
356                         .value()
357                 );
358
359             this.host = hostRootPath[0];
360             this.path.reset(pathBits);
361
362             this.$host.text(this.host);
363         },
364
365         reloadFiles: function() {
366             this.busy();
367
368             var view = this;
369
370             this.files.fetch({
371                 reset: true,
372                 data: {host: this.host, path: this.path.full()},
373                 success: function() {
374                     view.render();
375                     view.idle();
376                 },
377                 error: function(collection, response) {
378                     view.files.reset();
379
380                     var msg = (response.responseJSON || {}).error || 'Błąd serwera';
381
382                     view.$noItems.hide();
383                     view.$error.find('.msg').text(msg);
384                     view.$error.show();
385                     view.idle();
386                 }
387             });
388         },
389
390         addPath: function(bit) {
391             var view = new Filex.Breadcrumb({model: bit});
392             this.$('.path').append(view.render().el);
393         },
394
395         resetPath: function(models, options) {
396             _.each(options.previousModels, function(model) {
397                 model.trigger('remove');
398             });
399
400             this.path.each(this.addPath, this);
401         },
402
403         resetFiles: function(models, options) {
404             _.each(options.previousModels, function(model) {
405                 model.trigger('remove');
406             });
407
408             this.files.each(function(file) {
409                 var view = new Filex.ListingItem({model: file, view: this});
410                 this.$('tbody').append(view.render().el);
411             }, this);
412         },
413
414         toggleHidden: function() {
415             this.files.each(function(item) { item.trigger('hidden'); }, this);
416             this.render();
417         },
418
419         selectedDir: function(dir) {
420             this.path.add({'name': dir.get('name')});
421         },
422
423         selectedPath: function(bit) {
424             this.path.reset(this.path.slice(0, this.path.indexOf(bit) + 1));
425         },
426
427         showHidden: function() {
428             return this.$showHidden[0].checked;
429         },
430
431         busy: function() {
432             this.$el.addClass('busy');
433         },
434
435         idle: function() {
436             this.$el.removeClass('busy');
437         },
438
439         visibleFiles: function() {
440             return this.showHidden() ? this.files.models : this.files.visible();
441         },
442
443         selectedFiles: function() {
444             return _.filter(this.visibleFiles(), function(item) {
445                 return item.get('checked');
446             });
447         },
448
449         selectAll: function() {
450             var checked = this.$selectAll[0].checked;
451
452             _.each(this.visibleFiles(), function(item) {
453                 item.set('checked', checked);
454             })
455         },
456
457         updateSelectAll: function() {
458             if (this.visibleFiles().length) {
459                 this.$selectAll.prop('disabled', false);
460                 this.$selectAll.prop('checked', this.selectedFiles().length == this.visibleFiles().length);
461             }
462             else {
463                 this.$selectAll.prop('disabled', true);
464                 this.$selectAll.prop('checked', false);
465             }
466         },
467
468         clearSelection: function() {
469             _.each(this.visibleFiles(), function(item) {
470                 item.set('checked', false);
471             });
472         },
473
474         toggleFavorites: function() {
475             var $btn = this.$favorites,
476                 locations = this.hostSelectize,
477                 is_active = $btn.hasClass('active'),
478                 url = is_active ? '/filex/fav/delete/' : '/filex/fav/add/',
479                 data = {
480                     host: this.host,
481                     path: this.path.full()
482                 };
483
484             $btn.button('loading');
485
486             $.post(url, data, 'json').done(function (response) {
487                 $btn.button('reset');
488
489                 if (is_active)
490                     locations.removeOption(data.host + '/' + data.path);
491                 else
492                     locations.addOption(response);
493             }).fail(function() {
494                 $btn.button('reset');
495                 $btn.button('toggle');
496
497                 console.error(arguments);
498             });
499         },
500
501         initialLoad: function() {
502             if (!this.host) {
503                 var opts = this.hostSelectize.options;
504
505                 this.load(opts[Object.keys(opts)[0]].value);
506             }
507         },
508
509         changedPath: function () {
510             this.reloadFiles();
511             this.updateFavorites();
512         },
513
514         updateFavorites: function() {
515             var loc = this.host + '/' + this.path.full(),
516                 favorites = this.hostSelectize.options;
517
518             if (favorites.hasOwnProperty(loc)) {
519                 if (favorites[loc].group == 'sys') {
520                     this.$favorites.addClass('disabled').prop('disabled', true);
521                     if (this.$favorites.hasClass('active'))
522                         this.$favorites.button('toggle');
523                 }
524                 else {
525                     this.$favorites.removeClass('disabled').prop('disabled', false);
526                     if (!this.$favorites.hasClass('active'))
527                         this.$favorites.button('toggle');
528                 }
529             }
530             else {
531                 this.$favorites.removeClass('disabled').prop('disabled', false);
532                 if (this.$favorites.hasClass('active'))
533                     this.$favorites.button('toggle');
534             }
535         },
536
537         editHost: function() {
538             $('#host').addClass('edit');
539             this.hostSelectize.focus();
540         }
541     });
542 });