diff options
Diffstat (limited to 'apps/files/js')
-rw-r--r-- | apps/files/js/app.js | 147 | ||||
-rw-r--r-- | apps/files/js/breadcrumb.js | 12 | ||||
-rw-r--r-- | apps/files/js/file-upload.js | 15 | ||||
-rw-r--r-- | apps/files/js/fileactions.js | 435 | ||||
-rw-r--r-- | apps/files/js/filelist.js | 3123 | ||||
-rw-r--r-- | apps/files/js/files.js | 632 | ||||
-rw-r--r-- | apps/files/js/filesummary.js | 2 | ||||
-rw-r--r-- | apps/files/js/keyboardshortcuts.js | 3 | ||||
-rw-r--r-- | apps/files/js/navigation.js | 122 |
9 files changed, 2424 insertions, 2067 deletions
diff --git a/apps/files/js/app.js b/apps/files/js/app.js new file mode 100644 index 00000000000..9155fb38cdb --- /dev/null +++ b/apps/files/js/app.js @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2014 + * + * @author Vincent Petry + * @copyright 2014 Vincent Petry <pvince81@owncloud.com> + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +/* global dragOptions, folderDropOptions */ +(function() { + + if (!OCA.Files) { + OCA.Files = {}; + } + + var App = { + navigation: null, + + initialize: function() { + this.navigation = new OCA.Files.Navigation($('#app-navigation')); + + // TODO: ideally these should be in a separate class / app (the embedded "all files" app) + this.fileActions = OCA.Files.FileActions; + this.files = OCA.Files.Files; + + this.fileList = new OCA.Files.FileList( + $('#app-content-files'), { + scrollContainer: $('#app-content'), + dragOptions: dragOptions, + folderDropOptions: folderDropOptions + } + ); + this.files.initialize(); + this.fileActions.registerDefaultActions(this.fileList); + this.fileList.setFileActions(this.fileActions); + + // for backward compatibility, the global FileList will + // refer to the one of the "files" view + window.FileList = this.fileList; + + this._setupEvents(); + // trigger URL change event handlers + this._onPopState(OC.Util.History.parseUrlQuery()); + }, + + /** + * Returns the container of the currently visible app. + * + * @return app container + */ + getCurrentAppContainer: function() { + return this.navigation.getActiveContainer(); + }, + + /** + * Setup events based on URL changes + */ + _setupEvents: function() { + OC.Util.History.addOnPopStateHandler(_.bind(this._onPopState, this)); + + // detect when app changed their current directory + $('#app-content').delegate('>div', 'changeDirectory', _.bind(this._onDirectoryChanged, this)); + $('#app-content').delegate('>div', 'changeViewerMode', _.bind(this._onChangeViewerMode, this)); + + $('#app-navigation').on('itemChanged', _.bind(this._onNavigationChanged, this)); + }, + + /** + * Event handler for when the current navigation item has changed + */ + _onNavigationChanged: function(e) { + var params; + if (e && e.itemId) { + params = { + view: e.itemId, + dir: '/' + }; + this._changeUrl(params.view, params.dir); + this.navigation.getActiveContainer().trigger(new $.Event('urlChanged', params)); + } + }, + + /** + * Event handler for when an app notified that its directory changed + */ + _onDirectoryChanged: function(e) { + if (e.dir) { + this._changeUrl(this.navigation.getActiveItem(), e.dir); + } + }, + + /** + * Event handler for when an app notifies that it needs space + * for viewer mode. + */ + _onChangeViewerMode: function(e) { + var state = !!e.viewerModeEnabled; + $('#app-navigation').toggleClass('hidden', state); + $('.app-files').toggleClass('viewer-mode no-sidebar', state); + }, + + /** + * Event handler for when the URL changed + */ + _onPopState: function(params) { + params = _.extend({ + dir: '/', + view: 'files' + }, params); + var lastId = this.navigation.getActiveItem(); + if (!this.navigation.itemExists(params.view)) { + params.view = 'files'; + } + this.navigation.setActiveItem(params.view, {silent: true}); + if (lastId !== this.navigation.getActiveItem()) { + this.navigation.getActiveContainer().trigger(new $.Event('show')); + } + this.navigation.getActiveContainer().trigger(new $.Event('urlChanged', params)); + }, + + /** + * Change the URL to point to the given dir and view + */ + _changeUrl: function(view, dir) { + var params = {dir: dir}; + if (view !== 'files') { + params.view = view; + } + OC.Util.History.pushState(params); + } + }; + OCA.Files.App = App; +})(); + +$(document).ready(function() { + // wait for other apps/extensions to register their event handlers + // in the "ready" clause + _.defer(function() { + OCA.Files.App.initialize(); + }); +}); + diff --git a/apps/files/js/breadcrumb.js b/apps/files/js/breadcrumb.js index 5bc2fac1369..c017d710d6d 100644 --- a/apps/files/js/breadcrumb.js +++ b/apps/files/js/breadcrumb.js @@ -159,7 +159,11 @@ this.totalWidth = 64; // FIXME: this class should not know about global elements if ( $('#navigation').length ) { - this.totalWidth += $('#navigation').get(0).offsetWidth; + this.totalWidth += $('#navigation').outerWidth(); + } + + if ( $('#app-navigation').length && !$('#app-navigation').hasClass('hidden')) { + this.totalWidth += $('#app-navigation').outerWidth(); } this.hiddenBreadcrumbs = 0; @@ -167,8 +171,8 @@ this.totalWidth += $(this.breadcrumbs[i]).get(0).offsetWidth; } - $.each($('#controls .actions>div'), function(index, action) { - self.totalWidth += $(action).get(0).offsetWidth; + $.each($('#controls .actions'), function(index, action) { + self.totalWidth += $(action).outerWidth(); }); }, @@ -236,6 +240,6 @@ } }; - window.BreadCrumb = BreadCrumb; + OCA.Files.BreadCrumb = BreadCrumb; })(); diff --git a/apps/files/js/file-upload.js b/apps/files/js/file-upload.js index 67775b22c54..6b0ca793681 100644 --- a/apps/files/js/file-upload.js +++ b/apps/files/js/file-upload.js @@ -346,7 +346,7 @@ OC.Upload = { // noone set update parameters, we set the minimum data.formData = { requesttoken: oc_requesttoken, - dir: $('#dir').val(), + dir: FileList.getCurrentDirectory(), file_directory: fileDirectory }; } @@ -460,7 +460,6 @@ OC.Upload = { $('#uploadprogresswrapper input.stop').fadeOut(); $('#uploadprogressbar').fadeOut(); - Files.updateStorageStatistics(); }); fileupload.on('fileuploadfail', function(e, data) { OC.Upload.log('progress handle fileuploadfail', e, data); @@ -471,8 +470,6 @@ OC.Upload = { } }); - } else { - console.log('skipping file progress because your browser is broken'); } } @@ -595,7 +592,7 @@ OC.Upload = { if (FileList.lastAction) { FileList.lastAction(); } - var name = getUniqueName(newname); + var name = FileList.getUniqueName(newname); if (newname !== name) { FileList.checkName(name, newname, true); var hidden = true; @@ -607,7 +604,7 @@ OC.Upload = { $.post( OC.filePath('files', 'ajax', 'newfile.php'), { - dir: $('#dir').val(), + dir: FileList.getCurrentDirectory(), filename: name }, function(result) { @@ -623,7 +620,7 @@ OC.Upload = { $.post( OC.filePath('files','ajax','newfolder.php'), { - dir: $('#dir').val(), + dir: FileList.getCurrentDirectory(), foldername: name }, function(result) { @@ -648,7 +645,7 @@ OC.Upload = { } else { //or the domain localName = (localName.match(/:\/\/(.[^\/]+)/)[1]).replace('www.', ''); } - localName = getUniqueName(localName); + localName = FileList.getUniqueName(localName); //IE < 10 does not fire the necessary events for the progress bar. if ($('html.lte9').length === 0) { $('#uploadprogressbar').progressbar({value: 0}); @@ -658,7 +655,7 @@ OC.Upload = { var eventSource = new OC.EventSource( OC.filePath('files', 'ajax', 'newfile.php'), { - dir: $('#dir').val(), + dir: FileList.getCurrentDirectory(), source: name, filename: localName } diff --git a/apps/files/js/fileactions.js b/apps/files/js/fileactions.js index ecdfa72a477..b9cd9816d4c 100644 --- a/apps/files/js/fileactions.js +++ b/apps/files/js/fileactions.js @@ -8,242 +8,255 @@ * */ -/* global OC, FileList, Files */ /* global trashBinApp */ -var FileActions = { - actions: {}, - defaults: {}, - icons: {}, - currentFile: null, - register: function (mime, name, permissions, icon, action, displayName) { - if (!FileActions.actions[mime]) { - FileActions.actions[mime] = {}; - } - if (!FileActions.actions[mime][name]) { - FileActions.actions[mime][name] = {}; - } - if (!displayName) { - displayName = t('files', name); - } - FileActions.actions[mime][name]['action'] = action; - FileActions.actions[mime][name]['permissions'] = permissions; - FileActions.actions[mime][name]['displayName'] = displayName; - FileActions.icons[name] = icon; - }, - setDefault: function (mime, name) { - FileActions.defaults[mime] = name; - }, - get: function (mime, type, permissions) { - var actions = this.getActions(mime, type, permissions); - var filteredActions = {}; - $.each(actions, function (name, action) { - filteredActions[name] = action.action; - }); - return filteredActions; - }, - getActions: function (mime, type, permissions) { - var actions = {}; - if (FileActions.actions.all) { - actions = $.extend(actions, FileActions.actions.all); - } - if (type) {//type is 'dir' or 'file' - if (FileActions.actions[type]) { - actions = $.extend(actions, FileActions.actions[type]); +(function() { + + var FileActions = { + actions: {}, + defaults: {}, + icons: {}, + currentFile: null, + register: function (mime, name, permissions, icon, action, displayName) { + if (!this.actions[mime]) { + this.actions[mime] = {}; } - } - if (mime) { - var mimePart = mime.substr(0, mime.indexOf('/')); - if (FileActions.actions[mimePart]) { - actions = $.extend(actions, FileActions.actions[mimePart]); + if (!this.actions[mime][name]) { + this.actions[mime][name] = {}; } - if (FileActions.actions[mime]) { - actions = $.extend(actions, FileActions.actions[mime]); + if (!displayName) { + displayName = t('files', name); } - } - var filteredActions = {}; - $.each(actions, function (name, action) { - if (action.permissions & permissions) { - filteredActions[name] = action; + this.actions[mime][name]['action'] = action; + this.actions[mime][name]['permissions'] = permissions; + this.actions[mime][name]['displayName'] = displayName; + this.icons[name] = icon; + }, + clear: function() { + this.actions = {}; + this.defaults = {}; + this.icons = {}; + this.currentFile = null; + }, + setDefault: function (mime, name) { + this.defaults[mime] = name; + }, + get: function (mime, type, permissions) { + var actions = this.getActions(mime, type, permissions); + var filteredActions = {}; + $.each(actions, function (name, action) { + filteredActions[name] = action.action; + }); + return filteredActions; + }, + getActions: function (mime, type, permissions) { + var actions = {}; + if (this.actions.all) { + actions = $.extend(actions, this.actions.all); } - }); - return filteredActions; - }, - getDefault: function (mime, type, permissions) { - var mimePart; - if (mime) { - mimePart = mime.substr(0, mime.indexOf('/')); - } - var name = false; - if (mime && FileActions.defaults[mime]) { - name = FileActions.defaults[mime]; - } else if (mime && FileActions.defaults[mimePart]) { - name = FileActions.defaults[mimePart]; - } else if (type && FileActions.defaults[type]) { - name = FileActions.defaults[type]; - } else { - name = FileActions.defaults.all; - } - var actions = this.get(mime, type, permissions); - return actions[name]; - }, - /** - * Display file actions for the given element - * @param parent "td" element of the file for which to display actions - * @param triggerEvent if true, triggers the fileActionsReady on the file - * list afterwards (false by default) - */ - display: function (parent, triggerEvent) { - FileActions.currentFile = parent; - var actions = FileActions.getActions(FileActions.getCurrentMimeType(), FileActions.getCurrentType(), FileActions.getCurrentPermissions()); - var file = FileActions.getCurrentFile(); - var nameLinks; - if (FileList.findFileEl(file).data('renaming')) { - return; - } + if (type) {//type is 'dir' or 'file' + if (this.actions[type]) { + actions = $.extend(actions, this.actions[type]); + } + } + if (mime) { + var mimePart = mime.substr(0, mime.indexOf('/')); + if (this.actions[mimePart]) { + actions = $.extend(actions, this.actions[mimePart]); + } + if (this.actions[mime]) { + actions = $.extend(actions, this.actions[mime]); + } + } + var filteredActions = {}; + $.each(actions, function (name, action) { + if (action.permissions & permissions) { + filteredActions[name] = action; + } + }); + return filteredActions; + }, + getDefault: function (mime, type, permissions) { + var mimePart; + if (mime) { + mimePart = mime.substr(0, mime.indexOf('/')); + } + var name = false; + if (mime && this.defaults[mime]) { + name = this.defaults[mime]; + } else if (mime && this.defaults[mimePart]) { + name = this.defaults[mimePart]; + } else if (type && this.defaults[type]) { + name = this.defaults[type]; + } else { + name = this.defaults.all; + } + var actions = this.get(mime, type, permissions); + return actions[name]; + }, + /** + * Display file actions for the given element + * @param parent "td" element of the file for which to display actions + * @param triggerEvent if true, triggers the fileActionsReady on the file + * list afterwards (false by default) + */ + display: function (parent, triggerEvent) { + this.currentFile = parent; + var self = this; + var actions = this.getActions(this.getCurrentMimeType(), this.getCurrentType(), this.getCurrentPermissions()); + var file = this.getCurrentFile(); + var nameLinks; + if (parent.closest('tr').data('renaming')) { + return; + } + + // recreate fileactions + nameLinks = parent.children('a.name'); + nameLinks.find('.fileactions, .nametext .action').remove(); + nameLinks.append('<span class="fileactions" />'); + var defaultAction = this.getDefault(this.getCurrentMimeType(), this.getCurrentType(), this.getCurrentPermissions()); - // recreate fileactions - nameLinks = parent.children('a.name'); - nameLinks.find('.fileactions, .nametext .action').remove(); - nameLinks.append('<span class="fileactions" />'); - var defaultAction = FileActions.getDefault(FileActions.getCurrentMimeType(), FileActions.getCurrentType(), FileActions.getCurrentPermissions()); + var actionHandler = function (event) { + event.stopPropagation(); + event.preventDefault(); - var actionHandler = function (event) { - event.stopPropagation(); - event.preventDefault(); + self.currentFile = event.data.elem; + var file = self.getCurrentFile(); - FileActions.currentFile = event.data.elem; - var file = FileActions.getCurrentFile(); + event.data.actionFunc(file); + }; - event.data.actionFunc(file); - }; + var addAction = function (name, action, displayName) { - var addAction = function (name, action, displayName) { + if ((name === 'Download' || action !== defaultAction) && name !== 'Delete') { - if ((name === 'Download' || action !== defaultAction) && name !== 'Delete') { + var img = self.icons[name], + actionText = displayName, + actionContainer = 'a.name>span.fileactions'; - var img = FileActions.icons[name], - actionText = displayName, - actionContainer = 'a.name>span.fileactions'; + if (name === 'Rename') { + // rename has only an icon which appears behind + // the file name + actionText = ''; + actionContainer = 'a.name span.nametext'; + } + if (img.call) { + img = img(file); + } + var html = '<a href="#" class="action action-' + name.toLowerCase() + '" data-action="' + name + '">'; + if (img) { + html += '<img class ="svg" src="' + img + '" />'; + } + html += '<span> ' + actionText + '</span></a>'; - if (name === 'Rename') { - // rename has only an icon which appears behind - // the file name - actionText = ''; - actionContainer = 'a.name span.nametext'; + var element = $(html); + element.data('action', name); + element.on('click', {a: null, elem: parent, actionFunc: actions[name].action}, actionHandler); + parent.find(actionContainer).append(element); } + + }; + + $.each(actions, function (name, action) { + if (name !== 'Share') { + displayName = action.displayName; + ah = action.action; + + addAction(name, ah, displayName); + } + }); + if(actions.Share){ + displayName = t('files', 'Share'); + addAction('Share', actions.Share, displayName); + } + + // remove the existing delete action + parent.parent().children().last().find('.action.delete').remove(); + if (actions['Delete']) { + var img = self.icons['Delete']; + var html; if (img.call) { img = img(file); } - var html = '<a href="#" class="action action-' + name.toLowerCase() + '" data-action="' + name + '">'; - if (img) { - html += '<img class ="svg" src="' + img + '" />'; + if (typeof trashBinApp !== 'undefined' && trashBinApp) { + html = '<a href="#" original-title="' + t('files', 'Delete permanently') + '" class="action delete delete-icon" />'; + } else { + html = '<a href="#" class="action delete delete-icon" />'; } - html += '<span> ' + actionText + '</span></a>'; - var element = $(html); - element.data('action', name); - element.on('click', {a: null, elem: parent, actionFunc: actions[name].action}, actionHandler); - parent.find(actionContainer).append(element); + element.data('action', actions['Delete']); + element.on('click', {a: null, elem: parent, actionFunc: actions['Delete'].action}, actionHandler); + parent.parent().children().last().append(element); } - }; + if (triggerEvent){ + $('#fileList').trigger(jQuery.Event("fileActionsReady")); + } + }, + getCurrentFile: function () { + return this.currentFile.parent().attr('data-file'); + }, + getCurrentMimeType: function () { + return this.currentFile.parent().attr('data-mime'); + }, + getCurrentType: function () { + return this.currentFile.parent().attr('data-type'); + }, + getCurrentPermissions: function () { + return this.currentFile.parent().data('permissions'); + }, - $.each(actions, function (name, action) { - if (name !== 'Share') { - displayName = action.displayName; - ah = action.action; + /** + * Register the actions that are used by default for the files app. + */ + registerDefaultActions: function(fileList) { + // TODO: try to find a way to not make it depend on fileList, + // maybe get a handler or listener to trigger events on + this.register('all', 'Delete', OC.PERMISSION_DELETE, function () { + return OC.imagePath('core', 'actions/delete'); + }, function (filename) { + fileList.do_delete(filename); + $('.tipsy').remove(); + }); - addAction(name, ah, displayName); - } - }); - if(actions.Share){ - displayName = t('files', 'Share'); - addAction('Share', actions.Share, displayName); - } + // t('files', 'Rename') + this.register('all', 'Rename', OC.PERMISSION_UPDATE, function () { + return OC.imagePath('core', 'actions/rename'); + }, function (filename) { + fileList.rename(filename); + }); - // remove the existing delete action - parent.parent().children().last().find('.action.delete').remove(); - if (actions['Delete']) { - var img = FileActions.icons['Delete']; - var html; - if (img.call) { - img = img(file); - } - if (typeof trashBinApp !== 'undefined' && trashBinApp) { - html = '<a href="#" original-title="' + t('files', 'Delete permanently') + '" class="action delete delete-icon" />'; + this.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename) { + var dir = fileList.getCurrentDirectory(); + if (dir !== '/') { + dir = dir + '/'; + } + fileList.changeDirectory(dir + filename); + }); + + this.setDefault('dir', 'Open'); + var downloadScope; + if ($('#allowZipDownload').val() == 1) { + downloadScope = 'all'; } else { - html = '<a href="#" class="action delete delete-icon" />'; + downloadScope = 'file'; } - var element = $(html); - element.data('action', actions['Delete']); - element.on('click', {a: null, elem: parent, actionFunc: actions['Delete'].action}, actionHandler); - parent.parent().children().last().append(element); - } - if (triggerEvent){ - $('#fileList').trigger(jQuery.Event("fileActionsReady")); + this.register(downloadScope, 'Download', OC.PERMISSION_READ, function () { + return OC.imagePath('core', 'actions/download'); + }, function (filename) { + var url = OCA.Files.Files.getDownloadUrl(filename, fileList.getCurrentDirectory()); + if (url) { + OC.redirect(url); + } + }); + + fileList.$fileList.trigger(jQuery.Event("fileActionsReady")); } - }, - getCurrentFile: function () { - return FileActions.currentFile.parent().attr('data-file'); - }, - getCurrentMimeType: function () { - return FileActions.currentFile.parent().attr('data-mime'); - }, - getCurrentType: function () { - return FileActions.currentFile.parent().attr('data-type'); - }, - getCurrentPermissions: function () { - return FileActions.currentFile.parent().data('permissions'); - } -}; - -$(document).ready(function () { - var downloadScope; - if ($('#allowZipDownload').val() == 1) { - downloadScope = 'all'; - } else { - downloadScope = 'file'; - } - - if (typeof disableDownloadActions == 'undefined' || !disableDownloadActions) { - FileActions.register(downloadScope, 'Download', OC.PERMISSION_READ, function () { - return OC.imagePath('core', 'actions/download'); - }, function (filename) { - var url = Files.getDownloadUrl(filename); - if (url) { - OC.redirect(url); - } - }); - } - $('#fileList tr').each(function () { - FileActions.display($(this).children('td.filename')); - }); - - $('#fileList').trigger(jQuery.Event("fileActionsReady")); - -}); - -FileActions.register('all', 'Delete', OC.PERMISSION_DELETE, function () { - return OC.imagePath('core', 'actions/delete'); -}, function (filename) { - FileList.do_delete(filename); - $('.tipsy').remove(); -}); - -// t('files', 'Rename') -FileActions.register('all', 'Rename', OC.PERMISSION_UPDATE, function () { - return OC.imagePath('core', 'actions/rename'); -}, function (filename) { - FileList.rename(filename); -}); - -FileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename) { - var dir = $('#dir').val() || '/'; - if (dir !== '/') { - dir = dir + '/'; - } - FileList.changeDirectory(dir + filename); -}); - -FileActions.setDefault('dir', 'Open'); + }; + + OCA.Files.FileActions = FileActions; +})(); + +// for backward compatibility +window.FileActions = OCA.Files.FileActions; + diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index 73a441368bb..38766e2b801 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -8,1639 +8,1770 @@ * */ -/* global OC, t, n, FileList, FileActions, Files, FileSummary, BreadCrumb */ -/* global dragOptions, folderDropOptions */ -window.FileList = { - SORT_INDICATOR_ASC_CLASS: 'icon-triangle-s', - SORT_INDICATOR_DESC_CLASS: 'icon-triangle-n', - - appName: t('files', 'Files'), - isEmpty: true, - useUndo:true, - $el: $('#filestable'), - $fileList: $('#fileList'), - breadcrumb: null, - +(function() { /** - * Instance of FileSummary + * The FileList class manages a file list view. + * A file list view consists of a controls bar and + * a file list table. */ - fileSummary: null, - initialized: false, + var FileList = function($el, options) { + this.initialize($el, options); + }; + FileList.prototype = { + SORT_INDICATOR_ASC_CLASS: 'icon-triangle-s', + SORT_INDICATOR_DESC_CLASS: 'icon-triangle-n', + + id: 'files', + appName: t('files', 'Files'), + isEmpty: true, + useUndo:true, + + /** + * Top-level container with controls and file list + */ + $el: null, + + /** + * Files table + */ + $table: null, + + /** + * List of rows (table tbody) + */ + $fileList: null, + + breadcrumb: null, + + /** + * Instance of FileSummary + */ + fileSummary: null, + initialized: false, + + // number of files per page + pageSize: 20, + + /** + * Array of files in the current folder. + * The entries are of file data. + */ + files: [], + + /** + * File actions handler, defaults to OCA.Files.FileActions + */ + fileActions: null, + + /** + * Map of file id to file data + */ + _selectedFiles: {}, + + /** + * Summary of selected files. + * Instance of FileSummary. + */ + _selectionSummary: null, + + /** + * Sort attribute + */ + _sort: 'name', + + /** + * Sort direction: 'asc' or 'desc' + */ + _sortDirection: 'asc', + + /** + * Sort comparator function for the current sort + */ + _sortComparator: null, + + /** + * Current directory + */ + _currentDirectory: null, + + _dragOptions: null, + _folderDropOptions: null, + + /** + * Initialize the file list and its components + * + * @param $el container element with existing markup for the #controls + * and a table + * @param options map of options, see other parameters + * @param scrollContainer scrollable container, defaults to $(window) + * @param dragOptions drag options, disabled by default + * @param folderDropOptions folder drop options, disabled by default + */ + initialize: function($el, options) { + var self = this; + options = options || {}; + if (this.initialized) { + return; + } - // number of files per page - pageSize: 20, + if (options.dragOptions) { + this._dragOptions = options.dragOptions; + } + if (options.folderDropOptions) { + this._folderDropOptions = options.folderDropOptions; + } - /** - * Array of files in the current folder. - * The entries are of file data. - */ - files: [], + this.$el = $el; + this.$container = options.scrollContainer || $(window); + this.$table = $el.find('table:first'); + this.$fileList = $el.find('#fileList'); + this.fileActions = OCA.Files.FileActions; + this.files = []; + this._selectedFiles = {}; + this._selectionSummary = new OCA.Files.FileSummary(); - /** - * Map of file id to file data - */ - _selectedFiles: {}, + this.fileSummary = this._createSummary(); - /** - * Summary of selected files. - * Instance of FileSummary. - */ - _selectionSummary: null, + this.setSort('name', 'asc'); - /** - * Sort attribute - */ - _sort: 'name', + var breadcrumbOptions = { + onClick: _.bind(this._onClickBreadCrumb, this), + getCrumbUrl: function(part) { + return self.linkTo(part.dir); + } + }; + // if dropping on folders is allowed, then also allow on breadcrumbs + if (this._folderDropOptions) { + breadcrumbOptions.onDrop = _.bind(this._onDropOnBreadCrumb, this); + } + this.breadcrumb = new OCA.Files.BreadCrumb(breadcrumbOptions); - /** - * Sort direction: 'asc' or 'desc' - */ - _sortDirection: 'asc', + this.$el.find('#controls').prepend(this.breadcrumb.$el); - /** - * Sort comparator function for the current sort - */ - _sortComparator: null, + this.$el.find('thead th .columntitle').click(_.bind(this._onClickHeader, this)); - /** - * Initialize the file list and its components - */ - initialize: function() { - var self = this; - if (this.initialized) { - return; - } + $(window).resize(function() { + // TODO: debounce this ? + var width = $(this).width(); + self.breadcrumb.resize(width, false); + }); - // TODO: FileList should not know about global elements - this.$el = $('#filestable'); - this.$fileList = $('#fileList'); - this.files = []; - this._selectedFiles = {}; - this._selectionSummary = new FileSummary(); + this.$fileList.on('click','td.filename>a.name', _.bind(this._onClickFile, this)); + this.$fileList.on('change', 'td.filename>input:checkbox', _.bind(this._onClickFileCheckbox, this)); + this.$el.on('urlChanged', _.bind(this._onUrlChanged, 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)); - this.fileSummary = this._createSummary(); + this.setupUploadEvents(); - this.setSort('name', 'asc'); + this.$container.on('scroll', _.bind(this._onScroll, this)); + }, - this.breadcrumb = new BreadCrumb({ - onClick: this._onClickBreadCrumb, - onDrop: _.bind(this._onDropOnBreadCrumb, this), - getCrumbUrl: function(part, index) { - return self.linkTo(part.dir); + /** + * Event handler for when the URL changed + */ + _onUrlChanged: function(e) { + if (e && e.dir) { + this.changeDirectory(e.dir, false, true); } - }); - - $('#controls').prepend(this.breadcrumb.$el); - - this.$el.find('thead th .columntitle').click(_.bind(this._onClickHeader, this)); - - $(window).resize(function() { - // TODO: debounce this ? - var width = $(this).width(); - FileList.breadcrumb.resize(width, false); - }); - - this.$fileList.on('click','td.filename>a.name', _.bind(this._onClickFile, this)); - this.$fileList.on('change', 'td.filename>input:checkbox', _.bind(this._onClickFileCheckbox, this)); - this.$el.find('#select_all').click(_.bind(this._onClickSelectAll, this)); - this.$el.find('.download').click(_.bind(this._onClickDownloadSelected, this)); - this.$el.find('.delete-selected').click(_.bind(this._onClickDeleteSelected, this)); - }, - - /** - * Selected/deselects the given file element and updated - * the internal selection cache. - * - * @param $tr single file row element - * @param state true to select, false to deselect - */ - _selectFileEl: function($tr, state) { - var $checkbox = $tr.find('td.filename>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); - }, + }, + + /** + * 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('td.filename>input:checkbox'); + var oldData = !!this._selectedFiles[$tr.data('id')]; + var data; + $checkbox.prop('checked', state); + $tr.toggleClass('selected', state); + // already selected ? + if (state === oldData) { + return; + } + data = this.elementToFile($tr); + if (state) { + this._selectedFiles[$tr.data('id')] = data; + this._selectionSummary.add(data); + } + else { + delete this._selectedFiles[$tr.data('id')]; + this._selectionSummary.remove(data); + } + this.$el.find('.select-all').prop('checked', this._selectionSummary.getTotal() === this.files.length); + }, + + /** + * Event handler for when clicking on files to select them + */ + _onClickFile: function(event) { + var $tr = $(event.target).closest('tr'); + if (event.ctrlKey || event.shiftKey) { + event.preventDefault(); + if (event.shiftKey) { + var $lastTr = $(this._lastChecked); + var lastIndex = $lastTr.index(); + var currentIndex = $tr.index(); + var $rows = this.$fileList.children('tr'); + + // last clicked checkbox below current one ? + if (lastIndex > currentIndex) { + var aux = lastIndex; + lastIndex = currentIndex; + currentIndex = aux; + } - /** - * Event handler for when clicking on files to select them - */ - _onClickFile: function(event) { - var $tr = $(event.target).closest('tr'); - if (event.ctrlKey || event.shiftKey) { - event.preventDefault(); - if (event.shiftKey) { - var $lastTr = $(this._lastChecked); - var lastIndex = $lastTr.index(); - var currentIndex = $tr.index(); - var $rows = this.$fileList.children('tr'); - - // last clicked checkbox below current one ? - if (lastIndex > currentIndex) { - var aux = lastIndex; - lastIndex = currentIndex; - currentIndex = aux; + // auto-select everything in-between + for (var i = lastIndex + 1; i < currentIndex; i++) { + this._selectFileEl($rows.eq(i), true); + } } - - // auto-select everything in-between - for (var i = lastIndex + 1; i < currentIndex; i++) { - this._selectFileEl($rows.eq(i), true); + else { + this._lastChecked = $tr; + } + var $checkbox = $tr.find('td.filename>input:checkbox'); + this._selectFileEl($tr, !$checkbox.prop('checked')); + this.updateSelectionSummary(); + } else { + var filename = $tr.attr('data-file'); + var renaming = $tr.data('renaming'); + if (!renaming) { + this.fileActions.currentFile = $tr.find('td'); + var mime = this.fileActions.getCurrentMimeType(); + var type = this.fileActions.getCurrentType(); + var permissions = this.fileActions.getCurrentPermissions(); + var action = this.fileActions.getDefault(mime,type, permissions); + if (action) { + event.preventDefault(); + action(filename); + } } } - else { - this._lastChecked = $tr; + }, + + /** + * Event handler for when clicking on a file's checkbox + */ + _onClickFileCheckbox: function(e) { + var $tr = $(e.target).closest('tr'); + this._selectFileEl($tr, !$tr.hasClass('selected')); + this._lastChecked = $tr; + this.updateSelectionSummary(); + }, + + /** + * Event handler for when selecting/deselecting all files + */ + _onClickSelectAll: function(e) { + var checked = $(e.target).prop('checked'); + this.$fileList.find('td.filename>input:checkbox').prop('checked', checked) + .closest('tr').toggleClass('selected', checked); + this._selectedFiles = {}; + this._selectionSummary.clear(); + if (checked) { + for (var i = 0; i < this.files.length; i++) { + var fileData = this.files[i]; + this._selectedFiles[fileData.id] = fileData; + this._selectionSummary.add(fileData); + } } - var $checkbox = $tr.find('td.filename>input:checkbox'); - this._selectFileEl($tr, !$checkbox.prop('checked')); this.updateSelectionSummary(); - } else { - var filename = $tr.attr('data-file'); - var renaming = $tr.data('renaming'); - if (!renaming) { - FileActions.currentFile = $tr.find('td'); - var mime=FileActions.getCurrentMimeType(); - var type=FileActions.getCurrentType(); - var permissions = FileActions.getCurrentPermissions(); - var action=FileActions.getDefault(mime,type, permissions); - if (action) { - event.preventDefault(); - action(filename); + }, + + /** + * Event handler for when clicking on "Download" for the selected files + */ + _onClickDownloadSelected: function(event) { + var files; + var dir = this.getCurrentDirectory(); + if (this.isAllSelected()) { + files = OC.basename(dir); + dir = OC.dirname(dir) || '/'; + } + else { + files = _.pluck(this.getSelectedFiles(), 'name'); + } + OC.Notification.show(t('files','Your download is being prepared. This might take some time if the files are big.')); + OC.redirect(this.getDownloadUrl(files, dir)); + return false; + }, + + /** + * Event handler for when clicking on "Delete" for the selected files + */ + _onClickDeleteSelected: function(event) { + var files = null; + if (!this.isAllSelected()) { + files = _.pluck(this.getSelectedFiles(), 'name'); + } + this.do_delete(files); + event.preventDefault(); + return false; + }, + + /** + * Event handler when clicking on a table header + */ + _onClickHeader: function(e) { + var $target = $(e.target); + var sort; + if (!$target.is('a')) { + $target = $target.closest('a'); + } + sort = $target.attr('data-sort'); + if (sort) { + if (this._sort === sort) { + this.setSort(sort, (this._sortDirection === 'desc')?'asc':'desc'); + } + else { + this.setSort(sort, 'asc'); } + this.reload(); + } + }, + + /** + * Event handler when clicking on a bread crumb + */ + _onClickBreadCrumb: function(e) { + var $el = $(e.target).closest('.crumb'), + $targetDir = $el.data('dir'); + + if ($targetDir !== undefined) { + e.preventDefault(); + this.changeDirectory($targetDir); + } + }, + + /** + * Event handler for when scrolling the list container. + * This appends/renders the next page of entries when reaching the bottom. + */ + _onScroll: function(e) { + if (this.$container.scrollTop() + this.$container.height() > this.$el.height() - 100) { + this._nextPage(true); + } + }, + + /** + * Event handler when dropping on a breadcrumb + */ + _onDropOnBreadCrumb: function( event, ui ) { + 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 + '/'; + } + // do nothing if dragged on current dir + if (targetPath === dir || targetPath + '/' === dir) { + return; } - } - }, - - /** - * Event handler for when clicking on a file's checkbox - */ - _onClickFileCheckbox: function(e) { - var $tr = $(e.target).closest('tr'); - this._selectFileEl($tr, !$tr.hasClass('selected')); - this._lastChecked = $tr; - this.updateSelectionSummary(); - }, - /** - * Event handler for when selecting/deselecting all files - */ - _onClickSelectAll: function(e) { - var checked = $(e.target).prop('checked'); - this.$fileList.find('td.filename>input:checkbox').prop('checked', checked) - .closest('tr').toggleClass('selected', checked); - this._selectedFiles = {}; - this._selectionSummary.clear(); - if (checked) { - for (var i = 0; i < this.files.length; i++) { - var fileData = this.files[i]; - this._selectedFiles[fileData.id] = fileData; - this._selectionSummary.add(fileData); + var files = this.getSelectedFiles(); + if (files.length === 0) { + // single one selected without checkbox? + files = _.map(ui.helper.find('tr'), this.elementToFile); } - } - this.updateSelectionSummary(); - }, - /** - * Event handler for when clicking on "Download" for the selected files - */ - _onClickDownloadSelected: function(event) { - var files; - var dir = this.getCurrentDirectory(); - if (this.isAllSelected()) { - files = OC.basename(dir); - dir = OC.dirname(dir) || '/'; - } - else { - files = _.pluck(this.getSelectedFiles(), 'name'); - } - OC.Notification.show(t('files','Your download is being prepared. This might take some time if the files are big.')); - OC.redirect(Files.getDownloadUrl(files, dir)); - return false; - }, + this.move(_.pluck(files, 'name'), targetPath); + }, - /** - * Event handler for when clicking on "Delete" for the selected files - */ - _onClickDeleteSelected: function(event) { - var files = null; - if (!FileList.isAllSelected()) { - files = _.pluck(this.getSelectedFiles(), 'name'); - } - this.do_delete(files); - event.preventDefault(); - return false; - }, + /** + * Sets a new page title + */ + setPageTitle: function(title){ + if (title) { + title += ' - '; + } else { + title = ''; + } + title += this.appName; + // Sets the page title with the " - ownCloud" suffix as in templates + window.document.title = title + ' - ' + oc_defaults.title; - /** - * Event handler when clicking on a table header - */ - _onClickHeader: function(e) { - var $target = $(e.target); - var sort; - if (!$target.is('a')) { - $target = $target.closest('a'); - } - sort = $target.attr('data-sort'); - if (sort) { - if (this._sort === sort) { - this.setSort(sort, (this._sortDirection === 'desc')?'asc':'desc'); + return true; + }, + /** + * Returns the tr element for a given file name + * @param fileName file name + */ + findFileEl: function(fileName){ + // use filterAttr to avoid escaping issues + return this.$fileList.find('tr').filterAttr('data-file', fileName); + }, + + /** + * Returns the file data from a given file element. + * @param $el file tr element + * @return file data + */ + elementToFile: function($el){ + $el = $($el); + return { + id: parseInt($el.attr('data-id'), 10), + name: $el.attr('data-file'), + mimetype: $el.attr('data-mime'), + type: $el.attr('data-type'), + size: parseInt($el.attr('data-size'), 10), + etag: $el.attr('data-etag') + }; + }, + + /** + * Appends the next page of files into the table + * @param animate true to animate the new elements + */ + _nextPage: function(animate) { + var index = this.$fileList.children().length, + count = this.pageSize, + tr, + fileData, + newTrs = [], + isAllSelected = this.isAllSelected(); + + if (index >= this.files.length) { + return; } - else { - this.setSort(sort, 'asc'); + + while (count > 0 && index < this.files.length) { + fileData = this.files[index]; + tr = this._renderRow(fileData, {updateSummary: false}); + this.$fileList.append(tr); + if (isAllSelected || this._selectedFiles[fileData.id]) { + tr.addClass('selected'); + tr.find('input:checkbox').prop('checked', true); + } + if (animate) { + tr.addClass('appear transparent'); + newTrs.push(tr); + } + index++; + count--; } - this.reload(); - } - }, - /** - * Event handler when clicking on a bread crumb - */ - _onClickBreadCrumb: function(e) { - var $el = $(e.target).closest('.crumb'), - $targetDir = $el.data('dir'); + if (animate) { + // defer, for animation + window.setTimeout(function() { + for (var i = 0; i < newTrs.length; i++ ) { + newTrs[i].removeClass('transparent'); + } + }, 0); + } + }, - if ($targetDir !== undefined) { - e.preventDefault(); - FileList.changeDirectory($targetDir); - } - }, + /** + * Sets the files to be displayed in the list. + * This operation will re-render the list and update the summary. + * @param filesArray array of file data (map) + */ + setFiles: function(filesArray) { + // detach to make adding multiple rows faster + this.files = filesArray; - _onScroll: function(e) { - if ($(window).scrollTop() + $(window).height() > $(document).height() - 500) { - this._nextPage(true); - } - }, + this.$fileList.detach(); + this.$fileList.empty(); - /** - * Event handler when dropping on a breadcrumb - */ - _onDropOnBreadCrumb: function( event, ui ) { - 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 + '/'; - } - // do nothing if dragged on current dir - if (targetPath === dir || targetPath + '/' === dir) { - return; - } + // clear "Select all" checkbox + this.$el.find('.select-all').prop('checked', false); - var files = this.getSelectedFiles(); - if (files.length === 0) { - // single one selected without checkbox? - files = _.map(ui.helper.find('tr'), FileList.elementToFile); - } + this.isEmpty = this.files.length === 0; + this._nextPage(); - FileList.move(_.pluck(files, 'name'), targetPath); - }, + this.$el.find('thead').after(this.$fileList); - /** - * Sets a new page title - */ - setPageTitle: function(title){ - if (title) { - title += ' - '; - } else { - title = ''; - } - title += FileList.appName; - // Sets the page title with the " - ownCloud" suffix as in templates - window.document.title = title + ' - ' + oc_defaults.title; + this.updateEmptyContent(); + this.$fileList.trigger(jQuery.Event("fileActionsReady")); - return true; - }, - /** - * Returns the tr element for a given file name - * @param fileName file name - */ - findFileEl: function(fileName){ - // use filterAttr to avoid escaping issues - return this.$fileList.find('tr').filterAttr('data-file', fileName); - }, + this.fileSummary.calculate(filesArray); - /** - * Returns the file data from a given file element. - * @param $el file tr element - * @return file data - */ - elementToFile: function($el){ - $el = $($el); - return { - id: parseInt($el.attr('data-id'), 10), - name: $el.attr('data-file'), - mimetype: $el.attr('data-mime'), - type: $el.attr('data-type'), - size: parseInt($el.attr('data-size'), 10), - etag: $el.attr('data-etag') - }; - }, + this.updateSelectionSummary(); + $(window).scrollTop(0); + + this.$fileList.trigger(jQuery.Event("updated")); + }, + /** + * Creates a new table row element using the given file data. + * @param fileData map of file attributes + * @param options map of attribute "loading" whether the entry is currently loading + * @return new tr element (not appended to the table) + */ + _createRow: function(fileData, options) { + var td, simpleSize, basename, extension, sizeColor, + icon = OC.Util.replaceSVGIcon(fileData.icon), + name = fileData.name, + type = fileData.type || 'file', + mtime = parseInt(fileData.mtime, 10) || new Date().getTime(), + mime = fileData.mimetype, + linkUrl; + options = options || {}; + + if (type === 'dir') { + mime = mime || 'httpd/unix-directory'; + } - /** - * Appends the next page of files into the table - * @param animate true to animate the new elements - */ - _nextPage: function(animate) { - var index = this.$fileList.children().length, - count = this.pageSize, - tr, - fileData, - newTrs = [], - isAllSelected = this.isAllSelected(); - - if (index >= this.files.length) { - return; - } + // user should always be able to rename a share mount point + var allowRename = 0; + if (fileData.isShareMountPoint) { + allowRename = OC.PERMISSION_UPDATE; + } - while (count > 0 && index < this.files.length) { - fileData = this.files[index]; - tr = this._renderRow(fileData, {updateSummary: false}); - this.$fileList.append(tr); - if (isAllSelected || this._selectedFiles[fileData.id]) { - tr.addClass('selected'); - tr.find('input:checkbox').prop('checked', true); + //containing tr + var tr = $('<tr></tr>').attr({ + "data-id" : fileData.id, + "data-type": type, + "data-size": fileData.size, + "data-file": name, + "data-mime": mime, + "data-mtime": mtime, + "data-etag": fileData.etag, + "data-permissions": fileData.permissions | allowRename || this.getDirectoryPermissions() + }); + + if (type === 'dir') { + // use default folder icon + icon = icon || OC.imagePath('core', 'filetypes/folder'); } - if (animate) { - tr.addClass('appear transparent'); - newTrs.push(tr); + else { + icon = icon || OC.imagePath('core', 'filetypes/file'); } - index++; - count--; - } - if (animate) { - // defer, for animation - window.setTimeout(function() { - for (var i = 0; i < newTrs.length; i++ ) { - newTrs[i].removeClass('transparent'); - } - }, 0); - } - }, + // filename td + td = $('<td></td>').attr({ + "class": "filename", + "style": 'background-image:url(' + icon + '); background-size: 32px;' + }); - /** - * Sets the files to be displayed in the list. - * This operation will re-render the list and update the summary. - * @param filesArray array of file data (map) - */ - setFiles: function(filesArray) { - // detach to make adding multiple rows faster - this.files = filesArray; + // linkUrl + if (type === 'dir') { + linkUrl = this.linkTo(this.getCurrentDirectory() + '/' + name); + } + else { + linkUrl = this.getDownloadUrl(name, this.getCurrentDirectory()); + } + td.append('<input id="select-' + this.id + '-' + fileData.id + + '" type="checkbox" /><label for="select-' + this.id + '-' + fileData.id + '"></label>'); + var linkElem = $('<a></a>').attr({ + "class": "name", + "href": linkUrl + }); - this.$fileList.detach(); - this.$fileList.empty(); + // from here work on the display name + name = fileData.displayName || name; - // clear "Select all" checkbox - this.$el.find('#select_all').prop('checked', false); + // split extension from filename for non dirs + if (type !== 'dir' && name.indexOf('.') !== -1) { + basename = name.substr(0, name.lastIndexOf('.')); + extension = name.substr(name.lastIndexOf('.')); + } else { + basename = name; + extension = false; + } + var nameSpan=$('<span></span>').addClass('nametext').text(basename); + linkElem.append(nameSpan); + if (extension) { + nameSpan.append($('<span></span>').addClass('extension').text(extension)); + } + // dirs can show the number of uploaded files + if (type === 'dir') { + linkElem.append($('<span></span>').attr({ + 'class': 'uploadtext', + 'currentUploads': 0 + })); + } + td.append(linkElem); + tr.append(td); - this.isEmpty = this.files.length === 0; - this._nextPage(); + // size column + if (typeof(fileData.size) !== 'undefined' && fileData.size >= 0) { + simpleSize = humanFileSize(parseInt(fileData.size, 10)); + sizeColor = Math.round(160-Math.pow((fileData.size/(1024*1024)),2)); + } else { + simpleSize = t('files', 'Pending'); + } - this.$el.find('thead').after(this.$fileList); + td = $('<td></td>').attr({ + "class": "filesize", + "style": 'color:rgb(' + sizeColor + ',' + sizeColor + ',' + sizeColor + ')' + }).text(simpleSize); + tr.append(td); + + // date column + var modifiedColor = Math.round((Math.round((new Date()).getTime() / 1000) - mtime)/60/60/24*5); + td = $('<td></td>').attr({ "class": "date" }); + td.append($('<span></span>').attr({ + "class": "modified", + "title": formatDate(mtime), + "style": 'color:rgb('+modifiedColor+','+modifiedColor+','+modifiedColor+')' + }).text( relative_modified_date(mtime / 1000) )); + tr.find('.filesize').text(simpleSize); + tr.append(td); + return tr; + }, + + /** + * 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: + * - "updateSummary" true to update the summary after adding (default), false otherwise + * @return new tr element (not appended to the table) + */ + add: function(fileData, options) { + var index = -1; + var $tr; + var $rows; + var $insertionPoint; + options = options || {}; + + // there are three situations to cover: + // 1) insertion point is visible on the current page + // 2) insertion point is on a not visible page (visible after scrolling) + // 3) insertion point is at the end of the list + + $rows = this.$fileList.children(); + index = this._findInsertionIndex(fileData); + if (index > this.files.length) { + index = this.files.length; + } + else { + $insertionPoint = $rows.eq(index); + } - this.updateEmptyContent(); - this.$fileList.trigger(jQuery.Event("fileActionsReady")); - // "Files" might not be loaded in extending apps - if (window.Files) { - Files.setupDragAndDrop(); - } + // is the insertion point visible ? + if ($insertionPoint.length) { + // only render if it will really be inserted + $tr = this._renderRow(fileData, options); + $insertionPoint.before($tr); + } + else { + // if insertion point is after the last visible + // entry, append + if (index === $rows.length) { + $tr = this._renderRow(fileData, options); + this.$fileList.append($tr); + } + } - this.fileSummary.calculate(filesArray); + this.isEmpty = false; + this.files.splice(index, 0, fileData); - FileList.updateSelectionSummary(); - $(window).scrollTop(0); + if ($tr && options.animate) { + $tr.addClass('appear transparent'); + window.setTimeout(function() { + $tr.removeClass('transparent'); + }); + } - this.$fileList.trigger(jQuery.Event("updated")); - }, - /** - * Creates a new table row element using the given file data. - * @param fileData map of file attributes - * @param options map of attribute "loading" whether the entry is currently loading - * @return new tr element (not appended to the table) - */ - _createRow: function(fileData, options) { - var td, simpleSize, basename, extension, sizeColor, - icon = OC.Util.replaceSVGIcon(fileData.icon), - name = fileData.name, - type = fileData.type || 'file', - mtime = parseInt(fileData.mtime, 10) || new Date().getTime(), - mime = fileData.mimetype, - linkUrl; - options = options || {}; - - if (type === 'dir') { - mime = mime || 'httpd/unix-directory'; - } + // defaults to true if not defined + if (typeof(options.updateSummary) === 'undefined' || !!options.updateSummary) { + this.fileSummary.add(fileData, true); + this.updateEmptyContent(); + } - // user should always be able to rename a share mount point - var allowRename = 0; - if (fileData.isShareMountPoint) { - allowRename = OC.PERMISSION_UPDATE; - } + 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, + permissions = parseInt(fileData.permissions, 10) || 0; + + if (fileData.isShareMountPoint) { + permissions = permissions | OC.PERMISSION_UPDATE; + } - //containing tr - var tr = $('<tr></tr>').attr({ - "data-id" : fileData.id, - "data-type": type, - "data-size": fileData.size, - "data-file": name, - "data-mime": mime, - "data-mtime": mtime, - "data-etag": fileData.etag, - "data-permissions": fileData.permissions | allowRename || this.getDirectoryPermissions() - }); - - if (type === 'dir') { - // use default folder icon - icon = icon || OC.imagePath('core', 'filetypes/folder'); - } - else { - icon = icon || OC.imagePath('core', 'filetypes/file'); - } + if (type === 'dir') { + mime = mime || 'httpd/unix-directory'; + } + var tr = this._createRow( + fileData, + options + ); + var filenameTd = tr.find('td.filename'); + + // TODO: move dragging to FileActions ? + // enable drag only for deletable files + if (this._dragOptions && permissions & OC.PERMISSION_DELETE) { + filenameTd.draggable(this._dragOptions); + } + // allow dropping on folders + if (this._folderDropOptions && fileData.type === 'dir') { + filenameTd.droppable(this._folderDropOptions); + } - // filename td - td = $('<td></td>').attr({ - "class": "filename", - "style": 'background-image:url(' + icon + '); background-size: 32px;' - }); + if (options.hidden) { + tr.addClass('hidden'); + } - // linkUrl - if (type === 'dir') { - linkUrl = FileList.linkTo(FileList.getCurrentDirectory() + '/' + name); - } - else { - linkUrl = Files.getDownloadUrl(name, FileList.getCurrentDirectory()); - } - td.append('<input id="select-' + fileData.id + '" type="checkbox" /><label for="select-' + fileData.id + '"></label>'); - var linkElem = $('<a></a>').attr({ - "class": "name", - "href": linkUrl - }); - - // from here work on the display name - name = fileData.displayName || name; - - // split extension from filename for non dirs - if (type !== 'dir' && name.indexOf('.') !== -1) { - basename = name.substr(0, name.lastIndexOf('.')); - extension = name.substr(name.lastIndexOf('.')); - } else { - basename = name; - extension = false; - } - var nameSpan=$('<span></span>').addClass('nametext').text(basename); - linkElem.append(nameSpan); - if (extension) { - nameSpan.append($('<span></span>').addClass('extension').text(extension)); - } - // dirs can show the number of uploaded files - if (type === 'dir') { - linkElem.append($('<span></span>').attr({ - 'class': 'uploadtext', - 'currentUploads': 0 - })); - } - td.append(linkElem); - tr.append(td); - - // size column - if (typeof(fileData.size) !== 'undefined' && fileData.size >= 0) { - simpleSize = humanFileSize(parseInt(fileData.size, 10)); - sizeColor = Math.round(160-Math.pow((fileData.size/(1024*1024)),2)); - } else { - simpleSize = t('files', 'Pending'); - } + // display actions + this.fileActions.display(filenameTd, false); + + if (fileData.isPreviewAvailable) { + // lazy load / newly inserted td ? + if (!fileData.icon) { + this.lazyLoadPreview({ + path: this.getCurrentDirectory() + '/' + fileData.name, + mime: mime, + etag: fileData.etag, + callback: function(url) { + filenameTd.css('background-image', 'url(' + url + ')'); + } + }); + } + else { + // set the preview URL directly + var urlSpec = { + file: this.getCurrentDirectory() + '/' + fileData.name, + c: fileData.etag + }; + var previewUrl = this.generatePreviewUrl(urlSpec); + previewUrl = previewUrl.replace('(', '%28').replace(')', '%29'); + filenameTd.css('background-image', 'url(' + previewUrl + ')'); + } + } + return tr; + }, + /** + * Returns the current directory + * @return current directory + */ + getCurrentDirectory: function(){ + return this._currentDirectory || this.$el.find('#dir').val() || '/'; + }, + /** + * Returns the directory permissions + * @return permission value as integer + */ + getDirectoryPermissions: function() { + return parseInt(this.$el.find('#permissions').val(), 10); + }, + /** + * @brief Changes the current directory and reload the file list. + * @param targetDir target directory (non URL encoded) + * @param changeUrl false if the URL must not be changed (defaults to true) + * @param {boolean} force set to true to force changing directory + */ + changeDirectory: function(targetDir, changeUrl, force) { + var currentDir = this.getCurrentDirectory(); + targetDir = targetDir || '/'; + if (!force && currentDir === targetDir) { + return; + } + this._setCurrentDir(targetDir, changeUrl); + this.reload(); + }, + linkTo: function(dir) { + return OC.linkTo('files', 'index.php')+"?dir="+ encodeURIComponent(dir).replace(/%2F/g, '/'); + }, + + /** + * Sets the file actions handler + */ + setFileActions: function(fileActions) { + this.fileActions = fileActions; + }, + + /** + * Sets the current directory name and updates the breadcrumb. + * @param targetDir directory to display + * @param changeUrl true to also update the URL, false otherwise (default) + */ + _setCurrentDir: function(targetDir, changeUrl) { + var previousDir = this.getCurrentDirectory(), + baseDir = OC.basename(targetDir); + + if (baseDir !== '') { + this.setPageTitle(baseDir); + } + else { + this.setPageTitle(); + } - td = $('<td></td>').attr({ - "class": "filesize", - "style": 'color:rgb(' + sizeColor + ',' + sizeColor + ',' + sizeColor + ')' - }).text(simpleSize); - tr.append(td); - - // date column - var modifiedColor = Math.round((Math.round((new Date()).getTime() / 1000) - mtime)/60/60/24*5); - td = $('<td></td>').attr({ "class": "date" }); - td.append($('<span></span>').attr({ - "class": "modified", - "title": formatDate(mtime), - "style": 'color:rgb('+modifiedColor+','+modifiedColor+','+modifiedColor+')' - }).text( relative_modified_date(mtime / 1000) )); - tr.find('.filesize').text(simpleSize); - tr.append(td); - return tr; - }, + this._currentDirectory = targetDir; - /** - * 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: - * - "updateSummary" true to update the summary after adding (default), false otherwise - * @return new tr element (not appended to the table) - */ - add: function(fileData, options) { - var index = -1; - var $tr; - var $rows; - var $insertionPoint; - options = options || {}; - - // there are three situations to cover: - // 1) insertion point is visible on the current page - // 2) insertion point is on a not visible page (visible after scrolling) - // 3) insertion point is at the end of the list - - $rows = this.$fileList.children(); - index = this._findInsertionIndex(fileData); - if (index > this.files.length) { - index = this.files.length; - } - else { - $insertionPoint = $rows.eq(index); - } + // legacy stuff + this.$el.find('#dir').val(targetDir); - // 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); + if (changeUrl !== false) { + this.$el.trigger(jQuery.Event('changeDirectory', { + dir: targetDir, + previousDir: previousDir + })); } - } - - this.isEmpty = false; - this.files.splice(index, 0, fileData); - - if ($tr && options.animate) { - $tr.addClass('appear transparent'); - window.setTimeout(function() { - $tr.removeClass('transparent'); + this.breadcrumb.setDirectory(this.getCurrentDirectory()); + }, + /** + * Sets the current sorting and refreshes the list + * + * @param sort sort attribute name + * @param direction sort direction, one of "asc" or "desc" + */ + setSort: function(sort, direction) { + var comparator = FileList.Comparators[sort] || FileList.Comparators.name; + this._sort = sort; + this._sortDirection = (direction === 'desc')?'desc':'asc'; + this._sortComparator = comparator; + if (direction === 'desc') { + this._sortComparator = function(fileInfo1, fileInfo2) { + return -comparator(fileInfo1, fileInfo2); + }; + } + this.$el.find('thead th .sort-indicator') + .removeClass(this.SORT_INDICATOR_ASC_CLASS + ' ' + this.SORT_INDICATOR_DESC_CLASS); + this.$el.find('thead th.column-' + sort + ' .sort-indicator') + .addClass(direction === 'desc' ? this.SORT_INDICATOR_DESC_CLASS : this.SORT_INDICATOR_ASC_CLASS); + }, + /** + * @brief Reloads the file list using ajax call + */ + reload: function() { + var self = this; + this._selectedFiles = {}; + this._selectionSummary.clear(); + this.$el.find('.select-all').prop('checked', false); + this.showMask(); + if (this._reloadCall) { + this._reloadCall.abort(); + } + this._reloadCall = $.ajax({ + url: this.getAjaxUrl('list'), + data: { + dir : this.getCurrentDirectory(), + sort: this._sort, + sortdirection: this._sortDirection + }, + error: function(result) { + self.reloadCallback(result); + }, + success: function(result) { + self.reloadCallback(result); + } }); - } - - // 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, - permissions = parseInt(fileData.permissions, 10) || 0; - - if (fileData.isShareMountPoint) { - permissions = permissions | OC.PERMISSION_UPDATE; - } + }, + reloadCallback: function(result) { + delete this._reloadCall; + this.hideMask(); + + if (!result || result.status === 'error') { + OC.Notification.show(result.data.message); + return; + } - if (type === 'dir') { - mime = mime || 'httpd/unix-directory'; - } - var tr = this._createRow( - fileData, - options - ); - var filenameTd = tr.find('td.filename'); - - // TODO: move dragging to FileActions ? - // enable drag only for deletable files - if (permissions & OC.PERMISSION_DELETE) { - filenameTd.draggable(dragOptions); - } - // allow dropping on folders - if (fileData.type === 'dir') { - filenameTd.droppable(folderDropOptions); - } + if (result.status === 404) { + // go back home + this.changeDirectory('/'); + return; + } + // aborted ? + if (result.status === 0){ + return; + } - if (options.hidden) { - tr.addClass('hidden'); - } + // TODO: should rather return upload file size through + // the files list ajax call + this.updateStorageStatistics(true); - // display actions - FileActions.display(filenameTd, false); + if (result.data.permissions) { + this.setDirectoryPermissions(result.data.permissions); + } - if (fileData.isPreviewAvailable) { - // lazy load / newly inserted td ? - if (!fileData.icon) { - Files.lazyLoadPreview(getPathForPreview(fileData.name), mime, function(url) { - filenameTd.css('background-image', 'url(' + url + ')'); - }, null, null, fileData.etag); + this.setFiles(result.data.files); + }, + + updateStorageStatistics: function(force) { + OCA.Files.Files.updateStorageStatistics(this.getCurrentDirectory(), force); + }, + + getAjaxUrl: function(action, params) { + return OCA.Files.Files.getAjaxUrl(action, params); + }, + + getDownloadUrl: function(files, dir) { + return OCA.Files.Files.getDownloadUrl(files, dir || this.getCurrentDirectory()); + }, + + /** + * Generates a preview URL based on the URL space. + * @param urlSpec map with {x: width, y: height, file: file path} + * @return preview URL + */ + generatePreviewUrl: function(urlSpec) { + urlSpec = urlSpec || {}; + if (!urlSpec.x) { + urlSpec.x = this.$table.data('preview-x') || 36; } - else { - // set the preview URL directly - var urlSpec = { - file: FileList.getCurrentDirectory() + '/' + fileData.name, - c: fileData.etag - }; - var previewUrl = Files.generatePreviewUrl(urlSpec); - previewUrl = previewUrl.replace('(', '%28').replace(')', '%29'); - filenameTd.css('background-image', 'url(' + previewUrl + ')'); + if (!urlSpec.y) { + urlSpec.y = this.$table.data('preview-y') || 36; } - } - return tr; - }, - /** - * Returns the current directory - * @return current directory - */ - getCurrentDirectory: function(){ - return $('#dir').val() || '/'; - }, - /** - * Returns the directory permissions - * @return permission value as integer - */ - getDirectoryPermissions: function() { - return parseInt($('#permissions').val(), 10); - }, - /** - * @brief Changes the current directory and reload the file list. - * @param targetDir target directory (non URL encoded) - * @param changeUrl false if the URL must not be changed (defaults to true) - * @param {boolean} force set to true to force changing directory - */ - changeDirectory: function(targetDir, changeUrl, force) { - var $dir = $('#dir'), - currentDir = $dir.val() || '/'; - targetDir = targetDir || '/'; - if (!force && currentDir === targetDir) { - return; - } - FileList._setCurrentDir(targetDir, changeUrl); - $('#fileList').trigger( - jQuery.Event('changeDirectory', { - dir: targetDir, - previousDir: currentDir - } - )); - this.reload(); - }, - linkTo: function(dir) { - return OC.linkTo('files', 'index.php')+"?dir="+ encodeURIComponent(dir).replace(/%2F/g, '/'); - }, - - /** - * Sets the current directory name and updates the breadcrumb. - * @param targetDir directory to display - * @param changeUrl true to also update the URL, false otherwise (default) - */ - _setCurrentDir: function(targetDir, changeUrl) { - var url, - baseDir = OC.basename(targetDir); + urlSpec.y *= window.devicePixelRatio; + urlSpec.x *= window.devicePixelRatio; + urlSpec.forceIcon = 0; + return OC.generateUrl('/core/preview.png?') + $.param(urlSpec); + }, + + /** + * Lazy load a file's preview. + * + * @param path path of the file + * @param mime mime type + * @param callback callback function to call when the image was loaded + * @param etag file etag (for caching) + */ + lazyLoadPreview : function(options) { + var self = this; + var path = options.path; + var mime = options.mime; + var ready = options.callback; + var etag = options.etag; + + // get mime icon url + OCA.Files.Files.getMimeIcon(mime, function(iconURL) { + var previewURL, + urlSpec = {}; + ready(iconURL); // set mimeicon URL + + urlSpec.file = OCA.Files.Files.fixPath(path); + + if (etag){ + // use etag as cache buster + urlSpec.c = etag; + } + else { + console.warn('OCA.Files.FileList.lazyLoadPreview(): missing etag argument'); + } - if (baseDir !== '') { - FileList.setPageTitle(baseDir); - } - else { - FileList.setPageTitle(); - } + previewURL = self.generatePreviewUrl(urlSpec); + previewURL = previewURL.replace('(', '%28'); + previewURL = previewURL.replace(')', '%29'); + + // preload image to prevent delay + // this will make the browser cache the image + var img = new Image(); + img.onload = function(){ + // if loading the preview image failed (no preview for the mimetype) then img.width will < 5 + if (img.width > 5) { + ready(previewURL); + } + }; + img.src = previewURL; + }); + }, - $('#dir').val(targetDir); - if (changeUrl !== false) { - if (window.history.pushState && changeUrl !== false) { - url = FileList.linkTo(targetDir); - window.history.pushState({dir: targetDir}, '', url); + setDirectoryPermissions: function(permissions) { + var isCreatable = (permissions & OC.PERMISSION_CREATE) !== 0; + this.$el.find('#permissions').val(permissions); + this.$el.find('.creatable').toggleClass('hidden', !isCreatable); + this.$el.find('.notCreatable').toggleClass('hidden', isCreatable); + }, + /** + * Shows/hides action buttons + * + * @param show true for enabling, false for disabling + */ + showActions: function(show){ + this.$el.find('.actions,#file_action_panel').toggleClass('hidden', !show); + if (show){ + // make sure to display according to permissions + var permissions = this.getDirectoryPermissions(); + var isCreatable = (permissions & OC.PERMISSION_CREATE) !== 0; + this.$el.find('.creatable').toggleClass('hidden', !isCreatable); + this.$el.find('.notCreatable').toggleClass('hidden', isCreatable); + // remove old style breadcrumbs (some apps might create them) + this.$el.find('#controls .crumb').remove(); + // refresh breadcrumbs in case it was replaced by an app + this.breadcrumb.render(); } - // use URL hash for IE8 else{ - window.location.hash = '?dir='+ encodeURIComponent(targetDir).replace(/%2F/g, '/'); + this.$el.find('.creatable, .notCreatable').addClass('hidden'); + } + }, + /** + * Enables/disables viewer mode. + * In viewer mode, apps can embed themselves under the controls bar. + * In viewer mode, the actions of the file list will be hidden. + * @param show true for enabling, false for disabling + */ + setViewerMode: function(show){ + this.showActions(!show); + this.$el.find('#filestable').toggleClass('hidden', show); + this.$el.trigger(new $.Event('changeViewerMode', {viewerModeEnabled: show})); + }, + /** + * Removes a file entry from the list + * @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 = this.findFileEl(name); + var index = fileEl.index(); + if (!fileEl.length) { + return null; + } + if (this._selectedFiles[fileEl.data('id')]) { + // remove from selection first + this._selectFileEl(fileEl, false); + this.updateSelectionSummary(); + } + if (this._dragOptions && (fileEl.data('permissions') & OC.PERMISSION_DELETE)) { + // file is only draggable when delete permissions are set + fileEl.find('td.filename').draggable('destroy'); + } + this.files.splice(index, 1); + fileEl.remove(); + // TODO: improve performance on batch update + this.isEmpty = !this.files.length; + if (typeof(options.updateSummary) === 'undefined' || !!options.updateSummary) { + this.updateEmptyContent(); + this.fileSummary.remove({type: fileEl.attr('data-type'), size: fileEl.attr('data-size')}, true); } - } - this.breadcrumb.setDirectory(this.getCurrentDirectory()); - }, - /** - * Sets the current sorting and refreshes the list - * - * @param sort sort attribute name - * @param direction sort direction, one of "asc" or "desc" - */ - setSort: function(sort, direction) { - var comparator = this.Comparators[sort] || this.Comparators.name; - this._sort = sort; - this._sortDirection = (direction === 'desc')?'desc':'asc'; - this._sortComparator = comparator; - if (direction === 'desc') { - this._sortComparator = function(fileInfo1, fileInfo2) { - return -comparator(fileInfo1, fileInfo2); - }; - } - this.$el.find('thead th .sort-indicator') - .removeClass(this.SORT_INDICATOR_ASC_CLASS + ' ' + this.SORT_INDICATOR_DESC_CLASS); - this.$el.find('thead th.column-' + sort + ' .sort-indicator') - .addClass(direction === 'desc' ? this.SORT_INDICATOR_DESC_CLASS : this.SORT_INDICATOR_ASC_CLASS); - }, - /** - * @brief Reloads the file list using ajax call - */ - reload: function() { - this._selectedFiles = {}; - this._selectionSummary.clear(); - this.$el.find('#select_all').prop('checked', false); - FileList.showMask(); - if (FileList._reloadCall) { - FileList._reloadCall.abort(); - } - FileList._reloadCall = $.ajax({ - url: Files.getAjaxUrl('list'), - data: { - dir: $('#dir').val(), - sort: FileList._sort, - sortdirection: FileList._sortDirection - }, - error: function(result) { - FileList.reloadCallback(result); - }, - success: function(result) { - FileList.reloadCallback(result); - } - }); - }, - reloadCallback: function(result) { - delete this._reloadCall; - this.hideMask(); - - if (!result || result.status === 'error') { - OC.Notification.show(result.data.message); - return; - } - if (result.status === 404) { - // go back home - this.changeDirectory('/'); - return; - } - // aborted ? - if (result.status === 0){ - return; - } + 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); + } - // TODO: should rather return upload file size through - // the files list ajax call - Files.updateStorageStatistics(true); + return fileEl; + }, + /** + * 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._sortComparator(fileData, this.files[index]) > 0) { + index++; + } + 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); + } + ); + }); - if (result.data.permissions) { - this.setDirectoryPermissions(result.data.permissions); - } + }, + + /** + * 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 self = this; + var tr, td, input, form; + tr = this.findFileEl(oldname); + var oldFileInfo = this.files[tr.index()]; + tr.data('renaming',true); + td = tr.children('td.filename'); + input = $('<input type="text" class="filename"/>').val(oldname); + form = $('<form></form>'); + form.append(input); + td.children('a.name').hide(); + td.append(form); + input.focus(); + //preselect input + var len = input.val().lastIndexOf('.'); + if ( len === -1 || + tr.data('type') === 'dir' ) { + len = input.val().length; + } + input.selectRange(0, len); + var checkInput = function () { + var filename = input.val(); + if (filename !== oldname) { + // Files.isFileNameValid(filename) throws an exception itself + OCA.Files.Files.isFileNameValid(filename); + if (self.inList(filename)) { + throw t('files', '{new_name} already exists', {new_name: filename}); + } + } + return true; + }; - this.setFiles(result.data.files); - }, - setDirectoryPermissions: function(permissions) { - var isCreatable = (permissions & OC.PERMISSION_CREATE) !== 0; - $('#permissions').val(permissions); - $('.creatable').toggleClass('hidden', !isCreatable); - $('.notCreatable').toggleClass('hidden', isCreatable); - }, - /** - * Shows/hides action buttons - * - * @param show true for enabling, false for disabling - */ - showActions: function(show){ - $('.actions,#file_action_panel').toggleClass('hidden', !show); - if (show){ - // make sure to display according to permissions - var permissions = this.getDirectoryPermissions(); - var isCreatable = (permissions & OC.PERMISSION_CREATE) !== 0; - $('.creatable').toggleClass('hidden', !isCreatable); - $('.notCreatable').toggleClass('hidden', isCreatable); - // remove old style breadcrumbs (some apps might create them) - $('#controls .crumb').remove(); - // refresh breadcrumbs in case it was replaced by an app - this.breadcrumb.render(); - } - else{ - $('.creatable, .notCreatable').addClass('hidden'); - } - }, - /** - * Enables/disables viewer mode. - * In viewer mode, apps can embed themselves under the controls bar. - * In viewer mode, the actions of the file list will be hidden. - * @param show true for enabling, false for disabling - */ - setViewerMode: function(show){ - this.showActions(!show); - $('#filestable').toggleClass('hidden', show); - }, - /** - * Removes a file entry from the list - * @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); - var index = fileEl.index(); - if (!fileEl.length) { - return null; - } - if (this._selectedFiles[fileEl.data('id')]) { - // remove from selection first - this._selectFileEl(fileEl, false); - this.updateSelectionSummary(); - } - if (fileEl.data('permissions') & OC.PERMISSION_DELETE) { - // file is only draggable when delete permissions are set - fileEl.find('td.filename').draggable('destroy'); - } - this.files.splice(index, 1); - fileEl.remove(); - // TODO: improve performance on batch update - FileList.isEmpty = !this.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); - } + form.submit(function(event) { + event.stopPropagation(); + event.preventDefault(); + try { + var newName = input.val(); + if (newName !== oldname) { + checkInput(); + // mark as loading + td.css('background-image', 'url('+ OC.imagePath('core', 'loading.gif') + ')'); + $.ajax({ + url: OC.filePath('files','ajax','rename.php'), + data: { + dir : self.getCurrentDirectory(), + newname: newName, + file: oldname + }, + success: function(result) { + var fileInfo; + if (!result || result.status === 'error') { + OC.dialogs.alert(result.data.message, t('core', 'Could not rename file')); + fileInfo = oldFileInfo; + } + else { + fileInfo = result.data; + } + // reinsert row + self.files.splice(tr.index(), 1); + tr.remove(); + self.add(fileInfo); + } + }); + } + input.tipsy('hide'); + tr.data('renaming',false); + 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.find('a.name span.nametext').text(basename); + if (newName.indexOf('.') > 0 && tr.data('type') !== 'dir') { + if ( ! td.find('a.name span.extension').exists() ) { + td.find('a.name span.nametext').append('<span class="extension"></span>'); + } + td.find('a.name span.extension').text(newName.substr(newName.lastIndexOf('.'))); + } + form.remove(); + self.fileActions.display( tr.find('td.filename'), true); + td.children('a.name').show(); + } catch (error) { + input.attr('title', error); + input.tipsy({gravity: 'w', trigger: 'manual'}); + input.tipsy('show'); + input.addClass('error'); + } + return false; + }); + input.keyup(function(event) { + // verify filename on typing + try { + checkInput(); + input.tipsy('hide'); + input.removeClass('error'); + } catch (error) { + input.attr('title', error); + input.tipsy({gravity: 'w', trigger: 'manual'}); + input.tipsy('show'); + input.addClass('error'); + } + if (event.keyCode === 27) { + input.tipsy('hide'); + tr.data('renaming',false); + form.remove(); + td.children('a.name').show(); + } + }); + input.click(function(event) { + event.stopPropagation(); + event.preventDefault(); + }); + input.blur(function() { + form.trigger('submit'); + }); + }, + inList:function(file) { + return this.findFileEl(file).length; + }, + /** + * Delete the given files from the given dir + * @param files file names list (without path) + * @param dir directory in which to delete the files, defaults to the current + * directory + */ + do_delete:function(files, dir) { + var self = this; + var params; + if (files && files.substr) { + files=[files]; + } + if (files) { + for (var i=0; i<files.length; i++) { + var deleteAction = this.findFileEl(files[i]).children("td.date").children(".action.delete"); + deleteAction.removeClass('delete-icon').addClass('progress-icon'); + } + } + // Finish any existing actions + if (this.lastAction) { + this.lastAction(); + } - 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); - } + params = { + dir: dir || this.getCurrentDirectory() + }; + if (files) { + params.files = JSON.stringify(files); + } + else { + // no files passed, delete all in current dir + params.allfiles = true; + // show spinner for all files + this.$fileList.find('tr>td.date .action.delete').removeClass('delete-icon').addClass('progress-icon'); + } - return fileEl; - }, - /** - * 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._sortComparator(fileData, this.files[index]) > 0) { - index++; - } - 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) { + $.post(OC.filePath('files', 'ajax', 'delete.php'), + params, + function(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); + if (params.allfiles) { + self.setFiles([]); } + else { + $.each(files,function(index,file) { + var fileEl = self.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'); + self.fileSummary.remove({type: fileEl.attr('data-type'), size: fileEl.attr('data-size')}); + }); + } + // TODO: this info should be returned by the ajax call! + self.updateEmptyContent(); + self.fileSummary.update(); + self.updateSelectionSummary(); + self.updateStorageStatistics(); } 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.')); + OC.Notification.show(t('files', 'Error deleting file.')); } // hide notification after 10 sec setTimeout(function() { OC.Notification.hide(); }, 10000); - } - } else { - OC.dialogs.alert(t('files', 'Error moving file'), t('files', 'Error')); - } - $td.css('background-image', oldBackgroundImage); - }); - }); - - }, - - /** - * Triggers file rename input field for the given file name. - * If the user enters a new name, the file will be renamed. - * - * @param oldname file name of the file to rename - */ - rename: function(oldname) { - var tr, td, input, form; - tr = FileList.findFileEl(oldname); - var oldFileInfo = this.files[tr.index()]; - tr.data('renaming',true); - td = tr.children('td.filename'); - input = $('<input type="text" class="filename"/>').val(oldname); - form = $('<form></form>'); - form.append(input); - td.children('a.name').hide(); - td.append(form); - input.focus(); - //preselect input - var len = input.val().lastIndexOf('.'); - if ( len === -1 || - tr.data('type') === 'dir' ) { - len = input.val().length; - } - input.selectRange(0, len); - var checkInput = function () { - var filename = input.val(); - if (filename !== oldname) { - // Files.isFileNameValid(filename) throws an exception itself - Files.isFileNameValid(filename); - if (FileList.inList(filename)) { - throw t('files', '{new_name} already exists', {new_name: filename}); - } - } - return true; - }; - - form.submit(function(event) { - event.stopPropagation(); - event.preventDefault(); - try { - var newName = input.val(); - if (newName !== oldname) { - checkInput(); - // 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, - file: oldname - }, - success: function(result) { - var fileInfo; - if (!result || result.status === 'error') { - OC.dialogs.alert(result.data.message, t('core', 'Could not rename file')); - fileInfo = oldFileInfo; + if (params.allfiles) { + // reload the page as we don't know what files were deleted + // and which ones remain + self.reload(); } else { - fileInfo = result.data; + $.each(files,function(index,file) { + var deleteAction = self.findFileEl(file).find('.action.delete'); + deleteAction.removeClass('progress-icon').addClass('delete-icon'); + }); } - // reinsert row - FileList.files.splice(tr.index(), 1); - tr.remove(); - FileList.add(fileInfo); } }); - } - input.tipsy('hide'); - tr.data('renaming',false); - 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.find('a.name span.nametext').text(basename); - if (newName.indexOf('.') > 0 && tr.data('type') !== 'dir') { - if ( ! td.find('a.name span.extension').exists() ) { - td.find('a.name span.nametext').append('<span class="extension"></span>'); - } - td.find('a.name span.extension').text(newName.substr(newName.lastIndexOf('.'))); - } - form.remove(); - FileActions.display( tr.find('td.filename'), true); - td.children('a.name').show(); - } catch (error) { - input.attr('title', error); - input.tipsy({gravity: 'w', trigger: 'manual'}); - input.tipsy('show'); - input.addClass('error'); - } - return false; - }); - input.keyup(function(event) { - // verify filename on typing - try { - checkInput(); - input.tipsy('hide'); - input.removeClass('error'); - } catch (error) { - input.attr('title', error); - input.tipsy({gravity: 'w', trigger: 'manual'}); - input.tipsy('show'); - input.addClass('error'); - } - if (event.keyCode === 27) { - input.tipsy('hide'); - tr.data('renaming',false); - form.remove(); - td.children('a.name').show(); - } - }); - input.click(function(event) { - event.stopPropagation(); - event.preventDefault(); - }); - input.blur(function() { - form.trigger('submit'); - }); - }, - inList:function(file) { - return FileList.findFileEl(file).length; - }, - /** - * Delete the given files from the given dir - * @param files file names list (without path) - * @param dir directory in which to delete the files, defaults to the current - * directory - */ - do_delete:function(files, dir) { - var params; - if (files && files.substr) { - files=[files]; - } - if (files) { - 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'); + }, + /** + * Creates the file summary section + */ + _createSummary: function() { + var $tr = $('<tr class="summary"></tr>'); + this.$el.find('tfoot').append($tr); + + return new OCA.Files.FileSummary($tr); + }, + updateEmptyContent: function() { + var permissions = this.getDirectoryPermissions(); + var isCreatable = (permissions & OC.PERMISSION_CREATE) !== 0; + this.$el.find('#emptycontent').toggleClass('hidden', !isCreatable || !this.isEmpty); + this.$el.find('#filestable thead th').toggleClass('hidden', this.isEmpty); + }, + /** + * Shows the loading mask. + * + * @see #hideMask + */ + showMask: function() { + // in case one was shown before + var $mask = this.$el.find('.mask'); + if ($mask.exists()) { + return; } - } - // Finish any existing actions - if (FileList.lastAction) { - FileList.lastAction(); - } - params = { - dir: dir || FileList.getCurrentDirectory() - }; - if (files) { - params.files = JSON.stringify(files); - } - else { - // no files passed, delete all in current dir - params.allfiles = true; - // show spinner for all files - this.$fileList.find('tr>td.date .action.delete').removeClass('delete-icon').addClass('progress-icon'); - } - - $.post(OC.filePath('files', 'ajax', 'delete.php'), - params, - function(result) { - if (result.status === 'success') { - if (params.allfiles) { - 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')}); - }); - } - // TODO: this info should be returned by the ajax call! - checkTrashStatus(); - FileList.updateEmptyContent(); - FileList.fileSummary.update(); - FileList.updateSelectionSummary(); - Files.updateStorageStatistics(); - } else { - if (result.status === 'error' && result.data.message) { - OC.Notification.show(result.data.message); - } - else { - OC.Notification.show(t('files', 'Error deleting file.')); - } - // hide notification after 10 sec - setTimeout(function() { - OC.Notification.hide(); - }, 10000); - if (params.allfiles) { - // reload the page as we don't know what files were deleted - // and which ones remain - FileList.reload(); - } - else { - $.each(files,function(index,file) { - var deleteAction = FileList.findFileEl(file).find('.action.delete'); - deleteAction.removeClass('progress-icon').addClass('delete-icon'); - }); - } - } + this.$table.addClass('hidden'); + + $mask = $('<div class="mask transparent"></div>'); + + $mask.css('background-image', 'url('+ OC.imagePath('core', 'loading.gif') + ')'); + $mask.css('background-repeat', 'no-repeat'); + this.$el.append($mask); + + $mask.removeClass('transparent'); + }, + /** + * Hide the loading mask. + * @see #showMask + */ + hideMask: function() { + this.$el.find('.mask').remove(); + this.$table.removeClass('hidden'); + }, + scrollTo:function(file) { + //scroll to and highlight preselected file + var $scrollToRow = this.findFileEl(file); + if ($scrollToRow.exists()) { + $scrollToRow.addClass('searchresult'); + $(window).scrollTop($scrollToRow.position().top); + //remove highlight when hovered over + $scrollToRow.one('hover', function() { + $scrollToRow.removeClass('searchresult'); }); - }, - /** - * Creates the file summary section - */ - _createSummary: function() { - var $tr = $('<tr class="summary"></tr>'); - this.$el.find('tfoot').append($tr); - - return new FileSummary($tr); - }, - updateEmptyContent: function() { - var permissions = $('#permissions').val(); - var isCreatable = (permissions & OC.PERMISSION_CREATE) !== 0; - $('#emptycontent').toggleClass('hidden', !isCreatable || !FileList.isEmpty); - $('#filestable thead th').toggleClass('hidden', FileList.isEmpty); - }, - /** - * Shows the loading mask. - * - * @see #hideMask - */ - showMask: function() { - // in case one was shown before - var $mask = $('#content .mask'); - if ($mask.exists()) { - return; - } - - this.$el.addClass('hidden'); - - $mask = $('<div class="mask transparent"></div>'); - - $mask.css('background-image', 'url('+ OC.imagePath('core', 'loading.gif') + ')'); - $mask.css('background-repeat', 'no-repeat'); - $('#content').append($mask); - - $mask.removeClass('transparent'); - }, - /** - * Hide the loading mask. - * @see #showMask - */ - hideMask: function() { - $('#content .mask').remove(); - this.$el.removeClass('hidden'); - }, - scrollTo:function(file) { - //scroll to and highlight preselected file - var $scrollToRow = FileList.findFileEl(file); - if ($scrollToRow.exists()) { - $scrollToRow.addClass('searchresult'); - $(window).scrollTop($scrollToRow.position().top); - //remove highlight when hovered over - $scrollToRow.one('hover', function() { - $scrollToRow.removeClass('searchresult'); + } + }, + filter:function(query) { + this.$fileList.find('tr').each(function(i,e) { + if ($(e).data('file').toString().toLowerCase().indexOf(query.toLowerCase()) !== -1) { + $(e).addClass("searchresult"); + } else { + $(e).removeClass("searchresult"); + } }); - } - }, - filter:function(query) { - $('#fileList tr').each(function(i,e) { - if ($(e).data('file').toString().toLowerCase().indexOf(query.toLowerCase()) !== -1) { - $(e).addClass("searchresult"); - } else { + //do not use scrollto to prevent removing searchresult css class + var first = this.$fileList.find('tr.searchresult').first(); + if (first.exists()) { + $(window).scrollTop(first.position().top); + } + }, + unfilter:function() { + this.$fileList.find('tr.searchresult').each(function(i,e) { $(e).removeClass("searchresult"); + }); + }, + /** + * Update UI based on the current selection + */ + updateSelectionSummary: function() { + var summary = this._selectionSummary.summary; + var canDelete; + if (summary.totalFiles === 0 && summary.totalDirs === 0) { + this.$el.find('#headerName a.name>span:first').text(t('files','Name')); + this.$el.find('#headerSize a>span:first').text(t('files','Size')); + this.$el.find('#modified a>span:first').text(t('files','Modified')); + this.$el.find('table').removeClass('multiselect'); + this.$el.find('.selectedActions').addClass('hidden'); } - }); - //do not use scrollto to prevent removing searchresult css class - var first = $('#fileList tr.searchresult').first(); - if (first.exists()) { - $(window).scrollTop(first.position().top); - } - }, - unfilter:function() { - $('#fileList tr.searchresult').each(function(i,e) { - $(e).removeClass("searchresult"); - }); - }, - /** - * Update UI based on the current selection - */ - updateSelectionSummary: function() { - var summary = this._selectionSummary.summary; - if (summary.totalFiles === 0 && summary.totalDirs === 0) { - $('#headerName a.name>span:first').text(t('files','Name')); - $('#headerSize a>span:first').text(t('files','Size')); - $('#modified a>span:first').text(t('files','Modified')); - $('table').removeClass('multiselect'); - $('.selectedActions').addClass('hidden'); - } - else { - $('.selectedActions').removeClass('hidden'); - $('#headerSize a>span:first').text(OC.Util.humanFileSize(summary.totalSize)); - var selection = ''; - if (summary.totalDirs > 0) { - selection += n('files', '%n folder', '%n folders', summary.totalDirs); + else { + canDelete = (this.getDirectoryPermissions() & OC.PERMISSION_DELETE); + this.$el.find('.selectedActions').removeClass('hidden'); + this.$el.find('#headerSize a>span:first').text(OC.Util.humanFileSize(summary.totalSize)); + var selection = ''; + if (summary.totalDirs > 0) { + selection += n('files', '%n folder', '%n folders', summary.totalDirs); + if (summary.totalFiles > 0) { + selection += ' & '; + } + } if (summary.totalFiles > 0) { - selection += ' & '; + selection += n('files', '%n file', '%n files', summary.totalFiles); } + this.$el.find('#headerName a.name>span:first').text(selection); + this.$el.find('#modified a>span:first').text(''); + this.$el.find('table').addClass('multiselect'); + this.$el.find('.delete-selected').toggleClass('hidden', !canDelete); } - if (summary.totalFiles > 0) { - selection += n('files', '%n file', '%n files', summary.totalFiles); + }, + + /** + * Returns whether all files are selected + * @return true if all files are selected, false otherwise + */ + isAllSelected: function() { + return this.$el.find('.select-all').prop('checked'); + }, + + /** + * Returns the file info of the selected files + * + * @return array of file names + */ + getSelectedFiles: function() { + return _.values(this._selectedFiles); + }, + + getUniqueName: function(name) { + if (this.findFileEl(name).exists()) { + var numMatch; + var parts=name.split('.'); + var extension = ""; + if (parts.length > 1) { + extension=parts.pop(); + } + var base=parts.join('.'); + numMatch=base.match(/\((\d+)\)/); + var num=2; + if (numMatch && numMatch.length>0) { + num=parseInt(numMatch[numMatch.length-1])+1; + base=base.split('('); + base.pop(); + base=$.trim(base.join('(')); + } + name=base+' ('+num+')'; + if (extension) { + name = name+'.'+extension; + } + // FIXME: ugly recursion + return this.getUniqueName(name); } - $('#headerName a.name>span:first').text(selection); - $('#modified a>span:first').text(''); - $('table').addClass('multiselect'); - } - }, + return name; + }, - /** - * Returns whether all files are selected - * @return true if all files are selected, false otherwise - */ - isAllSelected: function() { - return this.$el.find('#select_all').prop('checked'); - }, + /** + * Setup file upload events related to the file-upload plugin + */ + setupUploadEvents: function() { + var self = this; - /** - * Returns the file info of the selected files - * - * @return array of file names - */ - getSelectedFiles: function() { - return _.values(this._selectedFiles); - } -}; + // handle upload events + var fileUploadStart = this.$el.find('#file_upload_start'); -$(document).ready(function() { - FileList.initialize(); + fileUploadStart.on('fileuploaddrop', function(e, data) { + OC.Upload.log('filelist handle fileuploaddrop', e, data); - // handle upload events - var fileUploadStart = $('#file_upload_start'); + var dropTarget = $(e.originalEvent.target).closest('tr, .crumb'); + // check if dropped inside this list at all + if (dropTarget && !self.$el.has(dropTarget).length) { + return false; + } - fileUploadStart.on('fileuploaddrop', function(e, data) { - OC.Upload.log('filelist handle fileuploaddrop', e, data); + if (dropTarget && (dropTarget.data('type') === 'dir' || dropTarget.hasClass('crumb'))) { // drag&drop upload to folder - var dropTarget = $(e.originalEvent.target).closest('tr, .crumb'); - if (dropTarget && (dropTarget.data('type') === 'dir' || dropTarget.hasClass('crumb'))) { // drag&drop upload to folder + // remember as context + data.context = dropTarget; - // remember as context - data.context = dropTarget; + var dir = dropTarget.data('file'); + // if from file list, need to prepend parent dir + if (dir) { + var parentDir = self.getCurrentDirectory(); + if (parentDir[parentDir.length - 1] !== '/') { + parentDir += '/'; + } + dir = parentDir + dir; + } + else{ + // read full path from crumb + dir = dropTarget.data('dir') || '/'; + } - var dir = dropTarget.data('file'); - // if from file list, need to prepend parent dir - if (dir) { - var parentDir = $('#dir').val() || '/'; - if (parentDir[parentDir.length - 1] !== '/') { - parentDir += '/'; + // update folder in form + data.formData = function(form) { + return [ + {name: 'dir', value: dir}, + {name: 'requesttoken', value: oc_requesttoken}, + {name: 'file_directory', value: data.files[0].relativePath} + ]; + }; + } else { + // cancel uploads to current dir if no permission + var isCreatable = (self.getDirectoryPermissions() & OC.PERMISSION_CREATE) !== 0; + if (!isCreatable) { + return false; + } } - dir = parentDir + dir; - } - else{ - // read full path from crumb - dir = dropTarget.data('dir') || '/'; - } - - // update folder in form - data.formData = function(form) { - return [ - {name: 'dir', value: dir}, - {name: 'requesttoken', value: oc_requesttoken}, - {name: 'file_directory', value: data.files[0].relativePath} - ]; - }; - } else { - // cancel uploads to current dir if no permission - var isCreatable = (FileList.getDirectoryPermissions() & OC.PERMISSION_CREATE) !== 0; - if (!isCreatable) { - return false; - } - } - }); - fileUploadStart.on('fileuploadadd', function(e, data) { - OC.Upload.log('filelist handle fileuploadadd', e, data); + }); + fileUploadStart.on('fileuploadadd', function(e, data) { + OC.Upload.log('filelist handle fileuploadadd', e, data); - //finish delete if we are uploading a deleted file - if (FileList.deleteFiles && FileList.deleteFiles.indexOf(data.files[0].name)!==-1) { - FileList.finishDelete(null, true); //delete file before continuing - } + //finish delete if we are uploading a deleted file + if (self.deleteFiles && self.deleteFiles.indexOf(data.files[0].name)!==-1) { + self.finishDelete(null, true); //delete file before continuing + } - // add ui visualization to existing folder - if (data.context && data.context.data('type') === 'dir') { - // add to existing folder - - // update upload counter ui - var uploadText = data.context.find('.uploadtext'); - var currentUploads = parseInt(uploadText.attr('currentUploads'), 10); - currentUploads += 1; - uploadText.attr('currentUploads', currentUploads); - - var translatedText = n('files', 'Uploading %n file', 'Uploading %n files', currentUploads); - if (currentUploads === 1) { - var img = OC.imagePath('core', 'loading.gif'); - data.context.find('td.filename').attr('style','background-image:url('+img+')'); - uploadText.text(translatedText); - uploadText.show(); - } else { - uploadText.text(translatedText); - } - } + // add ui visualization to existing folder + if (data.context && data.context.data('type') === 'dir') { + // add to existing folder + + // update upload counter ui + var uploadText = data.context.find('.uploadtext'); + var currentUploads = parseInt(uploadText.attr('currentUploads'), 10); + currentUploads += 1; + uploadText.attr('currentUploads', currentUploads); + + var translatedText = n('files', 'Uploading %n file', 'Uploading %n files', currentUploads); + if (currentUploads === 1) { + var img = OC.imagePath('core', 'loading.gif'); + data.context.find('td.filename').attr('style','background-image:url('+img+')'); + uploadText.text(translatedText); + uploadText.show(); + } else { + uploadText.text(translatedText); + } + } - }); - /* - * when file upload done successfully add row to filelist - * update counter when uploading to sub folder - */ - fileUploadStart.on('fileuploaddone', function(e, data) { - OC.Upload.log('filelist handle fileuploaddone', e, data); - - var response; - if (typeof data.result === 'string') { - response = data.result; - } else { - // fetch response from iframe - response = data.result[0].body.innerText; - } - var result=$.parseJSON(response); + }); + /* + * when file upload done successfully add row to filelist + * update counter when uploading to sub folder + */ + fileUploadStart.on('fileuploaddone', function(e, data) { + OC.Upload.log('filelist handle fileuploaddone', e, data); + + var response; + if (typeof data.result === 'string') { + response = data.result; + } else { + // fetch response from iframe + response = data.result[0].body.innerText; + } + var result=$.parseJSON(response); + + if (typeof result[0] !== 'undefined' && result[0].status === 'success') { + var file = result[0]; + var size = 0; + + if (data.context && data.context.data('type') === 'dir') { + + // update upload counter ui + var uploadText = data.context.find('.uploadtext'); + var currentUploads = parseInt(uploadText.attr('currentUploads'), 10); + currentUploads -= 1; + uploadText.attr('currentUploads', currentUploads); + var translatedText = n('files', 'Uploading %n file', 'Uploading %n files', currentUploads); + if (currentUploads === 0) { + var img = OC.imagePath('core', 'filetypes/folder'); + data.context.find('td.filename').attr('style','background-image:url('+img+')'); + uploadText.text(translatedText); + uploadText.hide(); + } else { + uploadText.text(translatedText); + } - if (typeof result[0] !== 'undefined' && result[0].status === 'success') { - var file = result[0]; - var size = 0; + // update folder size + size = parseInt(data.context.data('size'), 10); + size += parseInt(file.size, 10); + data.context.attr('data-size', size); + data.context.find('td.filesize').text(humanFileSize(size)); + } else { + // only append new file if uploaded into the current folder + if (file.directory !== '/' && file.directory !== self.getCurrentDirectory()) { + + var fileDirectory = file.directory.replace('/','').replace(/\/$/, "").split('/'); + + if (fileDirectory.length === 1) { + fileDirectory = fileDirectory[0]; + + // Get the directory + var fd = self.findFileEl(fileDirectory); + if (fd.length === 0) { + var dir = { + name: fileDirectory, + type: 'dir', + mimetype: 'httpd/unix-directory', + permissions: file.permissions, + size: 0, + id: file.parentId + }; + self.add(dir, {insert: true}); + } + } else { + fileDirectory = fileDirectory[0]; + } + + fileDirectory = self.findFileEl(fileDirectory); - if (data.context && data.context.data('type') === 'dir') { + // update folder size + size = parseInt(fileDirectory.attr('data-size'), 10); + size += parseInt(file.size, 10); + fileDirectory.attr('data-size', size); + fileDirectory.find('td.filesize').text(humanFileSize(size)); - // update upload counter ui - var uploadText = data.context.find('.uploadtext'); - var currentUploads = parseInt(uploadText.attr('currentUploads'), 10); - currentUploads -= 1; - uploadText.attr('currentUploads', currentUploads); - var translatedText = n('files', 'Uploading %n file', 'Uploading %n files', currentUploads); - if (currentUploads === 0) { - var img = OC.imagePath('core', 'filetypes/folder'); - data.context.find('td.filename').attr('style','background-image:url('+img+')'); - uploadText.text(translatedText); - uploadText.hide(); - } else { - uploadText.text(translatedText); - } + return; + } - // update folder size - size = parseInt(data.context.data('size'), 10); - size += parseInt(file.size, 10); - data.context.attr('data-size', size); - data.context.find('td.filesize').text(humanFileSize(size)); - } else { - // only append new file if uploaded into the current folder - if (file.directory !== '/' && file.directory !== FileList.getCurrentDirectory()) { - - var fileDirectory = file.directory.replace('/','').replace(/\/$/, "").split('/'); - - if (fileDirectory.length === 1) { - fileDirectory = fileDirectory[0]; - - // Get the directory - var fd = FileList.findFileEl(fileDirectory); - if (fd.length === 0) { - var dir = { - name: fileDirectory, - type: 'dir', - mimetype: 'httpd/unix-directory', - permissions: file.permissions, - size: 0, - id: file.parentId - }; - FileList.add(dir, {insert: true}); + // add as stand-alone row to filelist + size = t('files', 'Pending'); + if (data.files[0].size>=0) { + size=data.files[0].size; } - } else { - fileDirectory = fileDirectory[0]; - } - - fileDirectory = FileList.findFileEl(fileDirectory); + //should the file exist in the list remove it + self.remove(file.name); - // update folder size - size = parseInt(fileDirectory.attr('data-size'), 10); - size += parseInt(file.size, 10); - fileDirectory.attr('data-size', size); - fileDirectory.find('td.filesize').text(humanFileSize(size)); + // create new file context + data.context = self.add(file, {animate: true}); + } + } + }); + fileUploadStart.on('fileuploadstop', function(e, data) { + OC.Upload.log('filelist handle fileuploadstop', e, data); - return; + //if user pressed cancel hide upload chrome + if (data.errorThrown === 'abort') { + //cleanup uploading to a dir + var uploadText = $('tr .uploadtext'); + var img = OC.imagePath('core', 'filetypes/folder'); + uploadText.parents('td.filename').attr('style','background-image:url('+img+')'); + uploadText.fadeOut(); + uploadText.attr('currentUploads', 0); } + self.updateStorageStatistics(); + }); + fileUploadStart.on('fileuploadfail', function(e, data) { + OC.Upload.log('filelist handle fileuploadfail', e, data); - // add as stand-alone row to filelist - size = t('files', 'Pending'); - if (data.files[0].size>=0) { - size=data.files[0].size; + //if user pressed cancel hide upload chrome + if (data.errorThrown === 'abort') { + //cleanup uploading to a dir + var uploadText = $('tr .uploadtext'); + var img = OC.imagePath('core', 'filetypes/folder'); + uploadText.parents('td.filename').attr('style','background-image:url('+img+')'); + uploadText.fadeOut(); + uploadText.attr('currentUploads', 0); } - //should the file exist in the list remove it - FileList.remove(file.name); + self.updateStorageStatistics(); + }); - // create new file context - data.context = FileList.add(file, {animate: true}); - } - } - }); - fileUploadStart.on('fileuploadstop', function(e, data) { - OC.Upload.log('filelist handle fileuploadstop', e, data); - - //if user pressed cancel hide upload chrome - if (data.errorThrown === 'abort') { - //cleanup uploading to a dir - var uploadText = $('tr .uploadtext'); - var img = OC.imagePath('core', 'filetypes/folder'); - uploadText.parents('td.filename').attr('style','background-image:url('+img+')'); - uploadText.fadeOut(); - uploadText.attr('currentUploads', 0); } - }); - fileUploadStart.on('fileuploadfail', function(e, data) { - OC.Upload.log('filelist handle fileuploadfail', e, data); - - //if user pressed cancel hide upload chrome - if (data.errorThrown === 'abort') { - //cleanup uploading to a dir - var uploadText = $('tr .uploadtext'); - var img = OC.imagePath('core', 'filetypes/folder'); - uploadText.parents('td.filename').attr('style','background-image:url('+img+')'); - uploadText.fadeOut(); - uploadText.attr('currentUploads', 0); - } - }); + }; - $('#notification').hide(); - $('#notification:first-child').on('click', '.replace', function() { - OC.Notification.hide(function() { - FileList.replace( - $('#notification > span').attr('data-oldName'), - $('#notification > span').attr('data-newName'), - $('#notification > span').attr('data-isNewFile')); - }); - }); - $('#notification:first-child').on('click', '.suggest', function() { - var file = $('#notification > span').attr('data-oldName'); - FileList.findFileEl(file).removeClass('hidden'); - OC.Notification.hide(); - }); - $('#notification:first-child').on('click', '.cancel', function() { - if ($('#notification > span').attr('data-isNewFile')) { - FileList.deleteCanceled = false; - FileList.deleteFiles = [$('#notification > span').attr('data-oldName')]; + /** + * Sort comparators. + */ + FileList.Comparators = { + /** + * Compares two file infos by name, making directories appear + * first. + * + * @param fileInfo1 file info + * @param fileInfo2 file info + * @return -1 if the first file must appear before the second one, + * 0 if they are identify, 1 otherwise. + */ + name: 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); + }, + /** + * Compares two file infos by size. + * + * @param fileInfo1 file info + * @param fileInfo2 file info + * @return -1 if the first file must appear before the second one, + * 0 if they are identify, 1 otherwise. + */ + size: function(fileInfo1, fileInfo2) { + return fileInfo1.size - fileInfo2.size; + }, + /** + * Compares two file infos by timestamp. + * + * @param fileInfo1 file info + * @param fileInfo2 file info + * @return -1 if the first file must appear before the second one, + * 0 if they are identify, 1 otherwise. + */ + mtime: function(fileInfo1, fileInfo2) { + return fileInfo1.mtime - fileInfo2.mtime; } - }); - FileList.useUndo=(window.onbeforeunload)?true:false; + }; + + OCA.Files.FileList = FileList; +})(); + +$(document).ready(function() { + // FIXME: unused ? + OCA.Files.FileList.useUndo = (window.onbeforeunload)?true:false; $(window).bind('beforeunload', function () { - if (FileList.lastAction) { - FileList.lastAction(); + if (OCA.Files.FileList.lastAction) { + OCA.Files.FileList.lastAction(); } }); $(window).unload(function () { $(window).trigger('beforeunload'); }); - function decodeQuery(query) { - return query.replace(/\+/g, ' '); - } - - function parseHashQuery() { - var hash = window.location.hash, - pos = hash.indexOf('?'); - if (pos >= 0) { - return hash.substr(pos + 1); - } - return ''; - } - - function parseCurrentDirFromUrl() { - var query = parseHashQuery(), - params; - // try and parse from URL hash first - if (query) { - params = OC.parseQueryString(decodeQuery(query)); - } - // else read from query attributes - if (!params) { - params = OC.parseQueryString(decodeQuery(location.search)); - } - return (params && params.dir) || '/'; - } - - // disable ajax/history API for public app (TODO: until it gets ported) - // fallback to hashchange when no history support - if (!window.history.pushState) { - $(window).on('hashchange', function() { - FileList.changeDirectory(parseCurrentDirFromUrl(), false); - }); - } - window.onpopstate = function(e) { - var targetDir; - if (e.state && e.state.dir) { - targetDir = e.state.dir; - } - else{ - // read from URL - targetDir = parseCurrentDirFromUrl(); - } - if (targetDir) { - FileList.changeDirectory(targetDir, false); - } - }; - - $(window).scroll(function(e) {FileList._onScroll(e);}); - - var dir = parseCurrentDirFromUrl(); - // trigger ajax load, deferred to let sub-apps do their overrides first - setTimeout(function() { - FileList.changeDirectory(dir, false, true); - }, 0); }); - -/** - * Sort comparators. - */ -FileList.Comparators = { - /** - * Compares two file infos by name, making directories appear - * first. - * - * @param fileInfo1 file info - * @param fileInfo2 file info - * @return -1 if the first file must appear before the second one, - * 0 if they are identify, 1 otherwise. - */ - name: 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); - }, - /** - * Compares two file infos by size. - * - * @param fileInfo1 file info - * @param fileInfo2 file info - * @return -1 if the first file must appear before the second one, - * 0 if they are identify, 1 otherwise. - */ - size: function(fileInfo1, fileInfo2) { - return fileInfo1.size - fileInfo2.size; - }, - /** - * Compares two file infos by timestamp. - * - * @param fileInfo1 file info - * @param fileInfo2 file info - * @return -1 if the first file must appear before the second one, - * 0 if they are identify, 1 otherwise. - */ - mtime: function(fileInfo1, fileInfo2) { - return fileInfo1.mtime - fileInfo2.mtime; - } -}; - diff --git a/apps/files/js/files.js b/apps/files/js/files.js index 2373f6eba29..92f97f5d3c0 100644 --- a/apps/files/js/files.js +++ b/apps/files/js/files.js @@ -8,257 +8,296 @@ * */ -/* global OC, t, FileList */ /* global getURLParameter */ -var Files = { - // file space size sync - _updateStorageStatistics: function() { - Files._updateStorageStatisticsTimeout = null; - var currentDir = FileList.getCurrentDirectory(), - state = Files.updateStorageStatistics; - if (state.dir){ - if (state.dir === currentDir) { +/** + * Utility class for file related operations + */ +(function() { + var Files = { + // file space size sync + _updateStorageStatistics: function(currentDir) { + var state = Files.updateStorageStatistics; + if (state.dir){ + if (state.dir === currentDir) { + return; + } + // cancel previous call, as it was for another dir + state.call.abort(); + } + state.dir = currentDir; + state.call = $.getJSON(OC.filePath('files','ajax','getstoragestats.php') + '?dir=' + encodeURIComponent(currentDir),function(response) { + state.dir = null; + state.call = null; + Files.updateMaxUploadFilesize(response); + }); + }, + /** + * Update storage statistics such as free space, max upload, + * etc based on the given directory. + * + * Note this function is debounced to avoid making too + * many ajax calls in a row. + * + * @param dir directory + * @param force whether to force retrieving + */ + updateStorageStatistics: function(dir, force) { + if (!OC.currentUser) { return; } - // cancel previous call, as it was for another dir - state.call.abort(); - } - state.dir = currentDir; - state.call = $.getJSON(OC.filePath('files','ajax','getstoragestats.php') + '?dir=' + encodeURIComponent(currentDir),function(response) { - state.dir = null; - state.call = null; - Files.updateMaxUploadFilesize(response); - }); - }, - updateStorageStatistics: function(force) { - if (!OC.currentUser) { - return; - } - - // debounce to prevent calling too often - if (Files._updateStorageStatisticsTimeout) { - clearTimeout(Files._updateStorageStatisticsTimeout); - } - if (force) { - Files._updateStorageStatistics(); - } - else { - Files._updateStorageStatisticsTimeout = setTimeout(Files._updateStorageStatistics, 250); - } - }, - - updateMaxUploadFilesize:function(response) { - if (response === undefined) { - return; - } - if (response.data !== undefined && response.data.uploadMaxFilesize !== undefined) { - $('#max_upload').val(response.data.uploadMaxFilesize); - $('#free_space').val(response.data.freeSpace); - $('#upload.button').attr('original-title', response.data.maxHumanFilesize); - $('#usedSpacePercent').val(response.data.usedSpacePercent); - Files.displayStorageWarnings(); - } - if (response[0] === undefined) { - return; - } - if (response[0].uploadMaxFilesize !== undefined) { - $('#max_upload').val(response[0].uploadMaxFilesize); - $('#upload.button').attr('original-title', response[0].maxHumanFilesize); - $('#usedSpacePercent').val(response[0].usedSpacePercent); - Files.displayStorageWarnings(); - } - - }, - - /** - * Fix path name by removing double slash at the beginning, if any - */ - fixPath: function(fileName) { - if (fileName.substr(0, 2) == '//') { - return fileName.substr(1); - } - return fileName; - }, - /** - * Checks whether the given file name is valid. - * @param name file name to check - * @return true if the file name is valid. - * Throws a string exception with an error message if - * the file name is not valid - */ - isFileNameValid: function (name) { - var trimmedName = name.trim(); - if (trimmedName === '.' || trimmedName === '..') - { - throw t('files', '"{name}" is an invalid file name.', {name: name}); - } else if (trimmedName.length === 0) { - throw t('files', 'File name cannot be empty.'); - } - // check for invalid characters - var invalidCharacters = - ['\\', '/', '<', '>', ':', '"', '|', '?', '*', '\n']; - for (var i = 0; i < invalidCharacters.length; i++) { - if (trimmedName.indexOf(invalidCharacters[i]) !== -1) { - throw t('files', "Invalid name, '\\', '/', '<', '>', ':', '\"', '|', '?' and '*' are not allowed."); + if (force) { + Files._updateStorageStatistics(dir); } - } - return true; - }, - displayStorageWarnings: function() { - if (!OC.Notification.isHidden()) { - return; - } - - var usedSpacePercent = $('#usedSpacePercent').val(); - if (usedSpacePercent > 98) { - OC.Notification.show(t('files', 'Your storage is full, files can not be updated or synced anymore!')); - return; - } - if (usedSpacePercent > 90) { - OC.Notification.show(t('files', 'Your storage is almost full ({usedSpacePercent}%)', - {usedSpacePercent: usedSpacePercent})); - } - }, - - displayEncryptionWarning: function() { - - if (!OC.Notification.isHidden()) { - return; - } + else { + Files._updateStorageStatisticsDebounced(dir); + } + }, - var encryptedFiles = $('#encryptedFiles').val(); - var initStatus = $('#encryptionInitStatus').val(); - if (initStatus === '0') { // enc not initialized, but should be - OC.Notification.show(t('files', 'Encryption App is enabled but your keys are not initialized, please log-out and log-in again')); - return; - } - if (initStatus === '1') { // encryption tried to init but failed - OC.Notification.show(t('files', 'Invalid private key for Encryption App. Please update your private key password in your personal settings to recover access to your encrypted files.')); - return; - } - if (encryptedFiles === '1') { - OC.Notification.show(t('files', 'Encryption was disabled but your files are still encrypted. Please go to your personal settings to decrypt your files.')); - return; - } - }, + updateMaxUploadFilesize:function(response) { + if (response === undefined) { + return; + } + if (response.data !== undefined && response.data.uploadMaxFilesize !== undefined) { + $('#max_upload').val(response.data.uploadMaxFilesize); + $('#free_space').val(response.data.freeSpace); + $('#upload.button').attr('original-title', response.data.maxHumanFilesize); + $('#usedSpacePercent').val(response.data.usedSpacePercent); + Files.displayStorageWarnings(); + } + if (response[0] === undefined) { + return; + } + if (response[0].uploadMaxFilesize !== undefined) { + $('#max_upload').val(response[0].uploadMaxFilesize); + $('#upload.button').attr('original-title', response[0].maxHumanFilesize); + $('#usedSpacePercent').val(response[0].usedSpacePercent); + Files.displayStorageWarnings(); + } - // TODO: move to FileList class - setupDragAndDrop: function() { - var $fileList = $('#fileList'); + }, - //drag/drop of files - $fileList.find('tr td.filename').each(function(i,e) { - if ($(e).parent().data('permissions') & OC.PERMISSION_DELETE) { - $(e).draggable(dragOptions); + /** + * Fix path name by removing double slash at the beginning, if any + */ + fixPath: function(fileName) { + if (fileName.substr(0, 2) == '//') { + return fileName.substr(1); } - }); - - $fileList.find('tr[data-type="dir"] td.filename').each(function(i,e) { - if ($(e).parent().data('permissions') & OC.PERMISSION_CREATE) { - $(e).droppable(folderDropOptions); + return fileName; + }, + + /** + * Checks whether the given file name is valid. + * @param name file name to check + * @return true if the file name is valid. + * Throws a string exception with an error message if + * the file name is not valid + */ + isFileNameValid: function (name) { + var trimmedName = name.trim(); + if (trimmedName === '.' || trimmedName === '..') + { + throw t('files', '"{name}" is an invalid file name.', {name: name}); + } else if (trimmedName.length === 0) { + throw t('files', 'File name cannot be empty.'); + } + // check for invalid characters + var invalidCharacters = + ['\\', '/', '<', '>', ':', '"', '|', '?', '*', '\n']; + for (var i = 0; i < invalidCharacters.length; i++) { + if (trimmedName.indexOf(invalidCharacters[i]) !== -1) { + throw t('files', "Invalid name, '\\', '/', '<', '>', ':', '\"', '|', '?' and '*' are not allowed."); + } + } + return true; + }, + displayStorageWarnings: function() { + if (!OC.Notification.isHidden()) { + return; } - }); - }, - - /** - * Returns the download URL of the given file(s) - * @param filename string or array of file names to download - * @param dir optional directory in which the file name is, defaults to the current directory - */ - getDownloadUrl: function(filename, dir) { - if ($.isArray(filename)) { - filename = JSON.stringify(filename); - } - var params = { - dir: dir || FileList.getCurrentDirectory(), - files: filename - }; - return this.getAjaxUrl('download', params); - }, - /** - * Returns the ajax URL for a given action - * @param action action string - * @param params optional params map - */ - getAjaxUrl: function(action, params) { - var q = ''; - if (params) { - q = '?' + OC.buildQueryString(params); - } - return OC.filePath('files', 'ajax', action + '.php') + q; - } -}; -$(document).ready(function() { - // FIXME: workaround for trashbin app - if (window.trashBinApp) { - return; - } - Files.displayEncryptionWarning(); - Files.bindKeyboardShortcuts(document, jQuery); + var usedSpacePercent = $('#usedSpacePercent').val(); + if (usedSpacePercent > 98) { + OC.Notification.show(t('files', 'Your storage is full, files can not be updated or synced anymore!')); + return; + } + if (usedSpacePercent > 90) { + OC.Notification.show(t('files', 'Your storage is almost full ({usedSpacePercent}%)', + {usedSpacePercent: usedSpacePercent})); + } + }, - Files.setupDragAndDrop(); + displayEncryptionWarning: function() { - $('#file_action_panel').attr('activeAction', false); + if (!OC.Notification.isHidden()) { + return; + } - // Triggers invisible file input - $('#upload a').on('click', function() { - $(this).parent().children('#file_upload_start').trigger('click'); - return false; - }); + var encryptedFiles = $('#encryptedFiles').val(); + var initStatus = $('#encryptionInitStatus').val(); + if (initStatus === '0') { // enc not initialized, but should be + OC.Notification.show(t('files', 'Encryption App is enabled but your keys are not initialized, please log-out and log-in again')); + return; + } + if (initStatus === '1') { // encryption tried to init but failed + OC.Notification.show(t('files', 'Invalid private key for Encryption App. Please update your private key password in your personal settings to recover access to your encrypted files.')); + return; + } + if (encryptedFiles === '1') { + OC.Notification.show(t('files', 'Encryption was disabled but your files are still encrypted. Please go to your personal settings to decrypt your files.')); + return; + } + }, + + /** + * Returns the download URL of the given file(s) + * @param filename string or array of file names to download + * @param dir optional directory in which the file name is, defaults to the current directory + */ + getDownloadUrl: function(filename, dir) { + if ($.isArray(filename)) { + filename = JSON.stringify(filename); + } + var params = { + dir: dir, + files: filename + }; + return this.getAjaxUrl('download', params); + }, + + /** + * Returns the ajax URL for a given action + * @param action action string + * @param params optional params map + */ + getAjaxUrl: function(action, params) { + var q = ''; + if (params) { + q = '?' + OC.buildQueryString(params); + } + return OC.filePath('files', 'ajax', action + '.php') + q; + }, + + getMimeIcon: function(mime, ready) { + if (Files.getMimeIcon.cache[mime]) { + ready(Files.getMimeIcon.cache[mime]); + } else { + $.get( OC.filePath('files','ajax','mimeicon.php'), {mime: mime}, function(path) { + if(OC.Util.hasSVGSupport()){ + path = path.substr(0, path.length-4) + '.svg'; + } + Files.getMimeIcon.cache[mime]=path; + ready(Files.getMimeIcon.cache[mime]); + }); + } + }, + + /** + * Generates a preview URL based on the URL space. + * @param urlSpec map with {x: width, y: height, file: file path} + * @return preview URL + * @deprecated used OCA.Files.FileList.generatePreviewUrl instead + */ + generatePreviewUrl: function(urlSpec) { + console.warn('DEPRECATED: please use generatePreviewUrl() from an OCA.Files.FileList instance'); + return OCA.Files.App.fileList.generatePreviewUrl(urlSpec); + }, + + /** + * Lazy load preview + * @deprecated used OCA.Files.FileList.lazyLoadPreview instead + */ + lazyLoadPreview : function(path, mime, ready, width, height, etag) { + console.warn('DEPRECATED: please use lazyLoadPreview() from an OCA.Files.FileList instance'); + return OCA.Files.App.fileList.lazyLoadPreview({ + path: path, + mime: mime, + callback: ready, + width: width, + height: height, + etag: etag + }); + }, + + /** + * Initialize the files view + */ + initialize: function() { + Files.getMimeIcon.cache = {}; + Files.displayEncryptionWarning(); + Files.bindKeyboardShortcuts(document, $); + + // TODO: move file list related code (upload) to OCA.Files.FileList + $('#file_action_panel').attr('activeAction', false); + + // Triggers invisible file input + $('#upload a').on('click', function() { + $(this).parent().children('#file_upload_start').trigger('click'); + return false; + }); - // Trigger cancelling of file upload - $('#uploadprogresswrapper .stop').on('click', function() { - OC.Upload.cancelUploads(); - FileList.updateSelectionSummary(); - }); + // Trigger cancelling of file upload + $('#uploadprogresswrapper .stop').on('click', function() { + OC.Upload.cancelUploads(); + }); - // Show trash bin - $('#trash').on('click', function() { - window.location=OC.filePath('files_trashbin', '', 'index.php'); - }); + // drag&drop support using jquery.fileupload + // TODO use OC.dialogs + $(document).bind('drop dragover', function (e) { + e.preventDefault(); // prevent browser from doing anything, if file isn't dropped in dropZone + }); + + //do a background scan if needed + scanFiles(); + + // display storage warnings + setTimeout(Files.displayStorageWarnings, 100); + OC.Notification.setDefault(Files.displayStorageWarnings); + + // only possible at the moment if user is logged in or the files app is loaded + if (OC.currentUser && OCA.Files.App) { + // start on load - we ask the server every 5 minutes + var updateStorageStatisticsInterval = 5*60*1000; + var updateStorageStatisticsIntervalId = setInterval(OCA.Files.App.fileList.updateStorageStatistics, updateStorageStatisticsInterval); + + // TODO: this should also stop when switching to another view + // Use jquery-visibility to de-/re-activate file stats sync + if ($.support.pageVisibility) { + $(document).on({ + 'show.visibility': function() { + if (!updateStorageStatisticsIntervalId) { + updateStorageStatisticsIntervalId = setInterval(OCA.Files.App.fileList.updateStorageStatistics, updateStorageStatisticsInterval); + } + }, + 'hide.visibility': function() { + clearInterval(updateStorageStatisticsIntervalId); + updateStorageStatisticsIntervalId = 0; + } + }); + } + } - // drag&drop support using jquery.fileupload - // TODO use OC.dialogs - $(document).bind('drop dragover', function (e) { - e.preventDefault(); // prevent browser from doing anything, if file isn't dropped in dropZone - }); - - //do a background scan if needed - scanFiles(); - - // display storage warnings - setTimeout(Files.displayStorageWarnings, 100); - OC.Notification.setDefault(Files.displayStorageWarnings); - - // only possible at the moment if user is logged in - if (OC.currentUser) { - // start on load - we ask the server every 5 minutes - var updateStorageStatisticsInterval = 5*60*1000; - var updateStorageStatisticsIntervalId = setInterval(Files.updateStorageStatistics, updateStorageStatisticsInterval); - - // Use jquery-visibility to de-/re-activate file stats sync - if ($.support.pageVisibility) { - $(document).on({ - 'show.visibility': function() { - if (!updateStorageStatisticsIntervalId) { - updateStorageStatisticsIntervalId = setInterval(Files.updateStorageStatistics, updateStorageStatisticsInterval); - } - }, - 'hide.visibility': function() { - clearInterval(updateStorageStatisticsIntervalId); - updateStorageStatisticsIntervalId = 0; + $('#app-settings-header').on('click', function() { + var $settings = $('#app-settings'); + $settings.toggleClass('opened'); + if ($settings.hasClass('opened')) { + $settings.find('input').focus(); } }); + + //scroll to and highlight preselected file + /* + if (getURLParameter('scrollto')) { + FileList.scrollTo(getURLParameter('scrollto')); + } + */ } } - //scroll to and highlight preselected file - if (getURLParameter('scrollto')) { - FileList.scrollTo(getURLParameter('scrollto')); - } -}); + Files._updateStorageStatisticsDebounced = _.debounce(Files._updateStorageStatistics, 250); + OCA.Files.Files = Files; +})(); function scanFiles(force, dir, users) { if (!OC.currentUser) { @@ -292,7 +331,9 @@ function scanFiles(force, dir, users) { scannerEventSource.listen('done',function(count) { scanFiles.scanning=false; console.log('done after ' + count + ' files'); - Files.updateStorageStatistics(); + if (OCA.Files.App) { + OCA.Files.App.fileList.updateStorageStatistics(true); + } }); scannerEventSource.listen('user',function(user) { console.log('scanning files for ' + user); @@ -303,6 +344,7 @@ scanFiles.scanning=false; // TODO: move to FileList var createDragShadow = function(event) { //select dragged file + var FileList = OCA.Files.App.fileList; var isDragSelected = $(event.target).parents('tr').find('td input:first').prop('checked'); if (!isDragSelected) { //select dragged file @@ -323,7 +365,7 @@ var createDragShadow = function(event) { var tbody = $('<tbody></tbody>'); dragshadow.append(tbody); - var dir=$('#dir').val(); + var dir = FileList.getCurrentDirectory(); $(selectedFiles).each(function(i,elem) { var newtr = $('<tr/>') @@ -336,8 +378,8 @@ var createDragShadow = function(event) { if (elem.type === 'dir') { newtr.find('td.filename').attr('style','background-image:url('+OC.imagePath('core', 'filetypes/folder.png')+')'); } else { - var path = getPathForPreview(elem.name); - Files.lazyLoadPreview(path, elem.mime, function(previewpath) { + var path = dir + '/' + elem.name; + OCA.Files.App.files.lazyLoadPreview(path, elem.mime, function(previewpath) { newtr.find('td.filename').attr('style','background-image:url('+previewpath+')'); }, null, null, elem.etag); } @@ -350,9 +392,14 @@ var createDragShadow = function(event) { //start&stop handlers needs some cleaning up // TODO: move to FileList class var dragOptions={ - revert: 'invalid', revertDuration: 300, - opacity: 0.7, zIndex: 100, appendTo: 'body', cursorAt: { left: 24, top: 18 }, - helper: createDragShadow, cursor: 'move', + 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){ @@ -383,6 +430,7 @@ var folderDropOptions = { hoverClass: "canDrop", drop: function( event, ui ) { // don't allow moving a file into a selected folder + var FileList = OCA.Files.App.fileList; if ($(event.target).parents('tr').find('td input:first').prop('checked') === true) { return false; } @@ -400,115 +448,11 @@ var folderDropOptions = { tolerance: 'pointer' }; -Files.getMimeIcon = function(mime, ready) { - if (Files.getMimeIcon.cache[mime]) { - ready(Files.getMimeIcon.cache[mime]); - } else { - $.get( OC.filePath('files','ajax','mimeicon.php'), {mime: mime}, function(path) { - if(OC.Util.hasSVGSupport()){ - path = path.substr(0, path.length-4) + '.svg'; - } - Files.getMimeIcon.cache[mime]=path; - ready(Files.getMimeIcon.cache[mime]); - }); - } -} -Files.getMimeIcon.cache={}; - -function getPathForPreview(name) { - var path = $('#dir').val() + '/' + name; - return path; -} - -/** - * Generates a preview URL based on the URL space. - * @param urlSpec map with {x: width, y: height, file: file path} - * @return preview URL - */ -Files.generatePreviewUrl = function(urlSpec) { - urlSpec = urlSpec || {}; - if (!urlSpec.x) { - urlSpec.x = $('#filestable').data('preview-x'); - } - if (!urlSpec.y) { - urlSpec.y = $('#filestable').data('preview-y'); - } - urlSpec.y *= window.devicePixelRatio; - 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 - Files.getMimeIcon(mime, function(iconURL) { - var previewURL, - urlSpec = {}; - ready(iconURL); // set mimeicon URL - - urlSpec.file = Files.fixPath(path); - - if (etag){ - // use etag as cache buster - urlSpec.c = etag; - } - else { - console.warn('Files.lazyLoadPreview(): missing etag argument'); - } - - previewURL = Files.generatePreviewUrl(urlSpec); - previewURL = previewURL.replace('(', '%28'); - previewURL = previewURL.replace(')', '%29'); - - // preload image to prevent delay - // this will make the browser cache the image - var img = new Image(); - img.onload = function(){ - // if loading the preview image failed (no preview for the mimetype) then img.width will < 5 - if (img.width > 5) { - ready(previewURL); - } - }; - img.src = previewURL; - }); -}; - -function getUniqueName(name) { - if (FileList.findFileEl(name).exists()) { - var numMatch; - var parts=name.split('.'); - var extension = ""; - if (parts.length > 1) { - extension=parts.pop(); - } - var base=parts.join('.'); - numMatch=base.match(/\((\d+)\)/); - var num=2; - if (numMatch && numMatch.length>0) { - num=parseInt(numMatch[numMatch.length-1])+1; - base=base.split('('); - base.pop(); - base=$.trim(base.join('(')); - } - name=base+' ('+num+')'; - if (extension) { - name = name+'.'+extension; - } - return getUniqueName(name); - } - return name; -} - -function checkTrashStatus() { - $.post(OC.filePath('files_trashbin', 'ajax', 'isEmpty.php'), function(result) { - if (result.data.isEmpty === false) { - $("input[type=button][id=trash]").removeAttr("disabled"); - } - }); -} - // override core's fileDownloadPath (legacy) function fileDownloadPath(dir, file) { - return Files.getDownloadUrl(file, dir); + return OCA.Files.Files.getDownloadUrl(file, dir); } +// for backward compatibility +window.Files = OCA.Files.Files; + diff --git a/apps/files/js/filesummary.js b/apps/files/js/filesummary.js index b5130247cc9..104dabf1b04 100644 --- a/apps/files/js/filesummary.js +++ b/apps/files/js/filesummary.js @@ -190,6 +190,6 @@ this.$el.append($summary); } }; - window.FileSummary = FileSummary; + OCA.Files.FileSummary = FileSummary; })(); diff --git a/apps/files/js/keyboardshortcuts.js b/apps/files/js/keyboardshortcuts.js index 9d6c3ae8c33..b2f2cd0e582 100644 --- a/apps/files/js/keyboardshortcuts.js +++ b/apps/files/js/keyboardshortcuts.js @@ -12,7 +12,6 @@ * enter: open file/folder * delete/backspace: delete file/folder *****************************/ -var Files = Files || {}; (function(Files) { var keys = []; var keyCodes = { @@ -167,4 +166,4 @@ var Files = Files || {}; removeA(keys, event.keyCode); }); }; -})(Files); +})((OCA.Files && OCA.Files.Files) || {}); diff --git a/apps/files/js/navigation.js b/apps/files/js/navigation.js new file mode 100644 index 00000000000..c58a284e83f --- /dev/null +++ b/apps/files/js/navigation.js @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2014 + * + * @author Vincent Petry + * @copyright 2014 Vincent Petry <pvince81@owncloud.com> + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function() { + + var Navigation = function($el) { + this.initialize($el); + }; + + Navigation.prototype = { + + /** + * Currently selected item in the list + */ + _activeItem: null, + + /** + * Currently selected container + */ + $currentContent: null, + + /** + * Initializes the navigation from the given container + * @param $el element containing the navigation + */ + initialize: function($el) { + this.$el = $el; + this._activeItem = null; + this.$currentContent = null; + this._setupEvents(); + }, + + /** + * Setup UI events + */ + _setupEvents: function() { + this.$el.on('click', 'li a', _.bind(this._onClickItem, this)); + }, + + /** + * Returns the container of the currently active app. + * + * @return app container + */ + getActiveContainer: function() { + return this.$currentContent; + }, + + /** + * Returns the currently active item + * + * @return item ID + */ + getActiveItem: function() { + return this._activeItem; + }, + + /** + * Switch the currently selected item, mark it as selected and + * make the content container visible, if any. + * + * @param string itemId id of the navigation item to select + * @param array options "silent" to not trigger event + */ + setActiveItem: function(itemId, options) { + var oldItemId = this._activeItem; + if (itemId === this._activeItem) { + if (!options || !options.silent) { + this.$el.trigger( + new $.Event('itemChanged', {itemId: itemId, previousItemId: oldItemId}) + ); + } + return; + } + this.$el.find('li').removeClass('selected'); + if (this.$currentContent) { + this.$currentContent.addClass('hidden'); + this.$currentContent.trigger(jQuery.Event('hide')); + } + this._activeItem = itemId; + this.$el.find('li[data-id=' + itemId + ']').addClass('selected'); + this.$currentContent = $('#app-content-' + itemId); + this.$currentContent.removeClass('hidden'); + if (!options || !options.silent) { + this.$currentContent.trigger(jQuery.Event('show')); + this.$el.trigger( + new $.Event('itemChanged', {itemId: itemId, previousItemId: oldItemId}) + ); + } + }, + + /** + * Returns whether a given item exists + */ + itemExists: function(itemId) { + return this.$el.find('li[data-id=' + itemId + ']').length; + }, + + /** + * Event handler for when clicking on an item. + */ + _onClickItem: function(ev) { + var $target = $(ev.target); + var itemId = $target.closest('li').attr('data-id'); + this.setActiveItem(itemId); + return false; + } + }; + + OCA.Files.Navigation = Navigation; + +})(); |