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