signal selecting 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             e.preventDefault();
152             this.model.trigger(this.model.isDir() ? 'selected:dir' : 'selected:file', this.model);
153         }
154     });
155
156     Filex.Breadcrumb = Backbone.View.extend({
157         tagName: 'li',
158
159         events: {
160             'click a': 'selected'
161         },
162
163         initialize: function() {
164             this.listenTo(this.model, 'change:active', this.render);
165             this.listenTo(this.model, 'remove', this.remove);
166         },
167
168         render: function() {
169             if (this.model.get('active')) {
170                 this.$el.text(this.model.get('text'));
171             }
172             else {
173                 this.$el.html($('<a/>', {
174                     href: '#',
175                     text: this.model.get('text')
176                 }));
177             }
178             this.$el.toggleClass('active', this.model.get('active'));
179
180             return this;
181         },
182
183         selected: function(e) {
184             e.preventDefault();
185             this.model.trigger('selected', this.model);
186         }
187     });
188
189     Filex.FilexView = Backbone.View.extend({
190         el: $('#filex'),
191
192         events: {
193             'change #show-hidden': 'toggleHidden',
194             'click #host-controls .view': 'hostEdit'
195         },
196
197         initialize: function(options) {
198             this.$noItems = $('#no-items');
199             this.$error = $('#error');
200             this.$showHidden = $('#show-hidden');
201             this.$host = $('#host-controls');
202
203             this.host = options.host;
204             this.path = new Filex.Path();
205             this.files = new Filex.FileList();
206
207             this.listenTo(this.path, 'reset', this.resetPath);
208             this.listenTo(this.path, 'add', this.reloadFiles);
209             this.listenTo(this.path, 'add', this.addPath);
210             this.listenTo(this.path, 'selected', this.selectedPath);
211             this.listenTo(this.files, 'reset', this.resetFiles);
212             this.listenTo(this.files, 'selected:dir', this.selectedDir);
213             this.listenTo(this.files, 'selected:file', this.selectedFile);
214
215             // used in selectize callbacks
216             var view = this,
217                 optionTemplate = _.template('<div><div><%= host %></div><div class="small text-muted"><%= path %></div></div>');
218
219             this.$('#host-selector').selectize({
220                 valueField: 'host',
221                 labelField: 'host',
222                 searchField: ['host', 'path'],
223                 sortField: [
224                     {field: 'host', direction: 'asc'},
225                     {field: 'path', direction: 'asc'}
226                 ],
227                 items: [this.host],
228                 options: options.hostOptions,
229                 render: {
230                     option: function(item) {
231                         return optionTemplate(item);
232                     }
233                 },
234                 onDropdownClose: function() {
235                     this.blur();
236                 },
237                 onBlur: function() {
238                     view.$host.toggleClass('editing');
239
240                     var value = this.getValue();
241                     if (!value) {
242                         this.addItem(view.host, true);
243                         return;
244                     }
245
246                     if (value != view.host) {
247                         var location = this.options[value];
248
249                         view.host = location.host;
250                         view.navigate(location.path);
251                     }
252                 }
253             });
254             this.hostSelectize = this.$('#host-selector')[0].selectize;
255
256             this.render();
257             this.navigate(this.hostSelectize.options[this.host].path);
258         },
259
260         render: function() {
261             this.$host.find('.view').text(this.host);
262             this.$noItems.toggle((this.showHidden() ? !Boolean(this.files.length) : !Boolean(this.files.visible().length)));
263             this.$error.hide();
264         },
265
266         navigate: function(path) {
267             var pathBits = [new Filex.PathBit({'text': '/', 'path': ''})];
268
269             pathBits = pathBits.concat(_.map(path.replace(/(^\/+|\/+$)/g, '').split('/'), function(name) {
270                 return new Filex.PathBit({'text': name, 'path': name});
271             }));
272
273             this.path.reset(pathBits);
274         },
275
276         reloadFiles: function() {
277             this.busy();
278
279             var view = this;
280
281             this.files.fetch({
282                 reset: true,
283                 data: {host: this.host, path: this.path.full()},
284                 success: function() {
285                     view.render();
286                     view.idle();
287                 },
288                 error: function(collection, response) {
289                     view.files.reset();
290
291                     var msg = (response.responseJSON || {}).msg || 'Błąd serwera';
292
293                     view.$noItems.hide();
294                     view.$error.find('.msg').text(msg);
295                     view.$error.show();
296                     view.idle();
297                 }
298             });
299         },
300
301         addPath: function(bit) {
302             var view = new Filex.Breadcrumb({model: bit});
303             this.$('.path').append(view.render().el);
304         },
305
306         resetPath: function(models, options) {
307             this.reloadFiles();
308
309             _.each(options.previousModels, function(model) {
310                 model.trigger('remove');
311             });
312
313             this.path.each(this.addPath, this);
314         },
315
316         resetFiles: function(models, options) {
317             _.each(options.previousModels, function(model) {
318                 model.trigger('remove');
319             });
320
321             this.files.each(function(file) {
322                 var view = new Filex.ListingItem({model: file, view: this});
323                 this.$('tbody').append(view.render().el);
324             }, this);
325         },
326
327         toggleHidden: function() {
328             this.files.each(function(item) { item.trigger('hidden'); }, this);
329             this.render();
330         },
331
332         selectedDir: function(dir) {
333             this.path.add({'text': dir.get('name'), 'path': dir.get('name')});
334         },
335
336         selectedFile: function(file) {
337             this.trigger('selected:file', this.host + this.path.full() + '/' + file.get('name'));
338         },
339
340         selectedPath: function(bit) {
341             var newPath = this.path.slice(0, this.path.indexOf(bit) + 1);
342             this.path.set(newPath);
343             this.reloadFiles();
344         },
345
346         showHidden: function() {
347             return this.$showHidden[0].checked;
348         },
349
350         hostEdit: function() {
351             this.$host.toggleClass('editing');
352             this.hostSelectize.focus();
353         },
354
355         busy: function() {
356             this.$el.addClass('busy');
357         },
358
359         idle: function() {
360             this.$el.removeClass('busy');
361         }
362     });
363 });