don't save old value for host selector
[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                 onItemAdd: function(value) {
300                     view.load(value);
301                     this.blur();
302                 },
303                 onBlur: function() {
304                     $('#host').removeClass('edit');
305                     this.clear();
306                 }
307             });
308             this.hostSelectize = this.$('#host-selector')[0].selectize;
309
310             this.render();
311         },
312
313         render: function() {
314             this.updateSelectAll();
315             this.updateFavorites();
316             this.$noItems.toggle(!Boolean(this.visibleFiles().length));
317             this.$error.hide();
318         },
319
320         load: function(location) {
321             var path = location.replace(/(^\/+|\/+$)/g, '').split('/'),
322                 host = path.shift(),
323                 pathBits = [new Filex.PathBit({'text': '/', 'path': ''})].concat(_.map(path, function(name) {
324                     return new Filex.PathBit({'text': name, 'path': name});
325                 }));
326
327             this.host = host;
328             this.path.reset(pathBits);
329
330             this.$host.text(this.host);
331         },
332
333         reloadFiles: function() {
334             this.busy();
335
336             var view = this;
337
338             this.files.fetch({
339                 reset: true,
340                 data: {host: this.host, path: this.path.full()},
341                 success: function() {
342                     view.render();
343                     view.idle();
344                 },
345                 error: function(collection, response) {
346                     view.files.reset();
347
348                     var msg = (response.responseJSON || {}).msg || 'Błąd serwera';
349
350                     view.$noItems.hide();
351                     view.$error.find('.msg').text(msg);
352                     view.$error.show();
353                     view.idle();
354                 }
355             });
356         },
357
358         addPath: function(bit) {
359             var view = new Filex.Breadcrumb({model: bit});
360             this.$('.path').append(view.render().el);
361         },
362
363         resetPath: function(models, options) {
364             _.each(options.previousModels, function(model) {
365                 model.trigger('remove');
366             });
367
368             this.path.each(this.addPath, this);
369         },
370
371         resetFiles: function(models, options) {
372             _.each(options.previousModels, function(model) {
373                 model.trigger('remove');
374             });
375
376             this.files.each(function(file) {
377                 var view = new Filex.ListingItem({model: file, view: this});
378                 this.$('tbody').append(view.render().el);
379             }, this);
380         },
381
382         toggleHidden: function() {
383             this.files.each(function(item) { item.trigger('hidden'); }, this);
384             this.render();
385         },
386
387         selectedDir: function(dir) {
388             this.path.add({'text': dir.get('name'), 'path': dir.get('name')});
389         },
390
391         selectedPath: function(bit) {
392             this.path.reset(this.path.slice(0, this.path.indexOf(bit) + 1));
393         },
394
395         showHidden: function() {
396             return this.$showHidden[0].checked;
397         },
398
399         busy: function() {
400             this.$el.addClass('busy');
401         },
402
403         idle: function() {
404             this.$el.removeClass('busy');
405         },
406
407         visibleFiles: function() {
408             return this.showHidden() ? this.files.models : this.files.visible();
409         },
410
411         selectedFiles: function() {
412             return _.filter(this.visibleFiles(), function(item) {
413                 return item.get('checked');
414             });
415         },
416
417         selectAll: function() {
418             var checked = this.$selectAll[0].checked;
419
420             _.each(this.visibleFiles(), function(item) {
421                 item.set('checked', checked);
422             })
423         },
424
425         updateSelectAll: function() {
426             if (this.visibleFiles().length) {
427                 this.$selectAll.prop('disabled', false);
428                 this.$selectAll.prop('checked', this.selectedFiles().length == this.visibleFiles().length);
429             }
430             else {
431                 this.$selectAll.prop('disabled', true);
432                 this.$selectAll.prop('checked', false);
433             }
434         },
435
436         clearSelection: function() {
437             _.each(this.visibleFiles(), function(item) {
438                 item.set('checked', false);
439             });
440         },
441
442         toggleFavorites: function() {
443             var $btn = this.$favorites,
444                 locations = this.hostSelectize,
445                 is_active = $btn.hasClass('active'),
446                 url = is_active ? '/filex/fav/delete/' : '/filex/fav/add/',
447                 data = {
448                     host: this.host,
449                     path: this.path.full()
450                 };
451
452             $btn.button('loading');
453
454             $.post(url, data, 'json').done(function () {
455                 $btn.button('reset');
456
457                 if (is_active) {
458                     locations.removeOption(data.host + data.path);
459                 }
460                 else {
461                     locations.addOption({
462                         group: 'usr',
463                         host: data.host,
464                         path: data.path,
465                         value: data.host + data.path
466                     });
467                 }
468             }).fail(function() {
469                 $btn.button('reset');
470                 $btn.button('toggle');
471
472                 console.error(arguments);
473             });
474         },
475
476         initialLoad: function() {
477             if (!this.host) {
478                 var opts = this.hostSelectize.options;
479
480                 this.load(opts[Object.keys(opts)[0]].value);
481             }
482         },
483
484         changedPath: function () {
485             this.reloadFiles();
486             this.updateFavorites();
487         },
488
489         updateFavorites: function() {
490             var loc = this.host + this.path.full(),
491                 favorites = this.hostSelectize.options;
492
493             if (favorites.hasOwnProperty(loc)) {
494                 if (favorites[loc].group == 'sys') {
495                     this.$favorites.addClass('disabled').prop('disabled', true);
496                     if (this.$favorites.hasClass('active'))
497                         this.$favorites.button('toggle');
498                 }
499                 else {
500                     this.$favorites.removeClass('disabled').prop('disabled', false);
501                     if (!this.$favorites.hasClass('active'))
502                         this.$favorites.button('toggle');
503                 }
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
512         editHost: function() {
513             $('#host').addClass('edit');
514             this.hostSelectize.focus();
515         }
516     });
517 });