5132ec78446023c45cc6e330428071a1cdb66f49
[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', this.setActive);
99             this.listenTo(this, 'add', this.setActive);
100         },
101
102         setActive: function() {
103             _.each(this.initial(), function(bit) {
104                 bit.set('active', false);
105             });
106
107             this.last().set('active', true);
108         },
109
110         full: function() {
111             return this.pluck('path').join('/') || '/';
112         }
113     });
114
115
116     // ------------------------------------------------------------------------
117     // Views
118     // ------------------------------------------------------------------------
119
120     Filex.ListingItem = Backbone.View.extend({
121         tagName:  'tr',
122
123         events: {
124             'click': 'click'
125         },
126
127         initialize: function(options) {
128             this.view = options.view;
129
130             this.listenTo(this.model, 'change', this.render);
131             this.listenTo(this.model, 'remove', this.remove);
132             this.listenTo(this.model, 'hidden', this.toggleHidden);
133         },
134
135         template: function() {
136             var templateSelector = this.model.isDir() ? '#dir-template' : '#file-template';
137
138             return _.template($(templateSelector).html());
139         },
140
141         render: function() {
142             var data = this.model.toJSON();
143             data['url_params'] = $.param({
144                 host: this.view.host,
145                 path: this.view.path.full(),
146                 name: this.model.get('name')
147             });
148             data['cid'] = this.model.cid;
149
150             this.$el.html(this.template()(data));
151             this.toggleHidden();
152
153             return this;
154         },
155
156         toggleHidden: function() {
157             this.$el.toggleClass('hidden', this.model.isHidden() && !this.view.showHidden());
158         },
159
160         click: function(e) {
161             if (e.target.className == 'link') {
162                 if (this.model.isDir()) {
163                     e.preventDefault();
164                     this.model.trigger('selected:dir', this.model);
165                 }
166
167                 return;
168             }
169
170             this.model.toggle();
171         }
172     });
173
174     Filex.Breadcrumb = Backbone.View.extend({
175         tagName: 'li',
176
177         events: {
178             'click a': 'selected'
179         },
180
181         initialize: function() {
182             this.listenTo(this.model, 'change:active', this.render);
183             this.listenTo(this.model, 'remove', this.remove);
184         },
185
186         render: function() {
187             if (this.model.get('active')) {
188                 this.$el.text(this.model.get('text'));
189             }
190             else {
191                 this.$el.html($('<a/>', {
192                     href: '#',
193                     text: this.model.get('text')
194                 }));
195             }
196             this.$el.toggleClass('active', this.model.get('active'));
197
198             return this;
199         },
200
201         selected: function(e) {
202             e.preventDefault();
203             this.model.trigger('selected', this.model);
204         }
205     });
206
207     Filex.FilexView = Backbone.View.extend({
208         el: $('#filex'),
209
210         events: {
211             'change #show-hidden': 'toggleHidden',
212             'click #select-all': 'selectAll',
213             'click #btn-refresh': 'reloadFiles'
214         },
215
216         initialize: function(options) {
217             this.$noItems = $('#no-items');
218             this.$error = $('#error');
219             this.$showHidden = $('#show-hidden');
220             this.$selectAll = $('#select-all');
221
222             this.host = options.host;
223             this.path = new Filex.Path();
224             this.files = new Filex.FileList();
225
226             this.listenTo(this.path, 'reset', this.resetPath);
227             this.listenTo(this.path, 'add', this.reloadFiles);
228             this.listenTo(this.path, 'add', this.addPath);
229             this.listenTo(this.path, 'selected', this.selectedPath);
230             this.listenTo(this.files, 'reset', this.resetFiles);
231             this.listenTo(this.files, 'selected:dir', this.selectedDir);
232             this.listenTo(this.files, 'change:checked', this.updateSelectAll);
233
234             // used in selectize callbacks
235             var view = this,
236                 optionTemplate = _.template('<div><div><%= host %></div><div class="small text-muted"><%= path %></div></div>');
237
238             this.$('#host-selector').selectize({
239                 valueField: 'host',
240                 labelField: 'host',
241                 searchField: ['host', 'path'],
242                 sortField: [
243                     {field: 'host', direction: 'asc'},
244                     {field: 'path', direction: 'asc'}
245                 ],
246                 items: [this.host],
247                 options: options.hostOptions,
248                 render: {
249                     option: function(item) {
250                         return optionTemplate(item);
251                     }
252                 },
253                 onDropdownClose: function() {
254                     this.blur();
255                 },
256                 onBlur: function() {
257                     var value = this.getValue();
258                     if (!value) {
259                         this.addItem(view.host, true);
260                         return;
261                     }
262
263                     if (value != view.host) {
264                         var location = this.options[value];
265
266                         view.host = location.host;
267                         view.navigate(location.path);
268                     }
269                 }
270             });
271             this.hostSelectize = this.$('#host-selector')[0].selectize;
272
273             this.render();
274             this.navigate(this.hostSelectize.options[this.host].path);
275         },
276
277         render: function() {
278             this.updateSelectAll();
279             this.$noItems.toggle(!Boolean(this.visibleFiles().length));
280             this.$error.hide();
281         },
282
283         navigate: function(path) {
284             var pathBits = [new Filex.PathBit({'text': '/', 'path': ''})];
285
286             pathBits = pathBits.concat(_.map(path.replace(/(^\/+|\/+$)/g, '').split('/'), function(name) {
287                 return new Filex.PathBit({'text': name, 'path': name});
288             }));
289
290             this.path.reset(pathBits);
291         },
292
293         reloadFiles: function() {
294             this.busy();
295
296             var view = this;
297
298             this.files.fetch({
299                 reset: true,
300                 data: {host: this.host, path: this.path.full()},
301                 success: function() {
302                     view.render();
303                     view.idle();
304                 },
305                 error: function(collection, response) {
306                     view.files.reset();
307
308                     var msg = (response.responseJSON || {}).msg || 'Błąd serwera';
309
310                     view.$noItems.hide();
311                     view.$error.find('.msg').text(msg);
312                     view.$error.show();
313                     view.idle();
314                 }
315             });
316         },
317
318         addPath: function(bit) {
319             var view = new Filex.Breadcrumb({model: bit});
320             this.$('.path').append(view.render().el);
321         },
322
323         resetPath: function(models, options) {
324             this.reloadFiles();
325
326             _.each(options.previousModels, function(model) {
327                 model.trigger('remove');
328             });
329
330             this.path.each(this.addPath, this);
331         },
332
333         resetFiles: function(models, options) {
334             _.each(options.previousModels, function(model) {
335                 model.trigger('remove');
336             });
337
338             this.files.each(function(file) {
339                 var view = new Filex.ListingItem({model: file, view: this});
340                 this.$('tbody').append(view.render().el);
341             }, this);
342         },
343
344         toggleHidden: function() {
345             this.files.each(function(item) { item.trigger('hidden'); }, this);
346             this.render();
347         },
348
349         selectedDir: function(dir) {
350             this.path.add({'text': dir.get('name'), 'path': dir.get('name')});
351         },
352
353         selectedPath: function(bit) {
354             var newPath = this.path.slice(0, this.path.indexOf(bit) + 1);
355             this.path.set(newPath);
356             this.reloadFiles();
357         },
358
359         showHidden: function() {
360             return this.$showHidden[0].checked;
361         },
362
363         busy: function() {
364             this.$el.addClass('busy');
365         },
366
367         idle: function() {
368             this.$el.removeClass('busy');
369         },
370
371         visibleFiles: function() {
372             return this.showHidden() ? this.files.models : this.files.visible();
373         },
374
375         selectedFiles: function() {
376             return _.filter(this.visibleFiles(), function(item) {
377                 return item.get('checked');
378             });
379         },
380
381         selectAll: function() {
382             var checked = this.$selectAll[0].checked;
383
384             _.each(this.visibleFiles(), function(item) {
385                 item.set('checked', checked);
386             })
387         },
388
389         updateSelectAll: function() {
390             if (this.visibleFiles().length) {
391                 this.$selectAll.prop('disabled', false);
392                 this.$selectAll.prop('checked', this.selectedFiles().length == this.visibleFiles().length);
393             }
394             else {
395                 this.$selectAll.prop('disabled', true);
396                 this.$selectAll.prop('checked', false);
397             }
398         },
399
400         clearSelection: function() {
401             _.each(this.visibleFiles(), function(item) {
402                 item.set('checked', false);
403             });
404
405             this.updateSelectAll();
406         }
407     });
408 });