move css rules to css file
[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         isDir: function() {
19             return false;
20         },
21
22         isFile: function() {
23             return true;
24         },
25
26         isHidden: function() {
27             return this.get('name')[0] == '.';
28         }
29     });
30
31     Filex.Directory = Filex.File.extend({
32         isDir: function() {
33             return true;
34         },
35
36         isFile: function() {
37             return false;
38         }
39     });
40
41     Filex.PathBit = OfflineModel.extend({
42         defaults: {
43             'active': false
44         }
45     });
46
47
48     // ------------------------------------------------------------------------
49     // Collections
50     // ------------------------------------------------------------------------
51
52     Filex.FileList = Backbone.Collection.extend({
53         url: '/filex/list/',
54
55         model: function(attrs, options) {
56             switch (attrs['type']) {
57                 case 'directory':
58                     return new Filex.Directory(attrs, options);
59                 case 'file':
60                     return new Filex.File(attrs, options);
61                 default:
62                     console.log('Unknown model type:', attrs['type']);
63             }
64         },
65
66         comparator: function(a, b) {
67             if (a.isFile() == b.isFile())
68                 return a.get('name').toLowerCase().localeCompare(b.get('name').toLowerCase());
69
70             return (a.isFile() && b.isDir()) ? 1 : -1;
71         },
72
73         hidden: function() {
74             return this.filter(function (item) {
75                 return item.isHidden();
76             })
77         },
78
79         visible: function () {
80             return this.filter(function (item) {
81                 return !item.isHidden();
82             })
83         }
84     });
85
86     Filex.Path = Backbone.Collection.extend({
87         model: Filex.PathBit,
88
89         initialize: function() {
90             this.listenTo(this, 'reset', this.setActive);
91             this.listenTo(this, 'add', this.setActive);
92         },
93
94         setActive: function() {
95             _.each(this.initial(), function(bit) {
96                 bit.set('active', false);
97             });
98
99             this.last().set('active', true);
100         },
101
102         full: function() {
103             return this.pluck('path').join('/') || '/';
104         }
105     });
106
107
108     // ------------------------------------------------------------------------
109     // Views
110     // ------------------------------------------------------------------------
111
112     Filex.ListingItem = Backbone.View.extend({
113         tagName:  'tr',
114
115         events: {
116             'click .link': 'selected'
117         },
118
119         initialize: function(options) {
120             this.view = options.view;
121
122             this.listenTo(this.model, 'remove', this.remove);
123             this.listenTo(this.model, 'hidden', this.toggleHidden);
124         },
125
126         template: function() {
127             var templateSelector = this.model.isDir() ? '#dir-template' : '#file-template';
128
129             return _.template($(templateSelector).html());
130         },
131
132         render: function() {
133             var data = this.model.toJSON();
134             data['url_params'] = $.param({
135                 host: this.view.host,
136                 path: this.view.path.full(),
137                 name: this.model.get('name')
138             });
139
140             this.$el.html(this.template()(data));
141             this.toggleHidden();
142
143             return this;
144         },
145
146         toggleHidden: function() {
147             this.$el.toggleClass('hidden', this.model.isHidden() && !this.view.showHidden());
148         },
149
150         selected: function(e) {
151             if (this.model.isDir()) {
152                 e.preventDefault();
153                 this.model.trigger('selected:dir', this.model);
154             }
155         }
156     });
157
158     Filex.Breadcrumb = Backbone.View.extend({
159         tagName: 'li',
160
161         events: {
162             'click a': 'selected'
163         },
164
165         initialize: function() {
166             this.listenTo(this.model, 'change:active', this.render);
167             this.listenTo(this.model, 'remove', this.remove);
168         },
169
170         render: function() {
171             if (this.model.get('active')) {
172                 this.$el.text(this.model.get('text'));
173             }
174             else {
175                 this.$el.html($('<a/>', {
176                     href: '#',
177                     text: this.model.get('text')
178                 }));
179             }
180             this.$el.toggleClass('active', this.model.get('active'));
181
182             return this;
183         },
184
185         selected: function(e) {
186             e.preventDefault();
187             this.model.trigger('selected', this.model);
188         }
189     });
190
191     Filex.FilexView = Backbone.View.extend({
192         el: $('#filex'),
193
194         events: {
195             'change #show-hidden': 'toggleHidden',
196             'click #host-controls .view': 'hostEdit'
197         },
198
199         initialize: function(options) {
200             this.$noItems = $('#no-items');
201             this.$error = $('#error');
202             this.$showHidden = $('#show-hidden');
203             this.$host = $('#host-controls');
204
205             this.host = options.host;
206             this.path = new Filex.Path();
207             this.files = new Filex.FileList();
208
209             this.listenTo(this.path, 'reset', this.resetPath);
210             this.listenTo(this.path, 'add', this.reloadFiles);
211             this.listenTo(this.path, 'add', this.addPath);
212             this.listenTo(this.path, 'selected', this.selectedPath);
213             this.listenTo(this.files, 'reset', this.resetFiles);
214             this.listenTo(this.files, 'selected:dir', this.selectedDir);
215
216             // used in selectize callbacks
217             var view = this,
218                 optionTemplate = _.template('<div><div><%= host %></div><div class="small text-muted"><%= path %></div></div>');
219
220             this.$('#host-selector').selectize({
221                 valueField: 'host',
222                 labelField: 'host',
223                 searchField: ['host', 'path'],
224                 sortField: [
225                     {field: 'host', direction: 'asc'},
226                     {field: 'path', direction: 'asc'}
227                 ],
228                 items: [this.host],
229                 options: options.hostOptions,
230                 render: {
231                     option: function(item) {
232                         return optionTemplate(item);
233                     }
234                 },
235                 onDropdownClose: function() {
236                     this.blur();
237                 },
238                 onBlur: function() {
239                     view.$host.toggleClass('editing');
240
241                     var value = this.getValue();
242                     if (!value) {
243                         this.addItem(view.host, true);
244                         return;
245                     }
246
247                     if (value != view.host) {
248                         var location = this.options[value];
249
250                         view.host = location.host;
251                         view.navigate(location.path);
252                     }
253                 }
254             });
255             this.hostSelectize = this.$('#host-selector')[0].selectize;
256
257             this.render();
258             this.navigate(this.hostSelectize.options[this.host].path);
259         },
260
261         render: function() {
262             this.$host.find('.view').text(this.host);
263             this.$noItems.toggle((this.showHidden() ? !Boolean(this.files.length) : !Boolean(this.files.visible().length)));
264             this.$error.hide();
265         },
266
267         navigate: function(path) {
268             var pathBits = [new Filex.PathBit({'text': '/', 'path': ''})];
269
270             pathBits = pathBits.concat(_.map(path.replace(/(^\/+|\/+$)/g, '').split('/'), function(name) {
271                 return new Filex.PathBit({'text': name, 'path': name});
272             }));
273
274             this.path.reset(pathBits);
275         },
276
277         reloadFiles: function() {
278             this.busy();
279
280             var view = this;
281
282             this.files.fetch({
283                 reset: true,
284                 data: {host: this.host, path: this.path.full()},
285                 success: function() {
286                     view.render();
287                     view.idle();
288                 },
289                 error: function(collection, response) {
290                     view.files.reset();
291
292                     var msg = (response.responseJSON || {}).msg || 'Błąd serwera';
293
294                     view.$noItems.hide();
295                     view.$error.find('.msg').text(msg);
296                     view.$error.show();
297                     view.idle();
298                 }
299             });
300         },
301
302         addPath: function(bit) {
303             var view = new Filex.Breadcrumb({model: bit});
304             this.$('.path').append(view.render().el);
305         },
306
307         resetPath: function(models, options) {
308             this.reloadFiles();
309
310             _.each(options.previousModels, function(model) {
311                 model.trigger('remove');
312             });
313
314             this.path.each(this.addPath, this);
315         },
316
317         resetFiles: function(models, options) {
318             _.each(options.previousModels, function(model) {
319                 model.trigger('remove');
320             });
321
322             this.files.each(function(file) {
323                 var view = new Filex.ListingItem({model: file, view: this});
324                 this.$('tbody').append(view.render().el);
325             }, this);
326         },
327
328         toggleHidden: function() {
329             this.files.each(function(item) { item.trigger('hidden'); }, this);
330             this.render();
331         },
332
333         selectedDir: function(dir) {
334             this.path.add({'text': dir.get('name'), 'path': dir.get('name')});
335         },
336
337         selectedPath: function(bit) {
338             var newPath = this.path.slice(0, this.path.indexOf(bit) + 1);
339             this.path.set(newPath);
340             this.reloadFiles();
341         },
342
343         showHidden: function() {
344             return this.$showHidden[0].checked;
345         },
346
347         hostEdit: function() {
348             this.$host.toggleClass('editing');
349             this.hostSelectize.focus();
350         },
351
352         busy: function() {
353             this.$el.addClass('busy');
354         },
355
356         idle: function() {
357             this.$el.removeClass('busy');
358         }
359     });
360 });