From efdf0c4df0012fe03fb3de136ff596af1bb38677 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Tue, 11 Feb 2014 15:57:45 +0100 Subject: [PATCH 01/14] Added infinite scrolling (in-memory list) --- apps/files/js/filelist.js | 65 ++++++++++++++++++++++++++++++++++++--- core/css/apps.css | 12 +++++++- 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index c33b638b5a6..21ce1418210 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -19,6 +19,12 @@ window.FileList = { breadcrumb: null, initialized: false, + // number of files per page + pageSize: 20, + // zero based page number + pageNumber: 0, + totalPages: 0, + /** * Initialize the file list and its components */ @@ -62,6 +68,16 @@ window.FileList = { } }, + _onScroll: function(e) { + if (this.pageNumber + 1 >= this.totalPages) { + return; + } + var target = $(document); + if ($(window).scrollTop() + $(window).height() > $(document).height() - 20) { + this._nextPage(true); + } + }, + /** * Event handler when dropping on a breadcrumb */ @@ -129,6 +145,42 @@ window.FileList = { // use filterAttr to avoid escaping issues return this.$fileList.find('tr').filterAttr('data-file', fileName); }, + + /** + * Appends the next page of files into the table + * @param animate true to animate the new elements + */ + _nextPage: function(animate) { + var tr, index, count = this.pageSize, + newTrs = []; + + if (this.pageNumber + 1 >= this.totalPages) { + return; + } + + this.pageNumber++; + index = this.pageNumber * this.pageSize; + + while (count > 0 && index < this.files.length) { + tr = this.add(this.files[index], {updateSummary: false}); + if (animate) { + tr.addClass('appear transparent'); // TODO + 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. @@ -136,14 +188,15 @@ window.FileList = { */ setFiles:function(filesArray) { // detach to make adding multiple rows faster - this.$fileList.detach(); + this.files = filesArray; + this.pageNumber = -1; + this.totalPages = Math.ceil(filesArray.length / this.pageSize); + 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}); - } + this.isEmpty = this.files.length === 0; + this._nextPage(); this.$el.find('thead').after(this.$fileList); @@ -1255,6 +1308,8 @@ $(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() { 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; +} From 9f62059efa869ff677130f06bf1b46be49950515 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Tue, 11 Feb 2014 16:52:56 +0100 Subject: [PATCH 02/14] Fix file summary to use the whole file list - moved the summary code into a new class FileSummary - FileSummary is calculated only once, then updated with add/remove - added new OC.Util namespace for JS utility functions --- apps/files/index.php | 1 + apps/files/js/filelist.js | 136 ++++-------------- apps/files/js/filesummary.js | 176 ++++++++++++++++++++++++ apps/files/templates/index.php | 2 + apps/files/tests/js/filelistSpec.js | 48 +++---- apps/files/tests/js/filesummarySpec.js | 87 ++++++++++++ apps/files_sharing/public.php | 1 + apps/files_trashbin/index.php | 1 + apps/files_trashbin/js/trash.js | 6 +- apps/files_trashbin/templates/index.php | 2 + core/js/js.js | 3 + core/js/tests/specs/coreSpec.js | 17 +++ 12 files changed, 342 insertions(+), 138 deletions(-) create mode 100644 apps/files/js/filesummary.js create mode 100644 apps/files/tests/js/filesummarySpec.js 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/filelist.js b/apps/files/js/filelist.js index 21ce1418210..223f4bb4409 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -8,7 +8,7 @@ * */ -/* global OC, t, n, FileList, FileActions, Files, BreadCrumb */ +/* global OC, t, n, FileList, FileActions, Files, FileSummary, BreadCrumb */ /* global procesSelection, dragOptions, folderDropOptions */ window.FileList = { appName: t('files', 'Files'), @@ -17,6 +17,11 @@ window.FileList = { $el: $('#filestable'), $fileList: $('#fileList'), breadcrumb: null, + + /** + * Instance of FileSummary + */ + fileSummary: null, initialized: false, // number of files per page @@ -38,6 +43,8 @@ window.FileList = { this.$el = $('#filestable'); this.$fileList = $('#fileList'); + this.fileSummary = this._createSummary(); + this.breadcrumb = new BreadCrumb({ onClick: this._onClickBreadCrumb, onDrop: this._onDropOnBreadCrumb, @@ -72,7 +79,6 @@ window.FileList = { if (this.pageNumber + 1 >= this.totalPages) { return; } - var target = $(document); if ($(window).scrollTop() + $(window).height() > $(document).height() - 20) { this._nextPage(true); } @@ -206,7 +212,9 @@ window.FileList = { if (window.Files) { Files.setupDragAndDrop(); } - this.updateFileSummary(); + + this.fileSummary.calculate(filesArray); + procesSelection(); $(window).scrollTop(0); @@ -404,7 +412,7 @@ window.FileList = { // defaults to true if not defined if (typeof(options.updateSummary) === 'undefined' || !!options.updateSummary) { - this.updateFileSummary(); + this.fileSummary.add(fileData, true); this.updateEmptyContent(); } return tr; @@ -582,10 +590,10 @@ window.FileList = { } fileEl.remove(); // TODO: improve performance on batch update - FileList.isEmpty = !this.$fileList.find('tr:not(.summary)').length; + FileList.isEmpty = !this.$fileList.find('tr').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); } return fileEl; }, @@ -621,7 +629,6 @@ window.FileList = { } FileList.isEmpty = false; FileList.updateEmptyContent(); - FileList.updateFileSummary(); }, rename: function(oldname) { var tr, td, input, form; @@ -825,12 +832,13 @@ window.FileList = { var fileEl = FileList.remove(file, {updateSummary: false}); 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(); Files.updateStorageStatistics(); } else { if (result.status === 'error' && result.data.message) { @@ -857,108 +865,14 @@ window.FileList = { } }); }, - createFileSummary: function() { - if ( !FileList.isEmpty ) { - var summary = this._calculateFileSummary(); + /** + * Creates the file summary section + */ + _createSummary: function() { + var $tr = $(''); + this.$el.find('tfoot').append($tr); - // 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: ''+directoryInfo+'', - files: ''+fileInfo+'' - }; - - 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 = ''+humanFileSize(summary.totalSize)+''; - } - - var $summary = $(''+info+''+fileSize+''); - this.$fileList.append($summary); - - var $dirInfo = $summary.find('.dirinfo'); - var $fileInfo = $summary.find('.fileinfo'); - var $connector = $summary.find('.connector'); - - // 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(); @@ -1009,7 +923,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 { @@ -1315,7 +1229,5 @@ $(document).ready(function() { setTimeout(function() { FileList.changeDirectory(dir, false, true); }, 0); - - FileList.createFileSummary(); }); diff --git a/apps/files/js/filesummary.js b/apps/files/js/filesummary.js new file mode 100644 index 00000000000..bbe4d43ba49 --- /dev/null +++ b/apps/files/js/filesummary.js @@ -0,0 +1,176 @@ +/** +* ownCloud +* +* @author Vincent Petry +* @copyright 2014 Vincent Petry +* +* 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 . +* +*/ + +/* 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, summary) { + this.$el = $tr; + 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(); + } + }, + /** + * 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); + }, + /** + * 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.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() { + 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 fileSize; + + var infoVars = { + dirs: ''+directoryInfo+'', + files: ''+fileInfo+'' + }; + + // don't show the filesize column, if filesize is NaN (e.g. in trashbin) + var fileSize = ''; + if (!isNaN(summary.totalSize)) { + fileSize = '' + OC.Util.humanFileSize(summary.totalSize) + ''; + } + + var info = t('files', '{dirs} and {files}', infoVars); + + var $summary = $(''+info+''+fileSize+''); + + 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 @@ + +
diff --git a/apps/files/tests/js/filelistSpec.js b/apps/files/tests/js/filelistSpec.js index ca85a360cf5..6e80d78eee0 100644 --- a/apps/files/tests/js/filelistSpec.js +++ b/apps/files/tests/js/filelistSpec.js @@ -51,6 +51,7 @@ describe('FileList tests', function() { '' + '' + '' + + '' + '
' + '
Empty content message
' ); @@ -220,7 +221,7 @@ 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 fileData = { type: 'file', name: 'P comes after O.txt' @@ -228,7 +229,6 @@ describe('FileList tests', function() { 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() { var fileData = { @@ -249,8 +249,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 +268,11 @@ 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.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 +282,11 @@ 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.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 +318,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,10 +342,10 @@ 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($('#filestable thead th').hasClass('hidden')).toEqual(true); expect($('#emptycontent').hasClass('hidden')).toEqual(false); @@ -363,7 +363,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); }); @@ -459,14 +459,14 @@ describe('FileList tests', function() { var addSpy = sinon.spy(FileList, 'add'); FileList.setFiles(testFiles); expect(addSpy.callCount).toEqual(4); - expect($('#fileList tr:not(.summary)').length).toEqual(4); + expect($('#fileList tr').length).toEqual(4); addSpy.restore(); }); 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 +474,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); @@ -642,7 +642,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() { 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 +* +* 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 . +* +*/ + +/* global FileSummary */ +describe('FileSummary tests', function() { + var $container; + + beforeEach(function() { + $container = $('
').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/trash.js b/apps/files_trashbin/js/trash.js index f7724d07d2b..4ed5ba1c76e 100644 --- a/apps/files_trashbin/js/trash.js +++ b/apps/files_trashbin/js/trash.js @@ -34,10 +34,12 @@ $(document).ready(function() { } var files = result.data.success; + var $el; for (var i = 0; i < files.length; i++) { - FileList.remove(OC.basename(files[i].filename), {updateSummary: false}); + $el = FileList.remove(OC.basename(files[i].filename), {updateSummary: false}); + FileList.fileSummary.remove({type: $el.attr('data-type'), size: $el.attr('data-size')}); } - FileList.updateFileSummary(); + FileList.fileSummary.update(); FileList.updateEmptyContent(); enableActions(); } diff --git a/apps/files_trashbin/templates/index.php b/apps/files_trashbin/templates/index.php index b6c61c9b1c3..cb64ae9eafa 100644 --- a/apps/files_trashbin/templates/index.php +++ b/apps/files_trashbin/templates/index.php @@ -40,4 +40,6 @@ + + diff --git a/core/js/js.js b/core/js/js.js index 0aa8d12b3d6..325be6cdc53 100644 --- a/core/js/js.js +++ b/core/js/js.js @@ -1253,6 +1253,9 @@ function relative_modified_date(timestamp) { * @todo Write documentation */ 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]); + } + }); + }); + }); }); From 9c2fbea6a4396a29ce8c966c9ea7646aa8fc9be5 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Wed, 12 Feb 2014 14:50:23 +0100 Subject: [PATCH 03/14] Fix file selection for infinite scrolling - moved file selection code to FileList - fix selection summary when all files are selected - nextPage now auto-selects files if "select all" checkbox is checked - fixed trashbin to use the same selection logic as FileList --- apps/files/css/files.css | 1 - apps/files/js/filelist.js | 266 ++++++++++++++++++++++++++-- apps/files/js/files.js | 202 +-------------------- apps/files/tests/js/filelistSpec.js | 217 ++++++++++++++++++++++- apps/files/tests/js/filesSpec.js | 28 ++- apps/files_trashbin/js/filelist.js | 126 +++++++++++++ apps/files_trashbin/js/trash.js | 163 +---------------- core/js/js.js | 2 +- 8 files changed, 626 insertions(+), 379 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/js/filelist.js b/apps/files/js/filelist.js index 223f4bb4409..02754d7acbb 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -9,7 +9,7 @@ */ /* global OC, t, n, FileList, FileActions, Files, FileSummary, BreadCrumb */ -/* global procesSelection, dragOptions, folderDropOptions */ +/* global dragOptions, folderDropOptions */ window.FileList = { appName: t('files', 'Files'), isEmpty: true, @@ -60,6 +60,142 @@ window.FileList = { var width = $(this).width(); FileList.breadcrumb.resize(width, false); }); + + this.$fileList.on('click','td.filename a', this._onClickFile); + this.$fileList.on('change', 'td.filename input:checkbox', this._onClickFileCheckbox); + this.$el.find('#select_all').click(this._onClickSelectAll); + this.$el.find('.download').click(this._onClickDownloadSelected); + this.$el.find('.delete-selected').click(this._onClickDeleteSelected); + }, + + /** + * Event handler for when clicking on files to select them + */ + _onClickFile: function(event) { + if (event.ctrlKey || event.shiftKey) { + event.preventDefault(); + if (event.shiftKey) { + var last = $(FileList._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'); + FileList._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'); + } + } + FileList.updateSelectionSummary(); + } 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); + } + } + } + + }, + + /** + * Event handler for when clicking on a file's checkbox + */ + _onClickFileCheckbox: function(event) { + // FIXME: not sure what the difference is supposed to be with FileList._onClickFile + if (event.shiftKey) { + var last = $(FileList._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); + } + } + FileList.updateSelectionSummary(); + }, + + /** + * Event handler for when selecting/deselecting all files + */ + _onClickSelectAll: function(e) { + var checked = $(this).prop('checked'); + FileList.$fileList.find('td.filename input:checkbox').prop('checked', checked) + .parent().parent().toggleClass('selected', checked); + FileList.updateSelectionSummary(); + }, + + /** + * Event handler for when clicking on "Download" for the selected files + */ + _onClickDownloadSelected: function(event) { + var files; + var dir = FileList.getCurrentDirectory(); + if (FileList.isAllSelected()) { + files = OC.basename(dir); + dir = OC.dirname(dir) || '/'; + } + else { + files = FileList.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 = FileList.getSelectedFiles('name'); + } + FileList.do_delete(files); + event.preventDefault(); + return false; }, /** @@ -79,7 +215,7 @@ window.FileList = { if (this.pageNumber + 1 >= this.totalPages) { return; } - if ($(window).scrollTop() + $(window).height() > $(document).height() - 20) { + if ($(window).scrollTop() + $(window).height() > $(document).height() - 500) { this._nextPage(true); } }, @@ -113,7 +249,7 @@ window.FileList = { if (result) { if (result.status === 'success') { FileList.remove(file); - procesSelection(); + FileList.updateSelectionSummary(); $('#notification').hide(); } else { $('#notification').hide(); @@ -152,13 +288,30 @@ window.FileList = { 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){ + 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 tr, index, count = this.pageSize, - newTrs = []; + newTrs = [], + selected = this.isAllSelected(); if (this.pageNumber + 1 >= this.totalPages) { return; @@ -169,6 +322,10 @@ window.FileList = { while (count > 0 && index < this.files.length) { tr = this.add(this.files[index], {updateSummary: false}); + if (selected) { + tr.addClass('selected'); + tr.find('input:checkbox').prop('checked', true); + } if (animate) { tr.addClass('appear transparent'); // TODO newTrs.push(tr); @@ -201,6 +358,9 @@ window.FileList = { this.$fileList.detach(); this.$fileList.empty(); + // clear "Select all" checkbox + $('#select_all').prop('checked', false); + this.isEmpty = this.files.length === 0; this._nextPage(); @@ -215,7 +375,7 @@ window.FileList = { this.fileSummary.calculate(filesArray); - procesSelection(); + FileList.updateSelectionSummary(); $(window).scrollTop(0); this.$fileList.trigger(jQuery.Event("updated")); @@ -580,10 +740,14 @@ 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){ options = options || {}; var fileEl = FileList.findFileEl(name); + if (!fileEl.length) { + return null; + } if (fileEl.data('permissions') & OC.PERMISSION_DELETE) { // file is only draggable when delete permissions are set fileEl.find('td.filename').draggable('destroy'); @@ -824,21 +988,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.updateEmptyContent(); FileList.fileSummary.update(); + FileList.updateSelectionSummary(); Files.updateStorageStatistics(); } else { if (result.status === 'error' && result.data.message) { @@ -941,14 +1106,95 @@ window.FileList = { $(e).removeClass("searchresult"); }); }, + /** + * Update UI based on the current selection + */ + updateSelectionSummary: function() { + var allSelected = this.isAllSelected(); + var selected; + var summary = { + totalFiles: 0, + totalDirs: 0, + totalSize: 0 + }; + + if (allSelected) { + summary = this.fileSummary.summary; + } + else { + selected = this.getSelectedFiles(); + for (var i = 0; i < selected.length; i++ ){ + if (selected[i].type === 'dir') { + summary.totalDirs++; + } + else { + summary.totalFiles++; + } + summary.totalSize += parseInt(selected[i].size, 10) || 0; + } + } + 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'); + $('#select_all').removeAttr('checked'); + } + else { + $('.selectedActions').removeClass('hidden'); + $('#headerSize').text(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'); + }, + + /** + * @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 + */ + getSelectedFiles: function(property) { + var elements=$('td.filename input:checkbox:checked').parent().parent(); + var files=[]; + elements.each(function(i,element) { + // TODO: make the json format the same as in FileList.add() + var file = FileList.elementToFile($(element)); + // FIXME: legacy attributes + file.origin = file.id; + file.mime = file.mimetype; + if (property) { + files.push(file[property]); + } else { + files.push(file); + } + }); + return files; } -}; +} $(document).ready(function() { FileList.initialize(); diff --git a/apps/files/js/files.js b/apps/files/js/files.js index 5e669a796a9..6cb0d41a611 100644 --- a/apps/files/js/files.js +++ b/apps/files/js/files.js @@ -209,7 +209,7 @@ $(document).ready(function() { // Trigger cancelling of file upload $('#uploadprogresswrapper .stop').on('click', function() { OC.Upload.cancelUploads(); - procesSelection(); + FileList.updateSelectionSummary(); }); // Show trash bin @@ -217,130 +217,6 @@ $(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) { @@ -440,7 +316,7 @@ var createDragShadow = function(event) { $(event.target).parents('tr').find('td input:first').prop('checked',true); } - var selectedFiles = Files.getSelectedFiles(); + var selectedFiles = FileList.getSelectedFiles(); if (!isDragSelected && selectedFiles.length === 1) { //revert the selection @@ -539,7 +415,7 @@ var folderDropOptions={ oldFile.find('td.filesize').text(humanFileSize(newSize)); FileList.remove(file); - procesSelection(); + FileList.updateSelectionSummary(); $('#notification').hide(); } else { $('#notification').hide(); @@ -556,78 +432,6 @@ var folderDropOptions={ tolerance: 'pointer' }; -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 0) { - selection += n('files', '%n folder', '%n folders', selectedFolders.length); - if (selectedFiles.length > 0) { - selection += ' & '; - } - } - 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; -} - Files.getMimeIcon = function(mime, ready) { if (Files.getMimeIcon.cache[mime]) { ready(Files.getMimeIcon.cache[mime]); diff --git a/apps/files/tests/js/filelistSpec.js b/apps/files/tests/js/filelistSpec.js index 6e80d78eee0..93e7c81cb1f 100644 --- a/apps/files/tests/js/filelistSpec.js +++ b/apps/files/tests/js/filelistSpec.js @@ -48,8 +48,15 @@ describe('FileList tests', function() { '
' + '
' + // dummy table + // TODO: at some point this will be rendered by the FileList class itself! '' + - '' + + '' + '' + '' + '
' + @@ -61,25 +68,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(); @@ -380,7 +391,7 @@ describe('FileList tests', function() { $input.val('One_renamed.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'}); @@ -519,6 +530,16 @@ 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('file previews', function() { var previewLoadStub; @@ -811,4 +832,188 @@ 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('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('Clicking "select all" will select/deselect all files', function() { + $('#select_all').click(); + expect($('#select_all').prop('checked')).toEqual(true); + $('#fileList tr input:checkbox').each(function() { + expect($(this).prop('checked')).toEqual(true); + }); + + $('#select_all').click(); + expect($('#select_all').prop('checked')).toEqual(false); + + $('#fileList tr input:checkbox').each(function() { + expect($(this).prop('checked')).toEqual(false); + }); + }); + 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); + }); + 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); + }); + 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 files', function() { + var files = FileList.getSelectedFiles(); + expect(files.length).toEqual(3); + expect(files[0]).toEqual({ + id: 1, + name: 'One.txt', + mime: 'text/plain', + mimetype: 'text/plain', + type: 'file', + size: 12, + etag: 'abc', + origin: 1 + }); + expect(files[1]).toEqual({ + id: 3, + type: 'file', + name: 'Three.pdf', + mime: 'application/pdf', + mimetype: 'application/pdf', + size: 58009, + etag: '123', + origin: 3 + }); + expect(files[2]).toEqual({ + id: 4, + type: 'dir', + name: 'somedir', + mime: 'httpd/unix-directory', + mimetype: 'httpd/unix-directory', + size: 250, + etag: '456', + origin: 4 + }); + }); + 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_trashbin/js/filelist.js b/apps/files_trashbin/js/filelist.js index 7795daf2775..6c9b345086a 100644 --- a/apps/files_trashbin/js/filelist.js +++ b/apps/files_trashbin/js/filelist.js @@ -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 = 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.update(''); + 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 = 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 4ed5ba1c76e..5f2436de809 100644 --- a/apps/files_trashbin/js/trash.js +++ b/apps/files_trashbin/js/trash.js @@ -28,22 +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; - 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(); - } - Files.updateStorageStatistics = function() { // no op because the trashbin doesn't have // storage info like free space / used space @@ -59,7 +43,7 @@ $(document).ready(function() { files: JSON.stringify([filename]), dir: FileList.getCurrentDirectory() }, - removeCallback + FileList._removeCallback ); }, t('files_trashbin', 'Restore')); }; @@ -76,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/core/js/js.js b/core/js/js.js index 325be6cdc53..27bc3c651e3 100644 --- a/core/js/js.js +++ b/core/js/js.js @@ -1250,7 +1250,7 @@ function relative_modified_date(timestamp) { } /** - * @todo Write documentation + * Utility functions */ OC.Util = { // TODO: remove original functions from global namespace From f6586f6bdfe8f5b2175d9ee5e833ecb4ccbc5f7d Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Fri, 4 Apr 2014 14:34:07 +0200 Subject: [PATCH 04/14] Fixed file sorting to work with scrolling The FileList.files model is now updated with file operations. Adding files to the list will add to the model first, then to the DOM. If the insertion point isn't visible yet, the file won't be added to the DOM until the user scrolls down. Updated unit tests to include checking for the correct insertion point. --- apps/files/js/filelist.js | 172 +++++++++++++--------------- apps/files/tests/js/filelistSpec.js | 84 +++++++++++--- 2 files changed, 150 insertions(+), 106 deletions(-) diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index 02754d7acbb..53bb3a5c868 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -30,6 +30,20 @@ window.FileList = { pageNumber: 0, totalPages: 0, + /** + * 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 */ @@ -42,6 +56,7 @@ window.FileList = { // TODO: FileList should not know about global elements this.$el = $('#filestable'); this.$fileList = $('#fileList'); + this.files = []; this.fileSummary = this._createSummary(); @@ -321,7 +336,8 @@ window.FileList = { index = this.pageNumber * this.pageSize; while (count > 0 && index < this.files.length) { - tr = this.add(this.files[index], {updateSummary: false}); + tr = this._renderRow(this.files[index], {updateSummary: false}); + this.$fileList.append(tr); if (selected) { tr.addClass('selected'); tr.find('input:checkbox').prop('checked', true); @@ -497,8 +513,10 @@ 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 + * * @param fileData map of file attributes * @param options map of attributes: * - "insert" true to insert in a sorted manner, false to append (default) @@ -506,6 +524,47 @@ window.FileList = { * @return new tr element (not appended to the table) */ add: function(fileData, options) { + var index = -1; + var $tr = this._renderRow(fileData, options); + options = options || {}; + + this.isEmpty = false; + + if (options.insert) { + index = this._findInsertionIndex(fileData); + if (index < this.files.length) { + this.files.splice(index, 0, fileData); + this.$fileList.children().eq(index).before($tr); + } + else { + this.files.push(fileData); + this.$fileList.append($tr); + } + } + else { + this.files.push(fileData); + this.$fileList.append($tr); + } + + // 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, @@ -524,16 +583,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) { @@ -569,12 +618,6 @@ window.FileList = { filenameTd.css('background-image', 'url(' + previewUrl + ')'); } } - - // defaults to true if not defined - if (typeof(options.updateSummary) === 'undefined' || !!options.updateSummary) { - this.fileSummary.add(fileData, true); - this.updateEmptyContent(); - } return tr; }, /** @@ -742,9 +785,10 @@ window.FileList = { * "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; } @@ -752,51 +796,32 @@ window.FileList = { // 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').length; + FileList.isEmpty = !this.files.length; if (typeof(options.updateSummary) === 'undefined' || !!options.updateSummary) { FileList.updateEmptyContent(); this.fileSummary.remove({type: fileEl.attr('data-type'), size: fileEl.attr('data-size')}, 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 0 - && name.localeCompare($(fileElements[pos+1]).attr('data-file')) < 0) - { - break; - } - } + /** + * 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++; } - 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); - } - FileList.isEmpty = false; - FileList.updateEmptyContent(); + return index; }, 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 = $('').val(oldname); @@ -844,51 +869,18 @@ window.FileList = { 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(''); - } - 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, {insert: true}); } }); } diff --git a/apps/files/tests/js/filelistSpec.js b/apps/files/tests/js/filelistSpec.js index 93e7c81cb1f..1b155f4f1df 100644 --- a/apps/files/tests/js/filelistSpec.js +++ b/apps/files/tests/js/filelistSpec.js @@ -233,6 +233,7 @@ describe('FileList tests', function() { expect($tr.find('.filesize').text()).toEqual('0 B'); }); it('adds new file to the end of the list', function() { + var $tr; var fileData = { type: 'file', name: 'P comes after O.txt' @@ -241,15 +242,55 @@ describe('FileList tests', function() { $tr = FileList.add(fileData); expect($tr.index()).toEqual(4); }); - 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], {insert: true}); + } + 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); + for (var i = 0; i < testFiles.length; i++) { + FileList.add(testFiles[i], {insert: true}); + } $tr = FileList.add(fileData, {insert: true}); // 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], {insert: true}); + } + $tr = FileList.add(fileData, {insert: true}); 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], {insert: true}); + } + $tr = FileList.add(fileData, {insert: true}); + 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 = { @@ -280,6 +321,7 @@ describe('FileList tests', function() { expect($removedEl).toBeDefined(); expect($removedEl.attr('data-file')).toEqual('One.txt'); expect($('#fileList tr').length).toEqual(3); + expect(FileList.files.length).toEqual(3); expect(FileList.findFileEl('One.txt').length).toEqual(0); $summary = $('#filestable .summary'); @@ -294,6 +336,7 @@ describe('FileList tests', function() { FileList.setFiles([testFiles[0]]); FileList.remove('One.txt'); expect($('#fileList tr').length).toEqual(0); + expect(FileList.files.length).toEqual(0); expect(FileList.findFileEl('One.txt').length).toEqual(0); $summary = $('#filestable .summary'); @@ -358,6 +401,7 @@ describe('FileList tests', function() { $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); }); @@ -383,37 +427,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], {insert: true}); + } // 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); 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); }); @@ -429,7 +477,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); }); @@ -440,12 +489,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() { @@ -457,20 +506,23 @@ 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('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').length).toEqual(4); + expect(FileList.files.length).toEqual(4); + expect(FileList.files).toEqual(testFiles); addSpy.restore(); }); it('updates summary using the file sizes', function() { From 2883f231d0b08e8eea75715e912caa42f20d9682 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Fri, 4 Apr 2014 16:11:31 +0200 Subject: [PATCH 05/14] Fixed insertion of files Removed "insert" flag, inserting is by default for FileList.add(). Added "animate" flag to FileList.add(). Added logic to correctly detect when to insert/append elements whenever the insertion point is visible or not. Fixed "render next page" logic to work correctly when many pages of files have been added. --- apps/files/js/file-upload.js | 6 +- apps/files/js/filelist.js | 71 +++++++++----- apps/files/tests/js/fileactionsSpec.js | 1 + apps/files/tests/js/filelistSpec.js | 131 ++++++++++++++++++++++--- 4 files changed, 169 insertions(+), 40 deletions(-) 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 53bb3a5c868..3bf5b2d9672 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -26,8 +26,6 @@ window.FileList = { // number of files per page pageSize: 20, - // zero based page number - pageNumber: 0, totalPages: 0, /** @@ -227,9 +225,6 @@ window.FileList = { }, _onScroll: function(e) { - if (this.pageNumber + 1 >= this.totalPages) { - return; - } if ($(window).scrollTop() + $(window).height() > $(document).height() - 500) { this._nextPage(true); } @@ -324,16 +319,17 @@ window.FileList = { * @param animate true to animate the new elements */ _nextPage: function(animate) { - var tr, index, count = this.pageSize, + var index = this.$fileList.children().length, + count = this.pageSize, + tr, newTrs = [], selected = this.isAllSelected(); - if (this.pageNumber + 1 >= this.totalPages) { + if (index >= this.files.length) { return; } this.pageNumber++; - index = this.pageNumber * this.pageSize; while (count > 0 && index < this.files.length) { tr = this._renderRow(this.files[index], {updateSummary: false}); @@ -343,7 +339,7 @@ window.FileList = { tr.find('input:checkbox').prop('checked', true); } if (animate) { - tr.addClass('appear transparent'); // TODO + tr.addClass('appear transparent'); newTrs.push(tr); } index++; @@ -365,7 +361,7 @@ window.FileList = { * This operation will rerender 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.files = filesArray; this.pageNumber = -1; @@ -516,34 +512,57 @@ window.FileList = { /** * 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 = this._renderRow(fileData, options); + var $tr; + var $rows; + var $insertionPoint; options = options || {}; - this.isEmpty = false; + // 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 - if (options.insert) { - index = this._findInsertionIndex(fileData); - if (index < this.files.length) { - this.files.splice(index, 0, fileData); - this.$fileList.children().eq(index).before($tr); - } - else { - this.files.push(fileData); + $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); } } - else { - this.files.push(fileData); - 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 @@ -880,7 +899,7 @@ window.FileList = { // reinsert row FileList.files.splice(tr.index(), 1); tr.remove(); - FileList.add(fileInfo, {insert: true}); + FileList.add(fileInfo); } }); } @@ -1351,7 +1370,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}); } } }); 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(''); // dummy files table $filesTable = $body.append('
'); + 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 1b155f4f1df..7316cb75315 100644 --- a/apps/files/tests/js/filelistSpec.js +++ b/apps/files/tests/js/filelistSpec.js @@ -236,7 +236,7 @@ describe('FileList tests', function() { var $tr; var fileData = { type: 'file', - name: 'P comes after O.txt' + name: 'ZZZ.txt' }; FileList.setFiles(testFiles); $tr = FileList.add(fileData); @@ -245,7 +245,7 @@ describe('FileList tests', 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], {insert: true}); + FileList.add(testFiles[i]); } expect(FileList.files[0].name).toEqual('somedir'); expect(FileList.files[1].name).toEqual('One.txt'); @@ -259,9 +259,9 @@ describe('FileList tests', function() { name: 'P comes after O.txt' }; for (var i = 0; i < testFiles.length; i++) { - FileList.add(testFiles[i], {insert: true}); + FileList.add(testFiles[i]); } - $tr = FileList.add(fileData, {insert: true}); + $tr = FileList.add(fileData); // after "One.txt" expect($tr.index()).toEqual(2); expect(FileList.files[2]).toEqual(fileData); @@ -273,9 +273,9 @@ describe('FileList tests', function() { name: 'somedir2 comes after somedir' }; for (var i = 0; i < testFiles.length; i++) { - FileList.add(testFiles[i], {insert: true}); + FileList.add(testFiles[i]); } - $tr = FileList.add(fileData, {insert: true}); + $tr = FileList.add(fileData); expect($tr.index()).toEqual(1); expect(FileList.files[1]).toEqual(fileData); }); @@ -286,9 +286,9 @@ describe('FileList tests', function() { name: 'zzz.txt' }; for (var i = 0; i < testFiles.length; i++) { - FileList.add(testFiles[i], {insert: true}); + FileList.add(testFiles[i]); } - $tr = FileList.add(fileData, {insert: true}); + $tr = FileList.add(fileData); expect($tr.index()).toEqual(4); expect(FileList.files[4]).toEqual(fileData); }); @@ -428,7 +428,7 @@ describe('FileList tests', function() { var $input, request; for (var i = 0; i < testFiles.length; i++) { - FileList.add(testFiles[i], {insert: true}); + FileList.add(testFiles[i]); } // trigger rename prompt @@ -516,14 +516,12 @@ describe('FileList tests', function() { }); 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($('#fileList tr').length).toEqual(4); expect(FileList.files.length).toEqual(4); expect(FileList.files).toEqual(testFiles); - addSpy.restore(); }); it('updates summary using the file sizes', function() { var $summary; @@ -593,6 +591,117 @@ describe('FileList tests', function() { expect($summary.find('.info').text()).toEqual('0 folders and 1 file'); }); }); + describe('Rendering next page on scroll', function() { + + 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() { + 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); + }); + }); describe('file previews', function() { var previewLoadStub; From 8909b574facf29ca0a57ab5d75d9904c18cc6338 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Fri, 4 Apr 2014 16:38:27 +0200 Subject: [PATCH 06/14] Make sure there are always enough elements visible on the page --- apps/files/js/filelist.js | 10 ++++++++++ apps/files/tests/js/filelistSpec.js | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index 3bf5b2d9672..0847edd02bb 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -570,6 +570,7 @@ window.FileList = { this.fileSummary.add(fileData, true); this.updateEmptyContent(); } + return $tr; }, @@ -823,6 +824,15 @@ window.FileList = { FileList.updateEmptyContent(); 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; }, /** diff --git a/apps/files/tests/js/filelistSpec.js b/apps/files/tests/js/filelistSpec.js index 7316cb75315..23261759d03 100644 --- a/apps/files/tests/js/filelistSpec.js +++ b/apps/files/tests/js/filelistSpec.js @@ -701,6 +701,14 @@ describe('FileList tests', function() { 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; From a952d80ad9e57931f6a8fcb94ef6cab4f982149c Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Fri, 4 Apr 2014 16:43:35 +0200 Subject: [PATCH 07/14] Fix trashbin previews and "delete selected" --- apps/files_trashbin/js/filelist.js | 6 +++--- apps/files_trashbin/templates/index.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/files_trashbin/js/filelist.js b/apps/files_trashbin/js/filelist.js index 6c9b345086a..d4445e11c86 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){ diff --git a/apps/files_trashbin/templates/index.php b/apps/files_trashbin/templates/index.php index cb64ae9eafa..323e7495535 100644 --- a/apps/files_trashbin/templates/index.php +++ b/apps/files_trashbin/templates/index.php @@ -29,7 +29,7 @@ t( 'Deleted' )); ?> - + t('Delete'))?> <?php p($l->t('Delete'))?>" /> From fd982df6aea09492e02cc65de02ee8250a1a229c Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Fri, 4 Apr 2014 18:46:08 +0200 Subject: [PATCH 08/14] Fixed selection to be based on FileList.files The file selection is now based on the internal model array FileList.files instead of the visible checkboxes. This makes it possible to virtually select files that haven't been rendered yet (select all, then deselect a visible one) Added more unit tests for selection (with shift and ctrl as well) --- apps/files/js/filelist.js | 225 ++++++++++++++-------------- apps/files/js/files.js | 12 +- apps/files/js/filesummary.js | 22 ++- apps/files/tests/js/filelistSpec.js | 187 ++++++++++++++++++----- apps/files_trashbin/js/filelist.js | 4 +- 5 files changed, 289 insertions(+), 161 deletions(-) diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index 0847edd02bb..7c82ec92473 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -28,6 +28,23 @@ window.FileList = { pageSize: 20, totalPages: 0, + /** + * 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. @@ -55,6 +72,8 @@ window.FileList = { this.$el = $('#filestable'); this.$fileList = $('#fileList'); this.files = []; + this._selectedFiles = {}; + this._selectionSummary = new FileSummary(); this.fileSummary = this._createSummary(); @@ -81,49 +100,71 @@ window.FileList = { this.$el.find('.delete-selected').click(this._onClickDeleteSelected); }, + /** + * 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 = $(this).closest('tr'); if (event.ctrlKey || event.shiftKey) { event.preventDefault(); if (event.shiftKey) { - var last = $(FileList._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 $lastTr = $(FileList._lastChecked); + var lastIndex = $lastTr.index(); + var currentIndex = $tr.index(); + var $rows = FileList.$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++) { + FileList._selectFileEl($rows.eq(i), true); } } - var checkbox = $(this).parent().children('input:checkbox'); - FileList._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'); - } + else { + FileList._lastChecked = $tr; } + var $checkbox = $tr.find('input:checkbox'); + FileList._selectFileEl($tr, !$checkbox.prop('checked')); FileList.updateSelectionSummary(); } else { - var filename=$(this).parent().parent().attr('data-file'); - var tr = FileList.findFileEl(filename); - var renaming=tr.data('renaming'); + var filename = $tr.attr('data-file'); + var renaming = $tr.data('renaming'); if (!renaming) { - FileActions.currentFile = $(this).parent(); + FileActions.currentFile = $tr.find('td'); var mime=FileActions.getCurrentMimeType(); var type=FileActions.getCurrentType(); var permissions = FileActions.getCurrentPermissions(); @@ -134,49 +175,36 @@ window.FileList = { } } } - }, /** * Event handler for when clicking on a file's checkbox */ - _onClickFileCheckbox: function(event) { - // FIXME: not sure what the difference is supposed to be with FileList._onClickFile - if (event.shiftKey) { - var last = $(FileList._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); - } - } + _onClickFileCheckbox: function() { + var $tr = $(this).closest('tr'); + FileList._selectFileEl($tr, !$tr.hasClass('selected')); + FileList._lastChecked = $tr; FileList.updateSelectionSummary(); }, /** * Event handler for when selecting/deselecting all files */ - _onClickSelectAll: function(e) { + _onClickSelectAll: function() { var checked = $(this).prop('checked'); FileList.$fileList.find('td.filename input:checkbox').prop('checked', checked) - .parent().parent().toggleClass('selected', checked); + .closest('tr').toggleClass('selected', checked); + FileList._selectedFiles = {}; + if (checked) { + for (var i = 0; i < FileList.files.length; i++) { + var fileData = FileList.files[i]; + FileList._selectedFiles[fileData.id] = fileData; + FileList._selectionSummary.add(fileData); + } + } + else { + FileList._selectionSummary.clear(); + } FileList.updateSelectionSummary(); }, @@ -191,7 +219,7 @@ window.FileList = { dir = OC.dirname(dir) || '/'; } else { - files = FileList.getSelectedFiles('name'); + files = _.pluck(FileList.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)); @@ -204,7 +232,7 @@ window.FileList = { _onClickDeleteSelected: function(event) { var files = null; if (!FileList.isAllSelected()) { - files = FileList.getSelectedFiles('name'); + files = _.pluck(FileList.getSelectedFiles(), 'name'); } FileList.do_delete(files); event.preventDefault(); @@ -322,8 +350,9 @@ window.FileList = { var index = this.$fileList.children().length, count = this.pageSize, tr, + fileData, newTrs = [], - selected = this.isAllSelected(); + isAllSelected = this.isAllSelected(); if (index >= this.files.length) { return; @@ -332,9 +361,10 @@ window.FileList = { this.pageNumber++; while (count > 0 && index < this.files.length) { - tr = this._renderRow(this.files[index], {updateSummary: false}); + fileData = this.files[index]; + tr = this._renderRow(fileData, {updateSummary: false}); this.$fileList.append(tr); - if (selected) { + if (isAllSelected || this._selectedFiles[fileData.id]) { tr.addClass('selected'); tr.find('input:checkbox').prop('checked', true); } @@ -371,7 +401,7 @@ window.FileList = { this.$fileList.empty(); // clear "Select all" checkbox - $('#select_all').prop('checked', false); + this.$el.find('#select_all').prop('checked', false); this.isEmpty = this.files.length === 0; this._nextPage(); @@ -675,7 +705,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, '/'); @@ -812,6 +844,10 @@ window.FileList = { if (!fileEl.length) { return null; } + if (this._selectedFiles[fileEl.data('id')]) { + // remove from selection first + this._selectFileEl(fileEl, false); + } if (fileEl.data('permissions') & OC.PERMISSION_DELETE) { // file is only draggable when delete permissions are set fileEl.find('td.filename').draggable('destroy'); @@ -1131,40 +1167,17 @@ window.FileList = { * Update UI based on the current selection */ updateSelectionSummary: function() { - var allSelected = this.isAllSelected(); - var selected; - var summary = { - totalFiles: 0, - totalDirs: 0, - totalSize: 0 - }; - - if (allSelected) { - summary = this.fileSummary.summary; - } - else { - selected = this.getSelectedFiles(); - for (var i = 0; i < selected.length; i++ ){ - if (selected[i].type === 'dir') { - summary.totalDirs++; - } - else { - summary.totalFiles++; - } - summary.totalSize += parseInt(selected[i].size, 10) || 0; - } - } + 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'); - $('#select_all').removeAttr('checked'); } else { $('.selectedActions').removeClass('hidden'); - $('#headerSize').text(humanFileSize(summary.totalSize)); + $('#headerSize').text(OC.Util.humanFileSize(summary.totalSize)); var selection = ''; if (summary.totalDirs > 0) { selection += n('files', '%n folder', '%n folders', summary.totalDirs); @@ -1190,32 +1203,14 @@ window.FileList = { }, /** - * @brief get a list of selected files - * @param {string} property (option) the property of the file requested - * @return {array} + * Returns the file info of the selected files * - * 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 + * @return array of file names */ - getSelectedFiles: function(property) { - var elements=$('td.filename input:checkbox:checked').parent().parent(); - var files=[]; - elements.each(function(i,element) { - // TODO: make the json format the same as in FileList.add() - var file = FileList.elementToFile($(element)); - // FIXME: legacy attributes - file.origin = file.id; - file.mime = file.mimetype; - if (property) { - files.push(file[property]); - } else { - files.push(file); - } - }); - return files; + getSelectedFiles: function() { + return _.values(this._selectedFiles); } -} +}; $(document).ready(function() { FileList.initialize(); diff --git a/apps/files/js/files.js b/apps/files/js/files.js index 6cb0d41a611..41c762f0fa1 100644 --- a/apps/files/js/files.js +++ b/apps/files/js/files.js @@ -313,19 +313,15 @@ var createDragShadow = function(event) { 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 = FileList.getSelectedFiles(); + // do not show drag shadow for too many files + var selectedFiles = _.first(FileList.getSelectedFiles(), FileList.pageSize); 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 diff --git a/apps/files/js/filesummary.js b/apps/files/js/filesummary.js index bbe4d43ba49..b3e3beeb24a 100644 --- a/apps/files/js/filesummary.js +++ b/apps/files/js/filesummary.js @@ -28,8 +28,9 @@ * @param $tr table row element * $param summary optional initial summary value */ - var FileSummary = function($tr, summary) { + var FileSummary = function($tr) { this.$el = $tr; + this.clear(); this.render(); }; @@ -74,6 +75,12 @@ 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 @@ -98,6 +105,12 @@ } this.setSummary(summary); }, + /** + * Clears the summary + */ + clear: function() { + this.calculate([]); + }, /** * Sets the current summary values * @param summary map @@ -111,6 +124,9 @@ * Renders the file summary element */ update: function() { + if (!this.$el) { + return; + } if (!this.summary.totalFiles && !this.summary.totalDirs) { this.$el.addClass('hidden'); return; @@ -144,6 +160,10 @@ } }, 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); diff --git a/apps/files/tests/js/filelistSpec.js b/apps/files/tests/js/filelistSpec.js index 23261759d03..be285a7b636 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'); @@ -592,31 +619,6 @@ describe('FileList tests', function() { }); }); describe('Rendering next page on scroll', function() { - - 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() { FileList.setFiles(generateFiles(0, 64)); }); @@ -1012,17 +1014,91 @@ describe('FileList tests', function() { 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); @@ -1030,6 +1106,7 @@ describe('FileList tests', function() { $('#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(); @@ -1039,6 +1116,18 @@ describe('FileList tests', function() { $tr.find('input:checkbox').click(); expect($('#select_all').prop('checked')).toEqual(false); + expect(_.pluck(FileList.getSelectedFiles(), 'name').length).toEqual(3); + }); + 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'); @@ -1080,6 +1169,19 @@ describe('FileList tests', function() { 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() { @@ -1087,38 +1189,53 @@ describe('FileList tests', function() { FileList.findFileEl('Three.pdf').find('input:checkbox').click(); FileList.findFileEl('somedir').find('input:checkbox').click(); }); - it('getSelectedFiles returns the selected files', function() { + 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', - mime: 'text/plain', mimetype: 'text/plain', type: 'file', size: 12, - etag: 'abc', - origin: 1 + etag: 'abc' }); expect(files[1]).toEqual({ id: 3, type: 'file', name: 'Three.pdf', - mime: 'application/pdf', mimetype: 'application/pdf', size: 58009, - etag: '123', - origin: 3 + etag: '123' }); expect(files[2]).toEqual({ id: 4, type: 'dir', name: 'somedir', - mime: 'httpd/unix-directory', mimetype: 'httpd/unix-directory', size: 250, - etag: '456', - origin: 4 + 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() { diff --git a/apps/files_trashbin/js/filelist.js b/apps/files_trashbin/js/filelist.js index d4445e11c86..42ab89ef6a6 100644 --- a/apps/files_trashbin/js/filelist.js +++ b/apps/files_trashbin/js/filelist.js @@ -113,7 +113,7 @@ }; } else { - files = FileList.getSelectedFiles('name'); + 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'); @@ -155,7 +155,7 @@ }; } else { - files = FileList.getSelectedFiles('name'); + files = _.pluck(FileList.getSelectedFiles(), 'name'); params = { files: JSON.stringify(files), dir: FileList.getCurrentDirectory() From 3159c2ee641e329c2fbaa5efbf9ac994795efa1d Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Tue, 8 Apr 2014 16:56:02 +0200 Subject: [PATCH 09/14] Fixed drag shadow file sorting --- apps/files/js/files.js | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/files/js/files.js b/apps/files/js/files.js index 41c762f0fa1..f4c99d1128c 100644 --- a/apps/files/js/files.js +++ b/apps/files/js/files.js @@ -318,6 +318,7 @@ var createDragShadow = function(event) { // 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 From 3c006a5a4d00f8852de6a586fb987baa50a067ea Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Tue, 8 Apr 2014 16:56:12 +0200 Subject: [PATCH 10/14] Cleanup and fix trashbin "clear all files" operation --- apps/files/js/filelist.js | 6 +----- apps/files_trashbin/js/filelist.js | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index 7c82ec92473..cf359af4aa2 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -26,7 +26,6 @@ window.FileList = { // number of files per page pageSize: 20, - totalPages: 0, /** * Array of files in the current folder. @@ -358,8 +357,6 @@ window.FileList = { return; } - this.pageNumber++; - while (count > 0 && index < this.files.length) { fileData = this.files[index]; tr = this._renderRow(fileData, {updateSummary: false}); @@ -394,8 +391,6 @@ window.FileList = { setFiles: function(filesArray) { // detach to make adding multiple rows faster this.files = filesArray; - this.pageNumber = -1; - this.totalPages = Math.ceil(filesArray.length / this.pageSize); this.$fileList.detach(); this.$fileList.empty(); @@ -847,6 +842,7 @@ window.FileList = { 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 diff --git a/apps/files_trashbin/js/filelist.js b/apps/files_trashbin/js/filelist.js index 42ab89ef6a6..3bb3a92b60d 100644 --- a/apps/files_trashbin/js/filelist.js +++ b/apps/files_trashbin/js/filelist.js @@ -133,7 +133,7 @@ } FileList.hideMask(); // simply remove all files - FileList.update(''); + FileList.setFiles([]); enableActions(); } else { From c29e8b0bae8998ba55efd5f34f4c8d1e7034ffdc Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Tue, 8 Apr 2014 17:09:57 +0200 Subject: [PATCH 11/14] Cleanup of event handlers Now using _.bind() for event handlers so we can use "this" which is more readable than a static access to FileList. --- apps/files/js/filelist.js | 64 +++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index cf359af4aa2..723a22f24e8 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -92,11 +92,11 @@ window.FileList = { FileList.breadcrumb.resize(width, false); }); - this.$fileList.on('click','td.filename a', this._onClickFile); - this.$fileList.on('change', 'td.filename input:checkbox', this._onClickFileCheckbox); - this.$el.find('#select_all').click(this._onClickSelectAll); - this.$el.find('.download').click(this._onClickDownloadSelected); - this.$el.find('.delete-selected').click(this._onClickDeleteSelected); + 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)); }, /** @@ -132,14 +132,14 @@ window.FileList = { * Event handler for when clicking on files to select them */ _onClickFile: function(event) { - var $tr = $(this).closest('tr'); + var $tr = $(event.target).closest('tr'); if (event.ctrlKey || event.shiftKey) { event.preventDefault(); if (event.shiftKey) { - var $lastTr = $(FileList._lastChecked); + var $lastTr = $(this._lastChecked); var lastIndex = $lastTr.index(); var currentIndex = $tr.index(); - var $rows = FileList.$fileList.children('tr'); + var $rows = this.$fileList.children('tr'); // last clicked checkbox below current one ? if (lastIndex > currentIndex) { @@ -150,15 +150,15 @@ window.FileList = { // auto-select everything in-between for (var i = lastIndex + 1; i < currentIndex; i++) { - FileList._selectFileEl($rows.eq(i), true); + this._selectFileEl($rows.eq(i), true); } } else { - FileList._lastChecked = $tr; + this._lastChecked = $tr; } var $checkbox = $tr.find('input:checkbox'); - FileList._selectFileEl($tr, !$checkbox.prop('checked')); - FileList.updateSelectionSummary(); + this._selectFileEl($tr, !$checkbox.prop('checked')); + this.updateSelectionSummary(); } else { var filename = $tr.attr('data-file'); var renaming = $tr.data('renaming'); @@ -179,32 +179,32 @@ window.FileList = { /** * Event handler for when clicking on a file's checkbox */ - _onClickFileCheckbox: function() { - var $tr = $(this).closest('tr'); - FileList._selectFileEl($tr, !$tr.hasClass('selected')); - FileList._lastChecked = $tr; - FileList.updateSelectionSummary(); + _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() { - var checked = $(this).prop('checked'); - FileList.$fileList.find('td.filename input:checkbox').prop('checked', checked) + _onClickSelectAll: function(e) { + var checked = $(e.target).prop('checked'); + this.$fileList.find('td.filename input:checkbox').prop('checked', checked) .closest('tr').toggleClass('selected', checked); - FileList._selectedFiles = {}; + this._selectedFiles = {}; if (checked) { - for (var i = 0; i < FileList.files.length; i++) { - var fileData = FileList.files[i]; - FileList._selectedFiles[fileData.id] = fileData; - FileList._selectionSummary.add(fileData); + for (var i = 0; i < this.files.length; i++) { + var fileData = this.files[i]; + this._selectedFiles[fileData.id] = fileData; + this._selectionSummary.add(fileData); } } else { - FileList._selectionSummary.clear(); + this._selectionSummary.clear(); } - FileList.updateSelectionSummary(); + this.updateSelectionSummary(); }, /** @@ -212,13 +212,13 @@ window.FileList = { */ _onClickDownloadSelected: function(event) { var files; - var dir = FileList.getCurrentDirectory(); - if (FileList.isAllSelected()) { + var dir = this.getCurrentDirectory(); + if (this.isAllSelected()) { files = OC.basename(dir); dir = OC.dirname(dir) || '/'; } else { - files = _.pluck(FileList.getSelectedFiles(), 'name'); + 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)); @@ -231,9 +231,9 @@ window.FileList = { _onClickDeleteSelected: function(event) { var files = null; if (!FileList.isAllSelected()) { - files = _.pluck(FileList.getSelectedFiles(), 'name'); + files = _.pluck(this.getSelectedFiles(), 'name'); } - FileList.do_delete(files); + this.do_delete(files); event.preventDefault(); return false; }, From a15b68c10fa7401dc4b30a173c80084bf114dde9 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Thu, 10 Apr 2014 20:07:02 +0200 Subject: [PATCH 12/14] Fixed selection summary calculation issue --- apps/files/js/filelist.js | 4 +--- apps/files/tests/js/filelistSpec.js | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index 723a22f24e8..f9916b647b2 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -194,6 +194,7 @@ window.FileList = { 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]; @@ -201,9 +202,6 @@ window.FileList = { this._selectionSummary.add(fileData); } } - else { - this._selectionSummary.clear(); - } this.updateSelectionSummary(); }, diff --git a/apps/files/tests/js/filelistSpec.js b/apps/files/tests/js/filelistSpec.js index be285a7b636..da209220cca 100644 --- a/apps/files/tests/js/filelistSpec.js +++ b/apps/files/tests/js/filelistSpec.js @@ -1118,6 +1118,32 @@ describe('FileList tests', function() { 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(); From f99f451026ea289b6693e965ea8a3e15e6fe457a Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Fri, 11 Apr 2014 12:46:12 +0200 Subject: [PATCH 13/14] Fixed drag and drop into folder and onto breadcrumb Fixed drag and drop code to use FileList.getSelectedFiles() instead of the visible DOM elements. --- apps/files/js/filelist.js | 123 ++++++++++++++++++++-------- apps/files/js/files.js | 56 ++++--------- apps/files/tests/js/filelistSpec.js | 110 ++++++++++++++++++++++--- 3 files changed, 209 insertions(+), 80 deletions(-) diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index f9916b647b2..5667d07ede7 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -78,7 +78,7 @@ window.FileList = { this.breadcrumb = new BreadCrumb({ onClick: this._onClickBreadCrumb, - onDrop: this._onDropOnBreadCrumb, + onDrop: _.bind(this._onDropOnBreadCrumb, this), getCrumbUrl: function(part, index) { return self.linkTo(part.dir); } @@ -259,44 +259,31 @@ window.FileList = { * 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); - FileList.updateSelectionSummary(); - $('#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); }, /** @@ -329,6 +316,7 @@ window.FileList = { * @return file data */ elementToFile: function($el){ + $el = $($el); return { id: parseInt($el.attr('data-id'), 10), name: $el.attr('data-file'), @@ -877,6 +865,77 @@ window.FileList = { } 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); diff --git a/apps/files/js/files.js b/apps/files/js/files.js index f4c99d1128c..6857450602c 100644 --- a/apps/files/js/files.js +++ b/apps/files/js/files.js @@ -142,6 +142,7 @@ var Files = { } }, + // TODO: move to FileList class setupDragAndDrop: function() { var $fileList = $('#fileList'); @@ -308,6 +309,7 @@ function boolOperationFinished(data, callback) { } } +// TODO: move to FileList var createDragShadow = function(event) { //select dragged file var isDragSelected = $(event.target).parents('tr').find('td input:first').prop('checked'); @@ -333,9 +335,12 @@ var createDragShadow = function(event) { var dir=$('#dir').val(); $(selectedFiles).each(function(i,elem) { - var newtr = $('').attr('data-dir', dir).attr('data-filename', elem.name).attr('data-origin', elem.origin); + var newtr = $('') + .attr('data-dir', dir) + .attr('data-file', elem.name) + .attr('data-origin', elem.origin); newtr.append($('').addClass('filename').text(elem.name)); - newtr.append($('').addClass('size').text(humanFileSize(elem.size))); + newtr.append($('').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')+')'); @@ -352,6 +357,7 @@ 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 }, @@ -381,50 +387,24 @@ 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 targetPath = FileList.getCurrentDirectory() + '/' + $(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)); + var files = FileList.getSelectedFiles(); + if (files.length === 0) { + // single one selected without checkbox? + files = _.map(ui.helper.find('tr'), FileList.elementToFile); + } - FileList.remove(file); - FileList.updateSelectionSummary(); - $('#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); - }); - }); + FileList.move(_.pluck(files, 'name'), targetPath); }, tolerance: 'pointer' }; diff --git a/apps/files/tests/js/filelistSpec.js b/apps/files/tests/js/filelistSpec.js index da209220cca..eab364644cd 100644 --- a/apps/files/tests/js/filelistSpec.js +++ b/apps/files/tests/js/filelistSpec.js @@ -541,6 +541,100 @@ describe('FileList tests', function() { 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() { expect(FileList.files.length).toEqual(0); @@ -932,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([ - $(''), - $('') + $(''), + $('') ]); // 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); @@ -976,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([ - $(''), - $('') + $(''), + $('') ]); // 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); From bf61d841a2b3305bc51de6109917725466239061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Mon, 28 Apr 2014 16:51:57 +0200 Subject: [PATCH 14/14] typos, naming, remove unused code, identation --- apps/files/js/filelist.js | 28 +++++++--------- apps/files/js/files.js | 63 ++++++++++++++++-------------------- apps/files/js/filesummary.js | 1 - 3 files changed, 39 insertions(+), 53 deletions(-) diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index 5667d07ede7..40ec898635e 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -323,7 +323,7 @@ window.FileList = { mimetype: $el.attr('data-mime'), type: $el.attr('data-type'), size: parseInt($el.attr('data-size'), 10), - etag: $el.attr('data-etag'), + etag: $el.attr('data-etag') }; }, @@ -371,7 +371,7 @@ window.FileList = { /** * 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) { @@ -673,7 +673,6 @@ window.FileList = { */ changeDirectory: function(targetDir, changeUrl, force) { var $dir = $('#dir'), - url, currentDir = $dir.val() || '/'; targetDir = targetDir || '/'; if (!force && currentDir === targetDir) { @@ -971,19 +970,16 @@ 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) { @@ -1004,20 +1000,20 @@ window.FileList = { } 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(''); } - 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); diff --git a/apps/files/js/files.js b/apps/files/js/files.js index 6857450602c..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})); } }, @@ -222,7 +223,7 @@ $(document).ready(function() { // 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(); @@ -299,16 +300,6 @@ 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 @@ -362,25 +353,25 @@ 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) { @@ -446,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 index b3e3beeb24a..b5130247cc9 100644 --- a/apps/files/js/filesummary.js +++ b/apps/files/js/filesummary.js @@ -167,7 +167,6 @@ 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 fileSize; var infoVars = { dirs: ''+directoryInfo+'',