diff options
Diffstat (limited to 'apps/files/js')
-rw-r--r-- | apps/files/js/detailfileinfoview.js | 96 | ||||
-rw-r--r-- | apps/files/js/detailsview.js | 251 | ||||
-rw-r--r-- | apps/files/js/detailtabview.js | 136 | ||||
-rw-r--r-- | apps/files/js/fileactions.js | 330 | ||||
-rw-r--r-- | apps/files/js/fileactionsmenu.js | 132 | ||||
-rw-r--r-- | apps/files/js/filelist.js | 200 | ||||
-rw-r--r-- | apps/files/js/files.js | 27 | ||||
-rw-r--r-- | apps/files/js/mainfileinfodetailview.js | 98 | ||||
-rw-r--r-- | apps/files/js/tagsplugin.js | 1 |
9 files changed, 1118 insertions, 153 deletions
diff --git a/apps/files/js/detailfileinfoview.js b/apps/files/js/detailfileinfoview.js new file mode 100644 index 00000000000..9a88b5e2d8a --- /dev/null +++ b/apps/files/js/detailfileinfoview.js @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2015 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function() { + /** + * @class OCA.Files.DetailFileInfoView + * @classdesc + * + * Displays a block of details about the file info. + * + */ + var DetailFileInfoView = function() { + this.initialize(); + }; + /** + * @memberof OCA.Files + */ + DetailFileInfoView.prototype = { + /** + * jQuery element + */ + $el: null, + + _template: null, + + /** + * Currently displayed file info + * + * @type OCA.Files.FileInfo + */ + _fileInfo: null, + + /** + * Initialize the details view + */ + initialize: function() { + this.$el = $('<div class="detailFileInfoView"></div>'); + }, + + /** + * returns the jQuery object for HTML output + * + * @returns {jQuery} + */ + get$: function() { + return this.$el; + }, + + /** + * Destroy / uninitialize this instance. + */ + destroy: function() { + if (this.$el) { + this.$el.remove(); + } + }, + + /** + * Renders this details view + * + * @abstract + */ + render: function() { + // to be implemented in subclass + }, + + /** + * Sets the file info to be displayed in the view + * + * @param {OCA.Files.FileInfo} fileInfo file info to set + */ + setFileInfo: function(fileInfo) { + this._fileInfo = fileInfo; + this.render(); + }, + + /** + * Returns the file info. + * + * @return {OCA.Files.FileInfo} file info + */ + getFileInfo: function() { + return this._fileInfo; + } + }; + + OCA.Files.DetailFileInfoView = DetailFileInfoView; +})(); + diff --git a/apps/files/js/detailsview.js b/apps/files/js/detailsview.js new file mode 100644 index 00000000000..7b7bd013f9e --- /dev/null +++ b/apps/files/js/detailsview.js @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2015 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function() { + + var TEMPLATE = + '<div>' + + ' <div class="detailFileInfoContainer">' + + ' </div>' + + ' <div>' + + ' <ul class="tabHeaders">' + + ' </ul>' + + ' <div class="tabsContainer">' + + ' </div>' + + ' </div>' + + ' <a class="close icon-close" href="#" alt="{{closeLabel}}"></a>' + + '</div>'; + + var TEMPLATE_TAB_HEADER = + '<li class="tabHeader {{#if selected}}selected{{/if}}" data-tabid="{{tabId}}" data-tabindex="{{tabIndex}}"><a href="#">{{label}}</a></li>'; + + /** + * @class OCA.Files.DetailsView + * @classdesc + * + * The details view show details about a selected file. + * + */ + var DetailsView = function() { + this.initialize(); + }; + + /** + * @memberof OCA.Files + */ + DetailsView.prototype = { + + /** + * jQuery element + */ + $el: null, + + _template: null, + _templateTabHeader: null, + + /** + * Currently displayed file info + * + * @type OCA.Files.FileInfo + */ + _fileInfo: null, + + /** + * List of detail tab views + * + * @type Array<OCA.Files.DetailTabView> + */ + _tabViews: [], + + /** + * List of detail file info views + * + * @type Array<OCA.Files.DetailFileInfoView> + */ + _detailFileInfoViews: [], + + /** + * Id of the currently selected tab + * + * @type string + */ + _currentTabId: null, + + /** + * Initialize the details view + */ + initialize: function() { + this.$el = $('<div id="app-sidebar"></div>'); + this.fileInfo = null; + this._tabViews = []; + this._detailFileInfoViews = []; + + this.$el.on('click', 'a.close', function(event) { + OC.Apps.hideAppSidebar(); + event.preventDefault(); + }); + + this.$el.on('click', '.tabHeaders .tabHeader', _.bind(this._onClickTab, this)); + + // uncomment to add some dummy tabs for testing + //this._addTestTabs(); + }, + + /** + * Destroy / uninitialize this instance. + */ + destroy: function() { + if (this.$el) { + this.$el.remove(); + } + }, + + _onClickTab: function(e) { + var $target = $(e.target); + if (!$target.hasClass('tabHeader')) { + $target = $target.closest('.tabHeader'); + } + var tabIndex = $target.attr('data-tabindex'); + var targetTab; + if (_.isUndefined(tabIndex)) { + return; + } + + this.$el.find('.tabsContainer .tab').addClass('hidden'); + targetTab = this._tabViews[tabIndex]; + targetTab.$el.removeClass('hidden'); + + this.$el.find('.tabHeaders li').removeClass('selected'); + $target.addClass('selected'); + + e.preventDefault(); + }, + + _addTestTabs: function() { + for (var j = 0; j < 2; j++) { + var testView = new OCA.Files.DetailTabView('testtab' + j); + testView.index = j; + testView.getLabel = function() { return 'Test tab ' + this.index; }; + testView.render = function() { + this.$el.empty(); + for (var i = 0; i < 100; i++) { + this.$el.append('<div>Test tab ' + this.index + ' row ' + i + '</div>'); + } + }; + this._tabViews.push(testView); + } + }, + + /** + * Renders this details view + */ + render: function() { + var self = this; + this.$el.empty(); + + if (!this._template) { + this._template = Handlebars.compile(TEMPLATE); + } + + if (!this._templateTabHeader) { + this._templateTabHeader = Handlebars.compile(TEMPLATE_TAB_HEADER); + } + + var $el = $(this._template({ + closeLabel: t('files', 'Close') + })); + var $tabsContainer = $el.find('.tabsContainer'); + var $tabHeadsContainer = $el.find('.tabHeaders'); + var $detailsContainer = $el.find('.detailFileInfoContainer'); + + // render details + _.each(this._detailFileInfoViews, function(detailView) { + $detailsContainer.append(detailView.get$()); + }); + + if (this._tabViews.length > 0) { + if (!this._currentTab) { + this._currentTab = this._tabViews[0].getId(); + } + + // render tabs + _.each(this._tabViews, function(tabView, i) { + // hidden by default + var $el = tabView.get$(); + var isCurrent = (tabView.getId() === self._currentTab); + if (!isCurrent) { + $el.addClass('hidden'); + } + $tabsContainer.append($el); + + $tabHeadsContainer.append(self._templateTabHeader({ + tabId: tabView.getId(), + tabIndex: i, + label: tabView.getLabel(), + selected: isCurrent + })); + }); + } + + // TODO: select current tab + + this.$el.append($el); + }, + + /** + * Sets the file info to be displayed in the view + * + * @param {OCA.Files.FileInfo} fileInfo file info to set + */ + setFileInfo: function(fileInfo) { + this._fileInfo = fileInfo; + + this.render(); + + // notify all panels + _.each(this._tabViews, function(tabView) { + tabView.setFileInfo(fileInfo); + }); + _.each(this._detailFileInfoViews, function(detailView) { + detailView.setFileInfo(fileInfo); + }); + }, + + /** + * Returns the file info. + * + * @return {OCA.Files.FileInfo} file info + */ + getFileInfo: function() { + return this._fileInfo; + }, + + /** + * Adds a tab in the tab view + * + * @param {OCA.Files.DetailTabView} tab view + */ + addTabView: function(tabView) { + this._tabViews.push(tabView); + }, + + /** + * Adds a detail view for file info. + * + * @param {OCA.Files.DetailFileInfoView} detail view + */ + addDetailView: function(detailView) { + this._detailFileInfoViews.push(detailView); + } + }; + + OCA.Files.DetailsView = DetailsView; +})(); + diff --git a/apps/files/js/detailtabview.js b/apps/files/js/detailtabview.js new file mode 100644 index 00000000000..b9b1dda2ccc --- /dev/null +++ b/apps/files/js/detailtabview.js @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2015 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function() { + + /** + * @class OCA.Files.DetailTabView + * @classdesc + * + * Base class for tab views to display file information. + * + */ + var DetailTabView = function(id) { + this.initialize(id); + }; + + /** + * @memberof OCA.Files + */ + DetailTabView.prototype = { + /** + * jQuery element + */ + $el: null, + + /** + * Tab id + */ + _id: null, + + /** + * Tab label + */ + _label: null, + + _template: null, + + /** + * Currently displayed file info + * + * @type OCA.Files.FileInfo + */ + _fileInfo: null, + + /** + * Initialize the details view + * + * @param {string} id tab id + */ + initialize: function(id) { + if (!id) { + throw 'Argument "id" is required'; + } + this._id = id; + this.$el = $('<div class="tab"></div>'); + this.$el.attr('data-tabid', id); + }, + + /** + * Destroy / uninitialize this instance. + */ + destroy: function() { + if (this.$el) { + this.$el.remove(); + } + }, + + /** + * Returns the tab element id + * + * @return {string} tab id + */ + getId: function() { + return this._id; + }, + + /** + * Returns the tab label + * + * @return {String} label + */ + getLabel: function() { + return 'Tab ' + this._id; + }, + + /** + * returns the jQuery object for HTML output + * + * @returns {jQuery} + */ + get$: function() { + return this.$el; + }, + + /** + * Renders this details view + * + * @abstract + */ + render: function() { + // to be implemented in subclass + // FIXME: code is only for testing + this.$el.empty(); + this.$el.append('<div>Hello ' + this._id + '</div>'); + }, + + /** + * Sets the file info to be displayed in the view + * + * @param {OCA.Files.FileInfo} fileInfo file info to set + */ + setFileInfo: function(fileInfo) { + this._fileInfo = fileInfo; + this.render(); + }, + + /** + * Returns the file info. + * + * @return {OCA.Files.FileInfo} file info + */ + getFileInfo: function() { + return this._fileInfo; + } + }; + + OCA.Files.DetailTabView = DetailTabView; +})(); + diff --git a/apps/files/js/fileactions.js b/apps/files/js/fileactions.js index 1956fda0077..43f74c5816d 100644 --- a/apps/files/js/fileactions.js +++ b/apps/files/js/fileactions.js @@ -10,6 +10,12 @@ (function() { + var TEMPLATE_FILE_ACTION_TRIGGER = + '<a class="action action-{{nameLowerCase}}" href="#" data-action="{{name}}">' + + '{{#if icon}}<img class="svg" alt="{{altText}}" src="{{icon}}" />{{/if}}' + + '{{#if displayName}}<span> {{displayName}}</span>{{/if}}' + + '</a>'; + /** * Construct a new FileActions instance * @constructs FileActions @@ -18,6 +24,8 @@ var FileActions = function() { this.initialize(); }; + FileActions.TYPE_DROPDOWN = 0; + FileActions.TYPE_INLINE = 1; FileActions.prototype = { /** @lends FileActions.prototype */ actions: {}, @@ -38,6 +46,8 @@ */ _updateListeners: {}, + _fileActionTriggerTemplate: null, + /** * @private */ @@ -46,6 +56,8 @@ // abusing jquery for events until we get a real event lib this.$el = $('<div class="dummy-fileactions hidden"></div>'); $('body').append(this.$el); + + this._showMenuClosure = _.bind(this._showMenu, this); }, /** @@ -111,6 +123,7 @@ displayName: displayName || name }); }, + /** * Register action * @@ -125,15 +138,14 @@ displayName: action.displayName, mime: mime, icon: action.icon, - permissions: action.permissions + permissions: action.permissions, + type: action.type || FileActions.TYPE_DROPDOWN }; if (_.isUndefined(action.displayName)) { actionSpec.displayName = t('files', name); } if (_.isFunction(action.render)) { actionSpec.render = action.render; - } else { - actionSpec.render = _.bind(this._defaultRenderAction, this); } if (!this.actions[mime]) { this.actions[mime] = {}; @@ -162,6 +174,16 @@ this.defaults[mime] = name; this._notifyUpdateListeners('setDefault', {defaultAction: {mime: mime, name: name}}); }, + + /** + * Returns a map of file actions handlers matching the given conditions + * + * @param {string} mime mime type + * @param {string} type "dir" or "file" + * @param {int} permissions permissions + * + * @return {Object.<string,OCA.Files.FileActions~actionHandler>} map of action name to action spec + */ get: function (mime, type, permissions) { var actions = this.getActions(mime, type, permissions); var filteredActions = {}; @@ -170,6 +192,16 @@ }); return filteredActions; }, + + /** + * Returns an array of file actions matching the given conditions + * + * @param {string} mime mime type + * @param {string} type "dir" or "file" + * @param {int} permissions permissions + * + * @return {Array.<OCA.Files.FileAction>} array of action specs + */ getActions: function (mime, type, permissions) { var actions = {}; if (this.actions.all) { @@ -197,7 +229,37 @@ }); return filteredActions; }, + + /** + * Returns the default file action handler for the given conditions + * + * @param {string} mime mime type + * @param {string} type "dir" or "file" + * @param {int} permissions permissions + * + * @return {OCA.Files.FileActions~actionHandler} action handler + * + * @deprecated use getDefaultFileAction instead + */ getDefault: function (mime, type, permissions) { + var defaultActionSpec = this.getDefaultFileAction(mime, type, permissions); + if (defaultActionSpec) { + return defaultActionSpec.action; + } + return undefined; + }, + + /** + * Returns the default file action handler for the given conditions + * + * @param {string} mime mime type + * @param {string} type "dir" or "file" + * @param {int} permissions permissions + * + * @return {OCA.Files.FileActions~actionHandler} action handler + * @since 8.2 + */ + getDefaultFileAction: function(mime, type, permissions) { var mimePart; if (mime) { mimePart = mime.substr(0, mime.indexOf('/')); @@ -212,9 +274,10 @@ } else { name = this.defaults.all; } - var actions = this.get(mime, type, permissions); + var actions = this.getActions(mime, type, permissions); return actions[name]; }, + /** * Default function to render actions * @@ -224,87 +287,82 @@ * @param {OCA.Files.FileActionContext} context action context */ _defaultRenderAction: function(actionSpec, isDefault, context) { - var name = actionSpec.name; - if (name === 'Download' || !isDefault) { - var $actionLink = this._makeActionLink(actionSpec, context); + if (!isDefault) { + var params = { + name: actionSpec.name, + nameLowerCase: actionSpec.name.toLowerCase(), + displayName: actionSpec.displayName, + icon: actionSpec.icon, + altText: actionSpec.altText, + }; + if (_.isFunction(actionSpec.icon)) { + params.icon = actionSpec.icon(context.$file.attr('data-file')); + } + + var $actionLink = this._makeActionLink(params, context); context.$file.find('a.name>span.fileactions').append($actionLink); + $actionLink.addClass('permanent'); return $actionLink; } }, + /** * Renders the action link element * - * @param {OCA.Files.FileAction} actionSpec action object - * @param {OCA.Files.FileActionContext} context action context + * @param {Object} params action params */ - _makeActionLink: function(actionSpec, context) { - var img = actionSpec.icon; - if (img && img.call) { - img = img(context.$file.attr('data-file')); - } - var html = '<a href="#">'; - if (img) { - html += '<img class="svg" alt="" src="' + img + '" />'; + _makeActionLink: function(params) { + if (!this._fileActionTriggerTemplate) { + this._fileActionTriggerTemplate = Handlebars.compile(TEMPLATE_FILE_ACTION_TRIGGER); } - if (actionSpec.displayName) { - html += '<span> ' + actionSpec.displayName + '</span>'; - } - html += '</a>'; - return $(html); + return $(this._fileActionTriggerTemplate(params)); }, + /** - * Custom renderer for the "Rename" action. - * Displays the rename action as an icon behind the file name. + * Displays the file actions dropdown menu * - * @param {OCA.Files.FileAction} actionSpec file action to render - * @param {boolean} isDefault true if the action is a default action, - * false otherwise - * @param {OCAFiles.FileActionContext} context rendering context + * @param {string} fileName file name + * @param {OCA.Files.FileActionContext} context rendering context */ - _renderRenameAction: function(actionSpec, isDefault, context) { - var $actionEl = this._makeActionLink(actionSpec, context); - var $container = context.$file.find('a.name span.nametext'); - $actionEl.find('img').attr('alt', t('files', 'Rename')); - $container.find('.action-rename').remove(); - $container.append($actionEl); - return $actionEl; + _showMenu: function(fileName, context) { + var menu; + var $trigger = context.$file.closest('tr').find('.fileactions .action-menu'); + $trigger.addClass('open'); + + menu = new OCA.Files.FileActionsMenu(); + menu.$el.on('afterHide', function() { + context.$file.removeClass('mouseOver'); + $trigger.removeClass('open'); + menu.remove(); + }); + + context.$file.addClass('mouseOver'); + context.$file.find('td.filename').append(menu.$el); + menu.show(context); }, + /** - * Custom renderer for the "Delete" action. - * Displays the "Delete" action as a trash icon at the end of - * the table row. - * - * @param {OCA.Files.FileAction} actionSpec file action to render - * @param {boolean} isDefault true if the action is a default action, - * false otherwise - * @param {OCAFiles.FileActionContext} context rendering context + * Renders the menu trigger on the given file list row + * + * @param {Object} $tr file list row element + * @param {OCA.Files.FileActionContext} context rendering context */ - _renderDeleteAction: function(actionSpec, isDefault, context) { - var mountType = context.$file.attr('data-mounttype'); - var deleteTitle = t('files', 'Delete'); - if (mountType === 'external-root') { - deleteTitle = t('files', 'Disconnect storage'); - } else if (mountType === 'shared-root') { - deleteTitle = t('files', 'Unshare'); - } - var cssClasses = 'action delete icon-delete'; - if((context.$file.data('permissions') & OC.PERMISSION_DELETE) === 0) { - // add css class no-permission to delete icon - cssClasses += ' no-permission'; - deleteTitle = t('files', 'No permission to delete'); - } - var $actionLink = $('<a href="#" original-title="' + - escapeHTML(deleteTitle) + - '" class="' +cssClasses + '">' + - '<span class="hidden-visually">' + escapeHTML(deleteTitle) + '</span>' + - '</a>' - ); - var $container = context.$file.find('td:last'); - $container.find('.delete').remove(); - $container.append($actionLink); - return $actionLink; + _renderMenuTrigger: function($tr, context) { + // remove previous + $tr.find('.action-menu').remove(); + + var $el = this._renderInlineAction({ + name: 'menu', + displayName: '', + icon: OC.imagePath('core', 'actions/more'), + altText: t('files', 'Actions'), + action: this._showMenuClosure + }, false, context); + + $el.addClass('permanent'); }, + /** * Renders the action element by calling actionSpec.render() and * registers the click event to process the action. @@ -312,25 +370,32 @@ * @param {OCA.Files.FileAction} actionSpec file action to render * @param {boolean} isDefault true if the action is a default action, * false otherwise - * @param {OCAFiles.FileActionContext} context rendering context + * @param {OCA.Files.FileActionContext} context rendering context */ - _renderAction: function(actionSpec, isDefault, context) { - var $actionEl = actionSpec.render(actionSpec, isDefault, context); + _renderInlineAction: function(actionSpec, isDefault, context) { + var renderFunc = actionSpec.render || _.bind(this._defaultRenderAction, this); + var $actionEl = renderFunc(actionSpec, isDefault, context); if (!$actionEl || !$actionEl.length) { return; } - $actionEl.addClass('action action-' + actionSpec.name.toLowerCase()); - $actionEl.attr('data-action', actionSpec.name); $actionEl.on( 'click', { a: null }, function(event) { + event.stopPropagation(); + event.preventDefault(); + + if ($actionEl.hasClass('open')) { + return; + } + var $file = $(event.target).closest('tr'); + if ($file.hasClass('busy')) { + return; + } var currentFile = $file.find('td.filename'); var fileName = $file.attr('data-file'); - event.stopPropagation(); - event.preventDefault(); context.fileActions.currentFile = currentFile; // also set on global object for legacy apps @@ -346,6 +411,7 @@ ); return $actionEl; }, + /** * Display file actions for the given element * @param parent "td" element of the file for which to display actions @@ -376,36 +442,29 @@ nameLinks = parent.children('a.name'); nameLinks.find('.fileactions, .nametext .action').remove(); nameLinks.append('<span class="fileactions" />'); - var defaultAction = this.getDefault( + var defaultAction = this.getDefaultFileAction( this.getCurrentMimeType(), this.getCurrentType(), this.getCurrentPermissions() ); + var context = { + $file: $tr, + fileActions: this, + fileList: fileList + }; + $.each(actions, function (name, actionSpec) { - if (name !== 'Share') { - self._renderAction( + if (actionSpec.type === FileActions.TYPE_INLINE) { + self._renderInlineAction( actionSpec, - actionSpec.action === defaultAction, { - $file: $tr, - fileActions: this, - fileList : fileList - } + defaultAction && actionSpec.name === defaultAction.name, + context ); } }); - // added here to make sure it's always the last action - var shareActionSpec = actions.Share; - if (shareActionSpec){ - this._renderAction( - shareActionSpec, - shareActionSpec.action === defaultAction, { - $file: $tr, - fileActions: this, - fileList: fileList - } - ); - } + + this._renderMenuTrigger($tr, context); if (triggerEvent){ fileList.$fileList.trigger(jQuery.Event("fileActionsReady", {fileList: fileList, $files: $tr})); @@ -429,35 +488,42 @@ */ registerDefaultActions: function() { this.registerAction({ - name: 'Delete', - displayName: '', + name: 'Download', + displayName: t('files', 'Download'), mime: 'all', - // permission is READ because we show a hint instead if there is no permission permissions: OC.PERMISSION_READ, - icon: function() { - return OC.imagePath('core', 'actions/delete'); + icon: function () { + return OC.imagePath('core', 'actions/download'); }, - render: _.bind(this._renderDeleteAction, this), - actionHandler: function(fileName, context) { - // if there is no permission to delete do nothing - if((context.$file.data('permissions') & OC.PERMISSION_DELETE) === 0) { + actionHandler: function (filename, context) { + var dir = context.dir || context.fileList.getCurrentDirectory(); + var url = context.fileList.getDownloadUrl(filename, dir); + + var downloadFileaction = $(context.$file).find('.fileactions .action-download'); + + // don't allow a second click on the download action + if(downloadFileaction.hasClass('disabled')) { return; } - context.fileList.do_delete(fileName, context.dir); - $('.tipsy').remove(); + + if (url) { + var disableLoadingState = function() { + context.fileList.showFileBusyState(filename, false); + }; + + context.fileList.showFileBusyState(downloadFileaction, true); + OCA.Files.Files.handleDownload(url, disableLoadingState); + } } }); - // t('files', 'Rename') this.registerAction({ name: 'Rename', - displayName: '', mime: 'all', permissions: OC.PERMISSION_UPDATE, icon: function() { return OC.imagePath('core', 'actions/rename'); }, - render: _.bind(this._renderRenameAction, this), actionHandler: function (filename, context) { context.fileList.rename(filename); } @@ -471,23 +537,51 @@ context.fileList.changeDirectory(dir + filename); }); - this.setDefault('dir', 'Open'); - - this.register('all', 'Download', OC.PERMISSION_READ, function () { - return OC.imagePath('core', 'actions/download'); - }, function (filename, context) { - var dir = context.dir || context.fileList.getCurrentDirectory(); - var url = context.fileList.getDownloadUrl(filename, dir); - if (url) { - OC.redirect(url); + this.registerAction({ + name: 'Delete', + mime: 'all', + // permission is READ because we show a hint instead if there is no permission + permissions: OC.PERMISSION_READ, + icon: function() { + return OC.imagePath('core', 'actions/delete'); + }, + actionHandler: function(fileName, context) { + // if there is no permission to delete do nothing + if((context.$file.data('permissions') & OC.PERMISSION_DELETE) === 0) { + return; + } + context.fileList.do_delete(fileName, context.dir); + $('.tipsy').remove(); } - }, t('files', 'Download')); + }); + + this.setDefault('dir', 'Open'); } }; OCA.Files.FileActions = FileActions; /** + * Replaces the download icon with a loading spinner and vice versa + * - also adds the class disabled to the passed in element + * + * @param downloadButtonElement download fileaction + * @param {boolean} showIt whether to show the spinner(true) or to hide it(false) + */ + OCA.Files.FileActions.updateFileActionSpinner = function(downloadButtonElement, showIt) { + var icon = downloadButtonElement.find('img'), + sourceImage = icon.attr('src'); + + if(showIt) { + downloadButtonElement.addClass('disabled'); + icon.attr('src', sourceImage.replace('actions/download.svg', 'loading-small.gif')); + } else { + downloadButtonElement.removeClass('disabled'); + icon.attr('src', sourceImage.replace('loading-small.gif', 'actions/download.svg')); + } + }; + + /** * File action attributes. * * @todo make this a real class in the future diff --git a/apps/files/js/fileactionsmenu.js b/apps/files/js/fileactionsmenu.js new file mode 100644 index 00000000000..623ebde5442 --- /dev/null +++ b/apps/files/js/fileactionsmenu.js @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2014 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function() { + + var TEMPLATE_MENU = + '<ul>' + + '{{#each items}}' + + '<li>' + + '<a href="#" class="action action-{{nameLowerCase}} permanent" data-action="{{name}}">{{#if icon}}<img src="{{icon}}"/>{{else}}<span class="no-icon"></span>{{/if}}<span>{{displayName}}</span></a>' + + '</li>' + + '{{/each}}' + + '</ul>'; + + /** + * Construct a new FileActionsMenu instance + * @constructs FileActionsMenu + * @memberof OCA.Files + */ + var FileActionsMenu = OC.Backbone.View.extend({ + tagName: 'div', + className: 'fileActionsMenu bubble hidden open menu', + + /** + * Current context + * + * @type OCA.Files.FileActionContext + */ + _context: null, + + events: { + 'click a.action': '_onClickAction' + }, + + template: function(data) { + if (!OCA.Files.FileActionsMenu._TEMPLATE) { + OCA.Files.FileActionsMenu._TEMPLATE = Handlebars.compile(TEMPLATE_MENU); + } + return OCA.Files.FileActionsMenu._TEMPLATE(data); + }, + + /** + * Event handler whenever an action has been clicked within the menu + * + * @param {Object} event event object + */ + _onClickAction: function(event) { + var $target = $(event.target); + if (!$target.is('a')) { + $target = $target.closest('a'); + } + var fileActions = this._context.fileActions; + var actionName = $target.attr('data-action'); + var actions = fileActions.getActions( + fileActions.getCurrentMimeType(), + fileActions.getCurrentType(), + fileActions.getCurrentPermissions() + ); + var actionSpec = actions[actionName]; + var fileName = this._context.$file.attr('data-file'); + + event.stopPropagation(); + event.preventDefault(); + + OC.hideMenus(); + + actionSpec.action( + fileName, + this._context + ); + }, + + /** + * Renders the menu with the currently set items + */ + render: function() { + var fileActions = this._context.fileActions; + var actions = fileActions.getActions( + fileActions.getCurrentMimeType(), + fileActions.getCurrentType(), + fileActions.getCurrentPermissions() + ); + + var defaultAction = fileActions.getDefaultFileAction( + fileActions.getCurrentMimeType(), + fileActions.getCurrentType(), + fileActions.getCurrentPermissions() + ); + + var items = _.filter(actions, function(actionSpec) { + return ( + actionSpec.type === OCA.Files.FileActions.TYPE_DROPDOWN && + (!defaultAction || actionSpec.name !== defaultAction.name) + ); + }); + items = _.map(items, function(item) { + item.nameLowerCase = item.name.toLowerCase(); + return item; + }); + + this.$el.html(this.template({ + items: items + })); + }, + + /** + * Displays the menu under the given element + * + * @param {OCA.Files.FileActionContext} context context + * @param {Object} $trigger trigger element + */ + show: function(context) { + this._context = context; + + this.render(); + this.$el.removeClass('hidden'); + + OC.showMenu(null, this.$el); + } + }); + + OCA.Files.FileActionsMenu = FileActionsMenu; + +})(); + diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index 8236ef3b4ac..e297edcf11b 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -23,6 +23,7 @@ * @param [options.scrollContainer] scrollable container, defaults to $(window) * @param [options.dragOptions] drag options, disabled by default * @param [options.folderDropOptions] folder drop options, disabled by default + * @param [options.detailsViewEnabled=true] whether to enable details view */ var FileList = function($el, options) { this.initialize($el, options); @@ -65,6 +66,11 @@ fileSummary: null, /** + * @type OCA.Files.DetailsView + */ + _detailsView: null, + + /** * Whether the file list was initialized already. * @type boolean */ @@ -205,6 +211,13 @@ } this.breadcrumb = new OCA.Files.BreadCrumb(breadcrumbOptions); + if (_.isUndefined(options.detailsViewEnabled) || options.detailsViewEnabled) { + this._detailsView = new OCA.Files.DetailsView(); + this._detailsView.addDetailView(new OCA.Files.MainFileInfoDetailView()); + this._detailsView.$el.insertBefore(this.$el); + this._detailsView.$el.addClass('disappear'); + } + this.$el.find('#controls').prepend(this.breadcrumb.$el); this.$el.find('thead th .columntitle').click(_.bind(this._onClickHeader, this)); @@ -216,6 +229,13 @@ this.updateSearch(); + this.$el.on('click', function(event) { + var $target = $(event.target); + // click outside file row ? + if (!$target.closest('tbody').length && !$target.closest('#app-sidebar').length) { + self._updateDetailsView(null); + } + }); this.$fileList.on('click','td.filename>a.name', _.bind(this._onClickFile, this)); this.$fileList.on('change', 'td.filename>.selectCheckBox', _.bind(this._onClickFileCheckbox, this)); this.$el.on('urlChanged', _.bind(this._onUrlChanged, this)); @@ -263,6 +283,37 @@ }, /** + * Update the details view to display the given file + * + * @param {OCA.Files.FileInfo} fileInfo file info to display + */ + _updateDetailsView: function(fileInfo) { + if (!this._detailsView) { + return; + } + + var self = this; + var oldFileInfo = this._detailsView.getFileInfo(); + if (oldFileInfo) { + // TODO: use more efficient way, maybe track the highlight + this.$fileList.children().filterAttr('data-id', '' + oldFileInfo.id).removeClass('highlighted'); + } + + if (!fileInfo) { + OC.Apps.hideAppSidebar(); + this._detailsView.setFileInfo(null); + return; + } + + this.$fileList.children().filterAttr('data-id', '' + fileInfo.id).addClass('highlighted'); + this._detailsView.setFileInfo(_.extend({ + path: this.getCurrentDirectory() + }, fileInfo)); + this._detailsView.$el.scrollTop(0); + _.defer(OC.Apps.showAppSidebar); + }, + + /** * Event handler for when the window size changed */ _onResize: function() { @@ -315,6 +366,12 @@ delete this._selectedFiles[$tr.data('id')]; this._selectionSummary.remove(data); } + if (this._selectionSummary.getTotal() === 1) { + this._updateDetailsView(_.values(this._selectedFiles)[0]); + } else { + // show nothing when multiple files are selected + this._updateDetailsView(null); + } this.$el.find('.select-all').prop('checked', this._selectionSummary.getTotal() === this.files.length); }, @@ -350,27 +407,34 @@ 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(); - // also set on global object for legacy apps - window.FileActions.currentFile = this.fileActions.currentFile; - action(filename, { - $file: $tr, - fileList: this, - fileActions: this.fileActions, - dir: $tr.attr('data-path') || this.getCurrentDirectory() - }); + // clicked directly on the name + if (!this._detailsView || $(event.target).is('.nametext') || $(event.target).closest('.nametext').length) { + 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(); + // also set on global object for legacy apps + window.FileActions.currentFile = this.fileActions.currentFile; + action(filename, { + $file: $tr, + fileList: this, + fileActions: this.fileActions, + dir: $tr.attr('data-path') || this.getCurrentDirectory() + }); + } + // deselect row + $(event.target).closest('a').blur(); } - // deselect row - $(event.target).closest('a').blur(); + } else { + var fileInfo = this.files[$tr.index()]; + this._updateDetailsView(fileInfo); + event.preventDefault(); } } }, @@ -417,7 +481,21 @@ else { files = _.pluck(this.getSelectedFiles(), 'name'); } - OC.redirect(this.getDownloadUrl(files, dir)); + + var downloadFileaction = $('#selectedActionsList').find('.download'); + + // don't allow a second click on the download action + if(downloadFileaction.hasClass('disabled')) { + event.preventDefault(); + return; + } + + var disableLoadingState = function(){ + OCA.Files.FileActions.updateFileActionSpinner(downloadFileaction, false); + }; + + OCA.Files.FileActions.updateFileActionSpinner(downloadFileaction, true); + OCA.Files.Files.handleDownload(this.getDownloadUrl(files, dir), disableLoadingState); return false; }, @@ -811,7 +889,7 @@ var formatted; var text; if (mtime > 0) { - formatted = formatDate(mtime); + formatted = OC.Util.formatDate(mtime); text = OC.Util.relativeModifiedDate(mtime); } else { formatted = t('files', 'Unable to determine date'); @@ -1225,6 +1303,12 @@ ready(iconURL); // set mimeicon URL urlSpec.file = OCA.Files.Files.fixPath(path); + if (options.x) { + urlSpec.x = options.x; + } + if (options.y) { + urlSpec.y = options.y; + } if (etag){ // use etag as cache buster @@ -1360,9 +1444,7 @@ } _.each(fileNames, function(fileName) { var $tr = self.findFileEl(fileName); - var $thumbEl = $tr.find('.thumbnail'); - var oldBackgroundImage = $thumbEl.css('background-image'); - $thumbEl.css('background-image', 'url('+ OC.imagePath('core', 'loading.gif') + ')'); + self.showFileBusyState($tr, true); // TODO: improve performance by sending all file names in a single call $.post( OC.filePath('files', 'ajax', 'move.php'), @@ -1404,7 +1486,7 @@ } else { OC.dialogs.alert(t('files', 'Error moving file'), t('files', 'Error')); } - $thumbEl.css('background-image', oldBackgroundImage); + self.showFileBusyState($tr, false); } ); }); @@ -1465,14 +1547,13 @@ try { var newName = input.val(); - var $thumbEl = tr.find('.thumbnail'); input.tipsy('hide'); form.remove(); if (newName !== oldname) { checkInput(); // mark as loading (temp element) - $thumbEl.css('background-image', 'url('+ OC.imagePath('core', 'loading.gif') + ')'); + self.showFileBusyState(tr, true); tr.attr('data-file', newName); var basename = newName; if (newName.indexOf('.') > 0 && tr.data('type') !== 'dir') { @@ -1480,7 +1561,6 @@ } td.find('a.name span.nametext').text(basename); td.children('a.name').show(); - tr.find('.fileactions, .action').addClass('hidden'); $.ajax({ url: OC.filePath('files','ajax','rename.php'), @@ -1507,6 +1587,7 @@ tr.remove(); tr = self.add(fileInfo, {updateSummary: false, silent: true}); self.$fileList.trigger($.Event('fileActionsReady', {fileList: self, $files: $(tr)})); + self._updateDetailsView(fileInfo); } }); } else { @@ -1551,6 +1632,44 @@ inList:function(file) { return this.findFileEl(file).length; }, + + /** + * Shows busy state on a given file row or multiple + * + * @param {string|Array.<string>} files file name or array of file names + * @param {bool} [busy=true] busy state, true for busy, false to remove busy state + * + * @since 8.2 + */ + showFileBusyState: function(files, state) { + var self = this; + if (!_.isArray(files)) { + files = [files]; + } + + if (_.isUndefined(state)) { + state = true; + } + + _.each(files, function($tr) { + // jquery element already ? + if (!$tr.is) { + $tr = self.findFileEl($tr); + } + + var $thumbEl = $tr.find('.thumbnail'); + $tr.toggleClass('busy', state); + + if (state) { + $thumbEl.attr('data-oldimage', $thumbEl.css('background-image')); + $thumbEl.css('background-image', 'url('+ OC.imagePath('core', 'loading.gif') + ')'); + } else { + $thumbEl.css('background-image', $thumbEl.attr('data-oldimage')); + $thumbEl.removeAttr('data-oldimage'); + } + }); + }, + /** * Delete the given files from the given dir * @param files file names list (without path) @@ -1564,9 +1683,8 @@ files=[files]; } if (files) { + this.showFileBusyState(files, true); for (var i=0; i<files.length; i++) { - var deleteAction = this.findFileEl(files[i]).children("td.date").children(".action.delete"); - deleteAction.removeClass('icon-delete').addClass('icon-loading-small'); } } // Finish any existing actions @@ -1584,7 +1702,7 @@ // 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('icon-delete').addClass('icon-loading-small'); + this.$fileList.find('tr').addClass('busy'); } $.post(OC.filePath('files', 'ajax', 'delete.php'), @@ -1627,8 +1745,7 @@ } else { $.each(files,function(index,file) { - var deleteAction = self.findFileEl(file).find('.action.delete'); - deleteAction.removeClass('icon-loading-small').addClass('icon-delete'); + self.$fileList.find('tr').removeClass('busy'); }); } } @@ -1663,6 +1780,7 @@ } this.$table.addClass('hidden'); + this.$el.find('#emptycontent').addClass('hidden'); $mask = $('<div class="mask transparent"></div>'); @@ -2162,6 +2280,20 @@ } }); + }, + + /** + * Register a tab view to be added to all views + */ + registerTabView: function(tabView) { + this._detailsView.addTabView(tabView); + }, + + /** + * Register a detail view to be added to all views + */ + registerDetailView: function(detailView) { + this._detailsView.addDetailView(detailView); } }; diff --git a/apps/files/js/files.js b/apps/files/js/files.js index 034045ee40b..19cc3b26e44 100644 --- a/apps/files/js/files.js +++ b/apps/files/js/files.js @@ -271,8 +271,33 @@ FileList.scrollTo(getURLParameter('scrollto')); } */ + }, + + /** + * Handles the download and calls the callback function once the download has started + * - browser sends download request and adds parameter with a token + * - server notices this token and adds a set cookie to the download response + * - browser now adds this cookie for the domain + * - JS periodically checks for this cookie and then knows when the download has started to call the callback + * + * @param {string} url download URL + * @param {function} callback function to call once the download has started + */ + handleDownload: function(url, callback) { + var randomToken = Math.random().toString(36).substring(2), + checkForDownloadCookie = function() { + if (!OC.Util.isCookieSetToValue('ocDownloadStarted', randomToken)){ + return false; + } else { + callback(); + return true; + } + }; + + OC.redirect(url + '&downloadStartSecret=' + randomToken); + OC.Util.waitFor(checkForDownloadCookie, 500); } - } + }; Files._updateStorageStatisticsDebounced = _.debounce(Files._updateStorageStatistics, 250); OCA.Files.Files = Files; diff --git a/apps/files/js/mainfileinfodetailview.js b/apps/files/js/mainfileinfodetailview.js new file mode 100644 index 00000000000..a00d907d0d6 --- /dev/null +++ b/apps/files/js/mainfileinfodetailview.js @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2015 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function() { + var TEMPLATE = + '<div class="thumbnail"></div><div title="{{name}}" class="fileName ellipsis">{{name}}</div>' + + '<div class="file-details ellipsis">' + + ' <a href="#" ' + + ' alt="{{starAltText}}"' + + ' class="action action-favorite favorite">' + + ' <img class="svg" src="{{starIcon}}" />' + + ' </a>' + + ' <span class="size" title="{{altSize}}">{{size}}</span>, <span class="date" title="{{altDate}}">{{date}}</span>' + + '</div>'; + + /** + * @class OCA.Files.MainFileInfoDetailView + * @classdesc + * + * Displays main details about a file + * + */ + var MainFileInfoDetailView = function() { + this.initialize(); + }; + /** + * @memberof OCA.Files + */ + MainFileInfoDetailView.prototype = _.extend({}, OCA.Files.DetailFileInfoView.prototype, + /** @lends OCA.Files.MainFileInfoDetailView.prototype */ { + _template: null, + + /** + * Initialize the details view + */ + initialize: function() { + this.$el = $('<div class="mainFileInfoView"></div>'); + }, + + /** + * Renders this details view + */ + render: function() { + this.$el.empty(); + + if (!this._template) { + this._template = Handlebars.compile(TEMPLATE); + } + + if (this._fileInfo) { + var isFavorite = (this._fileInfo.tags || []).indexOf(OC.TAG_FAVORITE) >= 0; + this.$el.append(this._template({ + nameLabel: t('files', 'Name'), + name: this._fileInfo.name, + pathLabel: t('files', 'Path'), + path: this._fileInfo.path, + sizeLabel: t('files', 'Size'), + size: OC.Util.humanFileSize(this._fileInfo.size, true), + altSize: n('files', '%n byte', '%n bytes', this._fileInfo.size), + dateLabel: t('files', 'Modified'), + altDate: OC.Util.formatDate(this._fileInfo.mtime), + date: OC.Util.relativeModifiedDate(this._fileInfo.mtime), + starAltText: isFavorite ? t('files', 'Favorited') : t('files', 'Favorite'), + starIcon: OC.imagePath('core', isFavorite ? 'actions/starred' : 'actions/star') + })); + + // TODO: we really need OC.Previews + var $iconDiv = this.$el.find('.thumbnail'); + if (this._fileInfo.mimetype !== 'httpd/unix-directory') { + // TODO: inject utility class? + FileList.lazyLoadPreview({ + path: this._fileInfo.path + '/' + this._fileInfo.name, + mime: this._fileInfo.mimetype, + etag: this._fileInfo.etag, + x: 50, + y: 50, + callback: function(previewUrl) { + $iconDiv.css('background-image', 'url("' + previewUrl + '")'); + } + }); + } else { + // TODO: special icons / shared / external + $iconDiv.css('background-image', 'url("' + OC.MimeType.getIconUrl('dir') + '")'); + } + this.$el.find('[title]').tooltip({placement: 'bottom'}); + } + } + }); + + OCA.Files.MainFileInfoDetailView = MainFileInfoDetailView; +})(); diff --git a/apps/files/js/tagsplugin.js b/apps/files/js/tagsplugin.js index 293e25176f3..ec69ce4b965 100644 --- a/apps/files/js/tagsplugin.js +++ b/apps/files/js/tagsplugin.js @@ -81,6 +81,7 @@ displayName: 'Favorite', mime: 'all', permissions: OC.PERMISSION_READ, + type: OCA.Files.FileActions.TYPE_INLINE, render: function(actionSpec, isDefault, context) { var $file = context.$file; var isFavorite = $file.data('favorite') === true; |