68051c3d077c2a2d3eb99aa549aee556509d9e58
[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 #host-controls .view': 'hostEdit',
213             'click #select-all': 'selectAll',
214             'click #btn-refresh': 'reloadFiles'
215         },
216
217         initialize: function(options) {
218             this.$noItems = $('#no-items');
219             this.$error = $('#error');
220             this.$showHidden = $('#show-hidden');
221             this.$host = $('#host-controls');
222             this.$selectAll = $('#select-all');
223
224             this.host = options.host;
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.reloadFiles);
230             this.listenTo(this.path, 'add', this.addPath);
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                 valueField: 'host',
242                 labelField: 'host',
243                 searchField: ['host', 'path'],
244                 sortField: [
245                     {field: 'host', direction: 'asc'},
246                     {field: 'path', direction: 'asc'}
247                 ],
248                 items: [this.host],
249                 options: options.hostOptions,
250                 render: {
251                     option: function(item) {
252                         return optionTemplate(item);
253                     }
254                 },
255                 onDropdownClose: function() {
256                     this.blur();
257                 },
258                 onBlur: function() {
259                     view.$host.toggleClass('editing');
260
261                     var value = this.getValue();
262                     if (!value) {
263                         this.addItem(view.host, true);
264                         return;
265                     }
266
267                     if (value != view.host) {
268                         var location = this.options[value];
269
270                         view.host = location.host;
271                         view.navigate(location.path);
272                     }
273                 }
274             });
275             this.hostSelectize = this.$('#host-selector')[0].selectize;
276
277             this.render();
278             this.navigate(this.hostSelectize.options[this.host].path);
279         },
280
281         render: function() {
282             this.updateSelectAll();
283             this.$host.find('.view').text(this.host);
284             this.$noItems.toggle(!Boolean(this.visibleFiles().length));
285             this.$error.hide();
286         },
287
288         navigate: function(path) {
289             var pathBits = [new Filex.PathBit({'text': '/', 'path': ''})];
290
291             pathBits = pathBits.concat(_.map(path.replace(/(^\/+|\/+$)/g, '').split('/'), function(name) {
292                 return new Filex.PathBit({'text': name, 'path': name});
293             }));
294
295             this.path.reset(pathBits);
296         },
297
298         reloadFiles: function() {
299             this.busy();
300
301             var view = this;
302
303             this.files.fetch({
304                 reset: true,
305                 data: {host: this.host, path: this.path.full()},
306                 success: function() {
307                     view.render();
308                     view.idle();
309                 },
310                 error: function(collection, response) {
311                     view.files.reset();
312
313                     var msg = (response.responseJSON || {}).msg || 'Błąd serwera';
314
315                     view.$noItems.hide();
316                     view.$error.find('.msg').text(msg);
317                     view.$error.show();
318                     view.idle();
319                 }
320             });
321         },
322
323         addPath: function(bit) {
324             var view = new Filex.Breadcrumb({model: bit});
325             this.$('.path').append(view.render().el);
326         },
327
328         resetPath: function(models, options) {
329             this.reloadFiles();
330
331             _.each(options.previousModels, function(model) {
332                 model.trigger('remove');
333             });
334
335             this.path.each(this.addPath, this);
336         },
337
338         resetFiles: function(models, options) {
339             _.each(options.previousModels, function(model) {
340                 model.trigger('remove');
341             });
342
343             this.files.each(function(file) {
344                 var view = new Filex.ListingItem({model: file, view: this});
345                 this.$('tbody').append(view.render().el);
346             }, this);
347         },
348
349         toggleHidden: function() {
350             this.files.each(function(item) { item.trigger('hidden'); }, this);
351             this.render();
352         },
353
354         selectedDir: function(dir) {
355             this.path.add({'text': dir.get('name'), 'path': dir.get('name')});
356         },
357
358         selectedPath: function(bit) {
359             var newPath = this.path.slice(0, this.path.indexOf(bit) + 1);
360             this.path.set(newPath);
361             this.reloadFiles();
362         },
363
364         showHidden: function() {
365             return this.$showHidden[0].checked;
366         },
367
368         hostEdit: function() {
369             this.$host.toggleClass('editing');
370             this.hostSelectize.focus();
371         },
372
373         busy: function() {
374             this.$el.addClass('busy');
375         },
376
377         idle: function() {
378             this.$el.removeClass('busy');
379         },
380
381         visibleFiles: function() {
382             return this.showHidden() ? this.files.models : this.files.visible();
383         },
384
385         selectedFiles: function() {
386             return _.filter(this.visibleFiles(), function(item) {
387                 return item.get('checked');
388             });
389         },
390
391         selectAll: function() {
392             var checked = this.$selectAll[0].checked;
393
394             _.each(this.visibleFiles(), function(item) {
395                 item.set('checked', checked);
396             })
397         },
398
399         updateSelectAll: function() {
400             this.$selectAll.prop('checked', this.selectedFiles().length == this.visibleFiles().length);
401         },
402
403         clearSelection: function() {
404             _.each(this.visibleFiles(), function(item) {
405                 item.set('checked', false);
406             });
407
408             this.updateSelectAll();
409         }
410     });
411 });