· 8 years ago · Dec 04, 2017, 06:52 AM
1/*
2 * Copyright (c) 2014
3 *
4 * This file is licensed under the Affero General Public License version 3
5 * or later.
6 *
7 * See the COPYING-README file.
8 *
9 */
10
11(function() {
12
13 var TEMPLATE_ADDBUTTON = '<a href="#" class="button new">' +
14 '<span class="icon {{iconClass}}"></span>' +
15 '<span class="hidden-visually">{{addText}}</span>' +
16 '</a>';
17
18 /**
19 * @class OCA.Files.FileList
20 * @classdesc
21 *
22 * The FileList class manages a file list view.
23 * A file list view consists of a controls bar and
24 * a file list table.
25 *
26 * @param $el container element with existing markup for the #controls
27 * and a table
28 * @param {Object} [options] map of options, see other parameters
29 * @param {Object} [options.scrollContainer] scrollable container, defaults to $(window)
30 * @param {Object} [options.dragOptions] drag options, disabled by default
31 * @param {Object} [options.folderDropOptions] folder drop options, disabled by default
32 * @param {boolean} [options.detailsViewEnabled=true] whether to enable details view
33 * @param {boolean} [options.enableUpload=false] whether to enable uploader
34 * @param {OC.Files.Client} [options.filesClient] files client to use
35 */
36 var FileList = function($el, options) {
37 this.initialize($el, options);
38 };
39 /**
40 * @memberof OCA.Files
41 */
42 FileList.prototype = {
43 SORT_INDICATOR_ASC_CLASS: 'icon-triangle-n',
44 SORT_INDICATOR_DESC_CLASS: 'icon-triangle-s',
45
46 id: 'files',
47 appName: t('files', 'Files'),
48 isEmpty: true,
49 useUndo:true,
50
51 /**
52 * Top-level container with controls and file list
53 */
54 $el: null,
55
56 /**
57 * Card view
58 */
59 $cardView: null,
60
61 /**
62 * Files table
63 */
64 $table: null,
65
66 /**
67 * cardViewStatus
68 */
69 cardViewStatus: false,
70
71 /**
72 * List of rows (table tbody)
73 */
74 $fileList: null,
75
76 /**
77 * @type OCA.Files.BreadCrumb
78 */
79 breadcrumb: null,
80
81 /**
82 * @type OCA.Files.FileSummary
83 */
84 fileSummary: null,
85
86 /**
87 * @type OCA.Files.DetailsView
88 */
89 _detailsView: null,
90
91 /**
92 * Files client instance
93 *
94 * @type OC.Files.Client
95 */
96 filesClient: null,
97
98 /**
99 * Whether the file list was initialized already.
100 * @type boolean
101 */
102 initialized: false,
103
104 /**
105 * Wheater the file list was already shown once
106 * @type boolean
107 */
108 shown: false,
109
110 /**
111 * Number of files per page
112 *
113 * @return {int} page size
114 */
115 pageSize: function() {
116 return Math.ceil(this.$container.height() / 50);
117 },
118
119 /**
120 * Array of files in the current folder.
121 * The entries are of file data.
122 *
123 * @type Array.<OC.Files.FileInfo>
124 */
125 files: [],
126
127 /**
128 * Current directory entry
129 *
130 * @type OC.Files.FileInfo
131 */
132 dirInfo: null,
133
134 /**
135 * File actions handler, defaults to OCA.Files.FileActions
136 * @type OCA.Files.FileActions
137 */
138 fileActions: null,
139
140 /**
141 * Whether selection is allowed, checkboxes and selection overlay will
142 * be rendered
143 */
144 _allowSelection: true,
145
146 /**
147 * Map of file id to file data
148 * @type Object.<int, Object>
149 */
150 _selectedFiles: {},
151
152 /**
153 * Summary of selected files.
154 * @type OCA.Files.FileSummary
155 */
156 _selectionSummary: null,
157
158 /**
159 * If not empty, only files containing this string will be shown
160 * @type String
161 */
162 _filter: '',
163
164 /**
165 * @type Backbone.Model
166 */
167 _filesConfig: undefined,
168
169 /**
170 * Sort attribute
171 * @type String
172 */
173 _sort: 'name',
174
175 /**
176 * Sort direction: 'asc' or 'desc'
177 * @type String
178 */
179 _sortDirection: 'asc',
180
181 /**
182 * Sort comparator function for the current sort
183 * @type Function
184 */
185 _sortComparator: null,
186
187 /**
188 * Whether to do a client side sort.
189 * When false, clicking on a table header will call reload().
190 * When true, clicking on a table header will simply resort the list.
191 */
192 _clientSideSort: true,
193
194 /**
195 * Whether or not users can change the sort attribute or direction
196 */
197 _allowSorting: true,
198
199 /**
200 * Current directory
201 * @type String
202 */
203 _currentDirectory: null,
204
205 _dragOptions: null,
206 _folderDropOptions: null,
207
208 /**
209 * @type OC.Uploader
210 */
211 _uploader: null,
212
213 /**
214 * Initialize the file list and its components
215 *
216 * @param $el container element with existing markup for the #controls
217 * and a table
218 * @param options map of options, see other parameters
219 * @param options.scrollContainer scrollable container, defaults to $(window)
220 * @param options.dragOptions drag options, disabled by default
221 * @param options.folderDropOptions folder drop options, disabled by default
222 * @param options.scrollTo name of file to scroll to after the first load
223 * @param {OC.Files.Client} [options.filesClient] files API client
224 * @param {OC.Backbone.Model} [options.filesConfig] files app configuration
225 * @private
226 */
227 initialize: function($el, options) {
228 var self = this;
229 options = options || {};
230 if (this.initialized) {
231 return;
232 }
233
234 if (options.config) {
235 this._filesConfig = options.config;
236 } else if (!_.isUndefined(OCA.Files) && !_.isUndefined(OCA.Files.App)) {
237 this._filesConfig = OCA.Files.App.getFilesConfig();
238 } else {
239 this._filesConfig = new OC.Backbone.Model({
240 'showhidden': false
241 });
242 }
243
244 if (options.dragOptions) {
245 this._dragOptions = options.dragOptions;
246 }
247 if (options.folderDropOptions) {
248 this._folderDropOptions = options.folderDropOptions;
249 }
250 if (options.filesClient) {
251 this.filesClient = options.filesClient;
252 } else {
253 // default client if not specified
254 this.filesClient = OC.Files.getClient();
255 }
256
257 this.$el = $el;
258 if (options.id) {
259 this.id = options.id;
260 }
261 this.$container = options.scrollContainer || $(window);
262 this.$table = $el.find('table:first');
263 this.$fileList = $el.find('#fileList');
264 this.$cardView = $el.find('.cards-box');
265
266 this.$cardView.toggle();
267
268 var that = this;
269 var tableSwitch = $("#table-switch");
270 var cardviewSwitch = $("#cardview-switch");
271
272 $(".top-info").on('click', function(e){
273 var id = $(e.target).attr('id');
274 if (id === 'table-switch' || id === 'cardview-switch' ){
275 that._CardviewSwitch(self);
276 e.preventDefault();
277 return false;
278 }
279 //e.preventDefault();
280 });
281
282
283 if (!_.isUndefined(this._filesConfig)) {
284 this._filesConfig.on('change:showhidden', function() {
285 var showHidden = this.get('showhidden');
286 self.$el.toggleClass('hide-hidden-files', !showHidden);
287 self.updateSelectionSummary();
288
289 if (!showHidden) {
290 // hiding files could make the page too small, need to try rendering next page
291 self._onScroll();
292 }
293 });
294
295 this.$el.toggleClass('hide-hidden-files', !this._filesConfig.get('showhidden'));
296 }
297
298
299 if (_.isUndefined(options.detailsViewEnabled) || options.detailsViewEnabled) {
300 this._detailsView = new OCA.Files.DetailsView();
301 this._detailsView.$el.insertBefore(this.$el);
302 this._detailsView.$el.addClass('disappear');
303 }
304
305 this._initFileActions(options.fileActions);
306
307 if (this._detailsView) {
308 this._detailsView.addDetailView(new OCA.Files.MainFileInfoDetailView({fileList: this, fileActions: this.fileActions}));
309 }
310
311 this.files = [];
312 this._selectedFiles = {};
313 this._selectionSummary = new OCA.Files.FileSummary(undefined, {config: this._filesConfig});
314 // dummy root dir info
315 this.dirInfo = new OC.Files.FileInfo({});
316
317 this.fileSummary = this._createSummary();
318
319 if (options.sorting) {
320 this.setSort(options.sorting.mode, options.sorting.direction, false, false);
321 } else {
322 this.setSort('name', 'asc', false, false);
323 }
324
325 var breadcrumbOptions = {
326 onClick: _.bind(this._onClickBreadCrumb, this),
327 getCrumbUrl: function(part) {
328 return self.linkTo(part.dir);
329 }
330 };
331 // if dropping on folders is allowed, then also allow on breadcrumbs
332 if (this._folderDropOptions) {
333 breadcrumbOptions.onDrop = _.bind(this._onDropOnBreadCrumb, this);
334 breadcrumbOptions.onOver = function() {
335 self.$el.find('td.filename.ui-droppable').droppable('disable');
336 }
337 breadcrumbOptions.onOut = function() {
338 self.$el.find('td.filename.ui-droppable').droppable('enable');
339 }
340 }
341 this.breadcrumb = new OCA.Files.BreadCrumb(breadcrumbOptions);
342
343 var $controls = this.$el.find('#controls');
344 if ($controls.length > 0) {
345 $controls.prepend(this.breadcrumb.$el);
346 this.$table.addClass('has-controls');
347 }
348
349 this._renderNewButton();
350
351 this.$el.find('thead th .columntitle').click(_.bind(this._onClickHeader, this));
352
353 this._onResize = _.debounce(_.bind(this._onResize, this), 100);
354 $('#app-content').on('appresized', this._onResize);
355 $(window).resize(this._onResize);
356
357 this.$el.on('show', this._onResize);
358
359 this.updateSearch();
360
361 this.$fileList.on('click','td.filename>a.name, td.filesize, td.date', _.bind(this._onClickFile, this));
362
363 this.$fileList.on('change', 'td.filename>.selectCheckBox', _.bind(this._onClickFileCheckbox, this));
364 this.$el.on('show', _.bind(this._onShow, this));
365 this.$el.on('urlChanged', _.bind(this._onUrlChanged, this));
366 this.$el.find('.select-all').click(_.bind(this._onClickSelectAll, this));
367 this.$el.find('.download').click(_.bind(this._onClickDownloadSelected, this));
368 this.$el.find('.delete-selected').click(_.bind(this._onClickDeleteSelected, this));
369
370 this.$el.find('.selectedActions a').tooltip({placement:'top'});
371
372 this.$container.on('scroll', _.bind(this._onScroll, this));
373
374 if (options.scrollTo) {
375 this.$fileList.one('updated', function() {
376 self.scrollTo(options.scrollTo);
377 });
378 }
379
380 if (options.enableUpload) {
381 // TODO: auto-create this element
382 var $uploadEl = this.$el.find('#file_upload_start');
383 if ($uploadEl.exists()) {
384 this._uploader = new OC.Uploader($uploadEl, {
385 fileList: this,
386 filesClient: this.filesClient,
387 dropZone: $('#content')
388 });
389
390 this.setupUploadEvents(this._uploader);
391 }
392 }
393
394 OC.Plugins.attach('OCA.Files.FileList', this);
395 },
396
397 /**
398 * Destroy / uninitialize this instance.
399 */
400 destroy: function() {
401 if (this._newFileMenu) {
402 this._newFileMenu.remove();
403 }
404 if (this._newButton) {
405 this._newButton.remove();
406 }
407 if (this._detailsView) {
408 this._detailsView.remove();
409 }
410 // TODO: also unregister other event handlers
411 this.fileActions.off('registerAction', this._onFileActionsUpdated);
412 this.fileActions.off('setDefault', this._onFileActionsUpdated);
413 OC.Plugins.detach('OCA.Files.FileList', this);
414 $('#app-content').off('appresized', this._onResize);
415 },
416
417 /**
418 * Initializes the file actions, set up listeners.
419 *
420 * @param {OCA.Files.FileActions} fileActions file actions
421 */
422 _initFileActions: function(fileActions) {
423 var self = this;
424 this.fileActions = fileActions;
425 if (!this.fileActions) {
426 this.fileActions = new OCA.Files.FileActions();
427 this.fileActions.registerDefaultActions();
428 }
429
430 if (this._detailsView) {
431 this.fileActions.registerAction({
432 name: 'Details',
433 displayName: t('files', 'Details'),
434 mime: 'all',
435 order: -50,
436 iconClass: 'icon-details',
437 permissions: OC.PERMISSION_READ,
438 actionHandler: function(fileName, context) {
439 self._updateDetailsView(fileName);
440 }
441 });
442 }
443
444 this._onFileActionsUpdated = _.debounce(_.bind(this._onFileActionsUpdated, this), 100);
445 this.fileActions.on('registerAction', this._onFileActionsUpdated);
446 this.fileActions.on('setDefault', this._onFileActionsUpdated);
447 },
448
449 /**
450 * Returns a unique model for the given file name.
451 *
452 * @param {string|object} fileName file name or jquery row
453 * @return {OCA.Files.FileInfoModel} file info model
454 */
455 getModelForFile: function(fileName) {
456 var self = this;
457 var $tr;
458 // jQuery object ?
459 if (fileName.is) {
460 $tr = fileName;
461 fileName = $tr.attr('data-file');
462 } else {
463 $tr = this.findFileEl(fileName);
464 }
465
466 if (!$tr || !$tr.length) {
467 return null;
468 }
469
470 // if requesting the selected model, return it
471 if (this._currentFileModel && this._currentFileModel.get('name') === fileName) {
472 return this._currentFileModel;
473 }
474
475 // TODO: note, this is a temporary model required for synchronising
476 // state between different views.
477 // In the future the FileList should work with Backbone.Collection
478 // and contain existing models that can be used.
479 // This method would in the future simply retrieve the matching model from the collection.
480 var model = new OCA.Files.FileInfoModel(this.elementToFile($tr), {
481 filesClient: this.filesClient
482 });
483 if (!model.get('path')) {
484 model.set('path', this.getCurrentDirectory(), {silent: true});
485 }
486
487 model.on('change', function(model) {
488 // re-render row
489 var highlightState = $tr.hasClass('highlighted');
490 $tr = self.updateRow(
491 $tr,
492 model.toJSON(),
493 {updateSummary: true, silent: false, animate: true}
494 );
495
496 // restore selection state
497 var selected = !!self._selectedFiles[$tr.data('id')];
498 self._selectFileEl($tr, selected);
499
500 $tr.toggleClass('highlighted', highlightState);
501 });
502 model.on('busy', function(model, state) {
503 self.showFileBusyState($tr, state);
504 });
505
506 return model;
507 },
508
509 /**
510 * Displays the details view for the given file and
511 * selects the given tab
512 *
513 * @param {string|OCA.Files.FileInfoModel} fileName file name or FileInfoModel for which to show details
514 * @param {string} [tabId] optional tab id to select
515 */
516 showDetailsView: function(fileName, tabId) {
517 this._updateDetailsView(fileName);
518 if (tabId) {
519 this._detailsView.selectTab(tabId);
520 }
521 OC.Apps.showAppSidebar(this._detailsView.$el);
522 },
523
524 _CardviewSwitch: function (self) {
525
526 var $icons = $('.top-info').find('.icons');
527 self.cardViewStatus = !self.cardViewStatus;
528 var o = self.cardViewStatus;
529 $icons.html('<span class="icon' + (o ? ' active': '') + '" id="cardview-switch"></span><span class="icon' + (o ? '' : ' active') + '" id="table-switch"></span>');
530
531 // var ts = document.getElementById('table-switch');
532 // var cs = document.getElementById('cardview-switch');
533 // if (ts.className.indexOf('active') > -1){
534 // ts.className = 'icon';
535 // cs.className = 'icon active';
536 // }else{
537 // ts.className = 'icon active';
538 // cs.className = 'icon';
539 // }
540 self.$cardView.toggle();
541 self.$table.toggle();
542
543
544 },
545
546
547 /**
548 * Update the details view to display the given file
549 *
550 * @param {string|OCA.Files.FileInfoModel} fileName file name from the current list or a FileInfoModel object
551 * @param {boolean} [show=true] whether to open the sidebar if it was closed
552 */
553 _updateDetailsView: function(fileName, show) {
554 if (!this._detailsView) {
555 return;
556 }
557
558 // show defaults to true
559 show = _.isUndefined(show) || !!show;
560 var oldFileInfo = this._detailsView.getFileInfo();
561 if (oldFileInfo) {
562 // TODO: use more efficient way, maybe track the highlight
563 this.$fileList.children().filterAttr('data-id', '' + oldFileInfo.get('id')).removeClass('highlighted');
564 oldFileInfo.off('change', this._onSelectedModelChanged, this);
565 }
566
567 if (!fileName) {
568 this._detailsView.setFileInfo(null);
569 if (this._currentFileModel) {
570 this._currentFileModel.off();
571 }
572 this._currentFileModel = null;
573 OC.Apps.hideAppSidebar(this._detailsView.$el);
574 return;
575 }
576
577 if (show && this._detailsView.$el.hasClass('disappear')) {
578 OC.Apps.showAppSidebar(this._detailsView.$el);
579 }
580
581 if (fileName instanceof OCA.Files.FileInfoModel) {
582 var model = fileName;
583 } else {
584 var $tr = this.findFileEl(fileName);
585 var model = this.getModelForFile($tr);
586 $tr.addClass('highlighted');
587 }
588
589 this._currentFileModel = model;
590
591 this._detailsView.setFileInfo(model);
592 this._detailsView.$el.scrollTop(0);
593 },
594
595 /**
596 * Event handler for when the window size changed
597 */
598 _onResize: function() {
599 var containerWidth = this.$el.width();
600 var actionsWidth = 0;
601 $.each(this.$el.find('#controls .actions'), function(index, action) {
602 actionsWidth += $(action).outerWidth();
603 });
604
605 // subtract app navigation toggle when visible
606 containerWidth -= $('#app-navigation-toggle').width();
607
608 this.breadcrumb.setMaxWidth(containerWidth - actionsWidth - 10);
609
610 this.$table.find('>thead').width($('#app-content').width() - OC.Util.getScrollBarWidth());
611 },
612
613 /**
614 * Event handler when leaving previously hidden state
615 */
616 _onShow: function(e) {
617 if (this.shown) {
618 this.reload();
619 }
620 this.shown = true;
621 },
622
623 /**
624 * Event handler for when the URL changed
625 */
626 _onUrlChanged: function(e) {
627 if (e && _.isString(e.dir)) {
628 var currentDir = this.getCurrentDirectory();
629 // this._currentDirectory is NULL when fileList is first initialised
630 if( (this._currentDirectory || this.$el.find('#dir').val()) && currentDir === e.dir) {
631 return;
632 }
633 this.changeDirectory(e.dir, false, true);
634 }
635 },
636
637 /**
638 * Selected/deselects the given file element and updated
639 * the internal selection cache.
640 *
641 * @param {Object} $tr single file row element
642 * @param {bool} state true to select, false to deselect
643 */
644 _selectFileEl: function($tr, state, showDetailsView) {
645 var $checkbox = $tr.find('td.filename>.selectCheckBox');
646 var oldData = !!this._selectedFiles[$tr.data('id')];
647 var data;
648 $checkbox.prop('checked', state);
649 $tr.toggleClass('selected', state);
650 // already selected ?
651 if (state === oldData) {
652 return;
653 }
654 data = this.elementToFile($tr);
655 if (state) {
656 this._selectedFiles[$tr.data('id')] = data;
657 this._selectionSummary.add(data);
658 }
659 else {
660 delete this._selectedFiles[$tr.data('id')];
661 this._selectionSummary.remove(data);
662 }
663 if (this._detailsView && !this._detailsView.$el.hasClass('disappear')) {
664 // hide sidebar
665 this._updateDetailsView(null);
666 }
667 this.$el.find('.select-all').prop('checked', this._selectionSummary.getTotal() === this.files.length);
668 },
669
670 /**
671 * Event handler for when clicking on files to select them
672 */
673 _onClickFile: function(event) {
674 var $tr = $(event.target).closest('tr');
675 if ($tr.hasClass('dragging')) {
676 return;
677 }
678 if (this._allowSelection && (event.ctrlKey || event.shiftKey)) {
679 event.preventDefault();
680 if (event.shiftKey) {
681 var $lastTr = $(this._lastChecked);
682 var lastIndex = $lastTr.index();
683 var currentIndex = $tr.index();
684 var $rows = this.$fileList.children('tr');
685
686 // last clicked checkbox below current one ?
687 if (lastIndex > currentIndex) {
688 var aux = lastIndex;
689 lastIndex = currentIndex;
690 currentIndex = aux;
691 }
692
693 // auto-select everything in-between
694 for (var i = lastIndex + 1; i < currentIndex; i++) {
695 this._selectFileEl($rows.eq(i), true);
696 }
697 }
698 else {
699 this._lastChecked = $tr;
700 }
701 var $checkbox = $tr.find('td.filename>.selectCheckBox');
702 this._selectFileEl($tr, !$checkbox.prop('checked'));
703 this.updateSelectionSummary();
704 } else {
705 // clicked directly on the name
706 if (!this._detailsView || $(event.target).is('.nametext') || $(event.target).closest('.nametext').length) {
707 var filename = $tr.attr('data-file');
708 var renaming = $tr.data('renaming');
709 if (!renaming) {
710 this.fileActions.currentFile = $tr.find('td');
711 var mime = this.fileActions.getCurrentMimeType();
712 var type = this.fileActions.getCurrentType();
713 var permissions = this.fileActions.getCurrentPermissions();
714 var action = this.fileActions.getDefault(mime,type, permissions);
715 if (action) {
716 event.preventDefault();
717 // also set on global object for legacy apps
718 window.FileActions.currentFile = this.fileActions.currentFile;
719 action(filename, {
720 $file: $tr,
721 fileList: this,
722 fileActions: this.fileActions,
723 dir: $tr.attr('data-path') || this.getCurrentDirectory()
724 });
725 }
726 // deselect row
727 $(event.target).closest('a').blur();
728 }
729 } else {
730 this._updateDetailsView($tr.attr('data-file'));
731 event.preventDefault();
732 }
733 }
734 },
735
736 /**
737 * Event handler for when clicking on a file's checkbox
738 */
739 _onClickFileCheckbox: function(e) {
740 var $tr = $(e.target).closest('tr');
741 var state = !$tr.hasClass('selected');
742 this._selectFileEl($tr, state);
743 this._lastChecked = $tr;
744 this.updateSelectionSummary();
745 if (this._detailsView && !this._detailsView.$el.hasClass('disappear')) {
746 // hide sidebar
747 this._updateDetailsView(null);
748 }
749 },
750
751 /**
752 * Event handler for when selecting/deselecting all files
753 */
754 _onClickSelectAll: function(e) {
755 var checked = $(e.target).prop('checked');
756 this.$fileList.find('td.filename>.selectCheckBox').prop('checked', checked)
757 .closest('tr').toggleClass('selected', checked);
758 this._selectedFiles = {};
759 this._selectionSummary.clear();
760 if (checked) {
761 for (var i = 0; i < this.files.length; i++) {
762 var fileData = this.files[i];
763 this._selectedFiles[fileData.id] = fileData;
764 this._selectionSummary.add(fileData);
765 }
766 }
767 this.updateSelectionSummary();
768 if (this._detailsView && !this._detailsView.$el.hasClass('disappear')) {
769 // hide sidebar
770 this._updateDetailsView(null);
771 }
772 },
773
774 /**
775 * Event handler for when clicking on "Download" for the selected files
776 */
777 _onClickDownloadSelected: function(event) {
778 var files;
779 var dir = this.getCurrentDirectory();
780 if (this.isAllSelected() && this.getSelectedFiles().length > 1) {
781 files = OC.basename(dir);
782 dir = OC.dirname(dir) || '/';
783 }
784 else {
785 files = _.pluck(this.getSelectedFiles(), 'name');
786 }
787
788 var downloadFileaction = $('#selectedActionsList').find('.download');
789
790 // don't allow a second click on the download action
791 if(downloadFileaction.hasClass('disabled')) {
792 event.preventDefault();
793 return;
794 }
795
796 var disableLoadingState = function(){
797 OCA.Files.FileActions.updateFileActionSpinner(downloadFileaction, false);
798 };
799
800 OCA.Files.FileActions.updateFileActionSpinner(downloadFileaction, true);
801 if(this.getSelectedFiles().length > 1) {
802 OCA.Files.Files.handleDownload(this.getDownloadUrl(files, dir, true), disableLoadingState);
803 }
804 else {
805 first = this.getSelectedFiles()[0];
806 OCA.Files.Files.handleDownload(this.getDownloadUrl(first.name, dir, true), disableLoadingState);
807 }
808 return false;
809 },
810
811 /**
812 * Event handler for when clicking on "Delete" for the selected files
813 */
814 _onClickDeleteSelected: function(event) {
815 var files = null;
816 if (!this.isAllSelected()) {
817 files = _.pluck(this.getSelectedFiles(), 'name');
818 }
819 this.do_delete(files);
820 event.preventDefault();
821 return false;
822 },
823
824 /**
825 * Event handler when clicking on a table header
826 */
827 _onClickHeader: function(e) {
828 if (this.$table.hasClass('multiselect')) {
829 return;
830 }
831 var $target = $(e.target);
832 var sort;
833 if (!$target.is('a')) {
834 $target = $target.closest('a');
835 }
836 sort = $target.attr('data-sort');
837 if (sort && this._allowSorting) {
838 if (this._sort === sort) {
839 this.setSort(sort, (this._sortDirection === 'desc')?'asc':'desc', true, true);
840 }
841 else {
842 if ( sort === 'name' ) { //default sorting of name is opposite to size and mtime
843 this.setSort(sort, 'asc', true, true);
844 }
845 else {
846 this.setSort(sort, 'desc', true, true);
847 }
848 }
849 }
850 },
851
852 /**
853 * Event handler when clicking on a bread crumb
854 */
855 _onClickBreadCrumb: function(e) {
856 var $el = $(e.target).closest('.crumb'),
857 $targetDir = $el.data('dir');
858
859 if ($targetDir !== undefined && e.which === 1) {
860 e.preventDefault();
861 this.changeDirectory($targetDir);
862 this.updateSearch();
863 }
864 },
865
866 /**
867 * Event handler for when scrolling the list container.
868 * This appends/renders the next page of entries when reaching the bottom.
869 */
870 _onScroll: function(e) {
871 if (this.$container.scrollTop() + this.$container.height() > this.$el.height() - 300) {
872 this._nextPage(true);
873 }
874 },
875
876 /**
877 * Event handler when dropping on a breadcrumb
878 */
879 _onDropOnBreadCrumb: function( event, ui ) {
880 var self = this;
881 var $target = $(event.target);
882 if (!$target.is('.crumb')) {
883 $target = $target.closest('.crumb');
884 }
885 var targetPath = $(event.target).data('dir');
886 var dir = this.getCurrentDirectory();
887 while (dir.substr(0,1) === '/') {//remove extra leading /'s
888 dir = dir.substr(1);
889 }
890 dir = '/' + dir;
891 if (dir.substr(-1,1) !== '/') {
892 dir = dir + '/';
893 }
894 // do nothing if dragged on current dir
895 if (targetPath === dir || targetPath + '/' === dir) {
896 return;
897 }
898
899 var files = this.getSelectedFiles();
900 if (files.length === 0) {
901 // single one selected without checkbox?
902 files = _.map(ui.helper.find('tr'), function(el) {
903 return self.elementToFile($(el));
904 });
905 }
906
907 this.move(_.pluck(files, 'name'), targetPath);
908
909 // re-enable td elements to be droppable
910 // sometimes the filename drop handler is still called after re-enable,
911 // it seems that waiting for a short time before re-enabling solves the problem
912 setTimeout(function() {
913 self.$el.find('td.filename.ui-droppable').droppable('enable');
914 }, 10);
915 },
916
917 /**
918 * Sets a new page title
919 */
920 setPageTitle: function(title){
921 if (title) {
922 title += ' - ';
923 } else {
924 title = '';
925 }
926 title += this.appName;
927 // Sets the page title with the " - Nextcloud" suffix as in templates
928 window.document.title = title + ' - ' + oc_defaults.title;
929
930 return true;
931 },
932 /**
933 * Returns the file info for the given file name from the internal collection.
934 *
935 * @param {string} fileName file name
936 * @return {OCA.Files.FileInfo} file info or null if it was not found
937 *
938 * @since 8.2
939 */
940 findFile: function(fileName) {
941 return _.find(this.files, function(aFile) {
942 return (aFile.name === fileName);
943 }) || null;
944 },
945 /**
946 * Returns the tr element for a given file name, but only if it was already rendered.
947 *
948 * @param {string} fileName file name
949 * @return {Object} jQuery object of the matching row
950 */
951 findFileEl: function(fileName){
952 // use filterAttr to avoid escaping issues
953 return this.$fileList.find('tr').filterAttr('data-file', fileName);
954 },
955
956 /**
957 * Returns the file data from a given file element.
958 * @param $el file tr element
959 * @return file data
960 */
961 elementToFile: function($el){
962 $el = $($el);
963 var data = {
964 id: parseInt($el.attr('data-id'), 10),
965 name: $el.attr('data-file'),
966 mimetype: $el.attr('data-mime'),
967 mtime: parseInt($el.attr('data-mtime'), 10),
968 type: $el.attr('data-type'),
969 etag: $el.attr('data-etag'),
970 permissions: parseInt($el.attr('data-permissions'), 10),
971 hasPreview: $el.attr('data-has-preview') === 'true'
972 };
973 var size = $el.attr('data-size');
974 if (size) {
975 data.size = parseInt(size, 10);
976 }
977 var icon = $el.attr('data-icon');
978 if (icon) {
979 data.icon = icon;
980 }
981 var mountType = $el.attr('data-mounttype');
982 if (mountType) {
983 data.mountType = mountType;
984 }
985 var path = $el.attr('data-path');
986 if (path) {
987 data.path = path;
988 }
989 return data;
990 },
991
992 /**
993 * Appends the next page of files into the table
994 * @param animate true to animate the new elements
995 * @return array of DOM elements of the newly added files
996 */
997 _nextPage: function(animate) {
998 var index = this.$fileList.children().length,
999 count = this.pageSize(),
1000 hidden,
1001 tr,
1002 fileData,
1003 newTrs = [],
1004 isAllSelected = this.isAllSelected(),
1005 showHidden = this._filesConfig.get('showhidden');
1006
1007 if (index >= this.files.length) {
1008 return false;
1009 }
1010
1011 var opt = {updateSummary: false, silent: true, hidden: hidden};
1012 while (count > 0 && index < this.files.length) {
1013 fileData = this.files[index];
1014 if (this._filter) {
1015 hidden = fileData.name.toLowerCase().indexOf(this._filter.toLowerCase()) === -1;
1016 } else {
1017 hidden = false;
1018 }
1019 card = this._renderCard(fileData, opt);
1020 tr = this._renderRow(fileData, opt, card);
1021 this.$fileList.append(tr);
1022 this.$cardView.append(card);
1023 if (isAllSelected || this._selectedFiles[fileData.id]) {
1024 tr.addClass('selected');
1025 tr.find('.selectCheckBox').prop('checked', true);
1026 }
1027 if (animate) {
1028 tr.addClass('appear transparent');
1029 }
1030 newTrs.push(tr);
1031 index++;
1032 // only count visible rows
1033 if (showHidden || !tr.hasClass('hidden-file')) {
1034 count--;
1035 }
1036 }
1037
1038 // trigger event for newly added rows
1039 if (newTrs.length > 0) {
1040 this.$fileList.trigger($.Event('fileActionsReady', {fileList: this, $files: newTrs}));
1041 }
1042
1043 if (animate) {
1044 // defer, for animation
1045 window.setTimeout(function() {
1046 for (var i = 0; i < newTrs.length; i++ ) {
1047 newTrs[i].removeClass('transparent');
1048 }
1049 }, 0);
1050 }
1051 return newTrs;
1052 },
1053
1054 /**
1055 * Event handler for when file actions were updated.
1056 * This will refresh the file actions on the list.
1057 */
1058 _onFileActionsUpdated: function() {
1059 var self = this;
1060 var $files = this.$fileList.find('tr');
1061 if (!$files.length) {
1062 return;
1063 }
1064
1065 $files.each(function() {
1066 self.fileActions.display($(this).find('td.filename'), false, self);
1067 });
1068 this.$fileList.trigger($.Event('fileActionsReady', {fileList: this, $files: $files}));
1069
1070 },
1071
1072 /**
1073 * Sets the files to be displayed in the list.
1074 * This operation will re-render the list and update the summary.
1075 * @param filesArray array of file data (map)
1076 */
1077 setFiles: function(filesArray) {
1078 var self = this;
1079
1080 // detach to make adding multiple rows faster
1081 this.files = filesArray;
1082
1083 this.$fileList.empty();
1084 this.$cardView.empty();
1085
1086 // clear "Select all" checkbox
1087 this.$el.find('.select-all').prop('checked', false);
1088
1089 // Save full files list while rendering
1090
1091 this.isEmpty = this.files.length === 0;
1092 this._nextPage();
1093
1094 this.updateEmptyContent();
1095
1096 this.fileSummary.calculate(this.files);
1097
1098 this._selectedFiles = {};
1099 this._selectionSummary.clear();
1100 this.updateSelectionSummary();
1101 $(window).scrollTop(0);
1102
1103 this.$fileList.trigger(jQuery.Event('updated'));
1104 _.defer(function() {
1105 self.$el.closest('#app-content').trigger(jQuery.Event('apprendered'));
1106 });
1107 },
1108
1109 /**
1110 * Returns whether the given file info must be hidden
1111 *
1112 * @param {OC.Files.FileInfo} fileInfo file info
1113 *
1114 * @return {boolean} true if the file is a hidden file, false otherwise
1115 */
1116 _isHiddenFile: function(file) {
1117 return file.name && file.name.charAt(0) === '.';
1118 },
1119
1120 /**
1121 * Returns the icon URL matching the given file info
1122 *
1123 * @param {OC.Files.FileInfo} fileInfo file info
1124 *
1125 * @return {string} icon URL
1126 */
1127 _getIconUrl: function(fileInfo) {
1128 var mimeType = fileInfo.mimetype || 'application/octet-stream';
1129 if (mimeType === 'httpd/unix-directory') {
1130 // use default folder icon
1131 if (fileInfo.mountType === 'shared' || fileInfo.mountType === 'shared-root') {
1132 return OC.MimeType.getIconUrl('dir-shared');
1133 } else if (fileInfo.mountType === 'external-root') {
1134 return OC.MimeType.getIconUrl('dir-external');
1135 } else if (fileInfo.mountType !== undefined && fileInfo.mountType !== '') {
1136 return OC.MimeType.getIconUrl('dir-' + fileInfo.mountType);
1137 }
1138 return OC.MimeType.getIconUrl('dir');
1139 }
1140 return OC.MimeType.getIconUrl(mimeType);
1141 },
1142
1143 /**
1144 * Creates a new table row element using the given file data.
1145 * @param {OC.Files.FileInfo} fileData file info attributes
1146 * @param options map of attributes
1147 * @return new tr element (not appended to the table)
1148 */
1149 _createRow: function(fileData, options) {
1150 var td, simpleSize, basename, extension, sizeColor,
1151 icon = fileData.icon || this._getIconUrl(fileData),
1152 name = fileData.name,
1153 // TODO: get rid of type, only use mime type
1154 type = fileData.type || 'file',
1155 mtime = parseInt(fileData.mtime, 10),
1156 mime = fileData.mimetype,
1157 path = fileData.path,
1158 dataIcon = null,
1159 linkUrl;
1160 options = options || {};
1161
1162 if (isNaN(mtime)) {
1163 mtime = new Date().getTime();
1164 }
1165
1166 if (type === 'dir') {
1167 mime = mime || 'httpd/unix-directory';
1168
1169 if (fileData.mountType && fileData.mountType.indexOf('external') === 0) {
1170 icon = OC.MimeType.getIconUrl('dir-external');
1171 dataIcon = icon;
1172 }
1173 }
1174
1175 //containing tr
1176 var tr = $('<tr></tr>').attr({
1177 "data-id" : fileData.id,
1178 "data-type": type,
1179 "data-size": fileData.size,
1180 "data-file": name,
1181 "data-mime": mime,
1182 "data-mtime": mtime,
1183 "data-etag": fileData.etag,
1184 "data-permissions": fileData.permissions || this.getDirectoryPermissions(),
1185 "data-has-preview": fileData.hasPreview !== false
1186 });
1187
1188 if (dataIcon) {
1189 // icon override
1190 tr.attr('data-icon', dataIcon);
1191 } else {
1192 tr.attr('data-icon', icon);
1193 }
1194
1195 if (fileData.mountType) {
1196 // dirInfo (parent) only exist for the "real" file list
1197 if (this.dirInfo.id) {
1198 // FIXME: HACK: detect shared-root
1199 if (fileData.mountType === 'shared' && this.dirInfo.mountType !== 'shared' && this.dirInfo.mountType !== 'shared-root') {
1200 // if parent folder isn't share, assume the displayed folder is a share root
1201 fileData.mountType = 'shared-root';
1202 } else if (fileData.mountType === 'external' && this.dirInfo.mountType !== 'external' && this.dirInfo.mountType !== 'external-root') {
1203 // if parent folder isn't external, assume the displayed folder is the external storage root
1204 fileData.mountType = 'external-root';
1205 }
1206 }
1207 tr.attr('data-mounttype', fileData.mountType);
1208 }
1209
1210 if (!_.isUndefined(path)) {
1211 tr.attr('data-path', path);
1212 }
1213 else {
1214 path = this.getCurrentDirectory();
1215 }
1216
1217 // filename td
1218 td = $('<td class="filename"></td>');
1219
1220
1221 // linkUrl
1222 if (mime === 'httpd/unix-directory') {
1223 linkUrl = this.linkTo(path + '/' + name);
1224 }
1225 else {
1226 linkUrl = this.getDownloadUrl(name, path, type === 'dir');
1227 }
1228 if (this._allowSelection) {
1229 td.append(
1230 '<input id="select-' + this.id + '-' + fileData.id +
1231 '" type="checkbox" class="selectCheckBox checkbox"/><label for="select-' + this.id + '-' + fileData.id + '">' +
1232 '<div class="thumbnail" style="background-image:url(' + icon + '); background-size: 32px;"></div>' +
1233 '<span class="hidden-visually">' + t('files', 'Select') + '</span>' +
1234 '</label>'
1235 );
1236 } else {
1237 td.append('<div class="thumbnail" style="background-image:url(' + icon + '); background-size: 32px;"></div>');
1238 }
1239 var linkElem = $('<a></a>').attr({
1240 "class": "name",
1241 "href": linkUrl
1242 });
1243
1244 // from here work on the display name
1245 name = fileData.displayName || name;
1246
1247 // show hidden files (starting with a dot) completely in gray
1248 if(name.indexOf('.') === 0) {
1249 basename = '';
1250 extension = name;
1251 // split extension from filename for non dirs
1252 } else if (mime !== 'httpd/unix-directory' && name.indexOf('.') !== -1) {
1253 basename = name.substr(0, name.lastIndexOf('.'));
1254 extension = name.substr(name.lastIndexOf('.'));
1255 } else {
1256 basename = name;
1257 extension = false;
1258 }
1259 var nameSpan=$('<span></span>').addClass('nametext');
1260 var innernameSpan = $('<span></span>').addClass('innernametext').text(basename);
1261
1262 if (path && path !== '/') {
1263 var conflictingItems = this.$fileList.find('tr[data-file="' + this._jqSelEscape(name) + '"]');
1264 if (conflictingItems.length !== 0) {
1265 if (conflictingItems.length === 1) {
1266 // Update the path on the first conflicting item
1267 var $firstConflict = $(conflictingItems[0]),
1268 firstConflictPath = $firstConflict.attr('data-path') + '/';
1269 if (firstConflictPath.charAt(0) === '/') {
1270 firstConflictPath = firstConflictPath.substr(1);
1271 }
1272 $firstConflict.find('td.filename span.innernametext').prepend($('<span></span>').addClass('conflict-path').text(firstConflictPath));
1273 }
1274
1275 var conflictPath = path + '/';
1276 if (conflictPath.charAt(0) === '/') {
1277 conflictPath = conflictPath.substr(1);
1278 }
1279 nameSpan.append($('<span></span>').addClass('conflict-path').text(conflictPath));
1280 }
1281 }
1282
1283 nameSpan.append(innernameSpan);
1284 linkElem.append(nameSpan);
1285 if (extension) {
1286 nameSpan.append($('<span></span>').addClass('extension').text(extension));
1287 }
1288 if (fileData.extraData) {
1289 if (fileData.extraData.charAt(0) === '/') {
1290 fileData.extraData = fileData.extraData.substr(1);
1291 }
1292 nameSpan.addClass('extra-data').attr('title', fileData.extraData);
1293 nameSpan.tooltip({placement: 'right'});
1294 }
1295 // dirs can show the number of uploaded files
1296 if (mime === 'httpd/unix-directory') {
1297 linkElem.append($('<span></span>').attr({
1298 'class': 'uploadtext',
1299 'currentUploads': 0
1300 }));
1301 }
1302 td.append(linkElem);
1303 tr.append(td);
1304
1305 // date column (1000 milliseconds to seconds, 60 seconds, 60 minutes, 24 hours)
1306 // difference in days multiplied by 5 - brightest shade for files older than 32 days (160/5)
1307 var modifiedColor = Math.round(((new Date()).getTime() - mtime )/1000/60/60/24*5 );
1308 // ensure that the brightest color is still readable
1309 if (modifiedColor >= '160') {
1310 modifiedColor = 160;
1311 }
1312 var formatted;
1313 var text;
1314 if (mtime > 0) {
1315 formatted = OC.Util.formatDate(mtime);
1316 text = OC.Util.relativeModifiedDate(mtime);
1317 } else {
1318 formatted = t('files', 'Unable to determine date');
1319 text = '?';
1320 }
1321 td = $('<td></td>').attr({ "class": "date" });
1322 td.append($('<span></span>').attr({
1323 "class": "modified live-relative-timestamp",
1324 "title": formatted,
1325 "data-timestamp": mtime,
1326 "style": 'color:rgb('+modifiedColor+','+modifiedColor+','+modifiedColor+')'
1327 }).text(text)
1328 .tooltip({placement: 'top'})
1329 );
1330 tr.append(td);
1331
1332 // size column
1333 if (typeof(fileData.size) !== 'undefined' && fileData.size >= 0) {
1334 simpleSize = humanFileSize(parseInt(fileData.size, 10), true);
1335 sizeColor = Math.round(160-Math.pow((fileData.size/(1024*1024)),2));
1336 } else {
1337 simpleSize = t('files', 'Pending');
1338 }
1339
1340 td = $('<td></td>').attr({
1341 "class": "filesize",
1342 "style": 'color:rgb(' + sizeColor + ',' + sizeColor + ',' + sizeColor + ')'
1343 }).text(simpleSize);
1344 tr.append(td);
1345
1346 /*td = $('<td></td>').attr({
1347 "class": "actions"
1348 });
1349
1350 tr.append(td);*/
1351
1352 return tr;
1353 },
1354 /**
1355 * Creates a card using the given table row.
1356 * @param {OC.Files.FileInfo} fileData file info attributes
1357 * @param options map of attributes
1358 * @return new tr element (not appended to the table)
1359 */
1360 _createCard: function(fileData, options) {
1361 var td, simpleSize, basename, extension, sizeColor,
1362 icon = fileData.icon || this._getIconUrl(fileData),
1363 name = fileData.name,
1364 // TODO: get rid of type, only use mime type
1365 type = fileData.type || 'file',
1366 mtime = parseInt(fileData.mtime, 10),
1367 mime = fileData.mimetype,
1368 path = fileData.path,
1369 dataIcon = null,
1370 linkUrl;
1371 options = options || {};
1372
1373 if (isNaN(mtime)) {
1374 mtime = new Date().getTime();
1375 }
1376
1377 if (type === 'dir') {
1378 mime = mime || 'httpd/unix-directory';
1379
1380 if (fileData.mountType && fileData.mountType.indexOf('external') === 0) {
1381 icon = OC.MimeType.getIconUrl('dir-external');
1382 dataIcon = icon;
1383 }
1384 }
1385 //containing tr
1386 var card = $('<div></div>').attr({
1387 "class": 'card',
1388 "data-id" : fileData.id,
1389 "data-type": type,
1390 "data-size": fileData.size,
1391 "data-file": name,
1392 "data-mime": mime,
1393 "data-mtime": mtime,
1394 "data-etag": fileData.etag,
1395 "data-permissions": fileData.permissions || this.getDirectoryPermissions(),
1396 "data-has-preview": fileData.hasPreview !== false
1397 });
1398
1399 if (dataIcon) {
1400 // icon override
1401 card.attr('data-icon', dataIcon);
1402 } else {
1403 card.attr('data-icon', icon);
1404 }
1405
1406 if (!_.isUndefined(path)) {
1407 card.attr('data-path', path);
1408 }
1409 else {
1410 path = this.getCurrentDirectory();
1411 }
1412
1413 if (fileData.mountType) {
1414 // dirInfo (parent) only exist for the "real" file list
1415 if (this.dirInfo.id) {
1416 // FIXME: HACK: detect shared-root
1417 if (fileData.mountType === 'shared' && this.dirInfo.mountType !== 'shared' && this.dirInfo.mountType !== 'shared-root') {
1418 // if parent folder isn't share, assume the displayed folder is a share root
1419 fileData.mountType = 'shared-root';
1420 } else if (fileData.mountType === 'external' && this.dirInfo.mountType !== 'external' && this.dirInfo.mountType !== 'external-root') {
1421 // if parent folder isn't external, assume the displayed folder is the external storage root
1422 fileData.mountType = 'external-root';
1423 }
1424 }
1425 card.attr('data-mounttype', fileData.mountType);
1426 }
1427
1428 // linkUrl
1429 if (mime === 'httpd/unix-directory') {
1430 linkUrl = this.linkTo(path + '/' + name);
1431 }
1432 else {
1433 linkUrl = this.getDownloadUrl(name, path, type === 'dir');
1434 }
1435
1436 var linkElem = $('<a></a>').attr({
1437 "class": "name",
1438 "href": linkUrl
1439 });
1440
1441 var img = $('<img class="card-image" src="'+ (dataIcon ? dataIcon : icon) +'"/>');
1442 var fileName = $('<span class="card-name"><span>' + name + '</span></span>');
1443
1444 var $a = $('<a href="#" class="action action-favorite icon' + (typeof favorite !== 'undefined' ? ' icon-starred permanent' : '' ) + '"><span class="icon icon-star"></span><span class="hidden-visually">OblÃbené</span></a>');
1445 fileName.append($a);
1446 // } else {
1447 // fileName.append($('<div class="icon-star"></div>'));
1448 //}
1449
1450 //card.append(img);
1451 linkElem.append(img);
1452 linkElem.append(fileName);
1453 card.append(linkElem);
1454
1455 return card;
1456 },
1457 /* escape a selector expression for jQuery */
1458 _jqSelEscape: function (expression) {
1459 if (expression) {
1460 return expression.replace(/[!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~]/g, '\\$&');
1461 }
1462 return null;
1463 },
1464
1465 /**
1466 * Adds an entry to the files array and also into the DOM
1467 * in a sorted manner.
1468 *
1469 * @param {OC.Files.FileInfo} fileData map of file attributes
1470 * @param {Object} [options] map of attributes
1471 * @param {boolean} [options.updateSummary] true to update the summary
1472 * after adding (default), false otherwise. Defaults to true.
1473 * @param {boolean} [options.silent] true to prevent firing events like "fileActionsReady",
1474 * defaults to false.
1475 * @param {boolean} [options.animate] true to animate the thumbnail image after load
1476 * defaults to true.
1477 * @return new tr element (not appended to the table)
1478 */
1479 add: function(fileData, options) {
1480 var index = -1;
1481 var $tr;
1482 var $rows;
1483 var $insertionPoint;
1484 options = _.extend({animate: true}, options || {});
1485
1486 // there are three situations to cover:
1487 // 1) insertion point is visible on the current page
1488 // 2) insertion point is on a not visible page (visible after scrolling)
1489 // 3) insertion point is at the end of the list
1490
1491 $rows = this.$fileList.children();
1492 index = this._findInsertionIndex(fileData);
1493 if (index > this.files.length) {
1494 index = this.files.length;
1495 }
1496 else {
1497 $insertionPoint = $rows.eq(index);
1498 }
1499
1500 // is the insertion point visible ?
1501 if ($insertionPoint.length) {
1502 // only render if it will really be inserted
1503 $tr = this._renderRow(fileData, options);
1504 $insertionPoint.before($tr);
1505 }
1506 else {
1507 // if insertion point is after the last visible
1508 // entry, append
1509 if (index === $rows.length) {
1510 $tr = this._renderRow(fileData, options);
1511 this.$fileList.append($tr);
1512 }
1513 }
1514
1515 this.isEmpty = false;
1516 this.files.splice(index, 0, fileData);
1517
1518 if ($tr && options.animate) {
1519 $tr.addClass('appear transparent');
1520 window.setTimeout(function() {
1521 $tr.removeClass('transparent');
1522 });
1523 }
1524
1525 if (options.scrollTo) {
1526 this.scrollTo(fileData.name);
1527 }
1528
1529 // defaults to true if not defined
1530 if (typeof(options.updateSummary) === 'undefined' || !!options.updateSummary) {
1531 this.fileSummary.add(fileData, true);
1532 this.updateEmptyContent();
1533 }
1534
1535 return $tr;
1536 },
1537
1538 /**
1539 * Creates a new row element based on the given attributes
1540 * and returns it.
1541 *
1542 * @param {OC.Files.FileInfo} fileData map of file attributes
1543 * @param {Object} [options] map of attributes
1544 * @param {int} [options.index] index at which to insert the element
1545 * @param {boolean} [options.updateSummary] true to update the summary
1546 * after adding (default), false otherwise. Defaults to true.
1547 * @param {boolean} [options.animate] true to animate the thumbnail image after load
1548 * defaults to true.
1549 * @return new tr element (not appended to the table)
1550 */
1551 _renderRow: function(fileData, options, card) {
1552 options = options || {};
1553 var type = fileData.type || 'file',
1554 mime = fileData.mimetype,
1555 path = fileData.path || this.getCurrentDirectory(),
1556 permissions = parseInt(fileData.permissions, 10) || 0;
1557
1558 if (fileData.isShareMountPoint) {
1559 permissions = permissions | OC.PERMISSION_UPDATE;
1560 }
1561
1562 if (type === 'dir') {
1563 mime = mime || 'httpd/unix-directory';
1564 }
1565 var tr = this._createRow(
1566 fileData,
1567 options
1568 );
1569 var filenameTd = tr.find('td.filename');
1570 var filenameCard;
1571 if (card){
1572 filenameCard = card.find('span.card-name');
1573 }
1574 // TODO: move dragging to FileActions ?
1575 // enable drag only for deletable files
1576 if (this._dragOptions && permissions & OC.PERMISSION_DELETE) {
1577 filenameTd.draggable(this._dragOptions);
1578 }
1579 // allow dropping on folders
1580 if (this._folderDropOptions && mime === 'httpd/unix-directory') {
1581 tr.droppable(this._folderDropOptions);
1582 }
1583
1584 if (options.hidden) {
1585 tr.addClass('hidden');
1586 }
1587
1588 if (this._isHiddenFile(fileData)) {
1589 tr.addClass('hidden-file');
1590 }
1591
1592 // display actions
1593 this.fileActions.display(filenameTd, !options.silent, this, filenameCard);
1594
1595 if (mime !== 'httpd/unix-directory' && fileData.hasPreview !== false) {
1596 var iconDiv = filenameTd.find('.thumbnail');
1597 // lazy load / newly inserted td ?
1598 // the typeof check ensures that the default value of animate is true
1599 if (typeof(options.animate) === 'undefined' || !!options.animate) {
1600 this.lazyLoadPreview({
1601 path: path + '/' + fileData.name,
1602 mime: mime,
1603 etag: fileData.etag,
1604 callback: function(url) {
1605 iconDiv.css('background-image', 'url("' + url + '")');
1606 }
1607 });
1608 }
1609 else {
1610 // set the preview URL directly
1611 var urlSpec = {
1612 file: path + '/' + fileData.name,
1613 c: fileData.etag
1614 };
1615 var previewUrl = this.generatePreviewUrl(urlSpec);
1616 previewUrl = previewUrl.replace('(', '%28').replace(')', '%29');
1617 iconDiv.css('background-image', 'url("' + previewUrl + '")');
1618 }
1619 }
1620 return tr;
1621 },
1622 /**
1623 * Creates a new row element based on the given attributes
1624 * and returns it.
1625 *
1626 * @param {OC.Files.FileInfo} fileData map of file attributes
1627 * @param {Object} [options] map of attributes
1628 * @param {int} [options.index] index at which to insert the element
1629 * @param {boolean} [options.updateSummary] true to update the summary
1630 * after adding (default), false otherwise. Defaults to true.
1631 * @param {boolean} [options.animate] true to animate the thumbnail image after load
1632 * defaults to true.
1633 * @return new tr element (not appended to the table)
1634 */
1635 _renderCard: function(fileData, options) {
1636 var $card = this._createCard(fileData, options);
1637 //var $cardName = $card.find('.card-name');
1638 //var $td = row.find('td.filename');
1639 //this.fileActions.display($td, !options.silent, this);
1640 return $card;
1641 },
1642 /**
1643 * Returns the current directory
1644 * @method getCurrentDirectory
1645 * @return current directory
1646 */
1647 getCurrentDirectory: function(){
1648 return this._currentDirectory || this.$el.find('#dir').val() || '/';
1649 },
1650 /**
1651 * Returns the directory permissions
1652 * @return permission value as integer
1653 */
1654 getDirectoryPermissions: function() {
1655 return parseInt(this.$el.find('#permissions').val(), 10);
1656 },
1657 /**
1658 * Changes the current directory and reload the file list.
1659 * @param {string} targetDir target directory (non URL encoded)
1660 * @param {boolean} [changeUrl=true] if the URL must not be changed (defaults to true)
1661 * @param {boolean} [force=false] set to true to force changing directory
1662 * @param {string} [fileId] optional file id, if known, to be appended in the URL
1663 */
1664 changeDirectory: function(targetDir, changeUrl, force, fileId) {
1665 var self = this;
1666 var currentDir = this.getCurrentDirectory();
1667 targetDir = targetDir || '/';
1668 if (!force && currentDir === targetDir) {
1669 return;
1670 }
1671 this._setCurrentDir(targetDir, changeUrl, fileId);
1672
1673 // discard finished uploads list, we'll get it through a regular reload
1674 this._uploads = {};
1675 this.reload().then(function(success){
1676 if (!success) {
1677 self.changeDirectory(currentDir, true);
1678 }
1679 });
1680 },
1681 linkTo: function(dir) {
1682 return OC.linkTo('files', 'index.php')+"?dir="+ encodeURIComponent(dir).replace(/%2F/g, '/');
1683 },
1684
1685 /**
1686 * @param {string} path
1687 * @returns {boolean}
1688 */
1689 _isValidPath: function(path) {
1690 var sections = path.split('/');
1691 for (var i = 0; i < sections.length; i++) {
1692 if (sections[i] === '..') {
1693 return false;
1694 }
1695 }
1696
1697 return path.toLowerCase().indexOf(decodeURI('%0a')) === -1 &&
1698 path.toLowerCase().indexOf(decodeURI('%00')) === -1;
1699 },
1700
1701 /**
1702 * Sets the current directory name and updates the breadcrumb.
1703 * @param targetDir directory to display
1704 * @param changeUrl true to also update the URL, false otherwise (default)
1705 * @param {string} [fileId] file id
1706 */
1707 _setCurrentDir: function(targetDir, changeUrl, fileId) {
1708 targetDir = targetDir.replace(/\\/g, '/');
1709 if (!this._isValidPath(targetDir)) {
1710 targetDir = '/';
1711 changeUrl = true;
1712 }
1713 var previousDir = this.getCurrentDirectory(),
1714 baseDir = OC.basename(targetDir);
1715
1716 if (baseDir !== '') {
1717 this.setPageTitle(baseDir);
1718 }
1719 else {
1720 this.setPageTitle();
1721 }
1722
1723 if (targetDir.length > 0 && targetDir[0] !== '/') {
1724 targetDir = '/' + targetDir;
1725 }
1726 this._currentDirectory = targetDir;
1727
1728 // legacy stuff
1729 this.$el.find('#dir').val(targetDir);
1730
1731 if (changeUrl !== false) {
1732 var params = {
1733 dir: targetDir,
1734 previousDir: previousDir
1735 };
1736 if (fileId) {
1737 params.fileId = fileId;
1738 }
1739 this.$el.trigger(jQuery.Event('changeDirectory', params));
1740 }
1741 this.breadcrumb.setDirectory(this.getCurrentDirectory());
1742 },
1743 /**
1744 * Sets the current sorting and refreshes the list
1745 *
1746 * @param sort sort attribute name
1747 * @param direction sort direction, one of "asc" or "desc"
1748 * @param update true to update the list, false otherwise (default)
1749 * @param persist true to save changes in the database (default)
1750 */
1751 setSort: function(sort, direction, update, persist) {
1752 var comparator = FileList.Comparators[sort] || FileList.Comparators.name;
1753 this._sort = sort;
1754 this._sortDirection = (direction === 'desc')?'desc':'asc';
1755 this._sortComparator = function(fileInfo1, fileInfo2) {
1756 if(fileInfo1.isFavorite && !fileInfo2.isFavorite) {
1757 return -1;
1758 } else if(!fileInfo1.isFavorite && fileInfo2.isFavorite) {
1759 return 1;
1760 }
1761 return direction === 'asc' ? comparator(fileInfo1, fileInfo2) : -comparator(fileInfo1, fileInfo2);
1762 };
1763
1764 this.$el.find('thead th .sort-indicator')
1765 .removeClass(this.SORT_INDICATOR_ASC_CLASS)
1766 .removeClass(this.SORT_INDICATOR_DESC_CLASS)
1767 .toggleClass('hidden', true)
1768 .addClass(this.SORT_INDICATOR_DESC_CLASS);
1769
1770 this.$el.find('thead th.column-' + sort + ' .sort-indicator')
1771 .removeClass(this.SORT_INDICATOR_ASC_CLASS)
1772 .removeClass(this.SORT_INDICATOR_DESC_CLASS)
1773 .toggleClass('hidden', false)
1774 .addClass(direction === 'desc' ? this.SORT_INDICATOR_DESC_CLASS : this.SORT_INDICATOR_ASC_CLASS);
1775 if (update) {
1776 if (this._clientSideSort) {
1777 this.files.sort(this._sortComparator);
1778 this.setFiles(this.files);
1779 }
1780 else {
1781 this.reload();
1782 }
1783 }
1784
1785 if (persist) {
1786 $.post(OC.generateUrl('/apps/files/api/v1/sorting'), {
1787 mode: sort,
1788 direction: direction
1789 });
1790 }
1791 },
1792
1793 /**
1794 * Returns list of webdav properties to request
1795 */
1796 _getWebdavProperties: function() {
1797 return [].concat(this.filesClient.getPropfindProperties());
1798 },
1799
1800 /**
1801 * Reloads the file list using ajax call
1802 *
1803 * @return ajax call object
1804 */
1805 reload: function() {
1806 this._selectedFiles = {};
1807 this._selectionSummary.clear();
1808 if (this._currentFileModel) {
1809 this._currentFileModel.off();
1810 }
1811 this._currentFileModel = null;
1812 this.$el.find('.select-all').prop('checked', false);
1813 this.showMask();
1814 this._reloadCall = this.filesClient.getFolderContents(
1815 this.getCurrentDirectory(), {
1816 includeParent: true,
1817 properties: this._getWebdavProperties()
1818 }
1819 );
1820 if (this._detailsView) {
1821 // close sidebar
1822 this._updateDetailsView(null);
1823 }
1824 var callBack = this.reloadCallback.bind(this);
1825 return this._reloadCall.then(callBack, callBack);
1826 },
1827 reloadCallback: function(status, result) {
1828 delete this._reloadCall;
1829 this.hideMask();
1830
1831 if (status === 401) {
1832 return false;
1833 }
1834
1835 // Firewall Blocked request?
1836 if (status === 403) {
1837 // Go home
1838 this.changeDirectory('/');
1839 OC.Notification.show(t('files', 'This operation is forbidden'), {type: 'error'});
1840 return false;
1841 }
1842
1843 // Did share service die or something else fail?
1844 if (status === 500) {
1845 // Go home
1846 this.changeDirectory('/');
1847 OC.Notification.show(t('files', 'This directory is unavailable, please check the logs or contact the administrator'),
1848 {type: 'error'}
1849 );
1850 return false;
1851 }
1852
1853 if (status === 503) {
1854 // Go home
1855 if (this.getCurrentDirectory() !== '/') {
1856 this.changeDirectory('/');
1857 // TODO: read error message from exception
1858 OC.Notification.show(t('files', 'Storage is temporarily not available'),
1859 {type: 'error'}
1860 );
1861 }
1862 return false;
1863 }
1864
1865 if (status === 400 || status === 404 || status === 405) {
1866 // go back home
1867 this.changeDirectory('/');
1868 return false;
1869 }
1870 // aborted ?
1871 if (status === 0){
1872 return true;
1873 }
1874
1875 // TODO: parse remaining quota from PROPFIND response
1876 this.updateStorageStatistics(true);
1877
1878 // first entry is the root
1879 this.dirInfo = result.shift();
1880 this.breadcrumb.setDirectoryInfo(this.dirInfo);
1881
1882 if (this.dirInfo.permissions) {
1883 this.setDirectoryPermissions(this.dirInfo.permissions);
1884 }
1885
1886 result.sort(this._sortComparator);
1887 this.setFiles(result);
1888
1889 if (this.dirInfo) {
1890 var newFileId = this.dirInfo.id;
1891 // update fileid in URL
1892 var params = {
1893 dir: this.getCurrentDirectory()
1894 };
1895 if (newFileId) {
1896 params.fileId = newFileId;
1897 }
1898 this.$el.trigger(jQuery.Event('afterChangeDirectory', params));
1899 }
1900 return true;
1901 },
1902
1903 updateStorageStatistics: function(force) {
1904 OCA.Files.Files.updateStorageStatistics(this.getCurrentDirectory(), force);
1905 },
1906
1907 /**
1908 * @deprecated do not use nor override
1909 */
1910 getAjaxUrl: function(action, params) {
1911 return OCA.Files.Files.getAjaxUrl(action, params);
1912 },
1913
1914 getDownloadUrl: function(files, dir, isDir) {
1915 return OCA.Files.Files.getDownloadUrl(files, dir || this.getCurrentDirectory(), isDir);
1916 },
1917
1918 getUploadUrl: function(fileName, dir) {
1919 if (_.isUndefined(dir)) {
1920 dir = this.getCurrentDirectory();
1921 }
1922
1923 var pathSections = dir.split('/');
1924 if (!_.isUndefined(fileName)) {
1925 pathSections.push(fileName);
1926 }
1927 var encodedPath = '';
1928 _.each(pathSections, function(section) {
1929 if (section !== '') {
1930 encodedPath += '/' + encodeURIComponent(section);
1931 }
1932 });
1933 return OC.linkToRemoteBase('webdav') + encodedPath;
1934 },
1935
1936 /**
1937 * Generates a preview URL based on the URL space.
1938 * @param urlSpec attributes for the URL
1939 * @param {int} urlSpec.x width
1940 * @param {int} urlSpec.y height
1941 * @param {String} urlSpec.file path to the file
1942 * @return preview URL
1943 */
1944 generatePreviewUrl: function(urlSpec) {
1945 urlSpec = urlSpec || {};
1946 if (!urlSpec.x) {
1947 urlSpec.x = this.$table.data('preview-x') || 32;
1948 }
1949 if (!urlSpec.y) {
1950 urlSpec.y = this.$table.data('preview-y') || 32;
1951 }
1952 urlSpec.x *= window.devicePixelRatio;
1953 urlSpec.y *= window.devicePixelRatio;
1954 urlSpec.x = Math.ceil(urlSpec.x);
1955 urlSpec.y = Math.ceil(urlSpec.y);
1956 urlSpec.forceIcon = 0;
1957 return OC.generateUrl('/core/preview.png?') + $.param(urlSpec);
1958 },
1959
1960 /**
1961 * Lazy load a file's preview.
1962 *
1963 * @param path path of the file
1964 * @param mime mime type
1965 * @param callback callback function to call when the image was loaded
1966 * @param etag file etag (for caching)
1967 */
1968 lazyLoadPreview : function(options) {
1969 var self = this;
1970 var path = options.path;
1971 var mime = options.mime;
1972 var ready = options.callback;
1973 var etag = options.etag;
1974
1975 // get mime icon url
1976 var iconURL = OC.MimeType.getIconUrl(mime);
1977 var previewURL,
1978 urlSpec = {};
1979 ready(iconURL); // set mimeicon URL
1980
1981 urlSpec.file = OCA.Files.Files.fixPath(path);
1982 if (options.x) {
1983 urlSpec.x = options.x;
1984 }
1985 if (options.y) {
1986 urlSpec.y = options.y;
1987 }
1988 if (options.a) {
1989 urlSpec.a = options.a;
1990 }
1991 if (options.mode) {
1992 urlSpec.mode = options.mode;
1993 }
1994
1995 if (etag){
1996 // use etag as cache buster
1997 urlSpec.c = etag;
1998 }
1999
2000 previewURL = self.generatePreviewUrl(urlSpec);
2001 previewURL = previewURL.replace('(', '%28');
2002 previewURL = previewURL.replace(')', '%29');
2003
2004 // preload image to prevent delay
2005 // this will make the browser cache the image
2006 var img = new Image();
2007 img.onload = function(){
2008 // if loading the preview image failed (no preview for the mimetype) then img.width will < 5
2009 if (img.width > 5) {
2010 ready(previewURL, img);
2011 } else if (options.error) {
2012 options.error();
2013 }
2014 };
2015 if (options.error) {
2016 img.onerror = options.error;
2017 }
2018 img.src = previewURL;
2019 },
2020
2021 /**
2022 * @deprecated
2023 */
2024 setDirectoryPermissions: function(permissions) {
2025 var isCreatable = (permissions & OC.PERMISSION_CREATE) !== 0;
2026 this.$el.find('#permissions').val(permissions);
2027 this.$el.find('.creatable').toggleClass('hidden', !isCreatable);
2028 this.$el.find('.notCreatable').toggleClass('hidden', isCreatable);
2029 },
2030 /**
2031 * Shows/hides action buttons
2032 *
2033 * @param show true for enabling, false for disabling
2034 */
2035 showActions: function(show){
2036 this.$el.find('.actions,#file_action_panel').toggleClass('hidden', !show);
2037 if (show){
2038 // make sure to display according to permissions
2039 var permissions = this.getDirectoryPermissions();
2040 var isCreatable = (permissions & OC.PERMISSION_CREATE) !== 0;
2041 this.$el.find('.creatable').toggleClass('hidden', !isCreatable);
2042 this.$el.find('.notCreatable').toggleClass('hidden', isCreatable);
2043 // remove old style breadcrumbs (some apps might create them)
2044 this.$el.find('#controls .crumb').remove();
2045 // refresh breadcrumbs in case it was replaced by an app
2046 this.breadcrumb.render();
2047 }
2048 else{
2049 this.$el.find('.creatable, .notCreatable').addClass('hidden');
2050 }
2051 },
2052 /**
2053 * Enables/disables viewer mode.
2054 * In viewer mode, apps can embed themselves under the controls bar.
2055 * In viewer mode, the actions of the file list will be hidden.
2056 * @param show true for enabling, false for disabling
2057 */
2058 setViewerMode: function(show){
2059 this.showActions(!show);
2060 this.$el.find('#filestable').toggleClass('hidden', show);
2061 this.$el.trigger(new $.Event('changeViewerMode', {viewerModeEnabled: show}));
2062 },
2063 /**
2064 * Removes a file entry from the list
2065 * @param name name of the file to remove
2066 * @param {Object} [options] map of attributes
2067 * @param {boolean} [options.updateSummary] true to update the summary
2068 * after removing, false otherwise. Defaults to true.
2069 * @return deleted element
2070 */
2071 remove: function(name, options){
2072 options = options || {};
2073 var fileEl = this.findFileEl(name);
2074 var fileId = fileEl.data('id');
2075 var index = fileEl.index();
2076 if (!fileEl.length) {
2077 return null;
2078 }
2079 if (this._selectedFiles[fileId]) {
2080 // remove from selection first
2081 this._selectFileEl(fileEl, false);
2082 this.updateSelectionSummary();
2083 }
2084 if (this._dragOptions && (fileEl.data('permissions') & OC.PERMISSION_DELETE)) {
2085 // file is only draggable when delete permissions are set
2086 fileEl.find('td.filename').draggable('destroy');
2087 }
2088 this.files.splice(index, 1);
2089 if (this._currentFileModel && this._currentFileModel.get('id') === fileId) {
2090 // Note: in the future we should call destroy() directly on the model
2091 // and the model will take care of the deletion.
2092 // Here we only trigger the event to notify listeners that
2093 // the file was removed.
2094 this._currentFileModel.trigger('destroy');
2095 this._updateDetailsView(null);
2096 }
2097 fileEl.remove();
2098 // TODO: improve performance on batch update
2099 this.isEmpty = !this.files.length;
2100 if (typeof(options.updateSummary) === 'undefined' || !!options.updateSummary) {
2101 this.updateEmptyContent();
2102 this.fileSummary.remove({type: fileEl.attr('data-type'), size: fileEl.attr('data-size')}, true);
2103 }
2104
2105 var lastIndex = this.$fileList.children().length;
2106 // if there are less elements visible than one page
2107 // but there are still pending elements in the array,
2108 // then directly append the next page
2109 if (lastIndex < this.files.length && lastIndex < this.pageSize()) {
2110 this._nextPage(true);
2111 }
2112
2113 return fileEl;
2114 },
2115 /**
2116 * Finds the index of the row before which the given
2117 * fileData should be inserted, considering the current
2118 * sorting
2119 *
2120 * @param {OC.Files.FileInfo} fileData file info
2121 */
2122 _findInsertionIndex: function(fileData) {
2123 var index = 0;
2124 while (index < this.files.length && this._sortComparator(fileData, this.files[index]) > 0) {
2125 index++;
2126 }
2127 return index;
2128 },
2129 /**
2130 * Moves a file to a given target folder.
2131 *
2132 * @param fileNames array of file names to move
2133 * @param targetPath absolute target path
2134 */
2135 move: function(fileNames, targetPath) {
2136 var self = this;
2137 var dir = this.getCurrentDirectory();
2138 if (dir.charAt(dir.length - 1) !== '/') {
2139 dir += '/';
2140 }
2141 var target = OC.basename(targetPath);
2142 if (!_.isArray(fileNames)) {
2143 fileNames = [fileNames];
2144 }
2145 _.each(fileNames, function(fileName) {
2146 var $tr = self.findFileEl(fileName);
2147 self.showFileBusyState($tr, true);
2148 if (targetPath.charAt(targetPath.length - 1) !== '/') {
2149 // make sure we move the files into the target dir,
2150 // not overwrite it
2151 targetPath = targetPath + '/';
2152 }
2153 self.filesClient.move(dir + fileName, targetPath + fileName)
2154 .done(function() {
2155 // if still viewing the same directory
2156 if (OC.joinPaths(self.getCurrentDirectory(), '/') === dir) {
2157 // recalculate folder size
2158 var oldFile = self.findFileEl(target);
2159 var newFile = self.findFileEl(fileName);
2160 var oldSize = oldFile.data('size');
2161 var newSize = oldSize + newFile.data('size');
2162 oldFile.data('size', newSize);
2163 oldFile.find('td.filesize').text(OC.Util.humanFileSize(newSize));
2164
2165 // TODO: also update entry in FileList.files
2166 self.remove(fileName);
2167 }
2168 })
2169 .fail(function(status) {
2170 if (status === 412) {
2171 // TODO: some day here we should invoke the conflict dialog
2172 OC.Notification.show(t('files', 'Could not move "{file}", target exists',
2173 {file: fileName}), {type: 'error'}
2174 );
2175 } else {
2176 OC.Notification.show(t('files', 'Could not move "{file}"',
2177 {file: fileName}), {type: 'error'}
2178 );
2179 }
2180 })
2181 .always(function() {
2182 self.showFileBusyState($tr, false);
2183 });
2184 });
2185
2186 },
2187
2188 /**
2189 * Updates the given row with the given file info
2190 *
2191 * @param {Object} $tr row element
2192 * @param {OCA.Files.FileInfo} fileInfo file info
2193 * @param {Object} options options
2194 *
2195 * @return {Object} new row element
2196 */
2197 updateRow: function($tr, fileInfo, options) {
2198 this.files.splice($tr.index(), 1);
2199 $tr.remove();
2200 options = _.extend({silent: true}, options);
2201 options = _.extend(options, {updateSummary: false});
2202 $tr = this.add(fileInfo, options);
2203 this.$fileList.trigger($.Event('fileActionsReady', {fileList: this, $files: $tr}));
2204 return $tr;
2205 },
2206
2207 /**
2208 * Triggers file rename input field for the given file name.
2209 * If the user enters a new name, the file will be renamed.
2210 *
2211 * @param oldName file name of the file to rename
2212 */
2213 rename: function(oldName) {
2214 var self = this;
2215 var tr, td, input, form;
2216 tr = this.findFileEl(oldName);
2217 var oldFileInfo = this.files[tr.index()];
2218 tr.data('renaming',true);
2219 td = tr.children('td.filename');
2220 input = $('<input type="text" class="filename"/>').val(oldName);
2221 form = $('<form></form>');
2222 form.append(input);
2223 td.children('a.name').hide();
2224 td.append(form);
2225 input.focus();
2226 //preselect input
2227 var len = input.val().lastIndexOf('.');
2228 if ( len === -1 ||
2229 tr.data('type') === 'dir' ) {
2230 len = input.val().length;
2231 }
2232 input.selectRange(0, len);
2233 var checkInput = function () {
2234 var filename = input.val();
2235 if (filename !== oldName) {
2236 // Files.isFileNameValid(filename) throws an exception itself
2237 OCA.Files.Files.isFileNameValid(filename);
2238 if (self.inList(filename)) {
2239 throw t('files', '{newName} already exists', {newName: filename}, undefined, {
2240 escape: false
2241 });
2242 }
2243 }
2244 return true;
2245 };
2246
2247 function restore() {
2248 input.tooltip('hide');
2249 tr.data('renaming',false);
2250 form.remove();
2251 td.children('a.name').show();
2252 }
2253
2254 function updateInList(fileInfo) {
2255 self.updateRow(tr, fileInfo);
2256 self._updateDetailsView(fileInfo, false);
2257 }
2258
2259 // TODO: too many nested blocks, move parts into functions
2260 form.submit(function(event) {
2261 event.stopPropagation();
2262 event.preventDefault();
2263 if (input.hasClass('error')) {
2264 return;
2265 }
2266
2267 try {
2268 var newName = input.val();
2269 input.tooltip('hide');
2270 form.remove();
2271
2272 if (newName !== oldName) {
2273 checkInput();
2274 // mark as loading (temp element)
2275 self.showFileBusyState(tr, true);
2276 tr.attr('data-file', newName);
2277 var basename = newName;
2278 if (newName.indexOf('.') > 0 && tr.data('type') !== 'dir') {
2279 basename = newName.substr(0, newName.lastIndexOf('.'));
2280 }
2281 td.find('a.name span.nametext').text(basename);
2282 td.children('a.name').show();
2283
2284 var path = tr.attr('data-path') || self.getCurrentDirectory();
2285 self.filesClient.move(OC.joinPaths(path, oldName), OC.joinPaths(path, newName))
2286 .done(function() {
2287 oldFileInfo.name = newName;
2288 updateInList(oldFileInfo);
2289 })
2290 .fail(function(status) {
2291 // TODO: 409 means current folder does not exist, redirect ?
2292 if (status === 404) {
2293 // source not found, so remove it from the list
2294 OC.Notification.show(t('files', 'Could not rename "{fileName}", it does not exist any more',
2295 {fileName: oldName}), {timeout: 7, type: 'error'}
2296 );
2297
2298 self.remove(newName, {updateSummary: true});
2299 return;
2300 } else if (status === 412) {
2301 // target exists
2302 OC.Notification.show(
2303 t('files', 'The name "{targetName}" is already used in the folder "{dir}". Please choose a different name.',
2304 {
2305 targetName: newName,
2306 dir: self.getCurrentDirectory(),
2307 }),
2308 {
2309 type: 'error'
2310 }
2311 );
2312 } else {
2313 // restore the item to its previous state
2314 OC.Notification.show(t('files', 'Could not rename "{fileName}"',
2315 {fileName: oldName}), {type: 'error'}
2316 );
2317 }
2318 updateInList(oldFileInfo);
2319 });
2320 } else {
2321 // add back the old file info when cancelled
2322 self.files.splice(tr.index(), 1);
2323 tr.remove();
2324 tr = self.add(oldFileInfo, {updateSummary: false, silent: true});
2325 self.$fileList.trigger($.Event('fileActionsReady', {fileList: self, $files: $(tr)}));
2326 }
2327 } catch (error) {
2328 input.attr('title', error);
2329 input.tooltip({placement: 'right', trigger: 'manual'});
2330 input.tooltip('fixTitle');
2331 input.tooltip('show');
2332 input.addClass('error');
2333 }
2334 return false;
2335 });
2336 input.keyup(function(event) {
2337 // verify filename on typing
2338 try {
2339 checkInput();
2340 input.tooltip('hide');
2341 input.removeClass('error');
2342 } catch (error) {
2343 input.attr('title', error);
2344 input.tooltip({placement: 'right', trigger: 'manual'});
2345 input.tooltip('fixTitle');
2346 input.tooltip('show');
2347 input.addClass('error');
2348 }
2349 if (event.keyCode === 27) {
2350 restore();
2351 }
2352 });
2353 input.click(function(event) {
2354 event.stopPropagation();
2355 event.preventDefault();
2356 });
2357 input.blur(function() {
2358 form.trigger('submit');
2359 });
2360 },
2361
2362 /**
2363 * Create an empty file inside the current directory.
2364 *
2365 * @param {string} name name of the file
2366 *
2367 * @return {Promise} promise that will be resolved after the
2368 * file was created
2369 *
2370 * @since 8.2
2371 */
2372 createFile: function(name) {
2373 var self = this;
2374 var deferred = $.Deferred();
2375 var promise = deferred.promise();
2376
2377 OCA.Files.Files.isFileNameValid(name);
2378
2379 if (this.lastAction) {
2380 this.lastAction();
2381 }
2382
2383 name = this.getUniqueName(name);
2384 var targetPath = this.getCurrentDirectory() + '/' + name;
2385
2386 self.filesClient.putFileContents(
2387 targetPath,
2388 ' ', // dont create empty files which fails on some storage backends
2389 {
2390 contentType: 'text/plain',
2391 overwrite: true
2392 }
2393 )
2394 .done(function() {
2395 // TODO: error handling / conflicts
2396 self.addAndFetchFileInfo(targetPath, '', {scrollTo: true}).then(function(status, data) {
2397 deferred.resolve(status, data);
2398 }, function() {
2399 OC.Notification.show(t('files', 'Could not create file "{file}"',
2400 {file: name}), {type: 'error'}
2401 );
2402 });
2403 })
2404 .fail(function(status) {
2405 if (status === 412) {
2406 OC.Notification.show(t('files', 'Could not create file "{file}" because it already exists',
2407 {file: name}), {type: 'error'}
2408 );
2409 } else {
2410 OC.Notification.show(t('files', 'Could not create file "{file}"',
2411 {file: name}), {type: 'error'}
2412 );
2413 }
2414 deferred.reject(status);
2415 });
2416
2417 return promise;
2418 },
2419
2420 /**
2421 * Create a directory inside the current directory.
2422 *
2423 * @param {string} name name of the directory
2424 *
2425 * @return {Promise} promise that will be resolved after the
2426 * directory was created
2427 *
2428 * @since 8.2
2429 */
2430 createDirectory: function(name) {
2431 var self = this;
2432 var deferred = $.Deferred();
2433 var promise = deferred.promise();
2434
2435 OCA.Files.Files.isFileNameValid(name);
2436
2437 if (this.lastAction) {
2438 this.lastAction();
2439 }
2440
2441 name = this.getUniqueName(name);
2442 var targetPath = this.getCurrentDirectory() + '/' + name;
2443
2444 this.filesClient.createDirectory(targetPath)
2445 .done(function() {
2446 self.addAndFetchFileInfo(targetPath, '', {scrollTo:true}).then(function(status, data) {
2447 deferred.resolve(status, data);
2448 }, function() {
2449 OC.Notification.show(t('files', 'Could not create folder "{dir}"',
2450 {dir: name}), {type: 'error'}
2451 );
2452 });
2453 })
2454 .fail(function(createStatus) {
2455 // method not allowed, folder might exist already
2456 if (createStatus === 405) {
2457 // add it to the list, for completeness
2458 self.addAndFetchFileInfo(targetPath, '', {scrollTo:true})
2459 .done(function(status, data) {
2460 OC.Notification.show(t('files', 'Could not create folder "{dir}" because it already exists',
2461 {dir: name}), {type: 'error'}
2462 );
2463 // still consider a failure
2464 deferred.reject(createStatus, data);
2465 })
2466 .fail(function() {
2467 OC.Notification.show(t('files', 'Could not create folder "{dir}"',
2468 {dir: name}), {type: 'error'}
2469 );
2470 deferred.reject(status);
2471 });
2472 } else {
2473 OC.Notification.show(t('files', 'Could not create folder "{dir}"',
2474 {dir: name}), {type: 'error'}
2475 );
2476 deferred.reject(createStatus);
2477 }
2478 });
2479
2480 return promise;
2481 },
2482
2483 /**
2484 * Add file into the list by fetching its information from the server first.
2485 *
2486 * If the given directory does not match the current directory, nothing will
2487 * be fetched.
2488 *
2489 * @param {String} fileName file name
2490 * @param {String} [dir] optional directory, defaults to the current one
2491 * @param {Object} options same options as #add
2492 * @return {Promise} promise that resolves with the file info, or an
2493 * already resolved Promise if no info was fetched. The promise rejects
2494 * if the file was not found or an error occurred.
2495 *
2496 * @since 9.0
2497 */
2498 addAndFetchFileInfo: function(fileName, dir, options) {
2499 var self = this;
2500 var deferred = $.Deferred();
2501 if (_.isUndefined(dir)) {
2502 dir = this.getCurrentDirectory();
2503 } else {
2504 dir = dir || '/';
2505 }
2506
2507 var targetPath = OC.joinPaths(dir, fileName);
2508
2509 if ((OC.dirname(targetPath) || '/') !== this.getCurrentDirectory()) {
2510 // no need to fetch information
2511 deferred.resolve();
2512 return deferred.promise();
2513 }
2514
2515 var addOptions = _.extend({
2516 animate: true,
2517 scrollTo: false
2518 }, options || {});
2519
2520 this.filesClient.getFileInfo(targetPath, {
2521 properties: this._getWebdavProperties()
2522 })
2523 .then(function(status, data) {
2524 // remove first to avoid duplicates
2525 self.remove(data.name);
2526 self.add(data, addOptions);
2527 deferred.resolve(status, data);
2528 })
2529 .fail(function(status) {
2530 OC.Notification.show(t('files', 'Could not create file "{file}"',
2531 {file: name}), {type: 'error'}
2532 );
2533 deferred.reject(status);
2534 });
2535
2536 return deferred.promise();
2537 },
2538
2539 /**
2540 * Returns whether the given file name exists in the list
2541 *
2542 * @param {string} file file name
2543 *
2544 * @return {bool} true if the file exists in the list, false otherwise
2545 */
2546 inList:function(file) {
2547 return this.findFile(file);
2548 },
2549
2550 /**
2551 * Shows busy state on a given file row or multiple
2552 *
2553 * @param {string|Array.<string>} files file name or array of file names
2554 * @param {bool} [busy=true] busy state, true for busy, false to remove busy state
2555 *
2556 * @since 8.2
2557 */
2558 showFileBusyState: function(files, state) {
2559 var self = this;
2560 if (!_.isArray(files) && !files.is) {
2561 files = [files];
2562 }
2563
2564 if (_.isUndefined(state)) {
2565 state = true;
2566 }
2567
2568 _.each(files, function(fileName) {
2569 // jquery element already ?
2570 var $tr;
2571 if (_.isString(fileName)) {
2572 $tr = self.findFileEl(fileName);
2573 } else {
2574 $tr = $(fileName);
2575 }
2576
2577 var $thumbEl = $tr.find('.thumbnail');
2578 $tr.toggleClass('busy', state);
2579
2580 if (state) {
2581 $thumbEl.attr('data-oldimage', $thumbEl.css('background-image'));
2582 $thumbEl.css('background-image', 'url('+ OC.imagePath('core', 'loading.gif') + ')');
2583 } else {
2584 $thumbEl.css('background-image', $thumbEl.attr('data-oldimage'));
2585 $thumbEl.removeAttr('data-oldimage');
2586 }
2587 });
2588 },
2589
2590 /**
2591 * Delete the given files from the given dir
2592 * @param files file names list (without path)
2593 * @param dir directory in which to delete the files, defaults to the current
2594 * directory
2595 */
2596 do_delete:function(files, dir) {
2597 var self = this;
2598 if (files && files.substr) {
2599 files=[files];
2600 }
2601 if (!files) {
2602 // delete all files in directory
2603 files = _.pluck(this.files, 'name');
2604 }
2605 if (files) {
2606 this.showFileBusyState(files, true);
2607 }
2608 // Finish any existing actions
2609 if (this.lastAction) {
2610 this.lastAction();
2611 }
2612
2613 dir = dir || this.getCurrentDirectory();
2614
2615 function removeFromList(file) {
2616 var fileEl = self.remove(file, {updateSummary: false});
2617 // FIXME: not sure why we need this after the
2618 // element isn't even in the DOM any more
2619 fileEl.find('.selectCheckBox').prop('checked', false);
2620 fileEl.removeClass('selected');
2621 self.fileSummary.remove({type: fileEl.attr('data-type'), size: fileEl.attr('data-size')});
2622 // TODO: this info should be returned by the ajax call!
2623 self.updateEmptyContent();
2624 self.fileSummary.update();
2625 self.updateSelectionSummary();
2626 // FIXME: don't repeat this, do it once all files are done
2627 self.updateStorageStatistics();
2628 }
2629
2630 _.each(files, function(file) {
2631 self.filesClient.remove(dir + '/' + file)
2632 .done(function() {
2633 removeFromList(file);
2634 })
2635 .fail(function(status) {
2636 if (status === 404) {
2637 // the file already did not exist, remove it from the list
2638 removeFromList(file);
2639 } else {
2640 // only reset the spinner for that one file
2641 OC.Notification.show(t('files', 'Error deleting file "{fileName}".',
2642 {fileName: file}), {type: 'error'}
2643 );
2644 var deleteAction = self.findFileEl(file).find('.action.delete');
2645 deleteAction.removeClass('icon-loading-small').addClass('icon-delete');
2646 self.showFileBusyState(files, false);
2647 }
2648 });
2649 });
2650 },
2651 /**
2652 * Creates the file summary section
2653 */
2654 _createSummary: function() {
2655 var $tr = $('<tr class="summary"></tr>');
2656 this.$el.find('tfoot').append($tr);
2657
2658 return new OCA.Files.FileSummary($tr, {config: this._filesConfig});
2659 },
2660 updateEmptyContent: function() {
2661 var permissions = this.getDirectoryPermissions();
2662 var isCreatable = (permissions & OC.PERMISSION_CREATE) !== 0;
2663 this.$el.find('#emptycontent').toggleClass('hidden', !this.isEmpty);
2664 this.$el.find('#emptycontent .uploadmessage').toggleClass('hidden', !isCreatable || !this.isEmpty);
2665 this.$el.find('#filestable thead th').toggleClass('hidden', this.isEmpty);
2666 },
2667 /**
2668 * Shows the loading mask.
2669 *
2670 * @see OCA.Files.FileList#hideMask
2671 */
2672 showMask: function() {
2673 // in case one was shown before
2674 var $mask = this.$el.find('.mask');
2675 if ($mask.exists()) {
2676 return;
2677 }
2678
2679 this.$table.addClass('hidden');
2680 this.$el.find('#emptycontent').addClass('hidden');
2681
2682 $mask = $('<div class="mask transparent icon-loading"></div>');
2683
2684 this.$el.append($mask);
2685
2686 $mask.removeClass('transparent');
2687 },
2688 /**
2689 * Hide the loading mask.
2690 * @see OCA.Files.FileList#showMask
2691 */
2692 hideMask: function() {
2693 this.$el.find('.mask').remove();
2694 this.$table.removeClass('hidden');
2695 },
2696 scrollTo:function(file) {
2697 if (!_.isArray(file)) {
2698 file = [file];
2699 }
2700 if (file.length === 1) {
2701 _.defer(function() {
2702 this.showDetailsView(file[0]);
2703 }.bind(this));
2704 }
2705 this.highlightFiles(file, function($tr) {
2706 $tr.addClass('searchresult');
2707 $tr.one('hover', function() {
2708 $tr.removeClass('searchresult');
2709 });
2710 });
2711 },
2712 /**
2713 * @deprecated use setFilter(filter)
2714 */
2715 filter:function(query) {
2716 this.setFilter('');
2717 },
2718 /**
2719 * @deprecated use setFilter('')
2720 */
2721 unfilter:function() {
2722 this.setFilter('');
2723 },
2724 /**
2725 * hide files matching the given filter
2726 * @param filter
2727 */
2728 setFilter:function(filter) {
2729 var total = 0;
2730 if (this._filter === filter) {
2731 return;
2732 }
2733 this._filter = filter;
2734 this.fileSummary.setFilter(filter, this.files);
2735 total = this.fileSummary.getTotal();
2736 if (!this.$el.find('.mask').exists()) {
2737 this.hideIrrelevantUIWhenNoFilesMatch();
2738 }
2739
2740 var visibleCount = 0;
2741 filter = filter.toLowerCase();
2742
2743 function filterRows(tr) {
2744 var $e = $(tr);
2745 if ($e.data('file').toString().toLowerCase().indexOf(filter) === -1) {
2746 $e.addClass('hidden');
2747 } else {
2748 visibleCount++;
2749 $e.removeClass('hidden');
2750 }
2751 }
2752
2753 var $trs = this.$fileList.find('tr');
2754 do {
2755 _.each($trs, filterRows);
2756 if (visibleCount < total) {
2757 $trs = this._nextPage(false);
2758 }
2759 } while (visibleCount < total && $trs.length > 0);
2760
2761 this.$container.trigger('scroll');
2762 },
2763 hideIrrelevantUIWhenNoFilesMatch:function() {
2764 if (this._filter && this.fileSummary.summary.totalDirs + this.fileSummary.summary.totalFiles === 0) {
2765 this.$el.find('#filestable thead th').addClass('hidden');
2766 this.$el.find('#emptycontent').addClass('hidden');
2767 $('#searchresults').addClass('filter-empty');
2768 $('#searchresults .emptycontent').addClass('emptycontent-search');
2769 if ( $('#searchresults').length === 0 || $('#searchresults').hasClass('hidden') ) {
2770 var error = t('files', 'No search results in other folders for {tag}{filter}{endtag}', {filter:this._filter});
2771 this.$el.find('.nofilterresults').removeClass('hidden').
2772 find('p').html(error.replace('{tag}', '<strong>').replace('{endtag}', '</strong>'));
2773 }
2774 } else {
2775 $('#searchresults').removeClass('filter-empty');
2776 $('#searchresults .emptycontent').removeClass('emptycontent-search');
2777 this.$el.find('#filestable thead th').toggleClass('hidden', this.isEmpty);
2778 if (!this.$el.find('.mask').exists()) {
2779 this.$el.find('#emptycontent').toggleClass('hidden', !this.isEmpty);
2780 }
2781 this.$el.find('.nofilterresults').addClass('hidden');
2782 }
2783 },
2784 /**
2785 * get the current filter
2786 * @param filter
2787 */
2788 getFilter:function(filter) {
2789 return this._filter;
2790 },
2791 /**
2792 * update the search object to use this filelist when filtering
2793 */
2794 updateSearch:function() {
2795 if (OCA.Search.files) {
2796 OCA.Search.files.setFileList(this);
2797 }
2798 if (OC.Search) {
2799 OC.Search.clear();
2800 }
2801 },
2802 /**
2803 * Update UI based on the current selection
2804 */
2805 updateSelectionSummary: function() {
2806 var summary = this._selectionSummary.summary;
2807 var selection;
2808
2809 var showHidden = !!this._filesConfig.get('showhidden');
2810 if (summary.totalFiles === 0 && summary.totalDirs === 0) {
2811 this.$el.find('#headerName a.name>span:first').text(t('files','Name'));
2812 this.$el.find('#headerSize a>span:first').text(t('files','Size'));
2813 this.$el.find('#modified a>span:first').text(t('files','Modified'));
2814 this.$el.find('table').removeClass('multiselect');
2815 this.$el.find('.selectedActions').addClass('hidden');
2816 }
2817 else {
2818 this.$el.find('.selectedActions').removeClass('hidden');
2819 this.$el.find('#headerSize a>span:first').text(OC.Util.humanFileSize(summary.totalSize));
2820
2821 var directoryInfo = n('files', '%n folder', '%n folders', summary.totalDirs);
2822 var fileInfo = n('files', '%n file', '%n files', summary.totalFiles);
2823
2824 if (summary.totalDirs > 0 && summary.totalFiles > 0) {
2825 var selectionVars = {
2826 dirs: directoryInfo,
2827 files: fileInfo
2828 };
2829 selection = t('files', '{dirs} and {files}', selectionVars);
2830 } else if (summary.totalDirs > 0) {
2831 selection = directoryInfo;
2832 } else {
2833 selection = fileInfo;
2834 }
2835
2836 if (!showHidden && summary.totalHidden > 0) {
2837 var hiddenInfo = n('files', 'including %n hidden', 'including %n hidden', summary.totalHidden);
2838 selection += ' (' + hiddenInfo + ')';
2839 }
2840
2841 this.$el.find('#headerName a.name>span:first').text(selection);
2842 this.$el.find('#modified a>span:first').text('');
2843 this.$el.find('table').addClass('multiselect');
2844 this.$el.find('.delete-selected').toggleClass('hidden', !this.isSelectedDeletable());
2845 }
2846 },
2847
2848 /**
2849 * Check whether all selected files are deletable
2850 */
2851 isSelectedDeletable: function() {
2852 return _.reduce(this.getSelectedFiles(), function(deletable, file) {
2853 return deletable && (file.permissions & OC.PERMISSION_DELETE);
2854 }, true);
2855 },
2856
2857 /**
2858 * Returns whether all files are selected
2859 * @return true if all files are selected, false otherwise
2860 */
2861 isAllSelected: function() {
2862 return this.$el.find('.select-all').prop('checked');
2863 },
2864
2865 /**
2866 * Returns the file info of the selected files
2867 *
2868 * @return array of file names
2869 */
2870 getSelectedFiles: function() {
2871 return _.values(this._selectedFiles);
2872 },
2873
2874 getUniqueName: function(name) {
2875 if (this.findFileEl(name).exists()) {
2876 var numMatch;
2877 var parts=name.split('.');
2878 var extension = "";
2879 if (parts.length > 1) {
2880 extension=parts.pop();
2881 }
2882 var base=parts.join('.');
2883 numMatch=base.match(/\((\d+)\)/);
2884 var num=2;
2885 if (numMatch && numMatch.length>0) {
2886 num=parseInt(numMatch[numMatch.length-1], 10)+1;
2887 base=base.split('(');
2888 base.pop();
2889 base=$.trim(base.join('('));
2890 }
2891 name=base+' ('+num+')';
2892 if (extension) {
2893 name = name+'.'+extension;
2894 }
2895 // FIXME: ugly recursion
2896 return this.getUniqueName(name);
2897 }
2898 return name;
2899 },
2900
2901 /**
2902 * Shows a "permission denied" notification
2903 */
2904 _showPermissionDeniedNotification: function() {
2905 var message = t('core', 'You don’t have permission to upload or create files here');
2906 OC.Notification.show(message, {type: 'error'});
2907 },
2908
2909 /**
2910 * Setup file upload events related to the file-upload plugin
2911 *
2912 * @param {OC.Uploader} uploader
2913 */
2914 setupUploadEvents: function(uploader) {
2915 var self = this;
2916
2917 self._uploads = {};
2918
2919 // detect the progress bar resize
2920 uploader.on('resized', this._onResize);
2921
2922 uploader.on('drop', function(e, data) {
2923 self._uploader.log('filelist handle fileuploaddrop', e, data);
2924
2925 if (self.$el.hasClass('hidden')) {
2926 // do not upload to invisible lists
2927 e.preventDefault();
2928 return false;
2929 }
2930
2931 var dropTarget = $(e.delegatedEvent.target);
2932
2933 // check if dropped inside this container and not another one
2934 if (dropTarget.length
2935 && !self.$el.is(dropTarget) // dropped on list directly
2936 && !self.$el.has(dropTarget).length // dropped inside list
2937 && !dropTarget.is(self.$container) // dropped on main container
2938 ) {
2939 e.preventDefault();
2940 return false;
2941 }
2942
2943 // find the closest tr or crumb to use as target
2944 dropTarget = dropTarget.closest('tr, .crumb');
2945
2946 // if dropping on tr or crumb, drag&drop upload to folder
2947 if (dropTarget && (dropTarget.data('type') === 'dir' ||
2948 dropTarget.hasClass('crumb'))) {
2949
2950 // remember as context
2951 data.context = dropTarget;
2952
2953 // if permissions are specified, only allow if create permission is there
2954 var permissions = dropTarget.data('permissions');
2955 if (!_.isUndefined(permissions) && (permissions & OC.PERMISSION_CREATE) === 0) {
2956 self._showPermissionDeniedNotification();
2957 return false;
2958 }
2959 var dir = dropTarget.data('file');
2960 // if from file list, need to prepend parent dir
2961 if (dir) {
2962 var parentDir = self.getCurrentDirectory();
2963 if (parentDir[parentDir.length - 1] !== '/') {
2964 parentDir += '/';
2965 }
2966 dir = parentDir + dir;
2967 }
2968 else{
2969 // read full path from crumb
2970 dir = dropTarget.data('dir') || '/';
2971 }
2972
2973 // add target dir
2974 data.targetDir = dir;
2975 } else {
2976 // cancel uploads to current dir if no permission
2977 var isCreatable = (self.getDirectoryPermissions() & OC.PERMISSION_CREATE) !== 0;
2978 if (!isCreatable) {
2979 self._showPermissionDeniedNotification();
2980 return false;
2981 }
2982
2983 // we are dropping somewhere inside the file list, which will
2984 // upload the file to the current directory
2985 data.targetDir = self.getCurrentDirectory();
2986 }
2987 });
2988 uploader.on('add', function(e, data) {
2989 self._uploader.log('filelist handle fileuploadadd', e, data);
2990
2991 // add ui visualization to existing folder
2992 if (data.context && data.context.data('type') === 'dir') {
2993 // add to existing folder
2994
2995 // update upload counter ui
2996 var uploadText = data.context.find('.uploadtext');
2997 var currentUploads = parseInt(uploadText.attr('currentUploads'), 10);
2998 currentUploads += 1;
2999 uploadText.attr('currentUploads', currentUploads);
3000
3001 var translatedText = n('files', 'Uploading %n file', 'Uploading %n files', currentUploads);
3002 if (currentUploads === 1) {
3003 self.showFileBusyState(uploadText.closest('tr'), true);
3004 uploadText.text(translatedText);
3005 uploadText.show();
3006 } else {
3007 uploadText.text(translatedText);
3008 }
3009 }
3010
3011 if (!data.targetDir) {
3012 data.targetDir = self.getCurrentDirectory();
3013 }
3014
3015 });
3016 /*
3017 * when file upload done successfully add row to filelist
3018 * update counter when uploading to sub folder
3019 */
3020 uploader.on('done', function(e, upload) {
3021 self._uploader.log('filelist handle fileuploaddone', e, data);
3022
3023 var data = upload.data;
3024 var status = data.jqXHR.status;
3025 if (status < 200 || status >= 300) {
3026 // error was handled in OC.Uploads already
3027 return;
3028 }
3029
3030 var fileName = upload.getFileName();
3031 var fetchInfoPromise = self.addAndFetchFileInfo(fileName, upload.getFullPath());
3032 if (!self._uploads) {
3033 self._uploads = {};
3034 }
3035 if (OC.isSamePath(OC.dirname(upload.getFullPath() + '/'), self.getCurrentDirectory())) {
3036 self._uploads[fileName] = fetchInfoPromise;
3037 }
3038
3039 var uploadText = self.$fileList.find('tr .uploadtext');
3040 self.showFileBusyState(uploadText.closest('tr'), false);
3041 uploadText.fadeOut();
3042 uploadText.attr('currentUploads', 0);
3043 });
3044 uploader.on('createdfolder', function(fullPath) {
3045 self.addAndFetchFileInfo(OC.basename(fullPath), OC.dirname(fullPath));
3046 });
3047 uploader.on('stop', function() {
3048 self._uploader.log('filelist handle fileuploadstop');
3049
3050 // prepare list of uploaded file names in the current directory
3051 // and discard the other ones
3052 var promises = _.values(self._uploads);
3053 var fileNames = _.keys(self._uploads);
3054 self._uploads = [];
3055
3056 // as soon as all info is fetched
3057 $.when.apply($, promises).then(function() {
3058 // highlight uploaded files
3059 self.highlightFiles(fileNames);
3060 self.updateStorageStatistics();
3061 });
3062
3063 var uploadText = self.$fileList.find('tr .uploadtext');
3064 self.showFileBusyState(uploadText.closest('tr'), false);
3065 uploadText.fadeOut();
3066 uploadText.attr('currentUploads', 0);
3067 });
3068 uploader.on('fail', function(e, data) {
3069 self._uploader.log('filelist handle fileuploadfail', e, data);
3070 self._uploads = [];
3071
3072 //if user pressed cancel hide upload chrome
3073 //cleanup uploading to a dir
3074 var uploadText = self.$fileList.find('tr .uploadtext');
3075 self.showFileBusyState(uploadText.closest('tr'), false);
3076 uploadText.fadeOut();
3077 uploadText.attr('currentUploads', 0);
3078 self.updateStorageStatistics();
3079 });
3080
3081 },
3082
3083 /**
3084 * Scroll to the last file of the given list
3085 * Highlight the list of files
3086 * @param files array of filenames,
3087 * @param {Function} [highlightFunction] optional function
3088 * to be called after the scrolling is finished
3089 */
3090 highlightFiles: function(files, highlightFunction) {
3091 // Detection of the uploaded element
3092 var filename = files[files.length - 1];
3093 var $fileRow = this.findFileEl(filename);
3094
3095 while(!$fileRow.exists() && this._nextPage(false) !== false) { // Checking element existence
3096 $fileRow = this.findFileEl(filename);
3097 }
3098
3099 if (!$fileRow.exists()) { // Element not present in the file list
3100 return;
3101 }
3102
3103 var currentOffset = this.$container.scrollTop();
3104 var additionalOffset = this.$el.find("#controls").height()+this.$el.find("#controls").offset().top;
3105
3106 // Animation
3107 var _this = this;
3108 var $scrollContainer = this.$container;
3109 if ($scrollContainer[0] === window) {
3110 // need to use "body" to animate scrolling
3111 // when the scroll container is the window
3112 $scrollContainer = $('body');
3113 }
3114 $scrollContainer.animate({
3115 // Scrolling to the top of the new element
3116 scrollTop: currentOffset + $fileRow.offset().top - $fileRow.height() * 2 - additionalOffset
3117 }, {
3118 duration: 500,
3119 complete: function() {
3120 // Highlighting function
3121 var highlightRow = highlightFunction;
3122
3123 if (!highlightRow) {
3124 highlightRow = function($fileRow) {
3125 $fileRow.addClass("highlightUploaded");
3126 setTimeout(function() {
3127 $fileRow.removeClass("highlightUploaded");
3128 }, 2500);
3129 };
3130 }
3131
3132 // Loop over uploaded files
3133 for(var i=0; i<files.length; i++) {
3134 var $fileRow = _this.findFileEl(files[i]);
3135
3136 if($fileRow.length !== 0) { // Checking element existence
3137 highlightRow($fileRow);
3138 }
3139 }
3140
3141 }
3142 });
3143 },
3144
3145 _renderNewButton: function() {
3146 // if an upload button (legacy) already exists or no actions container exist, skip
3147 var $actionsContainer = this.$el.find('#controls .actions');
3148 if (!$actionsContainer.length || this.$el.find('.button.upload').length) {
3149 return;
3150 }
3151 if (!this._addButtonTemplate) {
3152 this._addButtonTemplate = Handlebars.compile(TEMPLATE_ADDBUTTON);
3153 }
3154 var $newButton = $(this._addButtonTemplate({
3155 addText: t('files', 'New'),
3156 iconClass: 'icon-add'
3157 }));
3158
3159 $actionsContainer.prepend($newButton);
3160 $newButton.tooltip({'placement': 'bottom'});
3161
3162 $newButton.click(_.bind(this._onClickNewButton, this));
3163 this._newButton = $newButton;
3164 },
3165
3166 _onClickNewButton: function(event) {
3167 var $target = $(event.target);
3168 if (!$target.hasClass('.button')) {
3169 $target = $target.closest('.button');
3170 }
3171 this._newButton.tooltip('hide');
3172 event.preventDefault();
3173 if ($target.hasClass('disabled')) {
3174 return false;
3175 }
3176 if (!this._newFileMenu) {
3177 this._newFileMenu = new OCA.Files.NewFileMenu({
3178 fileList: this
3179 });
3180 $('.actions').append(this._newFileMenu.$el);
3181 }
3182 this._newFileMenu.showAt($target);
3183
3184 return false;
3185 },
3186
3187 /**
3188 * Register a tab view to be added to all views
3189 */
3190 registerTabView: function(tabView) {
3191 if (this._detailsView) {
3192 this._detailsView.addTabView(tabView);
3193 }
3194 },
3195
3196 /**
3197 * Register a detail view to be added to all views
3198 */
3199 registerDetailView: function(detailView) {
3200 if (this._detailsView) {
3201 this._detailsView.addDetailView(detailView);
3202 }
3203 },
3204
3205 /**
3206 * Register a view to be added to the breadcrumb view
3207 */
3208 registerBreadCrumbDetailView: function(detailView) {
3209 if (this.breadcrumb) {
3210 this.breadcrumb.addDetailView(detailView);
3211 }
3212 },
3213
3214 /**
3215 * Returns the registered detail views.
3216 *
3217 * @return null|Array<OCA.Files.DetailFileInfoView> an array with the
3218 * registered DetailFileInfoViews, or null if the details view
3219 * is not enabled.
3220 */
3221 getRegisteredDetailViews: function() {
3222 if (this._detailsView) {
3223 return this._detailsView.getDetailViews();
3224 }
3225
3226 return null;
3227 }
3228 };
3229
3230 /**
3231 * Sort comparators.
3232 * @namespace OCA.Files.FileList.Comparators
3233 * @private
3234 */
3235 FileList.Comparators = {
3236 /**
3237 * Compares two file infos by name, making directories appear
3238 * first.
3239 *
3240 * @param {OC.Files.FileInfo} fileInfo1 file info
3241 * @param {OC.Files.FileInfo} fileInfo2 file info
3242 * @return {int} -1 if the first file must appear before the second one,
3243 * 0 if they are identify, 1 otherwise.
3244 */
3245 name: function(fileInfo1, fileInfo2) {
3246 if (fileInfo1.type === 'dir' && fileInfo2.type !== 'dir') {
3247 return -1;
3248 }
3249 if (fileInfo1.type !== 'dir' && fileInfo2.type === 'dir') {
3250 return 1;
3251 }
3252 return OC.Util.naturalSortCompare(fileInfo1.name, fileInfo2.name);
3253 },
3254 /**
3255 * Compares two file infos by size.
3256 *
3257 * @param {OC.Files.FileInfo} fileInfo1 file info
3258 * @param {OC.Files.FileInfo} fileInfo2 file info
3259 * @return {int} -1 if the first file must appear before the second one,
3260 * 0 if they are identify, 1 otherwise.
3261 */
3262 size: function(fileInfo1, fileInfo2) {
3263 return fileInfo1.size - fileInfo2.size;
3264 },
3265 /**
3266 * Compares two file infos by timestamp.
3267 *
3268 * @param {OC.Files.FileInfo} fileInfo1 file info
3269 * @param {OC.Files.FileInfo} fileInfo2 file info
3270 * @return {int} -1 if the first file must appear before the second one,
3271 * 0 if they are identify, 1 otherwise.
3272 */
3273 mtime: function(fileInfo1, fileInfo2) {
3274 return fileInfo1.mtime - fileInfo2.mtime;
3275 }
3276 };
3277
3278 /**
3279 * File info attributes.
3280 *
3281 * @typedef {Object} OC.Files.FileInfo
3282 *
3283 * @lends OC.Files.FileInfo
3284 *
3285 * @deprecated use OC.Files.FileInfo instead
3286 *
3287 */
3288 OCA.Files.FileInfo = OC.Files.FileInfo;
3289
3290 OCA.Files.FileList = FileList;
3291})();
3292
3293$(document).ready(function() {
3294 // FIXME: unused ?
3295 OCA.Files.FileList.useUndo = (window.onbeforeunload)?true:false;
3296 $(window).bind('beforeunload', function () {
3297 if (OCA.Files.FileList.lastAction) {
3298 OCA.Files.FileList.lastAction();
3299 }
3300 });
3301 $(window).on('unload', function () {
3302 $(window).trigger('beforeunload');
3303 });
3304
3305});