From ca34921cdf8db4075906b3531390aa1b1ae9216c Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Thu, 16 Jul 2015 15:28:45 +0200 Subject: 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 | 302 +++++++++++++++++++------------- 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, 501 insertions(+), 207 deletions(-) create mode 100644 apps/files/js/fileactionsmenu.js diff --git a/apps/files/css/files.css b/apps/files/css/files.css index 7e3318a962b..e93993affa8 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 dca3e5ae74d..a41ec059b55 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 8dd26d71c3e..3c13f087f7a 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')); - } - var html = ''; - if (img) { - html += ''; - } - if (actionSpec.displayName) { - html += ' ' + actionSpec.displayName + ''; + _makeActionLink: function(params) { + if (!this._fileActionTriggerTemplate) { + this._fileActionTriggerTemplate = Handlebars.compile(TEMPLATE_FILE_ACTION_TRIGGER); } - 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.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; + 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(); } + }); - 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')); + this.setDefault('dir', 'Open'); } }; diff --git a/apps/files/js/fileactionsmenu.js b/apps/files/js/fileactionsmenu.js new file mode 100644 index 00000000000..dabf530b177 --- /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 f5629ecd2c3..e297edcf11b 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 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; diff --git a/apps/files_sharing/js/share.js b/apps/files_sharing/js/share.js index 12bec0e8c9a..389cbf79a32 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) { - - 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'); - } + 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 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 ffe0472b2b1..2962f62520d 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 315349d293c..473cce88a71 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 72d4edd28dd..25baafde08f 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(); }); -- cgit v1.2.3 From dd4e0a8253d108e8b9cf9444990164d66b3d75f0 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Tue, 4 Aug 2015 18:25:35 +0200 Subject: 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 3c13f087f7a..6b95e3ee6cd 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'); }, /** -- cgit v1.2.3 From 9454e9043a7b18108f14a00d6ab8afa62fb894af Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Wed, 5 Aug 2015 12:48:42 +0200 Subject: 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 | 547 ++++++++++++++--------------- apps/files/tests/js/fileactionsmenuSpec.js | 291 +++++++++++++++ apps/files/tests/js/filelistSpec.js | 79 +++-- 5 files changed, 620 insertions(+), 319 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 6b95e3ee6cd..0d9161c6eb4 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 dabf530b177..1795fdeab11 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 e420ab828af..8c43b917fa9 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); - }); - 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' - }; - - // note: FileActions.display() is called implicitly - var $tr = fileList.add(fileData); - - // 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); - }); - 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); - - FileActions.display($tr.find('td.filename'), true, fileList); - FileActions.display($tr.find('td.filename'), true, fileList); - - // 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(); + fileActions.clear(); + expect(fileActions.actions).toEqual({}); + expect(fileActions.defaults).toEqual({}); + expect(fileActions.icons).toEqual({}); + expect(fileActions.currentFile).toBe(null); }); - 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); + describe('displaying actions', function() { + var $tr; - var $action = $tr.find('.action.delete'); + 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 + }; - 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(); + // 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('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'); + describe('action handler', function() { + var actionStub, $tr; + + 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'); + + // 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 00000000000..43439794975 --- /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 5c0c8c96bc5..57e16626403 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); + }); + }); + }); }); -- cgit v1.2.3 From b142fbe6d731e761d0098c0e41e42396ee23e24c Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Thu, 6 Aug 2015 10:58:59 +0200 Subject: 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 e93993affa8..e608a029e2b 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 1795fdeab11..0cc08a1ec90 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 5769120c5ed..d4752cdde7e 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; -- cgit v1.2.3 From 9acbb3e9026c9af3668607f2e6e085d7545b085d Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Mon, 10 Aug 2015 14:13:43 +0200 Subject: 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 389cbf79a32..04700b84011 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'), -- cgit v1.2.3 From 28abbb14855df0d5e15d4660c55ad84a6023aba4 Mon Sep 17 00:00:00 2001 From: Jan-Christoph Borchardt Date: Fri, 7 Aug 2015 17:58:37 +0200 Subject: 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 e608a029e2b..d283a62b70b 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 4881f7c70e4..3c51f15dc0f 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 d4752cdde7e..300b186bba2 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; -- cgit v1.2.3 From 5e7e0c7e2d62a56db5cf0f712989e006553b1cbb Mon Sep 17 00:00:00 2001 From: Jan-Christoph Borchardt Date: Fri, 7 Aug 2015 18:08:06 +0200 Subject: 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 d283a62b70b..231434ec38d 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; } } -- cgit v1.2.3 From 1283ecac23934f787e39f54d06e7b5701f902926 Mon Sep 17 00:00:00 2001 From: Jan-Christoph Borchardt Date: Fri, 7 Aug 2015 18:19:47 +0200 Subject: 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 231434ec38d..5e24e696209 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; } } -- cgit v1.2.3 From 08912308a05090ab757636cd8e542298ebd90942 Mon Sep 17 00:00:00 2001 From: Jan-Christoph Borchardt Date: Fri, 7 Aug 2015 18:26:07 +0200 Subject: 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 5e24e696209..26ba86b28c8 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 3c51f15dc0f..dd8244a2913 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; -- cgit v1.2.3 From 86e1eaf370ff5c290805bac130f3c3bbd1d1c774 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Mon, 10 Aug 2015 15:57:21 +0200 Subject: 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 0d9161c6eb4..5ec26d8b334 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 aa409285ca4..581e15caf93 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 99fd08c6411..57dd0dd6553 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'); -- cgit v1.2.3 From a5aa03a1a6c0f659c0528253d28c63f759d1ed50 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Tue, 11 Aug 2015 11:35:21 +0200 Subject: 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 1053debaa99..a67491c4a35 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", -- cgit v1.2.3 From 984ae8140d986e93a2fcea5951436e95c8e2c603 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Tue, 11 Aug 2015 11:35:46 +0200 Subject: 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 5ec26d8b334..43f74c5816d 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 0cc08a1ec90..623ebde5442 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 8c43b917fa9..236cff6cafd 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 43439794975..0cfd12a2d04 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 25baafde08f..89bb9a71430 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; -- cgit v1.2.3