From ca34921cdf8db4075906b3531390aa1b1ae9216c Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Thu, 16 Jul 2015 15:28:45 +0200 Subject: [PATCH 01/12] Implement file actions dropdown File actions now have two types "inline" and "dropdown". The default is "dropdown". The file actions will now be shown in a dropdown menu. --- apps/files/css/files.css | 45 +++- apps/files/index.php | 1 + apps/files/js/fileactions.js | 304 ++++++++++++++---------- apps/files/js/fileactionsmenu.js | 149 ++++++++++++ apps/files/js/filelist.js | 56 ++++- apps/files/js/tagsplugin.js | 1 + apps/files_sharing/js/share.js | 96 ++++---- apps/files_sharing/templates/public.php | 1 + apps/files_trashbin/js/app.js | 1 - core/js/js.js | 56 ++++- 10 files changed, 502 insertions(+), 208 deletions(-) create mode 100644 apps/files/js/fileactionsmenu.js diff --git a/apps/files/css/files.css b/apps/files/css/files.css index 7e3318a962..e93993affa 100644 --- a/apps/files/css/files.css +++ b/apps/files/css/files.css @@ -517,6 +517,9 @@ table td.filename .uploadtext { font-size: 11px; } +.busy .fileactions, .busy .action { + visibility: hidden; +} /* force show the loading icon, not only on hover */ #fileList .icon-loading-small { @@ -527,11 +530,6 @@ table td.filename .uploadtext { } #fileList img.move2trash { display:inline; margin:-8px 0; padding:16px 8px 16px 8px !important; float:right; } -#fileList a.action.delete { - position: absolute; - right: 15px; - padding: 17px 14px; -} #fileList .action.action-share-notification span, #fileList a.name { cursor: default !important; @@ -578,10 +576,6 @@ a.action>img { display:none; } -#fileList a.action[data-action="Rename"] { - padding: 16px 14px 17px !important; -} - .ie8 #fileList a.action img, #fileList tr:hover a.action, #fileList a.action.permanent, @@ -693,3 +687,36 @@ table.dragshadow td.size { .mask.transparent{ opacity: 0; } + +.fileActionsMenu { + /* FIXME: should be variable width, but default one is too big */ + width: 100px; +} + +.fileActionsMenu.hidden { + display: none; +} + +#fileList .fileActionsMenu .action { + display: block; + line-height: 30px; + padding-left: 5px; + color: #000; + padding: 0; +} + +.fileActionsMenu .action img, +.fileActionsMenu .action .no-icon { + display: inline-block; + width: 16px; + margin-right: 5px; +} + +.fileActionsMenu .action { + opacity: 0.5; +} + +.fileActionsMenu li:hover .action { + opacity: 1; +} + diff --git a/apps/files/index.php b/apps/files/index.php index dca3e5ae74..a41ec059b5 100644 --- a/apps/files/index.php +++ b/apps/files/index.php @@ -138,6 +138,7 @@ foreach ($navItems as $item) { } OCP\Util::addscript('files', 'fileactions'); +OCP\Util::addscript('files', 'fileactionsmenu'); OCP\Util::addscript('files', 'files'); OCP\Util::addscript('files', 'navigation'); OCP\Util::addscript('files', 'keyboardshortcuts'); diff --git a/apps/files/js/fileactions.js b/apps/files/js/fileactions.js index 8dd26d71c3..3c13f087f7 100644 --- a/apps/files/js/fileactions.js +++ b/apps/files/js/fileactions.js @@ -10,6 +10,12 @@ (function() { + var TEMPLATE_FILE_ACTION_TRIGGER = + '' + + '{{#if icon}}{{altText}}{{/if}}' + + '{{#if displayName}} {{displayName}}{{/if}}' + + ''; + /** * 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,15 @@ */ _updateListeners: {}, + _fileActionTriggerTemplate: null, + + /** + * File actions menu + * + * @type OCA.Files.FileActionsMenu + */ + _menu: null, + /** * @private */ @@ -46,6 +63,8 @@ // abusing jquery for events until we get a real event lib this.$el = $(''); $('body').append(this.$el); + + this._showMenuClosure = _.bind(this._showMenu, this); }, /** @@ -111,6 +130,7 @@ displayName: displayName || name }); }, + /** * Register action * @@ -125,15 +145,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 +181,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.} map of action name to action spec + */ get: function (mime, type, permissions) { var actions = this.getActions(mime, type, permissions); var filteredActions = {}; @@ -170,6 +199,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.} array of action specs + */ getActions: function (mime, type, permissions) { var actions = {}; if (this.actions.all) { @@ -197,7 +236,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 +281,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 * @@ -225,86 +295,68 @@ */ _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); 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')); + _makeActionLink: function(params) { + if (!this._fileActionTriggerTemplate) { + this._fileActionTriggerTemplate = Handlebars.compile(TEMPLATE_FILE_ACTION_TRIGGER); } - var html = ''; - if (img) { - html += ''; - } - if (actionSpec.displayName) { - html += ' ' + actionSpec.displayName + ''; - } - html += ''; - 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 $actionEl = context.$file.find('.action-menu'); + + this._menu = new OCA.Files.FileActionsMenu(); + this._menu.showAt($actionEl, 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 = $('' + - '' + escapeHTML(deleteTitle) + '' + - '' - ); - 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(); + $tr.find('.fileactions').append(this._renderInlineAction({ + name: 'menu', + displayName: '', + icon: OC.imagePath('core', 'actions/more'), + altText: t('files', 'Actions'), + action: this._showMenuClosure + }, false, context)); }, + /** * Renders the action element by calling actionSpec.render() and * registers the click event to process the action. @@ -312,21 +364,23 @@ * @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) { 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(); @@ -346,6 +400,7 @@ ); return $actionEl; }, + /** * Display file actions for the given element * @param parent "td" element of the file for which to display actions @@ -382,30 +437,23 @@ 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 - } + actionSpec.action === defaultAction, + 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 +477,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,30 +526,25 @@ context.fileList.changeDirectory(dir + filename); }); + 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(); + } + }); + 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); - - var downloadFileaction = $(context.$file).find('.fileactions .action-download'); - - // don't allow a second click on the download action - if(downloadFileaction.hasClass('disabled')) { - return; - } - - if (url) { - var disableLoadingState = function(){ - OCA.Files.FileActions.updateFileActionSpinner(downloadFileaction, false); - }; - - OCA.Files.FileActions.updateFileActionSpinner(downloadFileaction, true); - OCA.Files.Files.handleDownload(url, disableLoadingState); - } - }, t('files', 'Download')); } }; diff --git a/apps/files/js/fileactionsmenu.js b/apps/files/js/fileactionsmenu.js new file mode 100644 index 0000000000..dabf530b17 --- /dev/null +++ b/apps/files/js/fileactionsmenu.js @@ -0,0 +1,149 @@ +/* + * 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 = + ''; + + /** + * Construct a new FileActionsMenu instance + * @constructs FileActionsMenu + * @memberof OCA.Files + */ + var FileActionsMenu = function() { + this.initialize(); + }; + + FileActionsMenu.prototype = { + $el: null, + _template: null, + + /** + * Current context + * + * @type OCA.Files.FileActionContext + */ + _context: null, + + /** + * @private + */ + initialize: function(fileActions, fileList) { + this.$el = $(''); + this._template = Handlebars.compile(TEMPLATE_MENU); + + this.$el.on('click', 'a.action', _.bind(this._onClickAction, this)); + this.$el.on('afterHide', _.bind(this._onHide, this)); + }, + + /** + * 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.empty(); + this.$el.append(this._template({ + items: items + })); + }, + + /** + * Displays the menu under the given element + * + * @param {Object} $el target element + * @param {OCA.Files.FileActionContext} context context + */ + showAt: function($el, context) { + this._context = context; + + this.render(); + this.$el.removeClass('hidden'); + + $el.closest('td').append(this.$el); + + context.$file.addClass('mouseOver'); + + OC.showMenu(null, this.$el); + }, + + /** + * Whenever the menu is hidden + */ + _onHide: function() { + this._context.$file.removeClass('mouseOver'); + this.$el.remove(); + } + }; + + OCA.Files.FileActionsMenu = FileActionsMenu; + +})(); + diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index f5629ecd2c..e297edcf11 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -1444,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'), @@ -1488,7 +1486,7 @@ } else { OC.dialogs.alert(t('files', 'Error moving file'), t('files', 'Error')); } - $thumbEl.css('background-image', oldBackgroundImage); + self.showFileBusyState($tr, false); } ); }); @@ -1549,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') { @@ -1564,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'), @@ -1636,6 +1632,44 @@ inList:function(file) { return this.findFileEl(file).length; }, + + /** + * Shows busy state on a given file row or multiple + * + * @param {string|Array.} 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) @@ -1649,9 +1683,8 @@ files=[files]; } if (files) { + this.showFileBusyState(files, true); for (var i=0; itd.date .action.delete').removeClass('icon-delete').addClass('icon-loading-small'); + this.$fileList.find('tr').addClass('busy'); } $.post(OC.filePath('files', 'ajax', 'delete.php'), @@ -1712,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'); }); } } diff --git a/apps/files/js/tagsplugin.js b/apps/files/js/tagsplugin.js index 293e25176f..ec69ce4b96 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; diff --git a/apps/files_sharing/js/share.js b/apps/files_sharing/js/share.js index 12bec0e8c9..389cbf79a3 100644 --- a/apps/files_sharing/js/share.js +++ b/apps/files_sharing/js/share.js @@ -89,57 +89,59 @@ } }); - fileActions.register( - 'all', - 'Share', - OC.PERMISSION_SHARE, - OC.imagePath('core', 'actions/share'), - function(filename, context) { + fileActions.registerAction({ + name: 'Share', + displayName: t('files_sharing', 'Share'), + mime: 'all', + permissions: OC.PERMISSION_SHARE, + icon: OC.imagePath('core', 'actions/share'), + type: OCA.Files.FileActions.TYPE_INLINE, + actionHandler: function(filename, context) { + var $tr = context.$file; + var itemType = 'file'; + if ($tr.data('type') === 'dir') { + itemType = 'folder'; + } + var possiblePermissions = $tr.data('share-permissions'); + if (_.isUndefined(possiblePermissions)) { + possiblePermissions = $tr.data('permissions'); + } - var $tr = context.$file; - var itemType = 'file'; - if ($tr.data('type') === 'dir') { - itemType = 'folder'; - } - var possiblePermissions = $tr.data('share-permissions'); - if (_.isUndefined(possiblePermissions)) { - possiblePermissions = $tr.data('permissions'); - } - - var appendTo = $tr.find('td.filename'); - // Check if drop down is already visible for a different file - if (OC.Share.droppedDown) { - if ($tr.attr('data-id') !== $('#dropdown').attr('data-item-source')) { - OC.Share.hideDropDown(function () { - $tr.addClass('mouseOver'); - OC.Share.showDropDown(itemType, $tr.data('id'), appendTo, true, possiblePermissions, filename); - }); + var appendTo = $tr.find('td.filename'); + // Check if drop down is already visible for a different file + if (OC.Share.droppedDown) { + if ($tr.attr('data-id') !== $('#dropdown').attr('data-item-source')) { + OC.Share.hideDropDown(function () { + $tr.addClass('mouseOver'); + OC.Share.showDropDown(itemType, $tr.data('id'), appendTo, true, possiblePermissions, filename); + }); + } else { + OC.Share.hideDropDown(); + } } else { - OC.Share.hideDropDown(); + $tr.addClass('mouseOver'); + OC.Share.showDropDown(itemType, $tr.data('id'), appendTo, true, possiblePermissions, filename); } - } else { - $tr.addClass('mouseOver'); - OC.Share.showDropDown(itemType, $tr.data('id'), appendTo, true, possiblePermissions, filename); + $('#dropdown').on('sharesChanged', function(ev) { + // files app current cannot show recipients on load, so we don't update the + // icon when changed for consistency + if (context.fileList.$el.closest('#app-content-files').length) { + return; + } + var recipients = _.pluck(ev.shares[OC.Share.SHARE_TYPE_USER], 'share_with_displayname'); + var groupRecipients = _.pluck(ev.shares[OC.Share.SHARE_TYPE_GROUP], 'share_with_displayname'); + recipients = recipients.concat(groupRecipients); + // note: we only update the data attribute because updateIcon() + // is called automatically after this event + if (recipients.length) { + $tr.attr('data-share-recipients', OCA.Sharing.Util.formatRecipients(recipients)); + } + else { + $tr.removeAttr('data-share-recipients'); + } + }); } - $('#dropdown').on('sharesChanged', function(ev) { - // files app current cannot show recipients on load, so we don't update the - // icon when changed for consistency - if (context.fileList.$el.closest('#app-content-files').length) { - return; - } - var recipients = _.pluck(ev.shares[OC.Share.SHARE_TYPE_USER], 'share_with_displayname'); - var groupRecipients = _.pluck(ev.shares[OC.Share.SHARE_TYPE_GROUP], 'share_with_displayname'); - recipients = recipients.concat(groupRecipients); - // note: we only update the data attribute because updateIcon() - // is called automatically after this event - if (recipients.length) { - $tr.attr('data-share-recipients', OCA.Sharing.Util.formatRecipients(recipients)); - } - else { - $tr.removeAttr('data-share-recipients'); - } - }); - }, t('files_sharing', 'Share')); + }); OC.addScript('files_sharing', 'sharetabview').done(function() { fileList.registerTabView(new OCA.Sharing.ShareTabView('shareTabView')); diff --git a/apps/files_sharing/templates/public.php b/apps/files_sharing/templates/public.php index ffe0472b2b..2962f62520 100644 --- a/apps/files_sharing/templates/public.php +++ b/apps/files_sharing/templates/public.php @@ -7,6 +7,7 @@ OCP\Util::addStyle('files_sharing', 'public'); OCP\Util::addStyle('files_sharing', 'mobile'); OCP\Util::addScript('files_sharing', 'public'); OCP\Util::addScript('files', 'fileactions'); +OCP\Util::addScript('files', 'fileactionsmenu'); OCP\Util::addScript('files', 'jquery.iframe-transport'); OCP\Util::addScript('files', 'jquery.fileupload'); diff --git a/apps/files_trashbin/js/app.js b/apps/files_trashbin/js/app.js index 315349d293..473cce88a7 100644 --- a/apps/files_trashbin/js/app.js +++ b/apps/files_trashbin/js/app.js @@ -59,7 +59,6 @@ OCA.Trashbin.App = { fileActions.registerAction({ name: 'Delete', - displayName: '', mime: 'all', permissions: OC.PERMISSION_READ, icon: function() { diff --git a/core/js/js.js b/core/js/js.js index 72d4edd28d..25baafde08 100644 --- a/core/js/js.js +++ b/core/js/js.js @@ -571,21 +571,20 @@ var OC={ * @todo Write documentation */ registerMenu: function($toggle, $menuEl) { + var self = this; $menuEl.addClass('menu'); $toggle.on('click.menu', function(event) { // prevent the link event (append anchor to URL) event.preventDefault(); if ($menuEl.is(OC._currentMenu)) { - $menuEl.slideUp(OC.menuSpeed); - OC._currentMenu = null; - OC._currentMenuToggle = null; + self.hideMenus(); return; } // another menu was open? else if (OC._currentMenu) { // close it - OC._currentMenu.hide(); + self.hideMenus(); } $menuEl.slideToggle(OC.menuSpeed); OC._currentMenu = $menuEl; @@ -599,14 +598,50 @@ var OC={ unregisterMenu: function($toggle, $menuEl) { // close menu if opened if ($menuEl.is(OC._currentMenu)) { - $menuEl.slideUp(OC.menuSpeed); - OC._currentMenu = null; - OC._currentMenuToggle = null; + this.hideMenus(); } $toggle.off('click.menu').removeClass('menutoggle'); $menuEl.removeClass('menu'); }, + /** + * Hides any open menus + * + * @param {Function} complete callback when the hiding animation is done + */ + hideMenus: function(complete) { + if (OC._currentMenu) { + OC._currentMenu.trigger(new $.Event('beforeHide')); + OC._currentMenu.slideUp(OC.menuSpeed, complete); + OC._currentMenu.trigger(new $.Event('afterHide')); + } + OC._currentMenu = null; + OC._currentMenuToggle = null; + }, + + /** + * Shows a given element as menu + * + * @param {Object} [$toggle=null] menu toggle + * @param {Object} $menuEl menu element + * @param {Function} complete callback when the showing animation is done + */ + showMenu: function($toggle, $menuEl, complete) { + if ($menuEl.is(OC._currentMenu)) { + return; + } + this.hideMenus(); + OC._currentMenu = $menuEl; + OC._currentMenuToggle = $toggle; + $menuEl.trigger(new $.Event('beforeShow')); + $menuEl.show(); + $menuEl.trigger(new $.Event('afterShow')); + // no animation + if (_.isFunction()) { + complete(); + } + }, + /** * Wrapper for matchMedia * @@ -1256,11 +1291,8 @@ function initCore() { // don't close when clicking on the menu directly or a menu toggle return false; } - if (OC._currentMenu) { - OC._currentMenu.slideUp(OC.menuSpeed); - } - OC._currentMenu = null; - OC._currentMenuToggle = null; + + OC.hideMenus(); }); From dd4e0a8253d108e8b9cf9444990164d66b3d75f0 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Tue, 4 Aug 2015 18:25:35 +0200 Subject: [PATCH 02/12] Make file action menu icon permanent --- apps/files/js/fileactions.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/files/js/fileactions.js b/apps/files/js/fileactions.js index 3c13f087f7..6b95e3ee6c 100644 --- a/apps/files/js/fileactions.js +++ b/apps/files/js/fileactions.js @@ -294,7 +294,6 @@ * @param {OCA.Files.FileActionContext} context action context */ _defaultRenderAction: function(actionSpec, isDefault, context) { - var name = actionSpec.name; if (!isDefault) { var params = { name: actionSpec.name, @@ -348,13 +347,16 @@ _renderMenuTrigger: function($tr, context) { // remove previous $tr.find('.action-menu').remove(); - $tr.find('.fileactions').append(this._renderInlineAction({ + + var $el = this._renderInlineAction({ name: 'menu', displayName: '', icon: OC.imagePath('core', 'actions/more'), altText: t('files', 'Actions'), action: this._showMenuClosure - }, false, context)); + }, false, context); + + $el.addClass('permanent'); }, /** From 9454e9043a7b18108f14a00d6ab8afa62fb894af Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Wed, 5 Aug 2015 12:48:42 +0200 Subject: [PATCH 03/12] Updated unit tests for file actions and actions menu --- apps/files/js/fileactions.js | 8 +- apps/files/js/fileactionsmenu.js | 14 +- apps/files/tests/js/fileactionsSpec.js | 543 ++++++++++----------- apps/files/tests/js/fileactionsmenuSpec.js | 291 +++++++++++ apps/files/tests/js/filelistSpec.js | 79 ++- 5 files changed, 618 insertions(+), 317 deletions(-) create mode 100644 apps/files/tests/js/fileactionsmenuSpec.js diff --git a/apps/files/js/fileactions.js b/apps/files/js/fileactions.js index 6b95e3ee6c..0d9161c6eb 100644 --- a/apps/files/js/fileactions.js +++ b/apps/files/js/fileactions.js @@ -332,10 +332,8 @@ * @param {OCA.Files.FileActionContext} context rendering context */ _showMenu: function(fileName, context) { - var $actionEl = context.$file.find('.action-menu'); - this._menu = new OCA.Files.FileActionsMenu(); - this._menu.showAt($actionEl, context); + this._menu.showAt(context); }, /** @@ -433,7 +431,7 @@ nameLinks = parent.children('a.name'); nameLinks.find('.fileactions, .nametext .action').remove(); nameLinks.append(''); - var defaultAction = this.getDefault( + var defaultAction = this.getDefaultFileAction( this.getCurrentMimeType(), this.getCurrentType(), this.getCurrentPermissions() @@ -449,7 +447,7 @@ if (actionSpec.type === FileActions.TYPE_INLINE) { self._renderInlineAction( actionSpec, - actionSpec.action === defaultAction, + defaultAction && actionSpec.name === defaultAction.name, context ); } diff --git a/apps/files/js/fileactionsmenu.js b/apps/files/js/fileactionsmenu.js index dabf530b17..1795fdeab1 100644 --- a/apps/files/js/fileactionsmenu.js +++ b/apps/files/js/fileactionsmenu.js @@ -42,7 +42,7 @@ /** * @private */ - initialize: function(fileActions, fileList) { + initialize: function() { this.$el = $(''); this._template = Handlebars.compile(TEMPLATE_MENU); @@ -50,6 +50,10 @@ this.$el.on('afterHide', _.bind(this._onHide, this)); }, + destroy: function() { + this.$el.remove(); + }, + /** * Event handler whenever an action has been clicked within the menu * @@ -118,17 +122,15 @@ /** * Displays the menu under the given element * - * @param {Object} $el target element * @param {OCA.Files.FileActionContext} context context */ - showAt: function($el, context) { + showAt: function(context) { this._context = context; this.render(); this.$el.removeClass('hidden'); - $el.closest('td').append(this.$el); - + context.$file.find('td.filename').append(this.$el); context.$file.addClass('mouseOver'); OC.showMenu(null, this.$el); @@ -139,7 +141,7 @@ */ _onHide: function() { this._context.$file.removeClass('mouseOver'); - this.$el.remove(); + this.destroy(); } }; diff --git a/apps/files/tests/js/fileactionsSpec.js b/apps/files/tests/js/fileactionsSpec.js index e420ab828a..8c43b917fa 100644 --- a/apps/files/tests/js/fileactionsSpec.js +++ b/apps/files/tests/js/fileactionsSpec.js @@ -20,8 +20,7 @@ */ describe('OCA.Files.FileActions tests', function() { - var $filesTable, fileList; - var FileActions; + var fileList, fileActions; beforeEach(function() { // init horrible parameters @@ -29,211 +28,175 @@ describe('OCA.Files.FileActions tests', function() { $body.append(''); $body.append(''); // dummy files table - $filesTable = $body.append('
'); - fileList = new OCA.Files.FileList($('#testArea')); - FileActions = new OCA.Files.FileActions(); - FileActions.registerDefaultActions(); + fileActions = new OCA.Files.FileActions(); + fileActions.registerAction({ + name: 'Testdropdown', + displayName: 'Testdropdowndisplay', + mime: 'all', + permissions: OC.PERMISSION_READ, + icon: function () { + return OC.imagePath('core', 'actions/download'); + } + }); + + fileActions.registerAction({ + name: 'Testinline', + displayName: 'Testinlinedisplay', + type: OCA.Files.FileActions.TYPE_INLINE, + mime: 'all', + permissions: OC.PERMISSION_READ + }); + + fileActions.registerAction({ + name: 'Testdefault', + displayName: 'Testdefaultdisplay', + mime: 'all', + permissions: OC.PERMISSION_READ + }); + fileActions.setDefault('all', 'Testdefault'); + fileList = new OCA.Files.FileList($body, { + fileActions: fileActions + }); }); afterEach(function() { - FileActions = null; + fileActions = null; fileList.destroy(); fileList = undefined; $('#dir, #permissions, #filestable').remove(); }); it('calling clear() clears file actions', function() { - FileActions.clear(); - expect(FileActions.actions).toEqual({}); - expect(FileActions.defaults).toEqual({}); - expect(FileActions.icons).toEqual({}); - expect(FileActions.currentFile).toBe(null); + fileActions.clear(); + expect(fileActions.actions).toEqual({}); + expect(fileActions.defaults).toEqual({}); + expect(fileActions.icons).toEqual({}); + expect(fileActions.currentFile).toBe(null); }); - it('calling display() sets file actions', function() { - var fileData = { - id: 18, - type: 'file', - name: 'testName.txt', - mimetype: 'text/plain', - size: '1234', - etag: 'a01234c', - mtime: '123456' - }; + describe('displaying actions', function() { + var $tr; - // note: FileActions.display() is called implicitly - var $tr = fileList.add(fileData); + beforeEach(function() { + var fileData = { + id: 18, + type: 'file', + name: 'testName.txt', + mimetype: 'text/plain', + size: '1234', + etag: 'a01234c', + mtime: '123456', + permissions: OC.PERMISSION_READ | OC.PERMISSION_UPDATE + }; - // actions defined after call - expect($tr.find('.action.action-download').length).toEqual(1); - expect($tr.find('.action.action-download').attr('data-action')).toEqual('Download'); - expect($tr.find('.nametext .action.action-rename').length).toEqual(1); - expect($tr.find('.nametext .action.action-rename').attr('data-action')).toEqual('Rename'); - expect($tr.find('.action.delete').length).toEqual(1); + // note: FileActions.display() is called implicitly + $tr = fileList.add(fileData); + }); + it('renders inline file actions', function() { + // actions defined after call + expect($tr.find('.action.action-testinline').length).toEqual(1); + expect($tr.find('.action.action-testinline').attr('data-action')).toEqual('Testinline'); + }); + it('does not render dropdown actions', function() { + expect($tr.find('.action.action-testdropdown').length).toEqual(0); + }); + it('does not render default action', function() { + expect($tr.find('.action.action-testdefault').length).toEqual(0); + }); + it('replaces file actions when displayed twice', function() { + fileActions.display($tr.find('td.filename'), true, fileList); + fileActions.display($tr.find('td.filename'), true, fileList); + + expect($tr.find('.action.action-testinline').length).toEqual(1); + }); + it('renders actions menu trigger', function() { + expect($tr.find('.action.action-menu').length).toEqual(1); + expect($tr.find('.action.action-menu').attr('data-action')).toEqual('menu'); + }); + it('only renders actions relevant to the mime type', function() { + fileActions.registerAction({ + name: 'Match', + displayName: 'MatchDisplay', + type: OCA.Files.FileActions.TYPE_INLINE, + mime: 'text/plain', + permissions: OC.PERMISSION_READ + }); + fileActions.registerAction({ + name: 'Nomatch', + displayName: 'NoMatchDisplay', + type: OCA.Files.FileActions.TYPE_INLINE, + mime: 'application/octet-stream', + permissions: OC.PERMISSION_READ + }); + + fileActions.display($tr.find('td.filename'), true, fileList); + expect($tr.find('.action.action-match').length).toEqual(1); + expect($tr.find('.action.action-nomatch').length).toEqual(0); + }); + it('only renders actions relevant to the permissions', function() { + fileActions.registerAction({ + name: 'Match', + displayName: 'MatchDisplay', + type: OCA.Files.FileActions.TYPE_INLINE, + mime: 'text/plain', + permissions: OC.PERMISSION_UPDATE + }); + fileActions.registerAction({ + name: 'Nomatch', + displayName: 'NoMatchDisplay', + type: OCA.Files.FileActions.TYPE_INLINE, + mime: 'text/plain', + permissions: OC.PERMISSION_DELETE + }); + + fileActions.display($tr.find('td.filename'), true, fileList); + expect($tr.find('.action.action-match').length).toEqual(1); + expect($tr.find('.action.action-nomatch').length).toEqual(0); + }); }); - it('calling display() twice correctly replaces file actions', function() { - var fileData = { - id: 18, - type: 'file', - name: 'testName.txt', - mimetype: 'text/plain', - size: '1234', - etag: 'a01234c', - mtime: '123456' - }; - var $tr = fileList.add(fileData); + describe('action handler', function() { + var actionStub, $tr; - FileActions.display($tr.find('td.filename'), true, fileList); - FileActions.display($tr.find('td.filename'), true, fileList); + beforeEach(function() { + var fileData = { + id: 18, + type: 'file', + name: 'testName.txt', + mimetype: 'text/plain', + size: '1234', + etag: 'a01234c', + mtime: '123456' + }; + actionStub = sinon.stub(); + fileActions.registerAction({ + name: 'Test', + type: OCA.Files.FileActions.TYPE_INLINE, + mime: 'all', + icon: OC.imagePath('core', 'actions/test'), + permissions: OC.PERMISSION_READ, + actionHandler: actionStub + }); + $tr = fileList.add(fileData); + }); + it('passes context to action handler', function() { + $tr.find('.action-test').click(); + expect(actionStub.calledOnce).toEqual(true); + expect(actionStub.getCall(0).args[0]).toEqual('testName.txt'); + var context = actionStub.getCall(0).args[1]; + expect(context.$file.is($tr)).toEqual(true); + expect(context.fileList).toBeDefined(); + expect(context.fileActions).toBeDefined(); + expect(context.dir).toEqual('/subdir'); - // actions defined after cal - expect($tr.find('.action.action-download').length).toEqual(1); - expect($tr.find('.nametext .action.action-rename').length).toEqual(1); - expect($tr.find('.action.delete').length).toEqual(1); - }); - it('redirects to download URL when clicking download', function() { - var redirectStub = sinon.stub(OC, 'redirect'); - var fileData = { - id: 18, - type: 'file', - name: 'testName.txt', - mimetype: 'text/plain', - size: '1234', - etag: 'a01234c', - mtime: '123456' - }; - var $tr = fileList.add(fileData); - FileActions.display($tr.find('td.filename'), true, fileList); - - $tr.find('.action-download').click(); - - expect(redirectStub.calledOnce).toEqual(true); - expect(redirectStub.getCall(0).args[0]).toContain( - OC.webroot + - '/index.php/apps/files/ajax/download.php' + - '?dir=%2Fsubdir&files=testName.txt'); - redirectStub.restore(); - }); - it('takes the file\'s path into account when clicking download', function() { - var redirectStub = sinon.stub(OC, 'redirect'); - var fileData = { - id: 18, - type: 'file', - name: 'testName.txt', - path: '/anotherpath/there', - mimetype: 'text/plain', - size: '1234', - etag: 'a01234c', - mtime: '123456' - }; - var $tr = fileList.add(fileData); - FileActions.display($tr.find('td.filename'), true, fileList); - - $tr.find('.action-download').click(); - - expect(redirectStub.calledOnce).toEqual(true); - expect(redirectStub.getCall(0).args[0]).toContain( - OC.webroot + '/index.php/apps/files/ajax/download.php' + - '?dir=%2Fanotherpath%2Fthere&files=testName.txt' - ); - redirectStub.restore(); - }); - it('deletes file when clicking delete', function() { - var deleteStub = sinon.stub(fileList, 'do_delete'); - var fileData = { - id: 18, - type: 'file', - name: 'testName.txt', - path: '/somepath/dir', - mimetype: 'text/plain', - size: '1234', - etag: 'a01234c', - mtime: '123456' - }; - var $tr = fileList.add(fileData); - FileActions.display($tr.find('td.filename'), true, fileList); - - $tr.find('.action.delete').click(); - - expect(deleteStub.calledOnce).toEqual(true); - expect(deleteStub.getCall(0).args[0]).toEqual('testName.txt'); - expect(deleteStub.getCall(0).args[1]).toEqual('/somepath/dir'); - deleteStub.restore(); - }); - it('shows delete hint when no permission to delete', function() { - var deleteStub = sinon.stub(fileList, 'do_delete'); - var fileData = { - id: 18, - type: 'file', - name: 'testName.txt', - path: '/somepath/dir', - mimetype: 'text/plain', - size: '1234', - etag: 'a01234c', - mtime: '123456', - permissions: OC.PERMISSION_READ - }; - var $tr = fileList.add(fileData); - FileActions.display($tr.find('td.filename'), true, fileList); - - var $action = $tr.find('.action.delete'); - - expect($action.hasClass('no-permission')).toEqual(true); - deleteStub.restore(); - }); - it('shows delete hint not when permission to delete', function() { - var deleteStub = sinon.stub(fileList, 'do_delete'); - var fileData = { - id: 18, - type: 'file', - name: 'testName.txt', - path: '/somepath/dir', - mimetype: 'text/plain', - size: '1234', - etag: 'a01234c', - mtime: '123456', - permissions: OC.PERMISSION_DELETE - }; - var $tr = fileList.add(fileData); - FileActions.display($tr.find('td.filename'), true, fileList); - - var $action = $tr.find('.action.delete'); - - expect($action.hasClass('no-permission')).toEqual(false); - deleteStub.restore(); - }); - it('passes context to action handler', function() { - var actionStub = sinon.stub(); - var fileData = { - id: 18, - type: 'file', - name: 'testName.txt', - mimetype: 'text/plain', - size: '1234', - etag: 'a01234c', - mtime: '123456' - }; - var $tr = fileList.add(fileData); - FileActions.register( - 'all', - 'Test', - OC.PERMISSION_READ, - OC.imagePath('core', 'actions/test'), - actionStub - ); - FileActions.display($tr.find('td.filename'), true, fileList); - $tr.find('.action-test').click(); - expect(actionStub.calledOnce).toEqual(true); - expect(actionStub.getCall(0).args[0]).toEqual('testName.txt'); - var context = actionStub.getCall(0).args[1]; - expect(context.$file.is($tr)).toEqual(true); - expect(context.fileList).toBeDefined(); - expect(context.fileActions).toBeDefined(); - expect(context.dir).toEqual('/subdir'); - - // when data-path is defined - actionStub.reset(); - $tr.attr('data-path', '/somepath'); - $tr.find('.action-test').click(); - context = actionStub.getCall(0).args[1]; - expect(context.dir).toEqual('/somepath'); + // when data-path is defined + actionStub.reset(); + $tr.attr('data-path', '/somepath'); + $tr.find('.action-test').click(); + context = actionStub.getCall(0).args[1]; + expect(context.dir).toEqual('/somepath'); + }); + it('shows actions menu when clicking the menu trigger', function() { + expect($tr.find('.menu').length).toEqual(0); + $tr.find('.action-menu').click(); + expect($tr.find('.menu').length).toEqual(1); + }); }); describe('custom rendering', function() { var $tr; @@ -251,10 +214,11 @@ describe('OCA.Files.FileActions tests', function() { }); it('regular function', function() { var actionStub = sinon.stub(); - FileActions.registerAction({ + fileActions.registerAction({ name: 'Test', displayName: '', mime: 'all', + type: OCA.Files.FileActions.TYPE_INLINE, permissions: OC.PERMISSION_READ, render: function(actionSpec, isDefault, context) { expect(actionSpec.name).toEqual('Test'); @@ -266,13 +230,13 @@ describe('OCA.Files.FileActions tests', function() { expect(context.fileList).toEqual(fileList); expect(context.$file[0]).toEqual($tr[0]); - var $customEl = $('blabliblabla'); + var $customEl = $('blabliblabla'); $tr.find('td:first').append($customEl); return $customEl; }, actionHandler: actionStub }); - FileActions.display($tr.find('td.filename'), true, fileList); + fileActions.display($tr.find('td.filename'), true, fileList); var $actionEl = $tr.find('td:first .action-test'); expect($actionEl.length).toEqual(1); @@ -306,20 +270,22 @@ describe('OCA.Files.FileActions tests', function() { var actions2 = new OCA.Files.FileActions(); var actionStub1 = sinon.stub(); var actionStub2 = sinon.stub(); - actions1.register( - 'all', - 'Test', - OC.PERMISSION_READ, - OC.imagePath('core', 'actions/test'), - actionStub1 - ); - actions2.register( - 'all', - 'Test2', - OC.PERMISSION_READ, - OC.imagePath('core', 'actions/test'), - actionStub2 - ); + actions1.registerAction({ + name: 'Test', + type: OCA.Files.FileActions.TYPE_INLINE, + mime: 'all', + permissions: OC.PERMISSION_READ, + icon: OC.imagePath('core', 'actions/test'), + actionHandler: actionStub1 + }); + actions2.registerAction({ + name: 'Test2', + type: OCA.Files.FileActions.TYPE_INLINE, + mime: 'all', + permissions: OC.PERMISSION_READ, + icon: OC.imagePath('core', 'actions/test'), + actionHandler: actionStub2 + }); actions2.merge(actions1); actions2.display($tr.find('td.filename'), true, fileList); @@ -342,20 +308,22 @@ describe('OCA.Files.FileActions tests', function() { var actions2 = new OCA.Files.FileActions(); var actionStub1 = sinon.stub(); var actionStub2 = sinon.stub(); - actions1.register( - 'all', - 'Test', - OC.PERMISSION_READ, - OC.imagePath('core', 'actions/test'), - actionStub1 - ); - actions2.register( - 'all', - 'Test', // override - OC.PERMISSION_READ, - OC.imagePath('core', 'actions/test'), - actionStub2 - ); + actions1.registerAction({ + name: 'Test', + type: OCA.Files.FileActions.TYPE_INLINE, + mime: 'all', + permissions: OC.PERMISSION_READ, + icon: OC.imagePath('core', 'actions/test'), + actionHandler: actionStub1 + }); + actions2.registerAction({ + name: 'Test', // override + mime: 'all', + type: OCA.Files.FileActions.TYPE_INLINE, + permissions: OC.PERMISSION_READ, + icon: OC.imagePath('core', 'actions/test'), + actionHandler: actionStub2 + }); actions1.merge(actions2); actions1.display($tr.find('td.filename'), true, fileList); @@ -371,24 +339,26 @@ describe('OCA.Files.FileActions tests', function() { var actions2 = new OCA.Files.FileActions(); var actionStub1 = sinon.stub(); var actionStub2 = sinon.stub(); - actions1.register( - 'all', - 'Test', - OC.PERMISSION_READ, - OC.imagePath('core', 'actions/test'), - actionStub1 - ); + actions1.registerAction({ + mime: 'all', + name: 'Test', + type: OCA.Files.FileActions.TYPE_INLINE, + permissions: OC.PERMISSION_READ, + icon: OC.imagePath('core', 'actions/test'), + actionHandler: actionStub1 + }); actions1.merge(actions2); // late override - actions1.register( - 'all', - 'Test', // override - OC.PERMISSION_READ, - OC.imagePath('core', 'actions/test'), - actionStub2 - ); + actions1.registerAction({ + mime: 'all', + name: 'Test', // override + type: OCA.Files.FileActions.TYPE_INLINE, + permissions: OC.PERMISSION_READ, + icon: OC.imagePath('core', 'actions/test'), + actionHandler: actionStub2 + }); actions1.display($tr.find('td.filename'), true, fileList); @@ -403,25 +373,27 @@ describe('OCA.Files.FileActions tests', function() { var actions2 = new OCA.Files.FileActions(); var actionStub1 = sinon.stub(); var actionStub2 = sinon.stub(); - actions1.register( - 'all', - 'Test', - OC.PERMISSION_READ, - OC.imagePath('core', 'actions/test'), - actionStub1 - ); + actions1.registerAction({ + mime: 'all', + name: 'Test', + type: OCA.Files.FileActions.TYPE_INLINE, + permissions: OC.PERMISSION_READ, + icon: OC.imagePath('core', 'actions/test'), + actionHandler: actionStub1 + }); // copy the Test action to actions2 actions2.merge(actions1); // late override - actions2.register( - 'all', - 'Test', // override - OC.PERMISSION_READ, - OC.imagePath('core', 'actions/test'), - actionStub2 - ); + actions2.registerAction({ + mime: 'all', + name: 'Test', // override + type: OCA.Files.FileActions.TYPE_INLINE, + permissions: OC.PERMISSION_READ, + icon: OC.imagePath('core', 'actions/test'), + actionHandler: actionStub2 + }); // check if original actions still call the correct handler actions1.display($tr.find('td.filename'), true, fileList); @@ -444,42 +416,45 @@ describe('OCA.Files.FileActions tests', function() { it('notifies update event handlers once after multiple changes', function() { var actionStub = sinon.stub(); var handler = sinon.stub(); - FileActions.on('registerAction', handler); - FileActions.register( - 'all', - 'Test', - OC.PERMISSION_READ, - OC.imagePath('core', 'actions/test'), - actionStub - ); - FileActions.register( - 'all', - 'Test2', - OC.PERMISSION_READ, - OC.imagePath('core', 'actions/test'), - actionStub - ); + fileActions.on('registerAction', handler); + fileActions.registerAction({ + mime: 'all', + name: 'Test', + type: OCA.Files.FileActions.TYPE_INLINE, + permissions: OC.PERMISSION_READ, + icon: OC.imagePath('core', 'actions/test'), + actionHandler: actionStub + }); + fileActions.registerAction({ + mime: 'all', + name: 'Test2', + permissions: OC.PERMISSION_READ, + icon: OC.imagePath('core', 'actions/test'), + actionHandler: actionStub + }); expect(handler.calledTwice).toEqual(true); }); it('does not notifies update event handlers after unregistering', function() { var actionStub = sinon.stub(); var handler = sinon.stub(); - FileActions.on('registerAction', handler); - FileActions.off('registerAction', handler); - FileActions.register( - 'all', - 'Test', - OC.PERMISSION_READ, - OC.imagePath('core', 'actions/test'), - actionStub - ); - FileActions.register( - 'all', - 'Test2', - OC.PERMISSION_READ, - OC.imagePath('core', 'actions/test'), - actionStub - ); + fileActions.on('registerAction', handler); + fileActions.off('registerAction', handler); + fileActions.registerAction({ + mime: 'all', + name: 'Test', + type: OCA.Files.FileActions.TYPE_INLINE, + permissions: OC.PERMISSION_READ, + icon: OC.imagePath('core', 'actions/test'), + actionHandler: actionStub + }); + fileActions.registerAction({ + mime: 'all', + name: 'Test2', + type: OCA.Files.FileActions.TYPE_INLINE, + permissions: OC.PERMISSION_READ, + icon: OC.imagePath('core', 'actions/test'), + actionHandler: actionStub + }); expect(handler.notCalled).toEqual(true); }); }); diff --git a/apps/files/tests/js/fileactionsmenuSpec.js b/apps/files/tests/js/fileactionsmenuSpec.js new file mode 100644 index 0000000000..4343979497 --- /dev/null +++ b/apps/files/tests/js/fileactionsmenuSpec.js @@ -0,0 +1,291 @@ +/** +* ownCloud +* +* @author Vincent Petry +* @copyright 2015 Vincent Petry +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE +* License as published by the Free Software Foundation; either +* version 3 of the License, or any later version. +* +* This library is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU AFFERO GENERAL PUBLIC LICENSE for more details. +* +* You should have received a copy of the GNU Affero General Public +* License along with this library. If not, see . +* +*/ + +describe('OCA.Files.FileActionsMenu tests', function() { + var fileList, fileActions, menu, actionStub, $tr; + + beforeEach(function() { + // init horrible parameters + var $body = $('#testArea'); + $body.append(''); + $body.append(''); + // dummy files table + actionStub = sinon.stub(); + fileActions = new OCA.Files.FileActions(); + fileList = new OCA.Files.FileList($body, { + fileActions: fileActions + }); + + fileActions.registerAction({ + name: 'Testdropdown', + displayName: 'Testdropdowndisplay', + mime: 'all', + permissions: OC.PERMISSION_READ, + icon: function () { + return OC.imagePath('core', 'actions/download'); + }, + actionHandler: actionStub + }); + + fileActions.registerAction({ + name: 'Testdropdownnoicon', + displayName: 'Testdropdowndisplaynoicon', + mime: 'all', + permissions: OC.PERMISSION_READ, + actionHandler: actionStub + }); + + fileActions.registerAction({ + name: 'Testinline', + displayName: 'Testinlinedisplay', + type: OCA.Files.FileActions.TYPE_INLINE, + mime: 'all', + permissions: OC.PERMISSION_READ + }); + + fileActions.registerAction({ + name: 'Testdefault', + displayName: 'Testdefaultdisplay', + mime: 'all', + permissions: OC.PERMISSION_READ + }); + fileActions.setDefault('all', 'Testdefault'); + + var fileData = { + id: 18, + type: 'file', + name: 'testName.txt', + mimetype: 'text/plain', + size: '1234', + etag: 'a01234c', + mtime: '123456' + }; + $tr = fileList.add(fileData); + + var menuContext = { + $file: $tr, + fileList: fileList, + fileActions: fileActions, + dir: fileList.getCurrentDirectory() + }; + menu = new OCA.Files.FileActionsMenu(); + menu.showAt(menuContext); + }); + afterEach(function() { + fileActions = null; + fileList.destroy(); + fileList = undefined; + menu.destroy(); + $('#dir, #permissions, #filestable').remove(); + }); + + describe('rendering', function() { + it('displays menu in the row container', function() { + expect(menu.$el.closest('td.filename').length).toEqual(1); + expect($tr.find('.fileActionsMenu').length).toEqual(1); + }); + it('highlights the row in the file list', function() { + expect($tr.hasClass('mouseOver')).toEqual(true); + }); + it('renders dropdown actions in menu', function() { + var $action = menu.$el.find('a[data-action=Testdropdown]'); + expect($action.length).toEqual(1); + expect($action.find('img').attr('src')) + .toEqual(OC.imagePath('core', 'actions/download')); + expect($action.find('.no-icon').length).toEqual(0); + + $action = menu.$el.find('a[data-action=Testdropdownnoicon]'); + expect($action.length).toEqual(1); + expect($action.find('img').length).toEqual(0); + expect($action.find('.no-icon').length).toEqual(1); + }); + it('does not render default actions', function() { + expect(menu.$el.find('a[data-action=Testdefault]').length).toEqual(0); + }); + it('does not render inline actions', function() { + expect(menu.$el.find('a[data-action=Testinline]').length).toEqual(0); + }); + it('only renders actions relevant to the mime type', function() { + fileActions.registerAction({ + name: 'Match', + displayName: 'MatchDisplay', + mime: 'text/plain', + permissions: OC.PERMISSION_READ + }); + fileActions.registerAction({ + name: 'Nomatch', + displayName: 'NoMatchDisplay', + mime: 'application/octet-stream', + permissions: OC.PERMISSION_READ + }); + + menu.render(); + expect(menu.$el.find('a[data-action=Match]').length).toEqual(1); + expect(menu.$el.find('a[data-action=NoMatch]').length).toEqual(0); + }); + it('only renders actions relevant to the permissions', function() { + fileActions.registerAction({ + name: 'Match', + displayName: 'MatchDisplay', + mime: 'text/plain', + permissions: OC.PERMISSION_UPDATE + }); + fileActions.registerAction({ + name: 'Nomatch', + displayName: 'NoMatchDisplay', + mime: 'text/plain', + permissions: OC.PERMISSION_DELETE + }); + + menu.render(); + expect(menu.$el.find('a[data-action=Match]').length).toEqual(1); + expect(menu.$el.find('a[data-action=NoMatch]').length).toEqual(0); + }); + }); + + describe('action handler', function() { + it('calls action handler when clicking menu item', function() { + var $action = menu.$el.find('a[data-action=Testdropdown]'); + $action.click(); + + expect(actionStub.calledOnce).toEqual(true); + expect(actionStub.getCall(0).args[0]).toEqual('testName.txt'); + expect(actionStub.getCall(0).args[1].$file[0]).toEqual($tr[0]); + expect(actionStub.getCall(0).args[1].fileList).toEqual(fileList); + expect(actionStub.getCall(0).args[1].fileActions).toEqual(fileActions); + expect(actionStub.getCall(0).args[1].dir).toEqual('/subdir'); + }); + }); + describe('default actions from registerDefaultActions', function() { + beforeEach(function() { + fileActions.clear(); + fileActions.registerDefaultActions(); + }); + it('redirects to download URL when clicking download', function() { + var redirectStub = sinon.stub(OC, 'redirect'); + var fileData = { + id: 18, + type: 'file', + name: 'testName.txt', + mimetype: 'text/plain', + size: '1234', + etag: 'a01234c', + mtime: '123456' + }; + var $tr = fileList.add(fileData); + fileActions.display($tr.find('td.filename'), true, fileList); + + var menuContext = { + $file: $tr, + fileList: fileList, + fileActions: fileActions, + dir: fileList.getCurrentDirectory() + }; + menu = new OCA.Files.FileActionsMenu(); + menu.showAt(menuContext); + + menu.$el.find('.action-download').click(); + + expect(redirectStub.calledOnce).toEqual(true); + expect(redirectStub.getCall(0).args[0]).toContain( + OC.webroot + + '/index.php/apps/files/ajax/download.php' + + '?dir=%2Fsubdir&files=testName.txt'); + redirectStub.restore(); + }); + it('takes the file\'s path into account when clicking download', function() { + var redirectStub = sinon.stub(OC, 'redirect'); + var fileData = { + id: 18, + type: 'file', + name: 'testName.txt', + path: '/anotherpath/there', + mimetype: 'text/plain', + size: '1234', + etag: 'a01234c', + mtime: '123456' + }; + var $tr = fileList.add(fileData); + fileActions.display($tr.find('td.filename'), true, fileList); + + var menuContext = { + $file: $tr, + fileList: fileList, + fileActions: fileActions, + dir: '/anotherpath/there' + }; + menu = new OCA.Files.FileActionsMenu(); + menu.showAt(menuContext); + + menu.$el.find('.action-download').click(); + + expect(redirectStub.calledOnce).toEqual(true); + expect(redirectStub.getCall(0).args[0]).toContain( + OC.webroot + '/index.php/apps/files/ajax/download.php' + + '?dir=%2Fanotherpath%2Fthere&files=testName.txt' + ); + redirectStub.restore(); + }); + it('deletes file when clicking delete', function() { + var deleteStub = sinon.stub(fileList, 'do_delete'); + var fileData = { + id: 18, + type: 'file', + name: 'testName.txt', + path: '/somepath/dir', + mimetype: 'text/plain', + size: '1234', + etag: 'a01234c', + mtime: '123456' + }; + var $tr = fileList.add(fileData); + fileActions.display($tr.find('td.filename'), true, fileList); + + var menuContext = { + $file: $tr, + fileList: fileList, + fileActions: fileActions, + dir: '/somepath/dir' + }; + menu = new OCA.Files.FileActionsMenu(); + menu.showAt(menuContext); + + menu.$el.find('.action-delete').click(); + + expect(deleteStub.calledOnce).toEqual(true); + expect(deleteStub.getCall(0).args[0]).toEqual('testName.txt'); + expect(deleteStub.getCall(0).args[1]).toEqual('/somepath/dir'); + deleteStub.restore(); + }); + }); + describe('hiding', function() { + beforeEach(function() { + menu.$el.trigger(new $.Event('afterHide')); + }); + it('removes highlight on current row', function() { + expect($tr.hasClass('mouseOver')).toEqual(false); + }); + it('destroys its DOM element on hide', function() { + expect($tr.find('.fileActionsMenu').length).toEqual(0); + }); + }); +}); + diff --git a/apps/files/tests/js/filelistSpec.js b/apps/files/tests/js/filelistSpec.js index 5c0c8c96bc..57e1662640 100644 --- a/apps/files/tests/js/filelistSpec.js +++ b/apps/files/tests/js/filelistSpec.js @@ -456,19 +456,19 @@ describe('OCA.Files.FileList tests', function() { expect(notificationStub.notCalled).toEqual(true); }); - it('shows spinner on files to be deleted', function() { + it('shows busy state on files to be deleted', function() { fileList.setFiles(testFiles); doDelete(); - expect(fileList.findFileEl('One.txt').find('.icon-loading-small:not(.icon-delete)').length).toEqual(1); - expect(fileList.findFileEl('Three.pdf').find('.icon-delete:not(.icon-loading-small)').length).toEqual(1); + expect(fileList.findFileEl('One.txt').hasClass('busy')).toEqual(true); + expect(fileList.findFileEl('Three.pdf').hasClass('busy')).toEqual(false); }); - it('shows spinner on all files when deleting all', function() { + it('shows busy state on all files when deleting all', function() { fileList.setFiles(testFiles); fileList.do_delete(); - expect(fileList.$fileList.find('tr .icon-loading-small:not(.icon-delete)').length).toEqual(4); + expect(fileList.$fileList.find('tr.busy').length).toEqual(4); }); it('updates summary when deleting last file', function() { var $summary; @@ -625,7 +625,7 @@ describe('OCA.Files.FileList tests', function() { doCancelRename(); expect($summary.find('.info').text()).toEqual('1 folder and 3 files'); }); - it('Hides actions while rename in progress', function() { + it('Shows busy state while rename in progress', function() { var $tr; doRename(); @@ -634,8 +634,7 @@ describe('OCA.Files.FileList tests', function() { expect($tr.length).toEqual(1); expect(fileList.findFileEl('One.txt').length).toEqual(0); // file actions are hidden - expect($tr.find('.action').hasClass('hidden')).toEqual(true); - expect($tr.find('.fileactions').hasClass('hidden')).toEqual(true); + expect($tr.hasClass('busy')).toEqual(true); // input and form are gone expect(fileList.$fileList.find('input.filename').length).toEqual(0); @@ -1918,16 +1917,17 @@ describe('OCA.Files.FileList tests', function() { it('Clicking on a file name will trigger default action', function() { var actionStub = sinon.stub(); fileList.setFiles(testFiles); - fileList.fileActions.register( - 'text/plain', - 'Test', - OC.PERMISSION_ALL, - function() { + fileList.fileActions.registerAction({ + mime: 'text/plain', + name: 'Test', + type: OCA.Files.FileActions.TYPE_INLINE, + permissions: OC.PERMISSION_ALL, + icon: function() { // Specify icon for hitory button return OC.imagePath('core','actions/history'); }, - actionStub - ); + actionHandler: actionStub + }); fileList.fileActions.setDefault('text/plain', 'Test'); var $tr = fileList.findFileEl('One.txt'); $tr.find('td.filename .nametext').click(); @@ -1958,16 +1958,17 @@ describe('OCA.Files.FileList tests', function() { fileList.$fileList.on('fileActionsReady', readyHandler); - fileList.fileActions.register( - 'text/plain', - 'Test', - OC.PERMISSION_ALL, - function() { + fileList.fileActions.registerAction({ + mime: 'text/plain', + name: 'Test', + type: OCA.Files.FileActions.TYPE_INLINE, + permissions: OC.PERMISSION_ALL, + icon: function() { // Specify icon for hitory button return OC.imagePath('core','actions/history'); }, - actionStub - ); + actionHandler: actionStub + }); var $tr = fileList.findFileEl('One.txt'); expect($tr.find('.action-test').length).toEqual(0); expect(readyHandler.notCalled).toEqual(true); @@ -2256,6 +2257,8 @@ describe('OCA.Files.FileList tests', function() { }); }); describe('Handeling errors', function () { + var redirectStub; + beforeEach(function () { redirectStub = sinon.stub(OC, 'redirect'); @@ -2281,4 +2284,36 @@ describe('OCA.Files.FileList tests', function() { expect(redirectStub.calledWith(OC.generateUrl('apps/files'))).toEqual(true); }); }); + describe('showFileBusyState', function() { + var $tr; + + beforeEach(function() { + fileList.setFiles(testFiles); + $tr = fileList.findFileEl('Two.jpg'); + }); + it('shows spinner on busy rows', function() { + fileList.showFileBusyState('Two.jpg', true); + expect($tr.hasClass('busy')).toEqual(true); + expect(OC.TestUtil.getImageUrl($tr.find('.thumbnail'))) + .toEqual(OC.imagePath('core', 'loading.gif')); + + fileList.showFileBusyState('Two.jpg', false); + expect($tr.hasClass('busy')).toEqual(false); + expect(OC.TestUtil.getImageUrl($tr.find('.thumbnail'))) + .toEqual(OC.imagePath('core', 'filetypes/image.svg')); + }); + it('accepts multiple input formats', function() { + _.each([ + 'Two.jpg', + ['Two.jpg'], + $tr, + [$tr] + ], function(testCase) { + fileList.showFileBusyState(testCase, true); + expect($tr.hasClass('busy')).toEqual(true); + fileList.showFileBusyState(testCase, false); + expect($tr.hasClass('busy')).toEqual(false); + }); + }); + }); }); From b142fbe6d731e761d0098c0e41e42396ee23e24c Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Thu, 6 Aug 2015 10:58:59 +0200 Subject: [PATCH 04/12] Added bubble style, applied to file actions menu --- apps/files/css/files.css | 1 + apps/files/js/fileactionsmenu.js | 2 +- core/css/apps.css | 11 ++++++++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/files/css/files.css b/apps/files/css/files.css index e93993affa..e608a029e2 100644 --- a/apps/files/css/files.css +++ b/apps/files/css/files.css @@ -691,6 +691,7 @@ table.dragshadow td.size { .fileActionsMenu { /* FIXME: should be variable width, but default one is too big */ width: 100px; + padding: 10px; } .fileActionsMenu.hidden { diff --git a/apps/files/js/fileactionsmenu.js b/apps/files/js/fileactionsmenu.js index 1795fdeab1..0cc08a1ec9 100644 --- a/apps/files/js/fileactionsmenu.js +++ b/apps/files/js/fileactionsmenu.js @@ -43,7 +43,7 @@ * @private */ initialize: function() { - this.$el = $(''); + this.$el = $(''); this._template = Handlebars.compile(TEMPLATE_MENU); this.$el.on('click', 'a.action', _.bind(this._onClickAction, this)); diff --git a/core/css/apps.css b/core/css/apps.css index 5769120c5e..d4752cdde7 100644 --- a/core/css/apps.css +++ b/core/css/apps.css @@ -292,8 +292,8 @@ list-style-type: none; } +.bubble, #app-navigation .app-navigation-entry-menu { - display: none; position: absolute; background-color: #eee; color: #333; @@ -310,11 +310,17 @@ filter: drop-shadow(0 0 5px rgba(150, 150, 150, 0.75)); } +#app-navigation .app-navigation-entry-menu { + display: none; +} + #app-navigation .app-navigation-entry-menu.open { display: block; } /* miraculous border arrow stuff */ +.bubble:after, +.bubble:before, #app-navigation .app-navigation-entry-menu:after, #app-navigation .app-navigation-entry-menu:before { bottom: 100%; @@ -327,12 +333,15 @@ pointer-events: none; } +.bubble:after, #app-navigation .app-navigation-entry-menu:after { border-color: rgba(238, 238, 238, 0); border-bottom-color: #eee; border-width: 10px; margin-left: -10px; } + +.bubble:before, #app-navigation .app-navigation-entry-menu:before { border-color: rgba(187, 187, 187, 0); border-bottom-color: #bbb; From 9acbb3e9026c9af3668607f2e6e085d7545b085d Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Mon, 10 Aug 2015 14:13:43 +0200 Subject: [PATCH 05/12] Remove share action display name --- apps/files_sharing/js/share.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/files_sharing/js/share.js b/apps/files_sharing/js/share.js index 389cbf79a3..04700b8401 100644 --- a/apps/files_sharing/js/share.js +++ b/apps/files_sharing/js/share.js @@ -91,7 +91,7 @@ fileActions.registerAction({ name: 'Share', - displayName: t('files_sharing', 'Share'), + displayName: '', mime: 'all', permissions: OC.PERMISSION_SHARE, icon: OC.imagePath('core', 'actions/share'), From 28abbb14855df0d5e15d4660c55ad84a6023aba4 Mon Sep 17 00:00:00 2001 From: Jan-Christoph Borchardt Date: Fri, 7 Aug 2015 17:58:37 +0200 Subject: [PATCH 06/12] fix layout and design of actions dropdown --- apps/files/css/files.css | 15 ++++++++++++++- apps/files/css/mobile.css | 29 +++++++++++++++++------------ core/css/apps.css | 2 +- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/apps/files/css/files.css b/apps/files/css/files.css index e608a029e2..d283a62b70 100644 --- a/apps/files/css/files.css +++ b/apps/files/css/files.css @@ -521,6 +521,20 @@ table td.filename .uploadtext { visibility: hidden; } +/* fix position of bubble pointer for Files app */ +.bubble, +#app-navigation .app-navigation-entry-menu { + border-top-right-radius: 3px; +} +.bubble:after, +#app-navigation .app-navigation-entry-menu:after { + right: 6px; +} +.bubble:before, +#app-navigation .app-navigation-entry-menu:before { + right: 6px; +} + /* force show the loading icon, not only on hover */ #fileList .icon-loading-small { -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; @@ -720,4 +734,3 @@ table.dragshadow td.size { .fileActionsMenu li:hover .action { opacity: 1; } - diff --git a/apps/files/css/mobile.css b/apps/files/css/mobile.css index 4881f7c70e..3c51f15dc0 100644 --- a/apps/files/css/mobile.css +++ b/apps/files/css/mobile.css @@ -5,11 +5,6 @@ min-width: initial !important; } -/* do not show Deleted Files on mobile, not optimized yet and button too long */ -#controls #trash { - display: none; -} - /* hide size and date columns */ table th#headerSize, table td.filesize, @@ -38,7 +33,8 @@ table td.filename .nametext { } /* always show actions on mobile, not only on hover */ -#fileList a.action { +#fileList a.action, +#fileList a.action.action-menu.permanent { -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)" !important; filter: alpha(opacity=20) !important; opacity: .2 !important; @@ -50,17 +46,26 @@ table td.filename .nametext { filter: alpha(opacity=70) !important; opacity: .7 !important; } -/* do not show Rename or Versions on mobile */ -#fileList .action.action-rename, -#fileList .action.action-versions { - display: none !important; +#fileList a.action.action-menu img { + padding-left: 2px; +} + +#fileList .fileActionsMenu { + margin-right: 5px; } /* some padding for better clickability */ #fileList a.action img { padding: 0 6px 0 12px; } -/* hide text of the actions on mobile */ -#fileList a.action span { +#fileList .fileActionsMenu a.action img { + padding: initial; +} +#fileList .fileActionsMenu a.action { + padding: 12px; + margin: -12px; +} +/* hide text of the share action on mobile */ +#fileList a.action-share span { display: none; } diff --git a/core/css/apps.css b/core/css/apps.css index d4752cdde7..300b186bba 100644 --- a/core/css/apps.css +++ b/core/css/apps.css @@ -298,7 +298,7 @@ background-color: #eee; color: #333; border-radius: 3px; - border-top-right-radius: 0px; + border-top-right-radius: 0; z-index: 110; margin: -5px 14px 5px 10px; right: 0; From 5e7e0c7e2d62a56db5cf0f712989e006553b1cbb Mon Sep 17 00:00:00 2001 From: Jan-Christoph Borchardt Date: Fri, 7 Aug 2015 18:08:06 +0200 Subject: [PATCH 07/12] fix ellipsizing for file names --- apps/files/css/files.css | 28 ++++------------------------ 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/apps/files/css/files.css b/apps/files/css/files.css index d283a62b70..231434ec38 100644 --- a/apps/files/css/files.css +++ b/apps/files/css/files.css @@ -372,45 +372,25 @@ table td.filename .nametext .innernametext { @media only screen and (min-width: 1366px) { table td.filename .nametext .innernametext { - max-width: 760px; - } - - table tr:hover td.filename .nametext .innernametext, - table tr:focus td.filename .nametext .innernametext { - max-width: 480px; + max-width: 620px; } } @media only screen and (min-width: 1200px) and (max-width: 1366px) { table td.filename .nametext .innernametext { - max-width: 600px; - } - - table tr:hover td.filename .nametext .innernametext, - table tr:focus td.filename .nametext .innernametext { - max-width: 320px; + max-width: 460px; } } @media only screen and (min-width: 1000px) and (max-width: 1200px) { table td.filename .nametext .innernametext { - max-width: 400px; - } - - table tr:hover td.filename .nametext .innernametext, - table tr:focus td.filename .nametext .innernametext { - max-width: 120px; + max-width: 270px; } } @media only screen and (min-width: 768px) and (max-width: 1000px) { table td.filename .nametext .innernametext { - max-width: 320px; - } - - table tr:hover td.filename .nametext .innernametext, - table tr:focus td.filename .nametext .innernametext { - max-width: 40px; + max-width: 190px; } } From 1283ecac23934f787e39f54d06e7b5701f902926 Mon Sep 17 00:00:00 2001 From: Jan-Christoph Borchardt Date: Fri, 7 Aug 2015 18:19:47 +0200 Subject: [PATCH 08/12] remove whitespace on right cause of moved delete icon --- apps/files/css/files.css | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/apps/files/css/files.css b/apps/files/css/files.css index 231434ec38..5e24e69620 100644 --- a/apps/files/css/files.css +++ b/apps/files/css/files.css @@ -249,8 +249,8 @@ table th.column-last, table td.column-last { box-sizing: border-box; position: relative; /* this can not be just width, both need to be set … table styling */ - min-width: 176px; - max-width: 176px; + min-width: 130px; + max-width: 130px; } /* Multiselect bar */ @@ -326,14 +326,7 @@ table td.filename .nametext, .uploadtext, .modified, .column-last>span:first-chi position: relative; overflow: hidden; text-overflow: ellipsis; - width: 90%; -} -/* ellipsize long modified dates to make room for showing delete button */ -#fileList tr:hover .modified, -#fileList tr:focus .modified, -#fileList tr:hover .column-last>span:first-child, -#fileList tr:focus .column-last>span:first-child { - width: 75%; + width: 110px; } /* TODO fix usability bug (accidental file/folder selection) */ @@ -372,25 +365,27 @@ table td.filename .nametext .innernametext { @media only screen and (min-width: 1366px) { table td.filename .nametext .innernametext { - max-width: 620px; + max-width: 660px; } } - @media only screen and (min-width: 1200px) and (max-width: 1366px) { table td.filename .nametext .innernametext { - max-width: 460px; + max-width: 500px; } } - -@media only screen and (min-width: 1000px) and (max-width: 1200px) { +@media only screen and (min-width: 1100px) and (max-width: 1200px) { table td.filename .nametext .innernametext { - max-width: 270px; + max-width: 400px; + } +} +@media only screen and (min-width: 1000px) and (max-width: 1100px) { + table td.filename .nametext .innernametext { + max-width: 310px; } } - @media only screen and (min-width: 768px) and (max-width: 1000px) { table td.filename .nametext .innernametext { - max-width: 190px; + max-width: 240px; } } From 08912308a05090ab757636cd8e542298ebd90942 Mon Sep 17 00:00:00 2001 From: Jan-Christoph Borchardt Date: Fri, 7 Aug 2015 18:26:07 +0200 Subject: [PATCH 09/12] fix width of action dropdown and last layout details --- apps/files/css/files.css | 21 ++++++++++++++------- apps/files/css/mobile.css | 7 ------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/files/css/files.css b/apps/files/css/files.css index 5e24e69620..26ba86b28c 100644 --- a/apps/files/css/files.css +++ b/apps/files/css/files.css @@ -524,11 +524,10 @@ table td.filename .uploadtext { cursor: default !important; } -a.action>img { - max-height:16px; - max-width:16px; - vertical-align:text-bottom; - margin-bottom: -1px; +a.action > img { + max-height: 16px; + max-width: 16px; + vertical-align: text-bottom; } /* Actions for selected files */ @@ -678,9 +677,17 @@ table.dragshadow td.size { } .fileActionsMenu { - /* FIXME: should be variable width, but default one is too big */ - width: 100px; + padding: 4px 12px; +} +.fileActionsMenu li { + padding: 5px 0; +} +#fileList .fileActionsMenu a.action img { + padding: initial; +} +#fileList .fileActionsMenu a.action { padding: 10px; + margin: -10px; } .fileActionsMenu.hidden { diff --git a/apps/files/css/mobile.css b/apps/files/css/mobile.css index 3c51f15dc0..dd8244a291 100644 --- a/apps/files/css/mobile.css +++ b/apps/files/css/mobile.css @@ -57,13 +57,6 @@ table td.filename .nametext { #fileList a.action img { padding: 0 6px 0 12px; } -#fileList .fileActionsMenu a.action img { - padding: initial; -} -#fileList .fileActionsMenu a.action { - padding: 12px; - margin: -12px; -} /* hide text of the share action on mobile */ #fileList a.action-share span { display: none; From 86e1eaf370ff5c290805bac130f3c3bbd1d1c774 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Mon, 10 Aug 2015 15:57:21 +0200 Subject: [PATCH 10/12] Inline actions using default renderer are now always permanent Default renderer like the favorite icon can decide whether to use the permanent class or not. Fixed sharing code to properly update the icon according to sharing state modifications. --- apps/files/js/fileactions.js | 1 + apps/files_sharing/tests/js/shareSpec.js | 6 +++--- core/js/share.js | 4 +--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/apps/files/js/fileactions.js b/apps/files/js/fileactions.js index 0d9161c6eb..5ec26d8b33 100644 --- a/apps/files/js/fileactions.js +++ b/apps/files/js/fileactions.js @@ -308,6 +308,7 @@ var $actionLink = this._makeActionLink(params, context); context.$file.find('a.name>span.fileactions').append($actionLink); + $actionLink.addClass('permanent'); return $actionLink; } }, diff --git a/apps/files_sharing/tests/js/shareSpec.js b/apps/files_sharing/tests/js/shareSpec.js index aa409285ca..581e15caf9 100644 --- a/apps/files_sharing/tests/js/shareSpec.js +++ b/apps/files_sharing/tests/js/shareSpec.js @@ -97,7 +97,7 @@ describe('OCA.Sharing.Util tests', function() { }]); $tr = fileList.$el.find('tbody tr:first'); $action = $tr.find('.action-share'); - expect($action.hasClass('permanent')).toEqual(false); + expect($action.hasClass('permanent')).toEqual(true); expect(OC.basename($action.find('img').attr('src'))).toEqual('share.svg'); expect(OC.basename(getImageUrl($tr.find('.filename .thumbnail')))).toEqual('folder.svg'); expect($action.find('img').length).toEqual(1); @@ -257,7 +257,7 @@ describe('OCA.Sharing.Util tests', function() { $action = fileList.$el.find('tbody tr:first .action-share'); $tr = fileList.$el.find('tr:first'); - expect($action.hasClass('permanent')).toEqual(false); + expect($action.hasClass('permanent')).toEqual(true); $tr.find('.action-share').click(); @@ -344,7 +344,7 @@ describe('OCA.Sharing.Util tests', function() { expect($tr.attr('data-share-recipients')).not.toBeDefined(); OC.Share.updateIcon('file', 1); - expect($action.hasClass('permanent')).toEqual(false); + expect($action.hasClass('permanent')).toEqual(true); }); it('keep share text after updating reshare', function() { var $action, $tr; diff --git a/core/js/share.js b/core/js/share.js index 99fd08c641..57dd0dd655 100644 --- a/core/js/share.js +++ b/core/js/share.js @@ -266,7 +266,6 @@ OC.Share={ if (hasShares || owner) { recipients = $tr.attr('data-share-recipients'); - action.addClass('permanent'); message = t('core', 'Shared'); // even if reshared, only show "Shared by" if (owner) { @@ -281,8 +280,7 @@ OC.Share={ } } else { - action.removeClass('permanent'); - action.html(' '+ escapeHTML(t('core', 'Share'))+'').prepend(img); + action.html('').prepend(img); } if (hasLink) { image = OC.imagePath('core', 'actions/public'); From a5aa03a1a6c0f659c0528253d28c63f759d1ed50 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Tue, 11 Aug 2015 11:35:21 +0200 Subject: [PATCH 11/12] Load backbone when running unit tests --- core/js/core.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/js/core.json b/core/js/core.json index 1053debaa9..a67491c4a3 100644 --- a/core/js/core.json +++ b/core/js/core.json @@ -7,7 +7,8 @@ "moment/min/moment-with-locales.js", "handlebars/handlebars.js", "blueimp-md5/js/md5.js", - "bootstrap/js/tooltip.js" + "bootstrap/js/tooltip.js", + "backbone/backbone.js" ], "libraries": [ "jquery-showpassword.js", @@ -19,6 +20,7 @@ "jquery.ocdialog.js", "oc-dialogs.js", "js.js", + "oc-backbone.js", "l10n.js", "apps.js", "share.js", From 984ae8140d986e93a2fcea5951436e95c8e2c603 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Tue, 11 Aug 2015 11:35:46 +0200 Subject: [PATCH 12/12] Fixed file actions menu to close when reclicking trigger FileActionsMenu is now a backbone view. The trigger and highlight handling is now done in the FileActions.showMenu() method using events. --- apps/files/js/fileactions.js | 32 ++++++++++----- apps/files/js/fileactionsmenu.js | 47 +++++++--------------- apps/files/tests/js/fileactionsSpec.js | 24 +++++++++-- apps/files/tests/js/fileactionsmenuSpec.js | 28 +++---------- core/js/js.js | 9 ++++- 5 files changed, 67 insertions(+), 73 deletions(-) diff --git a/apps/files/js/fileactions.js b/apps/files/js/fileactions.js index 5ec26d8b33..43f74c5816 100644 --- a/apps/files/js/fileactions.js +++ b/apps/files/js/fileactions.js @@ -48,13 +48,6 @@ _fileActionTriggerTemplate: null, - /** - * File actions menu - * - * @type OCA.Files.FileActionsMenu - */ - _menu: null, - /** * @private */ @@ -333,8 +326,20 @@ * @param {OCA.Files.FileActionContext} context rendering context */ _showMenu: function(fileName, context) { - this._menu = new OCA.Files.FileActionsMenu(); - this._menu.showAt(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); }, /** @@ -378,14 +383,19 @@ 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 diff --git a/apps/files/js/fileactionsmenu.js b/apps/files/js/fileactionsmenu.js index 0cc08a1ec9..623ebde544 100644 --- a/apps/files/js/fileactionsmenu.js +++ b/apps/files/js/fileactionsmenu.js @@ -24,13 +24,9 @@ * @constructs FileActionsMenu * @memberof OCA.Files */ - var FileActionsMenu = function() { - this.initialize(); - }; - - FileActionsMenu.prototype = { - $el: null, - _template: null, + var FileActionsMenu = OC.Backbone.View.extend({ + tagName: 'div', + className: 'fileActionsMenu bubble hidden open menu', /** * Current context @@ -39,19 +35,15 @@ */ _context: null, - /** - * @private - */ - initialize: function() { - this.$el = $(''); - this._template = Handlebars.compile(TEMPLATE_MENU); - - this.$el.on('click', 'a.action', _.bind(this._onClickAction, this)); - this.$el.on('afterHide', _.bind(this._onHide, this)); + events: { + 'click a.action': '_onClickAction' }, - destroy: function() { - this.$el.remove(); + template: function(data) { + if (!OCA.Files.FileActionsMenu._TEMPLATE) { + OCA.Files.FileActionsMenu._TEMPLATE = Handlebars.compile(TEMPLATE_MENU); + } + return OCA.Files.FileActionsMenu._TEMPLATE(data); }, /** @@ -113,8 +105,7 @@ return item; }); - this.$el.empty(); - this.$el.append(this._template({ + this.$el.html(this.template({ items: items })); }, @@ -123,27 +114,17 @@ * Displays the menu under the given element * * @param {OCA.Files.FileActionContext} context context + * @param {Object} $trigger trigger element */ - showAt: function(context) { + show: function(context) { this._context = context; this.render(); this.$el.removeClass('hidden'); - context.$file.find('td.filename').append(this.$el); - context.$file.addClass('mouseOver'); - OC.showMenu(null, this.$el); - }, - - /** - * Whenever the menu is hidden - */ - _onHide: function() { - this._context.$file.removeClass('mouseOver'); - this.destroy(); } - }; + }); OCA.Files.FileActionsMenu = FileActionsMenu; diff --git a/apps/files/tests/js/fileactionsSpec.js b/apps/files/tests/js/fileactionsSpec.js index 8c43b917fa..236cff6caf 100644 --- a/apps/files/tests/js/fileactionsSpec.js +++ b/apps/files/tests/js/fileactionsSpec.js @@ -192,10 +192,26 @@ describe('OCA.Files.FileActions tests', function() { context = actionStub.getCall(0).args[1]; expect(context.dir).toEqual('/somepath'); }); - it('shows actions menu when clicking the menu trigger', function() { - expect($tr.find('.menu').length).toEqual(0); - $tr.find('.action-menu').click(); - expect($tr.find('.menu').length).toEqual(1); + describe('actions menu', function() { + it('shows actions menu inside row when clicking the menu trigger', function() { + expect($tr.find('td.filename .fileActionsMenu').length).toEqual(0); + $tr.find('.action-menu').click(); + expect($tr.find('td.filename .fileActionsMenu').length).toEqual(1); + }); + it('shows highlight on current row', function() { + $tr.find('.action-menu').click(); + expect($tr.hasClass('mouseOver')).toEqual(true); + }); + it('cleans up after hiding', function() { + var clock = sinon.useFakeTimers(); + $tr.find('.action-menu').click(); + expect($tr.find('.fileActionsMenu').length).toEqual(1); + OC.hideMenus(); + // sliding animation + clock.tick(500); + expect($tr.hasClass('mouseOver')).toEqual(false); + expect($tr.find('.fileActionsMenu').length).toEqual(0); + }); }); }); describe('custom rendering', function() { diff --git a/apps/files/tests/js/fileactionsmenuSpec.js b/apps/files/tests/js/fileactionsmenuSpec.js index 4343979497..0cfd12a2d0 100644 --- a/apps/files/tests/js/fileactionsmenuSpec.js +++ b/apps/files/tests/js/fileactionsmenuSpec.js @@ -87,24 +87,17 @@ describe('OCA.Files.FileActionsMenu tests', function() { dir: fileList.getCurrentDirectory() }; menu = new OCA.Files.FileActionsMenu(); - menu.showAt(menuContext); + menu.show(menuContext); }); afterEach(function() { fileActions = null; fileList.destroy(); fileList = undefined; - menu.destroy(); + menu.remove(); $('#dir, #permissions, #filestable').remove(); }); describe('rendering', function() { - it('displays menu in the row container', function() { - expect(menu.$el.closest('td.filename').length).toEqual(1); - expect($tr.find('.fileActionsMenu').length).toEqual(1); - }); - it('highlights the row in the file list', function() { - expect($tr.hasClass('mouseOver')).toEqual(true); - }); it('renders dropdown actions in menu', function() { var $action = menu.$el.find('a[data-action=Testdropdown]'); expect($action.length).toEqual(1); @@ -200,7 +193,7 @@ describe('OCA.Files.FileActionsMenu tests', function() { dir: fileList.getCurrentDirectory() }; menu = new OCA.Files.FileActionsMenu(); - menu.showAt(menuContext); + menu.show(menuContext); menu.$el.find('.action-download').click(); @@ -233,7 +226,7 @@ describe('OCA.Files.FileActionsMenu tests', function() { dir: '/anotherpath/there' }; menu = new OCA.Files.FileActionsMenu(); - menu.showAt(menuContext); + menu.show(menuContext); menu.$el.find('.action-download').click(); @@ -266,7 +259,7 @@ describe('OCA.Files.FileActionsMenu tests', function() { dir: '/somepath/dir' }; menu = new OCA.Files.FileActionsMenu(); - menu.showAt(menuContext); + menu.show(menuContext); menu.$el.find('.action-delete').click(); @@ -276,16 +269,5 @@ describe('OCA.Files.FileActionsMenu tests', function() { deleteStub.restore(); }); }); - describe('hiding', function() { - beforeEach(function() { - menu.$el.trigger(new $.Event('afterHide')); - }); - it('removes highlight on current row', function() { - expect($tr.hasClass('mouseOver')).toEqual(false); - }); - it('destroys its DOM element on hide', function() { - expect($tr.find('.fileActionsMenu').length).toEqual(0); - }); - }); }); diff --git a/core/js/js.js b/core/js/js.js index 25baafde08..89bb9a7143 100644 --- a/core/js/js.js +++ b/core/js/js.js @@ -611,9 +611,14 @@ var OC={ */ hideMenus: function(complete) { if (OC._currentMenu) { + var lastMenu = OC._currentMenu; OC._currentMenu.trigger(new $.Event('beforeHide')); - OC._currentMenu.slideUp(OC.menuSpeed, complete); - OC._currentMenu.trigger(new $.Event('afterHide')); + OC._currentMenu.slideUp(OC.menuSpeed, function() { + lastMenu.trigger(new $.Event('afterHide')); + if (complete) { + complete.apply(this, arguments); + } + }); } OC._currentMenu = null; OC._currentMenuToggle = null;