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