extract validators and decorator to utils
[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('#id_host').val(parts.shift());
269
270                     if (parts.length)
271                         $form.find('#id_path').val(parts.join('/'));
272
273                     $form.off();
274                     $form.on('submit', function(e) {
275                         var $this = $(this),
276                             $btn = $this.find('[type="submit"]');
277
278                         e.preventDefault();
279                         $btn.button('loading');
280
281                         $.post($this.attr('action'), $this.serialize(), function(data) {
282                             callback(data);
283                             callback_called = true;
284
285                             $this.modal('hide');
286                             $btn.button('reset');
287                         }, 'json').fail(function() {
288                             console.error(arguments);
289                             $btn.button('error');
290                         });
291                     });
292
293                     $form.one('hide.bs.modal', function() {
294                         if (!callback_called)
295                             callback();
296                         $form.off();
297                     });
298
299                     $form.modal();
300                 },
301                 render: {
302                     option: function(item) {
303                         return optionTemplate(item);
304                     },
305                     option_create : function(data, escape) {
306                         return '<div class="create">Dodaj <em>' + escape(data.input) + '</em>&hellip;</div>';
307                     }
308                 },
309                 onItemAdd: function(value) {
310                     view.load(value);
311                     this.blur();
312                 },
313                 onBlur: function() {
314                     $('#host').removeClass('edit');
315                     this.clear();
316                 }
317             });
318             this.hostSelectize = this.$('#host-selector')[0].selectize;
319
320             this.render();
321         },
322
323         render: function() {
324             this.updateSelectAll();
325             this.updateFavorites();
326             this.$noItems.toggle(!Boolean(this.visibleFiles().length));
327             this.$error.hide();
328         },
329
330         load: function(location) {
331             var hostRootPath = location.split(/\/(\/|~)(.*)/),
332                 pathBits = [new Filex.PathBit({'name': hostRootPath[1]})].concat(
333                     _.chain(hostRootPath[2].split('/'))
334                         .filter(_.identity)
335                         .map(function(name) { return new Filex.PathBit({'name': name}) })
336                         .value()
337                 );
338
339             this.host = hostRootPath[0];
340             this.path.reset(pathBits);
341
342             this.$host.text(this.host);
343         },
344
345         reloadFiles: function() {
346             this.busy();
347
348             var view = this;
349
350             this.files.fetch({
351                 reset: true,
352                 data: {host: this.host, path: this.path.full()},
353                 success: function() {
354                     view.render();
355                     view.idle();
356                 },
357                 error: function(collection, response) {
358                     view.files.reset();
359
360                     var msg = (response.responseJSON || {}).error || 'Błąd serwera';
361
362                     view.$noItems.hide();
363                     view.$error.find('.msg').text(msg);
364                     view.$error.show();
365                     view.idle();
366                 }
367             });
368         },
369
370         addPath: function(bit) {
371             var view = new Filex.Breadcrumb({model: bit});
372             this.$('.path').append(view.render().el);
373         },
374
375         resetPath: function(models, options) {
376             _.each(options.previousModels, function(model) {
377                 model.trigger('remove');
378             });
379
380             this.path.each(this.addPath, this);
381         },
382
383         resetFiles: function(models, options) {
384             _.each(options.previousModels, function(model) {
385                 model.trigger('remove');
386             });
387
388             this.files.each(function(file) {
389                 var view = new Filex.ListingItem({model: file, view: this});
390                 this.$('tbody').append(view.render().el);
391             }, this);
392         },
393
394         toggleHidden: function() {
395             this.files.each(function(item) { item.trigger('hidden'); }, this);
396             this.render();
397         },
398
399         selectedDir: function(dir) {
400             this.path.add({'name': dir.get('name')});
401         },
402
403         selectedPath: function(bit) {
404             this.path.reset(this.path.slice(0, this.path.indexOf(bit) + 1));
405         },
406
407         showHidden: function() {
408             return this.$showHidden[0].checked;
409         },
410
411         busy: function() {
412             this.$el.addClass('busy');
413         },
414
415         idle: function() {
416             this.$el.removeClass('busy');
417         },
418
419         visibleFiles: function() {
420             return this.showHidden() ? this.files.models : this.files.visible();
421         },
422
423         selectedFiles: function() {
424             return _.filter(this.visibleFiles(), function(item) {
425                 return item.get('checked');
426             });
427         },
428
429         selectAll: function() {
430             var checked = this.$selectAll[0].checked;
431
432             _.each(this.visibleFiles(), function(item) {
433                 item.set('checked', checked);
434             })
435         },
436
437         updateSelectAll: function() {
438             if (this.visibleFiles().length) {
439                 this.$selectAll.prop('disabled', false);
440                 this.$selectAll.prop('checked', this.selectedFiles().length == this.visibleFiles().length);
441             }
442             else {
443                 this.$selectAll.prop('disabled', true);
444                 this.$selectAll.prop('checked', false);
445             }
446         },
447
448         clearSelection: function() {
449             _.each(this.visibleFiles(), function(item) {
450                 item.set('checked', false);
451             });
452         },
453
454         toggleFavorites: function() {
455             var $btn = this.$favorites,
456                 locations = this.hostSelectize,
457                 is_active = $btn.hasClass('active'),
458                 url = is_active ? '/filex/fav/delete/' : '/filex/fav/add/',
459                 data = {
460                     host: this.host,
461                     path: this.path.full()
462                 };
463
464             $btn.button('loading');
465
466             $.post(url, data, 'json').done(function (response) {
467                 $btn.button('reset');
468
469                 if (is_active)
470                     locations.removeOption(data.host + '/' + data.path);
471                 else
472                     locations.addOption(response);
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 });