diff options
author | Thomas Müller <thomas.mueller@tmit.eu> | 2014-04-28 17:39:02 +0200 |
---|---|---|
committer | Thomas Müller <thomas.mueller@tmit.eu> | 2014-04-28 17:39:02 +0200 |
commit | e055a411ea4b2a32dcf20c910d332867dc91f516 (patch) | |
tree | b2cc655c7f5277e23386d88ae80af6b6c948c84f | |
parent | be6431bab05265835df79ec1245ccd7df900cca7 (diff) | |
parent | bf61d841a2b3305bc51de6109917725466239061 (diff) | |
download | nextcloud-server-e055a411ea4b2a32dcf20c910d332867dc91f516.tar.gz nextcloud-server-e055a411ea4b2a32dcf20c910d332867dc91f516.zip |
Merge pull request #7167 from owncloud/files-ajaxload-infscroll
Infinite scrolling for files app
-rw-r--r-- | apps/files/css/files.css | 1 | ||||
-rw-r--r-- | apps/files/index.php | 1 | ||||
-rw-r--r-- | apps/files/js/file-upload.js | 6 | ||||
-rw-r--r-- | apps/files/js/filelist.js | 798 | ||||
-rw-r--r-- | apps/files/js/files.js | 334 | ||||
-rw-r--r-- | apps/files/js/filesummary.js | 195 | ||||
-rw-r--r-- | apps/files/templates/index.php | 2 | ||||
-rw-r--r-- | apps/files/tests/js/fileactionsSpec.js | 1 | ||||
-rw-r--r-- | apps/files/tests/js/filelistSpec.js | 727 | ||||
-rw-r--r-- | apps/files/tests/js/filesSpec.js | 28 | ||||
-rw-r--r-- | apps/files/tests/js/filesummarySpec.js | 87 | ||||
-rw-r--r-- | apps/files_sharing/public.php | 1 | ||||
-rw-r--r-- | apps/files_trashbin/index.php | 1 | ||||
-rw-r--r-- | apps/files_trashbin/js/filelist.js | 132 | ||||
-rw-r--r-- | apps/files_trashbin/js/trash.js | 161 | ||||
-rw-r--r-- | apps/files_trashbin/templates/index.php | 4 | ||||
-rw-r--r-- | core/css/apps.css | 12 | ||||
-rw-r--r-- | core/js/js.js | 5 | ||||
-rw-r--r-- | core/js/tests/specs/coreSpec.js | 17 |
19 files changed, 1742 insertions, 771 deletions
diff --git a/apps/files/css/files.css b/apps/files/css/files.css index 474f1af0720..533050691d5 100644 --- a/apps/files/css/files.css +++ b/apps/files/css/files.css @@ -310,7 +310,6 @@ a.action>img { max-height:16px; max-width:16px; vertical-align:text-bottom; } /* Actions for selected files */ .selectedActions { - display: none; position: absolute; top: -1px; right: 0; diff --git a/apps/files/index.php b/apps/files/index.php index b8ff08c1b05..a4e9a938507 100644 --- a/apps/files/index.php +++ b/apps/files/index.php @@ -32,6 +32,7 @@ OCP\Util::addscript('files', 'file-upload'); OCP\Util::addscript('files', 'jquery.iframe-transport'); OCP\Util::addscript('files', 'jquery.fileupload'); OCP\Util::addscript('files', 'jquery-visibility'); +OCP\Util::addscript('files', 'filesummary'); OCP\Util::addscript('files', 'breadcrumb'); OCP\Util::addscript('files', 'filelist'); diff --git a/apps/files/js/file-upload.js b/apps/files/js/file-upload.js index 03ebdccb32d..963fc647828 100644 --- a/apps/files/js/file-upload.js +++ b/apps/files/js/file-upload.js @@ -606,7 +606,7 @@ OC.Upload = { {dir:$('#dir').val(), filename:name}, function(result) { if (result.status === 'success') { - FileList.add(result.data, {hidden: hidden, insert: true}); + FileList.add(result.data, {hidden: hidden, animate: true}); } else { OC.dialogs.alert(result.data.message, t('core', 'Could not create file')); } @@ -619,7 +619,7 @@ OC.Upload = { {dir:$('#dir').val(), foldername:name}, function(result) { if (result.status === 'success') { - FileList.add(result.data, {hidden: hidden, insert: true}); + FileList.add(result.data, {hidden: hidden, animate: true}); } else { OC.dialogs.alert(result.data.message, t('core', 'Could not create folder')); } @@ -657,7 +657,7 @@ OC.Upload = { var file = data; $('#uploadprogressbar').fadeOut(); - FileList.add(file, {hidden: hidden, insert: true}); + FileList.add(file, {hidden: hidden, animate: true}); }); eventSource.listen('error',function(error) { $('#uploadprogressbar').fadeOut(); diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index c33b638b5a6..40ec898635e 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -8,8 +8,8 @@ * */ -/* global OC, t, n, FileList, FileActions, Files, BreadCrumb */ -/* global procesSelection, dragOptions, folderDropOptions */ +/* global OC, t, n, FileList, FileActions, Files, FileSummary, BreadCrumb */ +/* global dragOptions, folderDropOptions */ window.FileList = { appName: t('files', 'Files'), isEmpty: true, @@ -17,8 +17,47 @@ window.FileList = { $el: $('#filestable'), $fileList: $('#fileList'), breadcrumb: null, + + /** + * Instance of FileSummary + */ + fileSummary: null, initialized: false, + // number of files per page + pageSize: 20, + + /** + * Array of files in the current folder. + * The entries are of file data. + */ + files: [], + + /** + * Map of file id to file data + */ + _selectedFiles: {}, + + /** + * Summary of selected files. + * Instance of FileSummary. + */ + _selectionSummary: null, + + /** + * Compare two file info objects, sorting by + * folders first, then by name. + */ + _fileInfoCompare: function(fileInfo1, fileInfo2) { + if (fileInfo1.type === 'dir' && fileInfo2.type !== 'dir') { + return -1; + } + if (fileInfo1.type !== 'dir' && fileInfo2.type === 'dir') { + return 1; + } + return fileInfo1.name.localeCompare(fileInfo2.name); + }, + /** * Initialize the file list and its components */ @@ -31,10 +70,15 @@ window.FileList = { // TODO: FileList should not know about global elements this.$el = $('#filestable'); this.$fileList = $('#fileList'); + this.files = []; + this._selectedFiles = {}; + this._selectionSummary = new FileSummary(); + + this.fileSummary = this._createSummary(); this.breadcrumb = new BreadCrumb({ onClick: this._onClickBreadCrumb, - onDrop: this._onDropOnBreadCrumb, + onDrop: _.bind(this._onDropOnBreadCrumb, this), getCrumbUrl: function(part, index) { return self.linkTo(part.dir); } @@ -47,6 +91,149 @@ window.FileList = { var width = $(this).width(); FileList.breadcrumb.resize(width, false); }); + + this.$fileList.on('click','td.filename a', _.bind(this._onClickFile, this)); + this.$fileList.on('change', 'td.filename input:checkbox', _.bind(this._onClickFileCheckbox, this)); + this.$el.find('#select_all').click(_.bind(this._onClickSelectAll, this)); + this.$el.find('.download').click(_.bind(this._onClickDownloadSelected, this)); + this.$el.find('.delete-selected').click(_.bind(this._onClickDeleteSelected, this)); + }, + + /** + * Selected/deselects the given file element and updated + * the internal selection cache. + * + * @param $tr single file row element + * @param state true to select, false to deselect + */ + _selectFileEl: function($tr, state) { + var $checkbox = $tr.find('input:checkbox'); + var oldData = !!this._selectedFiles[$tr.data('id')]; + var data; + $checkbox.prop('checked', state); + $tr.toggleClass('selected', state); + // already selected ? + if (state === oldData) { + return; + } + data = this.elementToFile($tr); + if (state) { + this._selectedFiles[$tr.data('id')] = data; + this._selectionSummary.add(data); + } + else { + delete this._selectedFiles[$tr.data('id')]; + this._selectionSummary.remove(data); + } + this.$el.find('#select_all').prop('checked', this._selectionSummary.getTotal() === this.files.length); + }, + + /** + * Event handler for when clicking on files to select them + */ + _onClickFile: function(event) { + var $tr = $(event.target).closest('tr'); + if (event.ctrlKey || event.shiftKey) { + event.preventDefault(); + if (event.shiftKey) { + var $lastTr = $(this._lastChecked); + var lastIndex = $lastTr.index(); + var currentIndex = $tr.index(); + var $rows = this.$fileList.children('tr'); + + // last clicked checkbox below current one ? + if (lastIndex > currentIndex) { + var aux = lastIndex; + lastIndex = currentIndex; + currentIndex = aux; + } + + // auto-select everything in-between + for (var i = lastIndex + 1; i < currentIndex; i++) { + this._selectFileEl($rows.eq(i), true); + } + } + else { + this._lastChecked = $tr; + } + var $checkbox = $tr.find('input:checkbox'); + this._selectFileEl($tr, !$checkbox.prop('checked')); + this.updateSelectionSummary(); + } else { + var filename = $tr.attr('data-file'); + var renaming = $tr.data('renaming'); + if (!renaming) { + FileActions.currentFile = $tr.find('td'); + var mime=FileActions.getCurrentMimeType(); + var type=FileActions.getCurrentType(); + var permissions = FileActions.getCurrentPermissions(); + var action=FileActions.getDefault(mime,type, permissions); + if (action) { + event.preventDefault(); + action(filename); + } + } + } + }, + + /** + * Event handler for when clicking on a file's checkbox + */ + _onClickFileCheckbox: function(e) { + var $tr = $(e.target).closest('tr'); + this._selectFileEl($tr, !$tr.hasClass('selected')); + this._lastChecked = $tr; + this.updateSelectionSummary(); + }, + + /** + * Event handler for when selecting/deselecting all files + */ + _onClickSelectAll: function(e) { + var checked = $(e.target).prop('checked'); + this.$fileList.find('td.filename input:checkbox').prop('checked', checked) + .closest('tr').toggleClass('selected', checked); + this._selectedFiles = {}; + this._selectionSummary.clear(); + if (checked) { + for (var i = 0; i < this.files.length; i++) { + var fileData = this.files[i]; + this._selectedFiles[fileData.id] = fileData; + this._selectionSummary.add(fileData); + } + } + this.updateSelectionSummary(); + }, + + /** + * Event handler for when clicking on "Download" for the selected files + */ + _onClickDownloadSelected: function(event) { + var files; + var dir = this.getCurrentDirectory(); + if (this.isAllSelected()) { + files = OC.basename(dir); + dir = OC.dirname(dir) || '/'; + } + else { + files = _.pluck(this.getSelectedFiles(), 'name'); + } + OC.Notification.show(t('files','Your download is being prepared. This might take some time if the files are big.')); + OC.redirect(Files.getDownloadUrl(files, dir)); + return false; + }, + + /** + * Event handler for when clicking on "Delete" for the selected files + */ + _onClickDeleteSelected: function(event) { + var files = null; + if (!FileList.isAllSelected()) { + files = _.pluck(this.getSelectedFiles(), 'name'); + } + this.do_delete(files); + event.preventDefault(); + return false; }, /** @@ -62,48 +249,41 @@ window.FileList = { } }, + _onScroll: function(e) { + if ($(window).scrollTop() + $(window).height() > $(document).height() - 500) { + this._nextPage(true); + } + }, + /** * Event handler when dropping on a breadcrumb */ _onDropOnBreadCrumb: function( event, ui ) { - var target=$(this).data('dir'); - var dir = FileList.getCurrentDirectory(); - while(dir.substr(0,1) === '/') {//remove extra leading /'s - dir=dir.substr(1); + var $target = $(event.target); + if (!$target.is('.crumb')) { + $target = $target.closest('.crumb'); + } + var targetPath = $(event.target).data('dir'); + var dir = this.getCurrentDirectory(); + while (dir.substr(0,1) === '/') {//remove extra leading /'s + dir = dir.substr(1); } dir = '/' + dir; if (dir.substr(-1,1) !== '/') { dir = dir + '/'; } - if (target === dir || target+'/' === dir) { + // do nothing if dragged on current dir + if (targetPath === dir || targetPath + '/' === dir) { return; } - var files = ui.helper.find('tr'); - $(files).each(function(i,row) { - var dir = $(row).data('dir'); - var file = $(row).data('filename'); - //slapdash selector, tracking down our original element that the clone budded off of. - var origin = $('tr[data-id=' + $(row).data('origin') + ']'); - var td = origin.children('td.filename'); - var oldBackgroundImage = td.css('background-image'); - td.css('background-image', 'url('+ OC.imagePath('core', 'loading.gif') + ')'); - $.post(OC.filePath('files', 'ajax', 'move.php'), { dir: dir, file: file, target: target }, function(result) { - if (result) { - if (result.status === 'success') { - FileList.remove(file); - procesSelection(); - $('#notification').hide(); - } else { - $('#notification').hide(); - $('#notification').text(result.data.message); - $('#notification').fadeIn(); - } - } else { - OC.dialogs.alert(t('files', 'Error moving file'), t('files', 'Error')); - } - td.css('background-image', oldBackgroundImage); - }); - }); + + var files = this.getSelectedFiles(); + if (files.length === 0) { + // single one selected without checkbox? + files = _.map(ui.helper.find('tr'), FileList.elementToFile); + } + + FileList.move(_.pluck(files, 'name'), targetPath); }, /** @@ -129,21 +309,83 @@ window.FileList = { // use filterAttr to avoid escaping issues return this.$fileList.find('tr').filterAttr('data-file', fileName); }, + + /** + * Returns the file data from a given file element. + * @param $el file tr element + * @return file data + */ + elementToFile: function($el){ + $el = $($el); + return { + id: parseInt($el.attr('data-id'), 10), + name: $el.attr('data-file'), + mimetype: $el.attr('data-mime'), + type: $el.attr('data-type'), + size: parseInt($el.attr('data-size'), 10), + etag: $el.attr('data-etag') + }; + }, + + /** + * Appends the next page of files into the table + * @param animate true to animate the new elements + */ + _nextPage: function(animate) { + var index = this.$fileList.children().length, + count = this.pageSize, + tr, + fileData, + newTrs = [], + isAllSelected = this.isAllSelected(); + + if (index >= this.files.length) { + return; + } + + while (count > 0 && index < this.files.length) { + fileData = this.files[index]; + tr = this._renderRow(fileData, {updateSummary: false}); + this.$fileList.append(tr); + if (isAllSelected || this._selectedFiles[fileData.id]) { + tr.addClass('selected'); + tr.find('input:checkbox').prop('checked', true); + } + if (animate) { + tr.addClass('appear transparent'); + newTrs.push(tr); + } + index++; + count--; + } + + if (animate) { + // defer, for animation + window.setTimeout(function() { + for (var i = 0; i < newTrs.length; i++ ) { + newTrs[i].removeClass('transparent'); + } + }, 0); + } + }, + /** * Sets the files to be displayed in the list. - * This operation will rerender the list and update the summary. + * This operation will re-render the list and update the summary. * @param filesArray array of file data (map) */ - setFiles:function(filesArray) { + setFiles: function(filesArray) { // detach to make adding multiple rows faster - this.$fileList.detach(); + this.files = filesArray; + this.$fileList.detach(); this.$fileList.empty(); - this.isEmpty = filesArray.length === 0; - for (var i = 0; i < filesArray.length; i++) { - this.add(filesArray[i], {updateSummary: false}); - } + // clear "Select all" checkbox + this.$el.find('#select_all').prop('checked', false); + + this.isEmpty = this.files.length === 0; + this._nextPage(); this.$el.find('thead').after(this.$fileList); @@ -153,8 +395,10 @@ window.FileList = { if (window.Files) { Files.setupDragAndDrop(); } - this.updateFileSummary(); - procesSelection(); + + this.fileSummary.calculate(filesArray); + + FileList.updateSelectionSummary(); $(window).scrollTop(0); this.$fileList.trigger(jQuery.Event("updated")); @@ -276,15 +520,82 @@ window.FileList = { tr.append(td); return tr; }, + /** - * Adds an entry to the files table using the data from the given file data + * Adds an entry to the files array and also into the DOM + * in a sorted manner. + * * @param fileData map of file attributes * @param options map of attributes: - * - "insert" true to insert in a sorted manner, false to append (default) * - "updateSummary" true to update the summary after adding (default), false otherwise * @return new tr element (not appended to the table) */ add: function(fileData, options) { + var index = -1; + var $tr; + var $rows; + var $insertionPoint; + options = options || {}; + + // there are three situations to cover: + // 1) insertion point is visible on the current page + // 2) insertion point is on a not visible page (visible after scrolling) + // 3) insertion point is at the end of the list + + $rows = this.$fileList.children(); + index = this._findInsertionIndex(fileData); + if (index > this.files.length) { + index = this.files.length; + } + else { + $insertionPoint = $rows.eq(index); + } + + // is the insertion point visible ? + if ($insertionPoint.length) { + // only render if it will really be inserted + $tr = this._renderRow(fileData, options); + $insertionPoint.before($tr); + } + else { + // if insertion point is after the last visible + // entry, append + if (index === $rows.length) { + $tr = this._renderRow(fileData, options); + this.$fileList.append($tr); + } + } + + this.isEmpty = false; + this.files.splice(index, 0, fileData); + + if ($tr && options.animate) { + $tr.addClass('appear transparent'); + window.setTimeout(function() { + $tr.removeClass('transparent'); + }); + } + + // defaults to true if not defined + if (typeof(options.updateSummary) === 'undefined' || !!options.updateSummary) { + this.fileSummary.add(fileData, true); + this.updateEmptyContent(); + } + + return $tr; + }, + + /** + * Creates a new row element based on the given attributes + * and returns it. + * + * @param fileData map of file attributes + * @param options map of attributes: + * - "index" optional index at which to insert the element + * - "updateSummary" true to update the summary after adding (default), false otherwise + * @return new tr element (not appended to the table) + */ + _renderRow: function(fileData, options) { options = options || {}; var type = fileData.type || 'file', mime = fileData.mimetype, @@ -303,16 +614,6 @@ window.FileList = { ); var filenameTd = tr.find('td.filename'); - // sorted insert is expensive, so needs to be explicitly - // requested - if (options.insert) { - this.insertElement(fileData.name, type, tr); - } - else { - this.$fileList.append(tr); - } - FileList.isEmpty = false; - // TODO: move dragging to FileActions ? // enable drag only for deletable files if (permissions & OC.PERMISSION_DELETE) { @@ -348,12 +649,6 @@ window.FileList = { filenameTd.css('background-image', 'url(' + previewUrl + ')'); } } - - // defaults to true if not defined - if (typeof(options.updateSummary) === 'undefined' || !!options.updateSummary) { - this.updateFileSummary(); - this.updateEmptyContent(); - } return tr; }, /** @@ -378,7 +673,6 @@ window.FileList = { */ changeDirectory: function(targetDir, changeUrl, force) { var $dir = $('#dir'), - url, currentDir = $dir.val() || '/'; targetDir = targetDir || '/'; if (!force && currentDir === targetDir) { @@ -391,7 +685,9 @@ window.FileList = { previousDir: currentDir } )); - FileList.reload(); + this._selectedFiles = {}; + this._selectionSummary.clear(); + this.reload(); }, linkTo: function(dir) { return OC.linkTo('files', 'index.php')+"?dir="+ encodeURIComponent(dir).replace(/%2F/g, '/'); @@ -519,60 +815,130 @@ window.FileList = { * @param name name of the file to remove * @param options optional options as map: * "updateSummary": true to update the summary (default), false otherwise + * @return deleted element */ - remove:function(name, options){ + remove: function(name, options){ options = options || {}; var fileEl = FileList.findFileEl(name); + var index = fileEl.index(); + if (!fileEl.length) { + return null; + } + if (this._selectedFiles[fileEl.data('id')]) { + // remove from selection first + this._selectFileEl(fileEl, false); + this.updateSelectionSummary(); + } if (fileEl.data('permissions') & OC.PERMISSION_DELETE) { // file is only draggable when delete permissions are set fileEl.find('td.filename').draggable('destroy'); } + this.files.splice(index, 1); fileEl.remove(); // TODO: improve performance on batch update - FileList.isEmpty = !this.$fileList.find('tr:not(.summary)').length; + FileList.isEmpty = !this.files.length; if (typeof(options.updateSummary) === 'undefined' || !!options.updateSummary) { FileList.updateEmptyContent(); - FileList.updateFileSummary(); + this.fileSummary.remove({type: fileEl.attr('data-type'), size: fileEl.attr('data-size')}, true); + } + + var lastIndex = this.$fileList.children().length; + // if there are less elements visible than one page + // but there are still pending elements in the array, + // then directly append the next page + if (lastIndex < this.files.length && lastIndex < this.pageSize) { + this._nextPage(true); } + return fileEl; }, - insertElement:function(name, type, element) { - // find the correct spot to insert the file or folder - var pos, - fileElements = this.$fileList.find('tr[data-file][data-type="'+type+'"]:not(.hidden)'); - if (name.localeCompare($(fileElements[0]).attr('data-file')) < 0) { - pos = -1; - } else if (name.localeCompare($(fileElements[fileElements.length-1]).attr('data-file')) > 0) { - pos = fileElements.length - 1; - } else { - for(pos = 0; pos<fileElements.length-1; pos++) { - if (name.localeCompare($(fileElements[pos]).attr('data-file')) > 0 - && name.localeCompare($(fileElements[pos+1]).attr('data-file')) < 0) - { - break; - } - } - } - if (fileElements.exists()) { - if (pos === -1) { - $(fileElements[0]).before(element); - } else { - $(fileElements[pos]).after(element); - } - } else if (type === 'dir' && !FileList.isEmpty) { - this.$fileList.find('tr[data-file]:first').before(element); - } else if (type === 'file' && !FileList.isEmpty) { - this.$fileList.find('tr[data-file]:last').before(element); - } else { - this.$fileList.append(element); + /** + * Finds the index of the row before which the given + * fileData should be inserted, considering the current + * sorting + */ + _findInsertionIndex: function(fileData) { + var index = 0; + while (index < this.files.length && this._fileInfoCompare(fileData, this.files[index]) > 0) { + index++; } - FileList.isEmpty = false; - FileList.updateEmptyContent(); - FileList.updateFileSummary(); + return index; }, + /** + * Moves a file to a given target folder. + * + * @param fileNames array of file names to move + * @param targetPath absolute target path + */ + move: function(fileNames, targetPath) { + var self = this; + var dir = this.getCurrentDirectory(); + var target = OC.basename(targetPath); + if (!_.isArray(fileNames)) { + fileNames = [fileNames]; + } + _.each(fileNames, function(fileName) { + var $tr = self.findFileEl(fileName); + var $td = $tr.children('td.filename'); + var oldBackgroundImage = $td.css('background-image'); + $td.css('background-image', 'url('+ OC.imagePath('core', 'loading.gif') + ')'); + // TODO: improve performance by sending all file names in a single call + $.post( + OC.filePath('files', 'ajax', 'move.php'), + { + dir: dir, + file: fileName, + target: targetPath + }, + function(result) { + if (result) { + if (result.status === 'success') { + // if still viewing the same directory + if (self.getCurrentDirectory() === dir) { + // recalculate folder size + var oldFile = self.findFileEl(target); + var newFile = self.findFileEl(fileName); + var oldSize = oldFile.data('size'); + var newSize = oldSize + newFile.data('size'); + oldFile.data('size', newSize); + oldFile.find('td.filesize').text(OC.Util.humanFileSize(newSize)); + + // TODO: also update entry in FileList.files + + self.remove(fileName); + } + } else { + OC.Notification.hide(); + if (result.status === 'error' && result.data.message) { + OC.Notification.show(result.data.message); + } + else { + OC.Notification.show(t('files', 'Error moving file.')); + } + // hide notification after 10 sec + setTimeout(function() { + OC.Notification.hide(); + }, 10000); + } + } else { + OC.dialogs.alert(t('files', 'Error moving file'), t('files', 'Error')); + } + $td.css('background-image', oldBackgroundImage); + }); + }); + + }, + + /** + * Triggers file rename input field for the given file name. + * If the user enters a new name, the file will be renamed. + * + * @param oldname file name of the file to rename + */ rename: function(oldname) { var tr, td, input, form; tr = FileList.findFileEl(oldname); + var oldFileInfo = this.files[tr.index()]; tr.data('renaming',true); td = tr.children('td.filename'); input = $('<input type="text" class="filename"/>').val(oldname); @@ -604,86 +970,50 @@ window.FileList = { event.stopPropagation(); event.preventDefault(); try { - var newname = input.val(); - var directory = FileList.getCurrentDirectory(); - if (newname !== oldname) { + var newName = input.val(); + if (newName !== oldname) { checkInput(); - // save background image, because it's replaced by a spinner while async request - var oldBackgroundImage = td.css('background-image'); // mark as loading td.css('background-image', 'url('+ OC.imagePath('core', 'loading.gif') + ')'); $.ajax({ url: OC.filePath('files','ajax','rename.php'), data: { dir : $('#dir').val(), - newname: newname, + newname: newName, file: oldname }, success: function(result) { + var fileInfo; if (!result || result.status === 'error') { OC.dialogs.alert(result.data.message, t('core', 'Could not rename file')); - // revert changes - newname = oldname; - tr.attr('data-file', newname); - var path = td.children('a.name').attr('href'); - td.children('a.name').attr('href', path.replace(encodeURIComponent(oldname), encodeURIComponent(newname))); - var basename = newname; - if (newname.indexOf('.') > 0 && tr.data('type') !== 'dir') { - basename = newname.substr(0,newname.lastIndexOf('.')); - } - td.find('a.name span.nametext').text(basename); - if (newname.indexOf('.') > 0 && tr.data('type') !== 'dir') { - if ( ! td.find('a.name span.extension').exists() ) { - td.find('a.name span.nametext').append('<span class="extension"></span>'); - } - td.find('a.name span.extension').text(newname.substr(newname.lastIndexOf('.'))); - } - tr.find('.fileactions').effect('highlight', {}, 5000); - tr.effect('highlight', {}, 5000); - // remove loading mark and recover old image - td.css('background-image', oldBackgroundImage); + fileInfo = oldFileInfo; } else { - var fileInfo = result.data; - tr.attr('data-mime', fileInfo.mime); - tr.attr('data-etag', fileInfo.etag); - if (fileInfo.isPreviewAvailable) { - Files.lazyLoadPreview(directory + '/' + fileInfo.name, result.data.mime, function(previewpath) { - tr.find('td.filename').attr('style','background-image:url('+previewpath+')'); - }, null, null, result.data.etag); - } - else { - tr.find('td.filename') - .removeClass('preview') - .attr('style','background-image:url(' - + OC.Util.replaceSVGIcon(fileInfo.icon) - + ')'); - } + fileInfo = result.data; } // reinsert row - tr.detach(); - FileList.insertElement( tr.attr('data-file'), tr.attr('data-type'),tr ); - // update file actions in case the extension changed - FileActions.display( tr.find('td.filename'), true); + FileList.files.splice(tr.index(), 1); + tr.remove(); + FileList.add(fileInfo); } }); } input.tipsy('hide'); tr.data('renaming',false); - tr.attr('data-file', newname); + tr.attr('data-file', newName); var path = td.children('a.name').attr('href'); // FIXME this will fail if the path contains the filename. - td.children('a.name').attr('href', path.replace(encodeURIComponent(oldname), encodeURIComponent(newname))); - var basename = newname; - if (newname.indexOf('.') > 0 && tr.data('type') !== 'dir') { - basename = newname.substr(0, newname.lastIndexOf('.')); + td.children('a.name').attr('href', path.replace(encodeURIComponent(oldname), encodeURIComponent(newName))); + var basename = newName; + if (newName.indexOf('.') > 0 && tr.data('type') !== 'dir') { + basename = newName.substr(0, newName.lastIndexOf('.')); } td.find('a.name span.nametext').text(basename); - if (newname.indexOf('.') > 0 && tr.data('type') !== 'dir') { + if (newName.indexOf('.') > 0 && tr.data('type') !== 'dir') { if ( ! td.find('a.name span.extension').exists() ) { td.find('a.name span.nametext').append('<span class="extension"></span>'); } - td.find('a.name span.extension').text(newname.substr(newname.lastIndexOf('.'))); + td.find('a.name span.extension').text(newName.substr(newName.lastIndexOf('.'))); } form.remove(); FileActions.display( tr.find('td.filename'), true); @@ -764,20 +1094,22 @@ window.FileList = { function(result) { if (result.status === 'success') { if (params.allfiles) { - // clear whole list - $('#fileList tr').remove(); + FileList.setFiles([]); } else { $.each(files,function(index,file) { var fileEl = FileList.remove(file, {updateSummary: false}); + // FIXME: not sure why we need this after the + // element isn't even in the DOM any more fileEl.find('input[type="checkbox"]').prop('checked', false); fileEl.removeClass('selected'); + FileList.fileSummary.remove({type: fileEl.attr('data-type'), size: fileEl.attr('data-size')}); }); } - procesSelection(); checkTrashStatus(); - FileList.updateFileSummary(); FileList.updateEmptyContent(); + FileList.fileSummary.update(); + FileList.updateSelectionSummary(); Files.updateStorageStatistics(); } else { if (result.status === 'error' && result.data.message) { @@ -804,108 +1136,14 @@ window.FileList = { } }); }, - createFileSummary: function() { - if ( !FileList.isEmpty ) { - var summary = this._calculateFileSummary(); - - // Get translations - var directoryInfo = n('files', '%n folder', '%n folders', summary.totalDirs); - var fileInfo = n('files', '%n file', '%n files', summary.totalFiles); - - var infoVars = { - dirs: '<span class="dirinfo">'+directoryInfo+'</span><span class="connector">', - files: '</span><span class="fileinfo">'+fileInfo+'</span>' - }; - - var info = t('files', '{dirs} and {files}', infoVars); - - // don't show the filesize column, if filesize is NaN (e.g. in trashbin) - var fileSize = ''; - if (!isNaN(summary.totalSize)) { - fileSize = '<td class="filesize">'+humanFileSize(summary.totalSize)+'</td>'; - } - - var $summary = $('<tr class="summary" data-file="undefined"><td><span class="info">'+info+'</span></td>'+fileSize+'<td></td></tr>'); - this.$fileList.append($summary); - - var $dirInfo = $summary.find('.dirinfo'); - var $fileInfo = $summary.find('.fileinfo'); - var $connector = $summary.find('.connector'); + /** + * Creates the file summary section + */ + _createSummary: function() { + var $tr = $('<tr class="summary"></tr>'); + this.$el.find('tfoot').append($tr); - // Show only what's necessary, e.g.: no files: don't show "0 files" - if (summary.totalDirs === 0) { - $dirInfo.addClass('hidden'); - $connector.addClass('hidden'); - } - if (summary.totalFiles === 0) { - $fileInfo.addClass('hidden'); - $connector.addClass('hidden'); - } - } - }, - _calculateFileSummary: function() { - var result = { - totalDirs: 0, - totalFiles: 0, - totalSize: 0 - }; - $.each($('tr[data-file]'), function(index, value) { - var $value = $(value); - if ($value.data('type') === 'dir') { - result.totalDirs++; - } else if ($value.data('type') === 'file') { - result.totalFiles++; - } - if ($value.data('size') !== undefined && $value.data('id') !== -1) { - //Skip shared as it does not count toward quota - result.totalSize += parseInt($value.data('size')); - } - }); - return result; - }, - updateFileSummary: function() { - var $summary = this.$el.find('.summary'); - - // always make it the last element - this.$fileList.append($summary.detach()); - - // Check if we should remove the summary to show "Upload something" - if (this.isEmpty && $summary.length === 1) { - $summary.remove(); - } - // If there's no summary create one (createFileSummary checks if there's data) - else if ($summary.length === 0) { - FileList.createFileSummary(); - } - // There's a summary and data -> Update the summary - else if (!this.isEmpty && $summary.length === 1) { - var fileSummary = this._calculateFileSummary(); - var $dirInfo = $('.summary .dirinfo'); - var $fileInfo = $('.summary .fileinfo'); - var $connector = $('.summary .connector'); - - // Substitute old content with new translations - $dirInfo.html(n('files', '%n folder', '%n folders', fileSummary.totalDirs)); - $fileInfo.html(n('files', '%n file', '%n files', fileSummary.totalFiles)); - $('.summary .filesize').html(humanFileSize(fileSummary.totalSize)); - - // Show only what's necessary (may be hidden) - if (fileSummary.totalDirs === 0) { - $dirInfo.addClass('hidden'); - $connector.addClass('hidden'); - } else { - $dirInfo.removeClass('hidden'); - } - if (fileSummary.totalFiles === 0) { - $fileInfo.addClass('hidden'); - $connector.addClass('hidden'); - } else { - $fileInfo.removeClass('hidden'); - } - if (fileSummary.totalDirs > 0 && fileSummary.totalFiles > 0) { - $connector.removeClass('hidden'); - } - } + return new FileSummary($tr); }, updateEmptyContent: function() { var permissions = $('#permissions').val(); @@ -956,7 +1194,7 @@ window.FileList = { } }, filter:function(query) { - $('#fileList tr:not(.summary)').each(function(i,e) { + $('#fileList tr').each(function(i,e) { if ($(e).data('file').toString().toLowerCase().indexOf(query.toLowerCase()) !== -1) { $(e).addClass("searchresult"); } else { @@ -975,11 +1213,51 @@ window.FileList = { }); }, /** + * Update UI based on the current selection + */ + updateSelectionSummary: function() { + var summary = this._selectionSummary.summary; + if (summary.totalFiles === 0 && summary.totalDirs === 0) { + $('#headerName span.name').text(t('files','Name')); + $('#headerSize').text(t('files','Size')); + $('#modified').text(t('files','Modified')); + $('table').removeClass('multiselect'); + $('.selectedActions').addClass('hidden'); + } + else { + $('.selectedActions').removeClass('hidden'); + $('#headerSize').text(OC.Util.humanFileSize(summary.totalSize)); + var selection = ''; + if (summary.totalDirs > 0) { + selection += n('files', '%n folder', '%n folders', summary.totalDirs); + if (summary.totalFiles > 0) { + selection += ' & '; + } + } + if (summary.totalFiles > 0) { + selection += n('files', '%n file', '%n files', summary.totalFiles); + } + $('#headerName span.name').text(selection); + $('#modified').text(''); + $('table').addClass('multiselect'); + } + }, + + /** * Returns whether all files are selected * @return true if all files are selected, false otherwise */ isAllSelected: function() { - return $('#select_all').prop('checked'); + return this.$el.find('#select_all').prop('checked'); + }, + + /** + * Returns the file info of the selected files + * + * @return array of file names + */ + getSelectedFiles: function() { + return _.values(this._selectedFiles); } }; @@ -1146,7 +1424,7 @@ $(document).ready(function() { FileList.remove(file.name); // create new file context - data.context = FileList.add(file, {insert: true}); + data.context = FileList.add(file, {animate: true}); } } }); @@ -1255,12 +1533,12 @@ $(document).ready(function() { } }; + $(window).scroll(function(e) {FileList._onScroll(e);}); + var dir = parseCurrentDirFromUrl(); // trigger ajax load, deferred to let sub-apps do their overrides first setTimeout(function() { FileList.changeDirectory(dir, false, true); }, 0); - - FileList.createFileSummary(); }); diff --git a/apps/files/js/files.js b/apps/files/js/files.js index 5e669a796a9..6d167851e64 100644 --- a/apps/files/js/files.js +++ b/apps/files/js/files.js @@ -8,8 +8,8 @@ * */ -/* global OC, t, n, FileList, FileActions */ -/* global getURLParameter, isPublic */ +/* global OC, t, FileList */ +/* global getURLParameter */ var Files = { // file space size sync _updateStorageStatistics: function() { @@ -96,10 +96,10 @@ var Files = { throw t('files', 'File name cannot be empty.'); } // check for invalid characters - var invalid_characters = + var invalidCharacters = ['\\', '/', '<', '>', ':', '"', '|', '?', '*', '\n']; - for (var i = 0; i < invalid_characters.length; i++) { - if (trimmedName.indexOf(invalid_characters[i]) !== -1) { + for (var i = 0; i < invalidCharacters.length; i++) { + if (trimmedName.indexOf(invalidCharacters[i]) !== -1) { throw t('files', "Invalid name, '\\', '/', '<', '>', ':', '\"', '|', '?' and '*' are not allowed."); } } @@ -116,7 +116,8 @@ var Files = { return; } if (usedSpacePercent > 90) { - OC.Notification.show(t('files', 'Your storage is almost full ({usedSpacePercent}%)', {usedSpacePercent: usedSpacePercent})); + OC.Notification.show(t('files', 'Your storage is almost full ({usedSpacePercent}%)', + {usedSpacePercent: usedSpacePercent})); } }, @@ -142,6 +143,7 @@ var Files = { } }, + // TODO: move to FileList class setupDragAndDrop: function() { var $fileList = $('#fileList'); @@ -209,7 +211,7 @@ $(document).ready(function() { // Trigger cancelling of file upload $('#uploadprogresswrapper .stop').on('click', function() { OC.Upload.cancelUploads(); - procesSelection(); + FileList.updateSelectionSummary(); }); // Show trash bin @@ -217,135 +219,11 @@ $(document).ready(function() { window.location=OC.filePath('files_trashbin', '', 'index.php'); }); - var lastChecked; - - // Sets the file link behaviour : - $('#fileList').on('click','td.filename a',function(event) { - if (event.ctrlKey || event.shiftKey) { - event.preventDefault(); - if (event.shiftKey) { - var last = $(lastChecked).parent().parent().prevAll().length; - var first = $(this).parent().parent().prevAll().length; - var start = Math.min(first, last); - var end = Math.max(first, last); - var rows = $(this).parent().parent().parent().children('tr'); - for (var i = start; i < end; i++) { - $(rows).each(function(index) { - if (index === i) { - var checkbox = $(this).children().children('input:checkbox'); - $(checkbox).attr('checked', 'checked'); - $(checkbox).parent().parent().addClass('selected'); - } - }); - } - } - var checkbox = $(this).parent().children('input:checkbox'); - lastChecked = checkbox; - if ($(checkbox).attr('checked')) { - $(checkbox).removeAttr('checked'); - $(checkbox).parent().parent().removeClass('selected'); - $('#select_all').removeAttr('checked'); - } else { - $(checkbox).attr('checked', 'checked'); - $(checkbox).parent().parent().toggleClass('selected'); - var selectedCount = $('td.filename input:checkbox:checked').length; - if (selectedCount === $('td.filename input:checkbox').length) { - $('#select_all').attr('checked', 'checked'); - } - } - procesSelection(); - } else { - var filename=$(this).parent().parent().attr('data-file'); - var tr = FileList.findFileEl(filename); - var renaming=tr.data('renaming'); - if (!renaming) { - FileActions.currentFile = $(this).parent(); - var mime=FileActions.getCurrentMimeType(); - var type=FileActions.getCurrentType(); - var permissions = FileActions.getCurrentPermissions(); - var action=FileActions.getDefault(mime,type, permissions); - if (action) { - event.preventDefault(); - action(filename); - } - } - } - - }); - - // Sets the select_all checkbox behaviour : - $('#select_all').click(function() { - if ($(this).attr('checked')) { - // Check all - $('td.filename input:checkbox').attr('checked', true); - $('td.filename input:checkbox').parent().parent().addClass('selected'); - } else { - // Uncheck all - $('td.filename input:checkbox').attr('checked', false); - $('td.filename input:checkbox').parent().parent().removeClass('selected'); - } - procesSelection(); - }); - - $('#fileList').on('change', 'td.filename input:checkbox',function(event) { - if (event.shiftKey) { - var last = $(lastChecked).parent().parent().prevAll().length; - var first = $(this).parent().parent().prevAll().length; - var start = Math.min(first, last); - var end = Math.max(first, last); - var rows = $(this).parent().parent().parent().children('tr'); - for (var i = start; i < end; i++) { - $(rows).each(function(index) { - if (index === i) { - var checkbox = $(this).children().children('input:checkbox'); - $(checkbox).attr('checked', 'checked'); - $(checkbox).parent().parent().addClass('selected'); - } - }); - } - } - var selectedCount=$('td.filename input:checkbox:checked').length; - $(this).parent().parent().toggleClass('selected'); - if (!$(this).attr('checked')) { - $('#select_all').attr('checked',false); - } else { - if (selectedCount===$('td.filename input:checkbox').length) { - $('#select_all').attr('checked',true); - } - } - procesSelection(); - }); - - $('.download').click('click',function(event) { - var files; - var dir = FileList.getCurrentDirectory(); - if (FileList.isAllSelected()) { - files = OC.basename(dir); - dir = OC.dirname(dir) || '/'; - } - else { - files = Files.getSelectedFiles('name'); - } - OC.Notification.show(t('files','Your download is being prepared. This might take some time if the files are big.')); - OC.redirect(Files.getDownloadUrl(files, dir)); - return false; - }); - - $('.delete-selected').click(function(event) { - var files = Files.getSelectedFiles('name'); - event.preventDefault(); - if (FileList.isAllSelected()) { - files = null; - } - FileList.do_delete(files); - return false; - }); - // drag&drop support using jquery.fileupload // TODO use OC.dialogs $(document).bind('drop dragover', function (e) { e.preventDefault(); // prevent browser from doing anything, if file isn't dropped in dropZone - }); + }); //do a background scan if needed scanFiles(); @@ -422,34 +300,22 @@ function scanFiles(force, dir, users) { } scanFiles.scanning=false; -function boolOperationFinished(data, callback) { - result = jQuery.parseJSON(data.responseText); - Files.updateMaxUploadFilesize(result); - if (result.status === 'success') { - callback.call(); - } else { - alert(result.data.message); - } -} - +// TODO: move to FileList var createDragShadow = function(event) { //select dragged file var isDragSelected = $(event.target).parents('tr').find('td input:first').prop('checked'); if (!isDragSelected) { //select dragged file - $(event.target).parents('tr').find('td input:first').prop('checked',true); + FileList._selectFileEl($(event.target).parents('tr:first'), true); } - var selectedFiles = Files.getSelectedFiles(); + // do not show drag shadow for too many files + var selectedFiles = _.first(FileList.getSelectedFiles(), FileList.pageSize); + selectedFiles.sort(FileList._fileInfoCompare); if (!isDragSelected && selectedFiles.length === 1) { //revert the selection - $(event.target).parents('tr').find('td input:first').prop('checked',false); - } - - //also update class when we dragged more than one file - if (selectedFiles.length > 1) { - $(event.target).parents('tr').addClass('selected'); + FileList._selectFileEl($(event.target).parents('tr:first'), false); } // build dragshadow @@ -460,9 +326,12 @@ var createDragShadow = function(event) { var dir=$('#dir').val(); $(selectedFiles).each(function(i,elem) { - var newtr = $('<tr/>').attr('data-dir', dir).attr('data-filename', elem.name).attr('data-origin', elem.origin); + var newtr = $('<tr/>') + .attr('data-dir', dir) + .attr('data-file', elem.name) + .attr('data-origin', elem.origin); newtr.append($('<td/>').addClass('filename').text(elem.name)); - newtr.append($('<td/>').addClass('size').text(humanFileSize(elem.size))); + newtr.append($('<td/>').addClass('size').text(OC.Util.humanFileSize(elem.size))); tbody.append(newtr); if (elem.type === 'dir') { newtr.find('td.filename').attr('style','background-image:url('+OC.imagePath('core', 'filetypes/folder.png')+')'); @@ -479,154 +348,57 @@ var createDragShadow = function(event) { //options for file drag/drop //start&stop handlers needs some cleaning up +// TODO: move to FileList class var dragOptions={ revert: 'invalid', revertDuration: 300, opacity: 0.7, zIndex: 100, appendTo: 'body', cursorAt: { left: 24, top: 18 }, helper: createDragShadow, cursor: 'move', - start: function(event, ui){ - var $selectedFiles = $('td.filename input:checkbox:checked'); - if($selectedFiles.length > 1){ - $selectedFiles.parents('tr').fadeTo(250, 0.2); - } - else{ - $(this).fadeTo(250, 0.2); - } - }, - stop: function(event, ui) { - var $selectedFiles = $('td.filename input:checkbox:checked'); - if($selectedFiles.length > 1){ - $selectedFiles.parents('tr').fadeTo(250, 1); - } - else{ - $(this).fadeTo(250, 1); - } - $('#fileList tr td.filename').addClass('ui-draggable'); + start: function(event, ui){ + var $selectedFiles = $('td.filename input:checkbox:checked'); + if($selectedFiles.length > 1){ + $selectedFiles.parents('tr').fadeTo(250, 0.2); + } + else{ + $(this).fadeTo(250, 0.2); + } + }, + stop: function(event, ui) { + var $selectedFiles = $('td.filename input:checkbox:checked'); + if($selectedFiles.length > 1){ + $selectedFiles.parents('tr').fadeTo(250, 1); + } + else{ + $(this).fadeTo(250, 1); } + $('#fileList tr td.filename').addClass('ui-draggable'); + } }; // sane browsers support using the distance option if ( $('html.ie').length === 0) { dragOptions['distance'] = 20; } -var folderDropOptions={ +// TODO: move to FileList class +var folderDropOptions = { hoverClass: "canDrop", drop: function( event, ui ) { - //don't allow moving a file into a selected folder + // don't allow moving a file into a selected folder if ($(event.target).parents('tr').find('td input:first').prop('checked') === true) { return false; } - var target = $(this).closest('tr').data('file'); - - var files = ui.helper.find('tr'); - $(files).each(function(i,row) { - var dir = $(row).data('dir'); - var file = $(row).data('filename'); - //slapdash selector, tracking down our original element that the clone budded off of. - var origin = $('tr[data-id=' + $(row).data('origin') + ']'); - var td = origin.children('td.filename'); - var oldBackgroundImage = td.css('background-image'); - td.css('background-image', 'url('+ OC.imagePath('core', 'loading.gif') + ')'); - $.post(OC.filePath('files', 'ajax', 'move.php'), { dir: dir, file: file, target: dir+'/'+target }, function(result) { - if (result) { - if (result.status === 'success') { - //recalculate folder size - var oldFile = FileList.findFileEl(target); - var newFile = FileList.findFileEl(file); - var oldSize = oldFile.data('size'); - var newSize = oldSize + newFile.data('size'); - oldFile.data('size', newSize); - oldFile.find('td.filesize').text(humanFileSize(newSize)); - - FileList.remove(file); - procesSelection(); - $('#notification').hide(); - } else { - $('#notification').hide(); - $('#notification').text(result.data.message); - $('#notification').fadeIn(); - } - } else { - OC.dialogs.alert(t('files', 'Error moving file'), t('files', 'Error')); - } - td.css('background-image', oldBackgroundImage); - }); - }); - }, - tolerance: 'pointer' -}; + var targetPath = FileList.getCurrentDirectory() + '/' + $(this).closest('tr').data('file'); -function procesSelection() { - var selected = Files.getSelectedFiles(); - var selectedFiles = selected.filter(function(el) { - return el.type==='file'; - }); - var selectedFolders = selected.filter(function(el) { - return el.type==='dir'; - }); - if (selectedFiles.length === 0 && selectedFolders.length === 0) { - $('#headerName span.name').text(t('files','Name')); - $('#headerSize').text(t('files','Size')); - $('#modified').text(t('files','Modified')); - $('table').removeClass('multiselect'); - $('.selectedActions').hide(); - $('#select_all').removeAttr('checked'); - } - else { - $('.selectedActions').show(); - var totalSize = 0; - for(var i=0; i<selectedFiles.length; i++) { - totalSize+=selectedFiles[i].size; - } - for(var i=0; i<selectedFolders.length; i++) { - totalSize+=selectedFolders[i].size; - } - $('#headerSize').text(humanFileSize(totalSize)); - var selection = ''; - if (selectedFolders.length > 0) { - selection += n('files', '%n folder', '%n folders', selectedFolders.length); - if (selectedFiles.length > 0) { - selection += ' & '; - } + var files = FileList.getSelectedFiles(); + if (files.length === 0) { + // single one selected without checkbox? + files = _.map(ui.helper.find('tr'), FileList.elementToFile); } - if (selectedFiles.length>0) { - selection += n('files', '%n file', '%n files', selectedFiles.length); - } - $('#headerName span.name').text(selection); - $('#modified').text(''); - $('table').addClass('multiselect'); - } -} -/** - * @brief get a list of selected files - * @param {string} property (option) the property of the file requested - * @return {array} - * - * possible values for property: name, mime, size and type - * if property is set, an array with that property for each file is returnd - * if it's ommited an array of objects with all properties is returned - */ -Files.getSelectedFiles = function(property) { - var elements=$('td.filename input:checkbox:checked').parent().parent(); - var files=[]; - elements.each(function(i,element) { - var file={ - name:$(element).attr('data-file'), - mime:$(element).data('mime'), - type:$(element).data('type'), - size:$(element).data('size'), - etag:$(element).data('etag'), - origin: $(element).data('id') - }; - if (property) { - files.push(file[property]); - } else { - files.push(file); - } - }); - return files; -} + FileList.move(_.pluck(files, 'name'), targetPath); + }, + tolerance: 'pointer' +}; Files.getMimeIcon = function(mime, ready) { if (Files.getMimeIcon.cache[mime]) { @@ -665,7 +437,7 @@ Files.generatePreviewUrl = function(urlSpec) { urlSpec.x *= window.devicePixelRatio; urlSpec.forceIcon = 0; return OC.generateUrl('/core/preview.png?') + $.param(urlSpec); -} +}; Files.lazyLoadPreview = function(path, mime, ready, width, height, etag) { // get mime icon url diff --git a/apps/files/js/filesummary.js b/apps/files/js/filesummary.js new file mode 100644 index 00000000000..b5130247cc9 --- /dev/null +++ b/apps/files/js/filesummary.js @@ -0,0 +1,195 @@ +/** +* ownCloud +* +* @author Vincent Petry +* @copyright 2014 Vincent Petry <pvince81@owncloud.com> +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE +* License as published by the Free Software Foundation; either +* version 3 of the License, or any later version. +* +* This library is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU AFFERO GENERAL PUBLIC LICENSE for more details. +* +* You should have received a copy of the GNU Affero General Public +* License along with this library. If not, see <http://www.gnu.org/licenses/>. +* +*/ + +/* global OC, n, t */ + +(function() { + /** + * The FileSummary class encapsulates the file summary values and + * the logic to render it in the given container + * @param $tr table row element + * $param summary optional initial summary value + */ + var FileSummary = function($tr) { + this.$el = $tr; + this.clear(); + this.render(); + }; + + FileSummary.prototype = { + summary: { + totalFiles: 0, + totalDirs: 0, + totalSize: 0 + }, + + /** + * Adds file + * @param file file to add + * @param update whether to update the display + */ + add: function(file, update) { + if (file.type === 'dir' || file.mime === 'httpd/unix-directory') { + this.summary.totalDirs++; + } + else { + this.summary.totalFiles++; + } + this.summary.totalSize += parseInt(file.size, 10) || 0; + if (!!update) { + this.update(); + } + }, + /** + * Removes file + * @param file file to remove + * @param update whether to update the display + */ + remove: function(file, update) { + if (file.type === 'dir' || file.mime === 'httpd/unix-directory') { + this.summary.totalDirs--; + } + else { + this.summary.totalFiles--; + } + this.summary.totalSize -= parseInt(file.size, 10) || 0; + if (!!update) { + this.update(); + } + }, + /** + * Returns the total of files and directories + */ + getTotal: function() { + return this.summary.totalDirs + this.summary.totalFiles; + }, + /** + * Recalculates the summary based on the given files array + * @param files array of files + */ + calculate: function(files) { + var file; + var summary = { + totalDirs: 0, + totalFiles: 0, + totalSize: 0 + }; + + for (var i = 0; i < files.length; i++) { + file = files[i]; + if (file.type === 'dir' || file.mime === 'httpd/unix-directory') { + summary.totalDirs++; + } + else { + summary.totalFiles++; + } + summary.totalSize += parseInt(file.size, 10) || 0; + } + this.setSummary(summary); + }, + /** + * Clears the summary + */ + clear: function() { + this.calculate([]); + }, + /** + * Sets the current summary values + * @param summary map + */ + setSummary: function(summary) { + this.summary = summary; + this.update(); + }, + + /** + * Renders the file summary element + */ + update: function() { + if (!this.$el) { + return; + } + if (!this.summary.totalFiles && !this.summary.totalDirs) { + this.$el.addClass('hidden'); + return; + } + // There's a summary and data -> Update the summary + this.$el.removeClass('hidden'); + var $dirInfo = this.$el.find('.dirinfo'); + var $fileInfo = this.$el.find('.fileinfo'); + var $connector = this.$el.find('.connector'); + + // Substitute old content with new translations + $dirInfo.html(n('files', '%n folder', '%n folders', this.summary.totalDirs)); + $fileInfo.html(n('files', '%n file', '%n files', this.summary.totalFiles)); + this.$el.find('.filesize').html(OC.Util.humanFileSize(this.summary.totalSize)); + + // Show only what's necessary (may be hidden) + if (this.summary.totalDirs === 0) { + $dirInfo.addClass('hidden'); + $connector.addClass('hidden'); + } else { + $dirInfo.removeClass('hidden'); + } + if (this.summary.totalFiles === 0) { + $fileInfo.addClass('hidden'); + $connector.addClass('hidden'); + } else { + $fileInfo.removeClass('hidden'); + } + if (this.summary.totalDirs > 0 && this.summary.totalFiles > 0) { + $connector.removeClass('hidden'); + } + }, + render: function() { + if (!this.$el) { + return; + } + // TODO: ideally this should be separate to a template or something + var summary = this.summary; + var directoryInfo = n('files', '%n folder', '%n folders', summary.totalDirs); + var fileInfo = n('files', '%n file', '%n files', summary.totalFiles); + + var infoVars = { + dirs: '<span class="dirinfo">'+directoryInfo+'</span><span class="connector">', + files: '</span><span class="fileinfo">'+fileInfo+'</span>' + }; + + // don't show the filesize column, if filesize is NaN (e.g. in trashbin) + var fileSize = ''; + if (!isNaN(summary.totalSize)) { + fileSize = '<td class="filesize">' + OC.Util.humanFileSize(summary.totalSize) + '</td>'; + } + + var info = t('files', '{dirs} and {files}', infoVars); + + var $summary = $('<td><span class="info">'+info+'</span></td>'+fileSize+'<td></td>'); + + if (!this.summary.totalFiles && !this.summary.totalDirs) { + this.$el.addClass('hidden'); + } + + this.$el.append($summary); + } + }; + window.FileSummary = FileSummary; +})(); + diff --git a/apps/files/templates/index.php b/apps/files/templates/index.php index a8437835d95..42263c880a7 100644 --- a/apps/files/templates/index.php +++ b/apps/files/templates/index.php @@ -91,6 +91,8 @@ </thead> <tbody id="fileList"> </tbody> + <tfoot> + </tfoot> </table> <div id="editor"></div><!-- FIXME Do not use this div in your app! It is deprecated and will be removed in the future! --> <div id="uploadsize-message" title="<?php p($l->t('Upload too large'))?>"> diff --git a/apps/files/tests/js/fileactionsSpec.js b/apps/files/tests/js/fileactionsSpec.js index 3c22c84b866..f5eafba509f 100644 --- a/apps/files/tests/js/fileactionsSpec.js +++ b/apps/files/tests/js/fileactionsSpec.js @@ -30,6 +30,7 @@ describe('FileActions tests', function() { $body.append('<input type="hidden" id="permissions" value="31"></input>'); // dummy files table $filesTable = $body.append('<table id="filestable"></table>'); + FileList.files = []; }); afterEach(function() { $('#dir, #permissions, #filestable').remove(); diff --git a/apps/files/tests/js/filelistSpec.js b/apps/files/tests/js/filelistSpec.js index ca85a360cf5..eab364644cd 100644 --- a/apps/files/tests/js/filelistSpec.js +++ b/apps/files/tests/js/filelistSpec.js @@ -24,6 +24,33 @@ describe('FileList tests', function() { var testFiles, alertStub, notificationStub, pushStateStub; + /** + * Generate test file data + */ + function generateFiles(startIndex, endIndex) { + var files = []; + var name; + for (var i = startIndex; i <= endIndex; i++) { + name = 'File with index '; + if (i < 10) { + // do not rely on localeCompare here + // and make the sorting predictable + // cross-browser + name += '0'; + } + name += i + '.txt'; + files.push({ + id: i, + type: 'file', + name: name, + mimetype: 'text/plain', + size: i * 2, + etag: 'abc' + }); + } + return files; + } + beforeEach(function() { // init horrible parameters var $body = $('body'); @@ -48,9 +75,17 @@ describe('FileList tests', function() { ' <div class="notCreatable"></div>' + '</div>' + // dummy table + // TODO: at some point this will be rendered by the FileList class itself! '<table id="filestable">' + - '<thead><tr><th class="hidden">Name</th></tr></thead>' + + '<thead><tr><th id="headerName" class="hidden">' + + '<input type="checkbox" id="select_all">' + + '<span class="name">Name</span>' + + '<span class="selectedActions hidden">' + + '<a href class="download">Download</a>' + + '<a href class="delete-selected">Delete</a></span>' + + '</th></tr></thead>' + '<tbody id="fileList"></tbody>' + + '<tfoot></tfoot>' + '</table>' + '<div id="emptycontent">Empty content message</div>' ); @@ -60,25 +95,29 @@ describe('FileList tests', function() { type: 'file', name: 'One.txt', mimetype: 'text/plain', - size: 12 + size: 12, + etag: 'abc' }, { id: 2, type: 'file', name: 'Two.jpg', mimetype: 'image/jpeg', - size: 12049 + size: 12049, + etag: 'def', }, { id: 3, type: 'file', name: 'Three.pdf', mimetype: 'application/pdf', - size: 58009 + size: 58009, + etag: '123', }, { id: 4, type: 'dir', name: 'somedir', mimetype: 'httpd/unix-directory', - size: 250 + size: 250, + etag: '456' }]; FileList.initialize(); @@ -220,25 +259,65 @@ describe('FileList tests', function() { var $tr = FileList.add(fileData); expect($tr.find('.filesize').text()).toEqual('0 B'); }); - it('adds new file to the end of the list before the summary', function() { + it('adds new file to the end of the list', function() { + var $tr; var fileData = { type: 'file', - name: 'P comes after O.txt' + name: 'ZZZ.txt' }; FileList.setFiles(testFiles); $tr = FileList.add(fileData); expect($tr.index()).toEqual(4); - expect($tr.next().hasClass('summary')).toEqual(true); }); - it('adds new file at correct position in insert mode', function() { + it('inserts files in a sorted manner when insert option is enabled', function() { + var $tr; + for (var i = 0; i < testFiles.length; i++) { + FileList.add(testFiles[i]); + } + expect(FileList.files[0].name).toEqual('somedir'); + expect(FileList.files[1].name).toEqual('One.txt'); + expect(FileList.files[2].name).toEqual('Three.pdf'); + expect(FileList.files[3].name).toEqual('Two.jpg'); + }); + it('inserts new file at correct position', function() { + var $tr; var fileData = { type: 'file', name: 'P comes after O.txt' }; - FileList.setFiles(testFiles); - $tr = FileList.add(fileData, {insert: true}); + for (var i = 0; i < testFiles.length; i++) { + FileList.add(testFiles[i]); + } + $tr = FileList.add(fileData); // after "One.txt" + expect($tr.index()).toEqual(2); + expect(FileList.files[2]).toEqual(fileData); + }); + it('inserts new folder at correct position in insert mode', function() { + var $tr; + var fileData = { + type: 'dir', + name: 'somedir2 comes after somedir' + }; + for (var i = 0; i < testFiles.length; i++) { + FileList.add(testFiles[i]); + } + $tr = FileList.add(fileData); expect($tr.index()).toEqual(1); + expect(FileList.files[1]).toEqual(fileData); + }); + it('inserts new file at the end correctly', function() { + var $tr; + var fileData = { + type: 'file', + name: 'zzz.txt' + }; + for (var i = 0; i < testFiles.length; i++) { + FileList.add(testFiles[i]); + } + $tr = FileList.add(fileData); + expect($tr.index()).toEqual(4); + expect(FileList.files[4]).toEqual(fileData); }); it('removes empty content message and shows summary when adding first file', function() { var fileData = { @@ -249,8 +328,8 @@ describe('FileList tests', function() { FileList.setFiles([]); expect(FileList.isEmpty).toEqual(true); FileList.add(fileData); - $summary = $('#fileList .summary'); - expect($summary.length).toEqual(1); + $summary = $('#filestable .summary'); + expect($summary.hasClass('hidden')).toEqual(false); // yes, ugly... expect($summary.find('.info').text()).toEqual('0 folders and 1 file'); expect($summary.find('.dirinfo').hasClass('hidden')).toEqual(true); @@ -268,11 +347,12 @@ describe('FileList tests', function() { $removedEl = FileList.remove('One.txt'); expect($removedEl).toBeDefined(); expect($removedEl.attr('data-file')).toEqual('One.txt'); - expect($('#fileList tr:not(.summary)').length).toEqual(3); + expect($('#fileList tr').length).toEqual(3); + expect(FileList.files.length).toEqual(3); expect(FileList.findFileEl('One.txt').length).toEqual(0); - $summary = $('#fileList .summary'); - expect($summary.length).toEqual(1); + $summary = $('#filestable .summary'); + expect($summary.hasClass('hidden')).toEqual(false); expect($summary.find('.info').text()).toEqual('1 folder and 2 files'); expect($summary.find('.dirinfo').hasClass('hidden')).toEqual(false); expect($summary.find('.fileinfo').hasClass('hidden')).toEqual(false); @@ -282,11 +362,12 @@ describe('FileList tests', function() { it('Shows empty content when removing last file', function() { FileList.setFiles([testFiles[0]]); FileList.remove('One.txt'); - expect($('#fileList tr:not(.summary)').length).toEqual(0); + expect($('#fileList tr').length).toEqual(0); + expect(FileList.files.length).toEqual(0); expect(FileList.findFileEl('One.txt').length).toEqual(0); - $summary = $('#fileList .summary'); - expect($summary.length).toEqual(0); + $summary = $('#filestable .summary'); + expect($summary.hasClass('hidden')).toEqual(true); expect($('#filestable thead th').hasClass('hidden')).toEqual(true); expect($('#emptycontent').hasClass('hidden')).toEqual(false); expect(FileList.isEmpty).toEqual(true); @@ -318,10 +399,10 @@ describe('FileList tests', function() { expect(FileList.findFileEl('One.txt').length).toEqual(0); expect(FileList.findFileEl('Two.jpg').length).toEqual(0); expect(FileList.findFileEl('Three.pdf').length).toEqual(1); - expect(FileList.$fileList.find('tr:not(.summary)').length).toEqual(2); + expect(FileList.$fileList.find('tr').length).toEqual(2); - $summary = $('#fileList .summary'); - expect($summary.length).toEqual(1); + $summary = $('#filestable .summary'); + expect($summary.hasClass('hidden')).toEqual(false); expect($summary.find('.info').text()).toEqual('1 folder and 1 file'); expect($summary.find('.dirinfo').hasClass('hidden')).toEqual(false); expect($summary.find('.fileinfo').hasClass('hidden')).toEqual(false); @@ -342,11 +423,12 @@ describe('FileList tests', function() { JSON.stringify({status: 'success'}) ); - expect(FileList.$fileList.find('tr:not(.summary)').length).toEqual(0); + expect(FileList.$fileList.find('tr').length).toEqual(0); - $summary = $('#fileList .summary'); - expect($summary.length).toEqual(0); + $summary = $('#filestable .summary'); + expect($summary.hasClass('hidden')).toEqual(true); expect(FileList.isEmpty).toEqual(true); + expect(FileList.files.length).toEqual(0); expect($('#filestable thead th').hasClass('hidden')).toEqual(true); expect($('#emptycontent').hasClass('hidden')).toEqual(false); }); @@ -363,7 +445,7 @@ describe('FileList tests', function() { // files are still in the list expect(FileList.findFileEl('One.txt').length).toEqual(1); expect(FileList.findFileEl('Two.jpg').length).toEqual(1); - expect(FileList.$fileList.find('tr:not(.summary)').length).toEqual(4); + expect(FileList.$fileList.find('tr').length).toEqual(4); expect(notificationStub.calledOnce).toEqual(true); }); @@ -372,37 +454,41 @@ describe('FileList tests', function() { function doRename() { var $input, request; - FileList.setFiles(testFiles); + for (var i = 0; i < testFiles.length; i++) { + FileList.add(testFiles[i]); + } // trigger rename prompt FileList.rename('One.txt'); $input = FileList.$fileList.find('input.filename'); - $input.val('One_renamed.txt').blur(); + $input.val('Tu_after_three.txt').blur(); expect(fakeServer.requests.length).toEqual(1); - var request = fakeServer.requests[0]; + request = fakeServer.requests[0]; expect(request.url.substr(0, request.url.indexOf('?'))).toEqual(OC.webroot + '/index.php/apps/files/ajax/rename.php'); - expect(OC.parseQueryString(request.url)).toEqual({'dir': '/subdir', newname: 'One_renamed.txt', file: 'One.txt'}); + expect(OC.parseQueryString(request.url)).toEqual({'dir': '/subdir', newname: 'Tu_after_three.txt', file: 'One.txt'}); // element is renamed before the request finishes expect(FileList.findFileEl('One.txt').length).toEqual(0); - expect(FileList.findFileEl('One_renamed.txt').length).toEqual(1); + expect(FileList.findFileEl('Tu_after_three.txt').length).toEqual(1); // input is gone expect(FileList.$fileList.find('input.filename').length).toEqual(0); } - it('Keeps renamed file entry if rename ajax call suceeded', function() { + it('Inserts renamed file entry at correct position if rename ajax call suceeded', function() { doRename(); fakeServer.requests[0].respond(200, {'Content-Type': 'application/json'}, JSON.stringify({ status: 'success', data: { - name: 'One_renamed.txt' + name: 'Tu_after_three.txt', + type: 'file' } })); // element stays renamed expect(FileList.findFileEl('One.txt').length).toEqual(0); - expect(FileList.findFileEl('One_renamed.txt').length).toEqual(1); + expect(FileList.findFileEl('Tu_after_three.txt').length).toEqual(1); + expect(FileList.findFileEl('Tu_after_three.txt').index()).toEqual(2); // after Two.txt expect(alertStub.notCalled).toEqual(true); }); @@ -418,7 +504,8 @@ describe('FileList tests', function() { // element was reverted expect(FileList.findFileEl('One.txt').length).toEqual(1); - expect(FileList.findFileEl('One_renamed.txt').length).toEqual(0); + expect(FileList.findFileEl('One.txt').index()).toEqual(1); // after somedir + expect(FileList.findFileEl('Tu_after_three.txt').length).toEqual(0); expect(alertStub.calledOnce).toEqual(true); }); @@ -429,12 +516,12 @@ describe('FileList tests', function() { fakeServer.requests[0].respond(200, {'Content-Type': 'application/json'}, JSON.stringify({ status: 'success', data: { - name: 'One_renamed.txt' + name: 'Tu_after_three.txt' } })); - $tr = FileList.findFileEl('One_renamed.txt'); - expect($tr.find('a.name').attr('href')).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2Fsubdir&files=One_renamed.txt'); + $tr = FileList.findFileEl('Tu_after_three.txt'); + expect($tr.find('a.name').attr('href')).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2Fsubdir&files=Tu_after_three.txt'); }); // FIXME: fix this in the source code! xit('Correctly updates file link after rename when path has same name', function() { @@ -446,27 +533,122 @@ describe('FileList tests', function() { fakeServer.requests[0].respond(200, {'Content-Type': 'application/json'}, JSON.stringify({ status: 'success', data: { - name: 'One_renamed.txt' + name: 'Tu_after_three.txt' } })); - $tr = FileList.findFileEl('One_renamed.txt'); + $tr = FileList.findFileEl('Tu_after_three.txt'); expect($tr.find('a.name').attr('href')).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2Fsubdir&files=One.txt'); }); }); + describe('Moving files', function() { + beforeEach(function() { + FileList.setFiles(testFiles); + }); + it('Moves single file to target folder', function() { + var request; + FileList.move('One.txt', '/somedir'); + + expect(fakeServer.requests.length).toEqual(1); + request = fakeServer.requests[0]; + expect(request.url).toEqual(OC.webroot + '/index.php/apps/files/ajax/move.php'); + expect(OC.parseQueryString(request.requestBody)).toEqual({dir: '/subdir', file: 'One.txt', target: '/somedir'}); + + fakeServer.requests[0].respond(200, {'Content-Type': 'application/json'}, JSON.stringify({ + status: 'success', + data: { + name: 'One.txt', + type: 'file' + } + })); + + expect(FileList.findFileEl('One.txt').length).toEqual(0); + + // folder size has increased + expect(FileList.findFileEl('somedir').data('size')).toEqual(262); + expect(FileList.findFileEl('somedir').find('.filesize').text()).toEqual('262 B'); + + expect(notificationStub.notCalled).toEqual(true); + }); + it('Moves list of files to target folder', function() { + var request; + FileList.move(['One.txt', 'Two.jpg'], '/somedir'); + + expect(fakeServer.requests.length).toEqual(2); + request = fakeServer.requests[0]; + expect(request.url).toEqual(OC.webroot + '/index.php/apps/files/ajax/move.php'); + expect(OC.parseQueryString(request.requestBody)).toEqual({dir: '/subdir', file: 'One.txt', target: '/somedir'}); + + request = fakeServer.requests[1]; + expect(request.url).toEqual(OC.webroot + '/index.php/apps/files/ajax/move.php'); + expect(OC.parseQueryString(request.requestBody)).toEqual({dir: '/subdir', file: 'Two.jpg', target: '/somedir'}); + + fakeServer.requests[0].respond(200, {'Content-Type': 'application/json'}, JSON.stringify({ + status: 'success', + data: { + name: 'One.txt', + type: 'file' + } + })); + + expect(FileList.findFileEl('One.txt').length).toEqual(0); + + // folder size has increased + expect(FileList.findFileEl('somedir').data('size')).toEqual(262); + expect(FileList.findFileEl('somedir').find('.filesize').text()).toEqual('262 B'); + + fakeServer.requests[1].respond(200, {'Content-Type': 'application/json'}, JSON.stringify({ + status: 'success', + data: { + name: 'Two.jpg', + type: 'file' + } + })); + + expect(FileList.findFileEl('Two.jpg').length).toEqual(0); + + // folder size has increased + expect(FileList.findFileEl('somedir').data('size')).toEqual(12311); + expect(FileList.findFileEl('somedir').find('.filesize').text()).toEqual('12 kB'); + + expect(notificationStub.notCalled).toEqual(true); + }); + it('Shows notification if a file could not be moved', function() { + var request; + FileList.move('One.txt', '/somedir'); + + expect(fakeServer.requests.length).toEqual(1); + request = fakeServer.requests[0]; + expect(request.url).toEqual(OC.webroot + '/index.php/apps/files/ajax/move.php'); + expect(OC.parseQueryString(request.requestBody)).toEqual({dir: '/subdir', file: 'One.txt', target: '/somedir'}); + + fakeServer.requests[0].respond(200, {'Content-Type': 'application/json'}, JSON.stringify({ + status: 'error', + data: { + message: 'Error while moving file', + } + })); + + expect(FileList.findFileEl('One.txt').length).toEqual(1); + + expect(notificationStub.calledOnce).toEqual(true); + expect(notificationStub.getCall(0).args[0]).toEqual('Error while moving file'); + }); + }); describe('List rendering', function() { it('renders a list of files using add()', function() { - var addSpy = sinon.spy(FileList, 'add'); + expect(FileList.files.length).toEqual(0); + expect(FileList.files).toEqual([]); FileList.setFiles(testFiles); - expect(addSpy.callCount).toEqual(4); - expect($('#fileList tr:not(.summary)').length).toEqual(4); - addSpy.restore(); + expect($('#fileList tr').length).toEqual(4); + expect(FileList.files.length).toEqual(4); + expect(FileList.files).toEqual(testFiles); }); it('updates summary using the file sizes', function() { var $summary; FileList.setFiles(testFiles); - $summary = $('#fileList .summary'); - expect($summary.length).toEqual(1); + $summary = $('#filestable .summary'); + expect($summary.hasClass('hidden')).toEqual(false); expect($summary.find('.info').text()).toEqual('1 folder and 3 files'); expect($summary.find('.filesize').text()).toEqual('69 kB'); }); @@ -474,20 +656,20 @@ describe('FileList tests', function() { FileList.setFiles(testFiles); expect($('#filestable thead th').hasClass('hidden')).toEqual(false); expect($('#emptycontent').hasClass('hidden')).toEqual(true); - expect(FileList.$fileList.find('.summary').length).toEqual(1); + expect(FileList.$el.find('.summary').hasClass('hidden')).toEqual(false); }); it('hides headers, summary and show empty content message after setting empty file list', function(){ FileList.setFiles([]); expect($('#filestable thead th').hasClass('hidden')).toEqual(true); expect($('#emptycontent').hasClass('hidden')).toEqual(false); - expect(FileList.$fileList.find('.summary').length).toEqual(0); + expect(FileList.$el.find('.summary').hasClass('hidden')).toEqual(true); }); it('hides headers, empty content message, and summary when list is empty and user has no creation permission', function(){ $('#permissions').val(0); FileList.setFiles([]); expect($('#filestable thead th').hasClass('hidden')).toEqual(true); expect($('#emptycontent').hasClass('hidden')).toEqual(true); - expect(FileList.$fileList.find('.summary').length).toEqual(0); + expect(FileList.$el.find('.summary').hasClass('hidden')).toEqual(true); }); it('calling findFileEl() can find existing file element', function() { FileList.setFiles(testFiles); @@ -519,6 +701,110 @@ describe('FileList tests', function() { FileList.setFiles(testFiles); expect(handler.calledOnce).toEqual(true); }); + it('does not update summary when removing non-existing files', function() { + // single file + FileList.setFiles([testFiles[0]]); + $summary = $('#filestable .summary'); + expect($summary.hasClass('hidden')).toEqual(false); + expect($summary.find('.info').text()).toEqual('0 folders and 1 file'); + FileList.remove('unexist.txt'); + expect($summary.hasClass('hidden')).toEqual(false); + expect($summary.find('.info').text()).toEqual('0 folders and 1 file'); + }); + }); + describe('Rendering next page on scroll', function() { + beforeEach(function() { + FileList.setFiles(generateFiles(0, 64)); + }); + it('renders only the first page', function() { + expect(FileList.files.length).toEqual(65); + expect($('#fileList tr').length).toEqual(20); + }); + it('renders the second page when scrolling down (trigger nextPage)', function() { + // TODO: can't simulate scrolling here, so calling nextPage directly + FileList._nextPage(true); + expect($('#fileList tr').length).toEqual(40); + FileList._nextPage(true); + expect($('#fileList tr').length).toEqual(60); + FileList._nextPage(true); + expect($('#fileList tr').length).toEqual(65); + FileList._nextPage(true); + // stays at 65 + expect($('#fileList tr').length).toEqual(65); + }); + it('inserts into the DOM if insertion point is in the visible page ', function() { + FileList.add({ + id: 2000, + type: 'file', + name: 'File with index 15b.txt' + }); + expect($('#fileList tr').length).toEqual(21); + expect(FileList.findFileEl('File with index 15b.txt').index()).toEqual(16); + }); + it('does not inserts into the DOM if insertion point is not the visible page ', function() { + FileList.add({ + id: 2000, + type: 'file', + name: 'File with index 28b.txt' + }); + expect($('#fileList tr').length).toEqual(20); + expect(FileList.findFileEl('File with index 28b.txt').length).toEqual(0); + FileList._nextPage(true); + expect($('#fileList tr').length).toEqual(40); + expect(FileList.findFileEl('File with index 28b.txt').index()).toEqual(29); + }); + it('appends into the DOM when inserting a file after the last visible element', function() { + FileList.add({ + id: 2000, + type: 'file', + name: 'File with index 19b.txt' + }); + expect($('#fileList tr').length).toEqual(21); + FileList._nextPage(true); + expect($('#fileList tr').length).toEqual(41); + }); + it('appends into the DOM when inserting a file on the last page when visible', function() { + FileList._nextPage(true); + expect($('#fileList tr').length).toEqual(40); + FileList._nextPage(true); + expect($('#fileList tr').length).toEqual(60); + FileList._nextPage(true); + expect($('#fileList tr').length).toEqual(65); + FileList._nextPage(true); + FileList.add({ + id: 2000, + type: 'file', + name: 'File with index 88.txt' + }); + expect($('#fileList tr').length).toEqual(66); + FileList._nextPage(true); + expect($('#fileList tr').length).toEqual(66); + }); + it('shows additional page when appending a page of files and scrolling down', function() { + var newFiles = generateFiles(66, 81); + for (var i = 0; i < newFiles.length; i++) { + FileList.add(newFiles[i]); + } + expect($('#fileList tr').length).toEqual(20); + FileList._nextPage(true); + expect($('#fileList tr').length).toEqual(40); + FileList._nextPage(true); + expect($('#fileList tr').length).toEqual(60); + FileList._nextPage(true); + expect($('#fileList tr').length).toEqual(80); + FileList._nextPage(true); + expect($('#fileList tr').length).toEqual(81); + FileList._nextPage(true); + expect($('#fileList tr').length).toEqual(81); + }); + it('automatically renders next page when there are not enough elements visible', function() { + // delete the 15 first elements + for (var i = 0; i < 15; i++) { + FileList.remove(FileList.files[0].name); + } + // still makes sure that there are 20 elements visible, if any + expect($('#fileList tr').length).toEqual(25); + }); }); describe('file previews', function() { var previewLoadStub; @@ -642,7 +928,7 @@ describe('FileList tests', function() { var query = url.substr(url.indexOf('?') + 1); expect(OC.parseQueryString(query)).toEqual({'dir': '/subdir'}); fakeServer.respond(); - expect($('#fileList tr:not(.summary)').length).toEqual(4); + expect($('#fileList tr').length).toEqual(4); expect(FileList.findFileEl('One.txt').length).toEqual(1); }); it('switches dir and fetches file list when calling changeDirectory()', function() { @@ -740,14 +1026,12 @@ describe('FileList tests', function() { } }; // returns a list of tr that were dragged - // FIXME: why are their attributes different than the - // regular file trs ? ui.helper.find.returns([ - $('<tr data-filename="One.txt" data-dir="' + testDir + '"></tr>'), - $('<tr data-filename="Two.jpg" data-dir="' + testDir + '"></tr>') + $('<tr data-file="One.txt" data-dir="' + testDir + '"></tr>'), + $('<tr data-file="Two.jpg" data-dir="' + testDir + '"></tr>') ]); // simulate drop event - FileList._onDropOnBreadCrumb.call($crumb, new $.Event('drop'), ui); + FileList._onDropOnBreadCrumb(new $.Event('drop', {target: $crumb}), ui); // will trigger two calls to move.php (first one was previous list.php) expect(fakeServer.requests.length).toEqual(3); @@ -784,14 +1068,12 @@ describe('FileList tests', function() { } }; // returns a list of tr that were dragged - // FIXME: why are their attributes different than the - // regular file trs ? ui.helper.find.returns([ - $('<tr data-filename="One.txt" data-dir="' + testDir + '"></tr>'), - $('<tr data-filename="Two.jpg" data-dir="' + testDir + '"></tr>') + $('<tr data-file="One.txt" data-dir="' + testDir + '"></tr>'), + $('<tr data-file="Two.jpg" data-dir="' + testDir + '"></tr>') ]); // simulate drop event - FileList._onDropOnBreadCrumb.call($crumb, new $.Event('drop'), ui); + FileList._onDropOnBreadCrumb(new $.Event('drop', {target: $crumb}), ui); // no extra server request expect(fakeServer.requests.length).toEqual(1); @@ -811,4 +1093,329 @@ describe('FileList tests', function() { expect(Files.getAjaxUrl('test', {a:1, b:'x y'})).toEqual(OC.webroot + '/index.php/apps/files/ajax/test.php?a=1&b=x%20y'); }); }); + describe('File selection', function() { + beforeEach(function() { + FileList.setFiles(testFiles); + }); + it('Selects a file when clicking its checkbox', function() { + var $tr = FileList.findFileEl('One.txt'); + expect($tr.find('input:checkbox').prop('checked')).toEqual(false); + $tr.find('td.filename input:checkbox').click(); + + expect($tr.find('input:checkbox').prop('checked')).toEqual(true); + }); + it('Selects/deselect a file when clicking on the name while holding Ctrl', function() { + var $tr = FileList.findFileEl('One.txt'); + var $tr2 = FileList.findFileEl('Three.pdf'); + var e; + expect($tr.find('input:checkbox').prop('checked')).toEqual(false); + expect($tr2.find('input:checkbox').prop('checked')).toEqual(false); + e = new $.Event('click'); + e.ctrlKey = true; + $tr.find('td.filename .name').trigger(e); + + expect($tr.find('input:checkbox').prop('checked')).toEqual(true); + expect($tr2.find('input:checkbox').prop('checked')).toEqual(false); + + // click on second entry, does not clear the selection + e = new $.Event('click'); + e.ctrlKey = true; + $tr2.find('td.filename .name').trigger(e); + expect($tr.find('input:checkbox').prop('checked')).toEqual(true); + expect($tr2.find('input:checkbox').prop('checked')).toEqual(true); + + expect(_.pluck(FileList.getSelectedFiles(), 'name')).toEqual(['One.txt', 'Three.pdf']); + + // deselect now + e = new $.Event('click'); + e.ctrlKey = true; + $tr2.find('td.filename .name').trigger(e); + expect($tr.find('input:checkbox').prop('checked')).toEqual(true); + expect($tr2.find('input:checkbox').prop('checked')).toEqual(false); + expect(_.pluck(FileList.getSelectedFiles(), 'name')).toEqual(['One.txt']); + }); + it('Selects a range when clicking on one file then Shift clicking on another one', function() { + var $tr = FileList.findFileEl('One.txt'); + var $tr2 = FileList.findFileEl('Three.pdf'); + var e; + $tr.find('td.filename input:checkbox').click(); + e = new $.Event('click'); + e.shiftKey = true; + $tr2.find('td.filename .name').trigger(e); + + expect($tr.find('input:checkbox').prop('checked')).toEqual(true); + expect($tr2.find('input:checkbox').prop('checked')).toEqual(true); + expect(FileList.findFileEl('Two.jpg').find('input:checkbox').prop('checked')).toEqual(true); + var selection = _.pluck(FileList.getSelectedFiles(), 'name'); + expect(selection.length).toEqual(3); + expect(selection).toContain('One.txt'); + expect(selection).toContain('Two.jpg'); + expect(selection).toContain('Three.pdf'); + }); + it('Selects a range when clicking on one file then Shift clicking on another one that is above the first one', function() { + var $tr = FileList.findFileEl('One.txt'); + var $tr2 = FileList.findFileEl('Three.pdf'); + var e; + $tr2.find('td.filename input:checkbox').click(); + e = new $.Event('click'); + e.shiftKey = true; + $tr.find('td.filename .name').trigger(e); + + expect($tr.find('input:checkbox').prop('checked')).toEqual(true); + expect($tr2.find('input:checkbox').prop('checked')).toEqual(true); + expect(FileList.findFileEl('Two.jpg').find('input:checkbox').prop('checked')).toEqual(true); + var selection = _.pluck(FileList.getSelectedFiles(), 'name'); + expect(selection.length).toEqual(3); + expect(selection).toContain('One.txt'); + expect(selection).toContain('Two.jpg'); + expect(selection).toContain('Three.pdf'); + }); + it('Selecting all files will automatically check "select all" checkbox', function() { + expect($('#select_all').prop('checked')).toEqual(false); + $('#fileList tr td.filename input:checkbox').click(); + expect($('#select_all').prop('checked')).toEqual(true); + }); + it('Selecting all files on the first visible page will not automatically check "select all" checkbox', function() { + FileList.setFiles(generateFiles(0, 41)); + expect($('#select_all').prop('checked')).toEqual(false); + $('#fileList tr td.filename input:checkbox').click(); + expect($('#select_all').prop('checked')).toEqual(false); + }); + it('Clicking "select all" will select/deselect all files', function() { + FileList.setFiles(generateFiles(0, 41)); + $('#select_all').click(); + expect($('#select_all').prop('checked')).toEqual(true); + $('#fileList tr input:checkbox').each(function() { + expect($(this).prop('checked')).toEqual(true); + }); + expect(_.pluck(FileList.getSelectedFiles(), 'name').length).toEqual(42); + + $('#select_all').click(); + expect($('#select_all').prop('checked')).toEqual(false); + + $('#fileList tr input:checkbox').each(function() { + expect($(this).prop('checked')).toEqual(false); + }); + expect(_.pluck(FileList.getSelectedFiles(), 'name').length).toEqual(0); + }); + it('Clicking "select all" then deselecting a file will uncheck "select all"', function() { + $('#select_all').click(); + expect($('#select_all').prop('checked')).toEqual(true); + + var $tr = FileList.findFileEl('One.txt'); + $tr.find('input:checkbox').click(); + + expect($('#select_all').prop('checked')).toEqual(false); + expect(_.pluck(FileList.getSelectedFiles(), 'name').length).toEqual(3); + }); + it('Updates the selection summary when doing a few manipulations with "Select all"', function() { + $('#select_all').click(); + expect($('#select_all').prop('checked')).toEqual(true); + + var $tr = FileList.findFileEl('One.txt'); + // unselect one + $tr.find('input:checkbox').click(); + + expect($('#select_all').prop('checked')).toEqual(false); + expect(_.pluck(FileList.getSelectedFiles(), 'name').length).toEqual(3); + + // select all + $('#select_all').click(); + expect($('#select_all').prop('checked')).toEqual(true); + expect(_.pluck(FileList.getSelectedFiles(), 'name').length).toEqual(4); + + // unselect one + $tr.find('input:checkbox').click(); + expect($('#select_all').prop('checked')).toEqual(false); + expect(_.pluck(FileList.getSelectedFiles(), 'name').length).toEqual(3); + + // re-select it + $tr.find('input:checkbox').click(); + expect($('#select_all').prop('checked')).toEqual(true); + expect(_.pluck(FileList.getSelectedFiles(), 'name').length).toEqual(4); + }); + it('Auto-selects files on next page when "select all" is checked', function() { + FileList.setFiles(generateFiles(0, 41)); + $('#select_all').click(); + + expect(FileList.$fileList.find('tr input:checkbox:checked').length).toEqual(20); + FileList._nextPage(true); + expect(FileList.$fileList.find('tr input:checkbox:checked').length).toEqual(40); + FileList._nextPage(true); + expect(FileList.$fileList.find('tr input:checkbox:checked').length).toEqual(42); + expect(_.pluck(FileList.getSelectedFiles(), 'name').length).toEqual(42); + }); + it('Selecting files updates selection summary', function() { + var $summary = $('#headerName span.name'); + expect($summary.text()).toEqual('Name'); + FileList.findFileEl('One.txt').find('input:checkbox').click(); + FileList.findFileEl('Three.pdf').find('input:checkbox').click(); + FileList.findFileEl('somedir').find('input:checkbox').click(); + expect($summary.text()).toEqual('1 folder & 2 files'); + }); + it('Unselecting files hides selection summary', function() { + var $summary = $('#headerName span.name'); + FileList.findFileEl('One.txt').find('input:checkbox').click().click(); + expect($summary.text()).toEqual('Name'); + }); + it('Select/deselect files shows/hides file actions', function() { + var $actions = $('#headerName .selectedActions'); + var $checkbox = FileList.findFileEl('One.txt').find('input:checkbox'); + expect($actions.hasClass('hidden')).toEqual(true); + $checkbox.click(); + expect($actions.hasClass('hidden')).toEqual(false); + $checkbox.click(); + expect($actions.hasClass('hidden')).toEqual(true); + }); + it('Selection is cleared when switching dirs', function() { + $('#select_all').click(); + var data = { + status: 'success', + data: { + files: testFiles, + permissions: 31 + } + }; + fakeServer.respondWith(/\/index\.php\/apps\/files\/ajax\/list.php/, [ + 200, { + "Content-Type": "application/json" + }, + JSON.stringify(data) + ]); + FileList.changeDirectory('/'); + fakeServer.respond(); + expect($('#select_all').prop('checked')).toEqual(false); + expect(_.pluck(FileList.getSelectedFiles(), 'name')).toEqual([]); + }); + it('getSelectedFiles returns the selected files even when they are on the next page', function() { + var selectedFiles; + FileList.setFiles(generateFiles(0, 41)); + $('#select_all').click(); + // unselect one to not have the "allFiles" case + FileList.$fileList.find('tr input:checkbox:first').click(); + + // only 20 files visible, must still return all the selected ones + selectedFiles = _.pluck(FileList.getSelectedFiles(), 'name'); + + expect(selectedFiles.length).toEqual(41); + }); + describe('Actions', function() { + beforeEach(function() { + FileList.findFileEl('One.txt').find('input:checkbox').click(); + FileList.findFileEl('Three.pdf').find('input:checkbox').click(); + FileList.findFileEl('somedir').find('input:checkbox').click(); + }); + it('getSelectedFiles returns the selected file data', function() { + var files = FileList.getSelectedFiles(); + expect(files.length).toEqual(3); + expect(files[0]).toEqual({ + id: 1, + name: 'One.txt', + mimetype: 'text/plain', + type: 'file', + size: 12, + etag: 'abc' + }); + expect(files[1]).toEqual({ + id: 3, + type: 'file', + name: 'Three.pdf', + mimetype: 'application/pdf', + size: 58009, + etag: '123' + }); + expect(files[2]).toEqual({ + id: 4, + type: 'dir', + name: 'somedir', + mimetype: 'httpd/unix-directory', + size: 250, + etag: '456' + }); + }); + it('Removing a file removes it from the selection', function() { + FileList.remove('Three.pdf'); + var files = FileList.getSelectedFiles(); + expect(files.length).toEqual(2); + expect(files[0]).toEqual({ + id: 1, + name: 'One.txt', + mimetype: 'text/plain', + type: 'file', + size: 12, + etag: 'abc' + }); + expect(files[1]).toEqual({ + id: 4, + type: 'dir', + name: 'somedir', + mimetype: 'httpd/unix-directory', + size: 250, + etag: '456' + }); + }); + describe('Download', function() { + it('Opens download URL when clicking "Download"', function() { + var redirectStub = sinon.stub(OC, 'redirect'); + $('.selectedActions .download').click(); + expect(redirectStub.calledOnce).toEqual(true); + expect(redirectStub.getCall(0).args[0]).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2Fsubdir&files=%5B%22One.txt%22%2C%22Three.pdf%22%2C%22somedir%22%5D'); + redirectStub.restore(); + }); + it('Downloads root folder when all selected in root folder', function() { + $('#dir').val('/'); + $('#select_all').click(); + var redirectStub = sinon.stub(OC, 'redirect'); + $('.selectedActions .download').click(); + expect(redirectStub.calledOnce).toEqual(true); + expect(redirectStub.getCall(0).args[0]).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2F&files='); + redirectStub.restore(); + }); + it('Downloads parent folder when all selected in subfolder', function() { + $('#select_all').click(); + var redirectStub = sinon.stub(OC, 'redirect'); + $('.selectedActions .download').click(); + expect(redirectStub.calledOnce).toEqual(true); + expect(redirectStub.getCall(0).args[0]).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2F&files=subdir'); + redirectStub.restore(); + }); + }); + describe('Delete', function() { + it('Deletes selected files when "Delete" clicked', function() { + var request; + $('.selectedActions .delete-selected').click(); + expect(fakeServer.requests.length).toEqual(1); + request = fakeServer.requests[0]; + expect(request.url).toEqual(OC.webroot + '/index.php/apps/files/ajax/delete.php'); + expect(OC.parseQueryString(request.requestBody)) + .toEqual({'dir': '/subdir', files: '["One.txt","Three.pdf","somedir"]'}); + fakeServer.requests[0].respond( + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify({status: 'success'}) + ); + expect(FileList.findFileEl('One.txt').length).toEqual(0); + expect(FileList.findFileEl('Three.pdf').length).toEqual(0); + expect(FileList.findFileEl('somedir').length).toEqual(0); + expect(FileList.findFileEl('Two.jpg').length).toEqual(1); + }); + it('Deletes all files when all selected when "Delete" clicked', function() { + var request; + $('#select_all').click(); + $('.selectedActions .delete-selected').click(); + expect(fakeServer.requests.length).toEqual(1); + request = fakeServer.requests[0]; + expect(request.url).toEqual(OC.webroot + '/index.php/apps/files/ajax/delete.php'); + expect(OC.parseQueryString(request.requestBody)) + .toEqual({'dir': '/subdir', allfiles: 'true'}); + fakeServer.requests[0].respond( + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify({status: 'success'}) + ); + expect(FileList.isEmpty).toEqual(true); + }); + }); + }); + }); }); diff --git a/apps/files/tests/js/filesSpec.js b/apps/files/tests/js/filesSpec.js index 018c8ef0f3c..7f8848619f5 100644 --- a/apps/files/tests/js/filesSpec.js +++ b/apps/files/tests/js/filesSpec.js @@ -19,7 +19,7 @@ * */ -/* global Files */ +/* global OC, Files */ describe('Files tests', function() { describe('File name validation', function() { it('Validates correct file names', function() { @@ -82,4 +82,30 @@ describe('Files tests', function() { } }); }); + describe('getDownloadUrl', function() { + var curDirStub; + beforeEach(function() { + curDirStub = sinon.stub(FileList, 'getCurrentDirectory'); + }); + afterEach(function() { + curDirStub.restore(); + }); + it('returns the ajax download URL when only filename specified', function() { + curDirStub.returns('/subdir'); + var url = Files.getDownloadUrl('test file.txt'); + expect(url).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2Fsubdir&files=test%20file.txt'); + }); + it('returns the ajax download URL when filename and dir specified', function() { + var url = Files.getDownloadUrl('test file.txt', '/subdir'); + expect(url).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2Fsubdir&files=test%20file.txt'); + }); + it('returns the ajax download URL when filename and root dir specific', function() { + var url = Files.getDownloadUrl('test file.txt', '/'); + expect(url).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2F&files=test%20file.txt'); + }); + it('returns the ajax download URL when multiple files specified', function() { + var url = Files.getDownloadUrl(['test file.txt', 'abc.txt'], '/subdir'); + expect(url).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2Fsubdir&files=%5B%22test%20file.txt%22%2C%22abc.txt%22%5D'); + }); + }); }); diff --git a/apps/files/tests/js/filesummarySpec.js b/apps/files/tests/js/filesummarySpec.js new file mode 100644 index 00000000000..c493700de38 --- /dev/null +++ b/apps/files/tests/js/filesummarySpec.js @@ -0,0 +1,87 @@ +/** +* ownCloud +* +* @author Vincent Petry +* @copyright 2014 Vincent Petry <pvince81@owncloud.com> +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE +* License as published by the Free Software Foundation; either +* version 3 of the License, or any later version. +* +* This library is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU AFFERO GENERAL PUBLIC LICENSE for more details. +* +* You should have received a copy of the GNU Affero General Public +* License along with this library. If not, see <http://www.gnu.org/licenses/>. +* +*/ + +/* global FileSummary */ +describe('FileSummary tests', function() { + var $container; + + beforeEach(function() { + $container = $('<table><tr></tr></table>').find('tr'); + }); + afterEach(function() { + $container = null; + }); + + it('renders summary as text', function() { + var s = new FileSummary($container); + s.setSummary({ + totalDirs: 5, + totalFiles: 2, + totalSize: 256000 + }); + expect($container.hasClass('hidden')).toEqual(false); + expect($container.find('.info').text()).toEqual('5 folders and 2 files'); + expect($container.find('.filesize').text()).toEqual('250 kB'); + }); + it('hides summary when no files or folders', function() { + var s = new FileSummary($container); + s.setSummary({ + totalDirs: 0, + totalFiles: 0, + totalSize: 0 + }); + expect($container.hasClass('hidden')).toEqual(true); + }); + it('increases summary when adding files', function() { + var s = new FileSummary($container); + s.setSummary({ + totalDirs: 5, + totalFiles: 2, + totalSize: 256000 + }); + s.add({type: 'file', size: 256000}); + s.add({type: 'dir', size: 100}); + s.update(); + expect($container.hasClass('hidden')).toEqual(false); + expect($container.find('.info').text()).toEqual('6 folders and 3 files'); + expect($container.find('.filesize').text()).toEqual('500 kB'); + expect(s.summary.totalDirs).toEqual(6); + expect(s.summary.totalFiles).toEqual(3); + expect(s.summary.totalSize).toEqual(512100); + }); + it('decreases summary when removing files', function() { + var s = new FileSummary($container); + s.setSummary({ + totalDirs: 5, + totalFiles: 2, + totalSize: 256000 + }); + s.remove({type: 'file', size: 128000}); + s.remove({type: 'dir', size: 100}); + s.update(); + expect($container.hasClass('hidden')).toEqual(false); + expect($container.find('.info').text()).toEqual('4 folders and 1 file'); + expect($container.find('.filesize').text()).toEqual('125 kB'); + expect(s.summary.totalDirs).toEqual(4); + expect(s.summary.totalFiles).toEqual(1); + expect(s.summary.totalSize).toEqual(127900); + }); +}); diff --git a/apps/files_sharing/public.php b/apps/files_sharing/public.php index ce51eca6ddb..3abcbf291ff 100644 --- a/apps/files_sharing/public.php +++ b/apps/files_sharing/public.php @@ -138,6 +138,7 @@ if (isset($path)) { OCP\Util::addStyle('files', 'files'); OCP\Util::addStyle('files', 'upload'); + OCP\Util::addScript('files', 'filesummary'); OCP\Util::addScript('files', 'breadcrumb'); OCP\Util::addScript('files', 'files'); OCP\Util::addScript('files', 'filelist'); diff --git a/apps/files_trashbin/index.php b/apps/files_trashbin/index.php index e63fe1e4188..6e6a8a38307 100644 --- a/apps/files_trashbin/index.php +++ b/apps/files_trashbin/index.php @@ -11,6 +11,7 @@ $tmpl = new OCP\Template('files_trashbin', 'index', 'user'); OCP\Util::addStyle('files', 'files'); OCP\Util::addStyle('files_trashbin', 'trash'); +OCP\Util::addScript('files', 'filesummary'); OCP\Util::addScript('files', 'breadcrumb'); OCP\Util::addScript('files', 'filelist'); // filelist overrides diff --git a/apps/files_trashbin/js/filelist.js b/apps/files_trashbin/js/filelist.js index 7795daf2775..3bb3a92b60d 100644 --- a/apps/files_trashbin/js/filelist.js +++ b/apps/files_trashbin/js/filelist.js @@ -49,8 +49,8 @@ } }; - var oldAdd = FileList.add; - FileList.add = function(fileData, options) { + var oldRenderRow = FileList._renderRow; + FileList._renderRow = function(fileData, options) { options = options || {}; var dir = FileList.getCurrentDirectory(); var dirListing = dir !== '' && dir !== '/'; @@ -62,7 +62,7 @@ fileData.displayName = fileData.name; fileData.name = fileData.name + '.d' + Math.floor(fileData.mtime / 1000); } - return oldAdd.call(this, fileData, options); + return oldRenderRow.call(this, fileData, options); }; FileList.linkTo = function(dir){ @@ -75,4 +75,130 @@ $('#emptycontent').toggleClass('hidden', exists); $('#filestable th').toggleClass('hidden', !exists); }; + + var oldInit = FileList.initialize; + FileList.initialize = function() { + var result = oldInit.apply(this, arguments); + $('.undelete').click('click', FileList._onClickRestoreSelected); + return result; + }; + + FileList._removeCallback = function(result) { + if (result.status !== 'success') { + OC.dialogs.alert(result.data.message, t('files_trashbin', 'Error')); + } + + var files = result.data.success; + var $el; + for (var i = 0; i < files.length; i++) { + $el = FileList.remove(OC.basename(files[i].filename), {updateSummary: false}); + FileList.fileSummary.remove({type: $el.attr('data-type'), size: $el.attr('data-size')}); + } + FileList.fileSummary.update(); + FileList.updateEmptyContent(); + enableActions(); + } + + FileList._onClickRestoreSelected = function(event) { + event.preventDefault(); + var allFiles = $('#select_all').is(':checked'); + var files = []; + var params = {}; + disableActions(); + if (allFiles) { + FileList.showMask(); + params = { + allfiles: true, + dir: FileList.getCurrentDirectory() + }; + } + else { + files = _.pluck(FileList.getSelectedFiles(), 'name'); + for (var i = 0; i < files.length; i++) { + var deleteAction = FileList.findFileEl(files[i]).children("td.date").children(".action.delete"); + deleteAction.removeClass('delete-icon').addClass('progress-icon'); + } + params = { + files: JSON.stringify(files), + dir: FileList.getCurrentDirectory() + }; + } + + $.post(OC.filePath('files_trashbin', 'ajax', 'undelete.php'), + params, + function(result) { + if (allFiles) { + if (result.status !== 'success') { + OC.dialogs.alert(result.data.message, t('files_trashbin', 'Error')); + } + FileList.hideMask(); + // simply remove all files + FileList.setFiles([]); + enableActions(); + } + else { + FileList._removeCallback(result); + } + } + ); + }; + + FileList._onClickDeleteSelected = function(event) { + event.preventDefault(); + var allFiles = $('#select_all').is(':checked'); + var files = []; + var params = {}; + if (allFiles) { + params = { + allfiles: true, + dir: FileList.getCurrentDirectory() + }; + } + else { + files = _.pluck(FileList.getSelectedFiles(), 'name'); + params = { + files: JSON.stringify(files), + dir: FileList.getCurrentDirectory() + }; + } + + disableActions(); + if (allFiles) { + FileList.showMask(); + } + else { + for (var i = 0; i < files.length; i++) { + var deleteAction = FileList.findFileEl(files[i]).children("td.date").children(".action.delete"); + deleteAction.removeClass('delete-icon').addClass('progress-icon'); + } + } + + $.post(OC.filePath('files_trashbin', 'ajax', 'delete.php'), + params, + function(result) { + if (allFiles) { + if (result.status !== 'success') { + OC.dialogs.alert(result.data.message, t('files_trashbin', 'Error')); + } + FileList.hideMask(); + // simply remove all files + FileList.setFiles([]); + enableActions(); + } + else { + FileList._removeCallback(result); + } + } + ); + }; + + var oldClickFile = FileList._onClickFile; + FileList._onClickFile = function(event) { + var mime = $(this).parent().parent().data('mime'); + if (mime !== 'httpd/unix-directory') { + event.preventDefault(); + } + return oldClickFile.apply(this, arguments); + }; + })(); diff --git a/apps/files_trashbin/js/trash.js b/apps/files_trashbin/js/trash.js index f7724d07d2b..5f2436de809 100644 --- a/apps/files_trashbin/js/trash.js +++ b/apps/files_trashbin/js/trash.js @@ -28,20 +28,6 @@ $(document).ready(function() { return name; } - function removeCallback(result) { - if (result.status !== 'success') { - OC.dialogs.alert(result.data.message, t('files_trashbin', 'Error')); - } - - var files = result.data.success; - for (var i = 0; i < files.length; i++) { - FileList.remove(OC.basename(files[i].filename), {updateSummary: false}); - } - FileList.updateFileSummary(); - FileList.updateEmptyContent(); - enableActions(); - } - Files.updateStorageStatistics = function() { // no op because the trashbin doesn't have // storage info like free space / used space @@ -57,7 +43,7 @@ $(document).ready(function() { files: JSON.stringify([filename]), dir: FileList.getCurrentDirectory() }, - removeCallback + FileList._removeCallback ); }, t('files_trashbin', 'Restore')); }; @@ -74,153 +60,10 @@ $(document).ready(function() { files: JSON.stringify([filename]), dir: FileList.getCurrentDirectory() }, - removeCallback + FileList._removeCallback ); }); - // Sets the select_all checkbox behaviour : - $('#select_all').click(function() { - if ($(this).attr('checked')) { - // Check all - $('td.filename input:checkbox').attr('checked', true); - $('td.filename input:checkbox').parent().parent().addClass('selected'); - } else { - // Uncheck all - $('td.filename input:checkbox').attr('checked', false); - $('td.filename input:checkbox').parent().parent().removeClass('selected'); - } - procesSelection(); - }); - $('.undelete').click('click', function(event) { - event.preventDefault(); - var allFiles = $('#select_all').is(':checked'); - var files = []; - var params = {}; - disableActions(); - if (allFiles) { - FileList.showMask(); - params = { - allfiles: true, - dir: FileList.getCurrentDirectory() - }; - } - else { - files = Files.getSelectedFiles('name'); - for (var i = 0; i < files.length; i++) { - var deleteAction = FileList.findFileEl(files[i]).children("td.date").children(".action.delete"); - deleteAction.removeClass('delete-icon').addClass('progress-icon'); - } - params = { - files: JSON.stringify(files), - dir: FileList.getCurrentDirectory() - }; - } - - $.post(OC.filePath('files_trashbin', 'ajax', 'undelete.php'), - params, - function(result) { - if (allFiles) { - if (result.status !== 'success') { - OC.dialogs.alert(result.data.message, t('files_trashbin', 'Error')); - } - FileList.hideMask(); - // simply remove all files - FileList.update(''); - enableActions(); - } - else { - removeCallback(result); - } - } - ); - }); - - $('.delete').click('click', function(event) { - event.preventDefault(); - var allFiles = $('#select_all').is(':checked'); - var files = []; - var params = {}; - if (allFiles) { - params = { - allfiles: true, - dir: FileList.getCurrentDirectory() - }; - } - else { - files = Files.getSelectedFiles('name'); - params = { - files: JSON.stringify(files), - dir: FileList.getCurrentDirectory() - }; - } - - disableActions(); - if (allFiles) { - FileList.showMask(); - } - else { - for (var i = 0; i < files.length; i++) { - var deleteAction = FileList.findFileEl(files[i]).children("td.date").children(".action.delete"); - deleteAction.removeClass('delete-icon').addClass('progress-icon'); - } - } - - $.post(OC.filePath('files_trashbin', 'ajax', 'delete.php'), - params, - function(result) { - if (allFiles) { - if (result.status !== 'success') { - OC.dialogs.alert(result.data.message, t('files_trashbin', 'Error')); - } - FileList.hideMask(); - // simply remove all files - FileList.setFiles([]); - enableActions(); - } - else { - removeCallback(result); - } - } - ); - - }); - - $('#fileList').on('click', 'td.filename input', function() { - var checkbox = $(this).parent().children('input:checkbox'); - $(checkbox).parent().parent().toggleClass('selected'); - if ($(checkbox).is(':checked')) { - var selectedCount = $('td.filename input:checkbox:checked').length; - if (selectedCount === $('td.filename input:checkbox').length) { - $('#select_all').prop('checked', true); - } - } else { - $('#select_all').prop('checked',false); - } - procesSelection(); - }); - - $('#fileList').on('click', 'td.filename a', function(event) { - var mime = $(this).parent().parent().data('mime'); - if (mime !== 'httpd/unix-directory') { - event.preventDefault(); - } - var filename = $(this).parent().parent().attr('data-file'); - var tr = FileList.findFileEl(filename); - var renaming = tr.data('renaming'); - if(!renaming){ - if(mime.substr(0, 5) === 'text/'){ //no texteditor for now - return; - } - var type = $(this).parent().parent().data('type'); - var permissions = $(this).parent().parent().data('permissions'); - var action = FileActions.getDefault(mime, type, permissions); - if(action){ - event.preventDefault(); - action(filename); - } - } - }); - /** * Override crumb URL maker (hacky!) */ diff --git a/apps/files_trashbin/templates/index.php b/apps/files_trashbin/templates/index.php index b6c61c9b1c3..323e7495535 100644 --- a/apps/files_trashbin/templates/index.php +++ b/apps/files_trashbin/templates/index.php @@ -29,7 +29,7 @@ <th id="headerDate"> <span id="modified"><?php p($l->t( 'Deleted' )); ?></span> <span class="selectedActions"> - <a href="" class="delete"> + <a href="" class="delete-selected"> <?php p($l->t('Delete'))?> <img class="svg" alt="<?php p($l->t('Delete'))?>" src="<?php print_unescaped(OCP\image_path("core", "actions/delete.svg")); ?>" /> @@ -40,4 +40,6 @@ </thead> <tbody id="fileList"> </tbody> + <tfoot> + </tfoot> </table> diff --git a/core/css/apps.css b/core/css/apps.css index a8dfc5b7ed1..a0bb262854d 100644 --- a/core/css/apps.css +++ b/core/css/apps.css @@ -243,7 +243,6 @@ button.loading { padding-right: 30px; } - /* general styles for the content area */ .section { display: block; @@ -264,3 +263,14 @@ button.loading { vertical-align: -2px; margin-right: 4px; } +.appear { + opacity: 1; + transition: opacity 500ms ease 0s; + -moz-transition: opacity 500ms ease 0s; + -ms-transition: opacity 500ms ease 0s; + -o-transition: opacity 500ms ease 0s; + -webkit-transition: opacity 500ms ease 0s; +} +.appear.transparent { + opacity: 0; +} diff --git a/core/js/js.js b/core/js/js.js index 0aa8d12b3d6..27bc3c651e3 100644 --- a/core/js/js.js +++ b/core/js/js.js @@ -1250,9 +1250,12 @@ function relative_modified_date(timestamp) { } /** - * @todo Write documentation + * Utility functions */ OC.Util = { + // TODO: remove original functions from global namespace + humanFileSize: humanFileSize, + formatDate: formatDate, /** * Returns whether the browser supports SVG * @return {boolean} true if the browser supports SVG, false otherwise diff --git a/core/js/tests/specs/coreSpec.js b/core/js/tests/specs/coreSpec.js index ccd9f7a1288..65f768fbc51 100644 --- a/core/js/tests/specs/coreSpec.js +++ b/core/js/tests/specs/coreSpec.js @@ -474,5 +474,22 @@ describe('Core base tests', function() { ); }); }); + describe('Util', function() { + describe('humanFileSize', function() { + it('renders file sizes with the correct unit', function() { + var data = [ + [0, '0 B'], + [125, '125 B'], + [128000, '125 kB'], + [128000000, '122.1 MB'], + [128000000000, '119.2 GB'], + [128000000000000, '116.4 TB'] + ]; + for (var i = 0; i < data.length; i++) { + expect(OC.Util.humanFileSize(data[i][0])).toEqual(data[i][1]); + } + }); + }); + }); }); |