diff options
author | Jan-Christoph Borchardt <hey@jancborchardt.net> | 2015-08-11 15:13:59 +0200 |
---|---|---|
committer | Jan-Christoph Borchardt <hey@jancborchardt.net> | 2015-08-11 15:13:59 +0200 |
commit | d04a6bce6f81df4ee4351a311a630b14e8ecd8a3 (patch) | |
tree | aa37e75da3021abf7e9fff06c419e8cd1461970b | |
parent | cd0a2874de1f52827bca8d747128909099229cf4 (diff) | |
parent | 984ae8140d986e93a2fcea5951436e95c8e2c603 (diff) | |
download | nextcloud-server-d04a6bce6f81df4ee4351a311a630b14e8ecd8a3.tar.gz nextcloud-server-d04a6bce6f81df4ee4351a311a630b14e8ecd8a3.zip |
Merge pull request #17709 from owncloud/fileactions-dropdown
Move file actions to dropdown
-rw-r--r-- | apps/files/css/files.css | 123 | ||||
-rw-r--r-- | apps/files/css/mobile.css | 22 | ||||
-rw-r--r-- | apps/files/index.php | 1 | ||||
-rw-r--r-- | apps/files/js/fileactions.js | 321 | ||||
-rw-r--r-- | apps/files/js/fileactionsmenu.js | 132 | ||||
-rw-r--r-- | apps/files/js/filelist.js | 56 | ||||
-rw-r--r-- | apps/files/js/tagsplugin.js | 1 | ||||
-rw-r--r-- | apps/files/tests/js/fileactionsSpec.js | 563 | ||||
-rw-r--r-- | apps/files/tests/js/fileactionsmenuSpec.js | 273 | ||||
-rw-r--r-- | apps/files/tests/js/filelistSpec.js | 79 | ||||
-rw-r--r-- | apps/files_sharing/js/share.js | 96 | ||||
-rw-r--r-- | apps/files_sharing/templates/public.php | 1 | ||||
-rw-r--r-- | apps/files_sharing/tests/js/shareSpec.js | 6 | ||||
-rw-r--r-- | apps/files_trashbin/js/app.js | 1 | ||||
-rw-r--r-- | core/css/apps.css | 13 | ||||
-rw-r--r-- | core/js/core.json | 4 | ||||
-rw-r--r-- | core/js/js.js | 61 | ||||
-rw-r--r-- | core/js/share.js | 4 |
18 files changed, 1176 insertions, 581 deletions
diff --git a/apps/files/css/files.css b/apps/files/css/files.css index 7e3318a962b..26ba86b28c8 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,45 +365,27 @@ 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: 660px; } } - @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: 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: 400px; } - - table tr:hover td.filename .nametext .innernametext, - table tr:focus td.filename .nametext .innernametext { - max-width: 120px; +} +@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: 320px; - } - - table tr:hover td.filename .nametext .innernametext, - table tr:focus td.filename .nametext .innernametext { - max-width: 40px; + max-width: 240px; } } @@ -517,6 +492,23 @@ table td.filename .uploadtext { font-size: 11px; } +.busy .fileactions, .busy .action { + 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 { @@ -527,21 +519,15 @@ 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; } -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 */ @@ -578,10 +564,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 +675,44 @@ table.dragshadow td.size { .mask.transparent{ opacity: 0; } + +.fileActionsMenu { + 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 { + 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/css/mobile.css b/apps/files/css/mobile.css index 4881f7c70e4..dd8244a2913 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,19 @@ 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 { +/* hide text of the share action on mobile */ +#fileList a.action-share span { display: none; } 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..43f74c5816d 100644 --- a/apps/files/js/fileactions.js +++ b/apps/files/js/fileactions.js @@ -10,6 +10,12 @@ (function() { + var TEMPLATE_FILE_ACTION_TRIGGER = + '<a class="action action-{{nameLowerCase}}" href="#" data-action="{{name}}">' + + '{{#if icon}}<img class="svg" alt="{{altText}}" src="{{icon}}" />{{/if}}' + + '{{#if displayName}}<span> {{displayName}}</span>{{/if}}' + + '</a>'; + /** * Construct a new FileActions instance * @constructs FileActions @@ -18,6 +24,8 @@ var FileActions = function() { this.initialize(); }; + FileActions.TYPE_DROPDOWN = 0; + FileActions.TYPE_INLINE = 1; FileActions.prototype = { /** @lends FileActions.prototype */ actions: {}, @@ -38,6 +46,8 @@ */ _updateListeners: {}, + _fileActionTriggerTemplate: null, + /** * @private */ @@ -46,6 +56,8 @@ // abusing jquery for events until we get a real event lib this.$el = $('<div class="dummy-fileactions hidden"></div>'); $('body').append(this.$el); + + this._showMenuClosure = _.bind(this._showMenu, this); }, /** @@ -111,6 +123,7 @@ displayName: displayName || name }); }, + /** * Register action * @@ -125,15 +138,14 @@ displayName: action.displayName, mime: mime, icon: action.icon, - permissions: action.permissions + permissions: action.permissions, + type: action.type || FileActions.TYPE_DROPDOWN }; if (_.isUndefined(action.displayName)) { actionSpec.displayName = t('files', name); } if (_.isFunction(action.render)) { actionSpec.render = action.render; - } else { - actionSpec.render = _.bind(this._defaultRenderAction, this); } if (!this.actions[mime]) { this.actions[mime] = {}; @@ -162,6 +174,16 @@ this.defaults[mime] = name; this._notifyUpdateListeners('setDefault', {defaultAction: {mime: mime, name: name}}); }, + + /** + * Returns a map of file actions handlers matching the given conditions + * + * @param {string} mime mime type + * @param {string} type "dir" or "file" + * @param {int} permissions permissions + * + * @return {Object.<string,OCA.Files.FileActions~actionHandler>} map of action name to action spec + */ get: function (mime, type, permissions) { var actions = this.getActions(mime, type, permissions); var filteredActions = {}; @@ -170,6 +192,16 @@ }); return filteredActions; }, + + /** + * Returns an array of file actions matching the given conditions + * + * @param {string} mime mime type + * @param {string} type "dir" or "file" + * @param {int} permissions permissions + * + * @return {Array.<OCA.Files.FileAction>} array of action specs + */ getActions: function (mime, type, permissions) { var actions = {}; if (this.actions.all) { @@ -197,7 +229,37 @@ }); return filteredActions; }, + + /** + * Returns the default file action handler for the given conditions + * + * @param {string} mime mime type + * @param {string} type "dir" or "file" + * @param {int} permissions permissions + * + * @return {OCA.Files.FileActions~actionHandler} action handler + * + * @deprecated use getDefaultFileAction instead + */ getDefault: function (mime, type, permissions) { + var defaultActionSpec = this.getDefaultFileAction(mime, type, permissions); + if (defaultActionSpec) { + return defaultActionSpec.action; + } + return undefined; + }, + + /** + * Returns the default file action handler for the given conditions + * + * @param {string} mime mime type + * @param {string} type "dir" or "file" + * @param {int} permissions permissions + * + * @return {OCA.Files.FileActions~actionHandler} action handler + * @since 8.2 + */ + getDefaultFileAction: function(mime, type, permissions) { var mimePart; if (mime) { mimePart = mime.substr(0, mime.indexOf('/')); @@ -212,9 +274,10 @@ } else { name = this.defaults.all; } - var actions = this.get(mime, type, permissions); + var actions = this.getActions(mime, type, permissions); return actions[name]; }, + /** * Default function to render actions * @@ -224,87 +287,82 @@ * @param {OCA.Files.FileActionContext} context action context */ _defaultRenderAction: function(actionSpec, isDefault, context) { - var name = actionSpec.name; - if (name === 'Download' || !isDefault) { - var $actionLink = this._makeActionLink(actionSpec, context); + if (!isDefault) { + var params = { + name: actionSpec.name, + nameLowerCase: actionSpec.name.toLowerCase(), + displayName: actionSpec.displayName, + icon: actionSpec.icon, + altText: actionSpec.altText, + }; + if (_.isFunction(actionSpec.icon)) { + params.icon = actionSpec.icon(context.$file.attr('data-file')); + } + + var $actionLink = this._makeActionLink(params, context); context.$file.find('a.name>span.fileactions').append($actionLink); + $actionLink.addClass('permanent'); return $actionLink; } }, + /** * Renders the action link element * - * @param {OCA.Files.FileAction} actionSpec action object - * @param {OCA.Files.FileActionContext} context action context + * @param {Object} params action params */ - _makeActionLink: function(actionSpec, context) { - var img = actionSpec.icon; - if (img && img.call) { - img = img(context.$file.attr('data-file')); - } - var html = '<a href="#">'; - if (img) { - html += '<img class="svg" alt="" src="' + img + '" />'; - } - if (actionSpec.displayName) { - html += '<span> ' + actionSpec.displayName + '</span>'; + _makeActionLink: function(params) { + if (!this._fileActionTriggerTemplate) { + this._fileActionTriggerTemplate = Handlebars.compile(TEMPLATE_FILE_ACTION_TRIGGER); } - html += '</a>'; - return $(html); + return $(this._fileActionTriggerTemplate(params)); }, + /** - * Custom renderer for the "Rename" action. - * Displays the rename action as an icon behind the file name. + * Displays the file actions dropdown menu * - * @param {OCA.Files.FileAction} actionSpec file action to render - * @param {boolean} isDefault true if the action is a default action, - * false otherwise - * @param {OCAFiles.FileActionContext} context rendering context + * @param {string} fileName file name + * @param {OCA.Files.FileActionContext} context rendering context */ - _renderRenameAction: function(actionSpec, isDefault, context) { - var $actionEl = this._makeActionLink(actionSpec, context); - var $container = context.$file.find('a.name span.nametext'); - $actionEl.find('img').attr('alt', t('files', 'Rename')); - $container.find('.action-rename').remove(); - $container.append($actionEl); - return $actionEl; + _showMenu: function(fileName, context) { + var menu; + var $trigger = context.$file.closest('tr').find('.fileactions .action-menu'); + $trigger.addClass('open'); + + menu = new OCA.Files.FileActionsMenu(); + menu.$el.on('afterHide', function() { + context.$file.removeClass('mouseOver'); + $trigger.removeClass('open'); + menu.remove(); + }); + + context.$file.addClass('mouseOver'); + context.$file.find('td.filename').append(menu.$el); + menu.show(context); }, + /** - * Custom renderer for the "Delete" action. - * Displays the "Delete" action as a trash icon at the end of - * the table row. - * - * @param {OCA.Files.FileAction} actionSpec file action to render - * @param {boolean} isDefault true if the action is a default action, - * false otherwise - * @param {OCAFiles.FileActionContext} context rendering context + * Renders the menu trigger on the given file list row + * + * @param {Object} $tr file list row element + * @param {OCA.Files.FileActionContext} context rendering context */ - _renderDeleteAction: function(actionSpec, isDefault, context) { - var mountType = context.$file.attr('data-mounttype'); - var deleteTitle = t('files', 'Delete'); - if (mountType === 'external-root') { - deleteTitle = t('files', 'Disconnect storage'); - } else if (mountType === 'shared-root') { - deleteTitle = t('files', 'Unshare'); - } - var cssClasses = 'action delete icon-delete'; - if((context.$file.data('permissions') & OC.PERMISSION_DELETE) === 0) { - // add css class no-permission to delete icon - cssClasses += ' no-permission'; - deleteTitle = t('files', 'No permission to delete'); - } - var $actionLink = $('<a href="#" original-title="' + - escapeHTML(deleteTitle) + - '" class="' +cssClasses + '">' + - '<span class="hidden-visually">' + escapeHTML(deleteTitle) + '</span>' + - '</a>' - ); - var $container = context.$file.find('td:last'); - $container.find('.delete').remove(); - $container.append($actionLink); - return $actionLink; + _renderMenuTrigger: function($tr, context) { + // remove previous + $tr.find('.action-menu').remove(); + + var $el = this._renderInlineAction({ + name: 'menu', + displayName: '', + icon: OC.imagePath('core', 'actions/more'), + altText: t('files', 'Actions'), + action: this._showMenuClosure + }, false, context); + + $el.addClass('permanent'); }, + /** * Renders the action element by calling actionSpec.render() and * registers the click event to process the action. @@ -312,25 +370,32 @@ * @param {OCA.Files.FileAction} actionSpec file action to render * @param {boolean} isDefault true if the action is a default action, * false otherwise - * @param {OCAFiles.FileActionContext} context rendering context + * @param {OCA.Files.FileActionContext} context rendering context */ - _renderAction: function(actionSpec, isDefault, context) { - var $actionEl = actionSpec.render(actionSpec, isDefault, context); + _renderInlineAction: function(actionSpec, isDefault, context) { + var renderFunc = actionSpec.render || _.bind(this._defaultRenderAction, this); + var $actionEl = renderFunc(actionSpec, isDefault, context); if (!$actionEl || !$actionEl.length) { return; } - $actionEl.addClass('action action-' + actionSpec.name.toLowerCase()); - $actionEl.attr('data-action', actionSpec.name); $actionEl.on( 'click', { a: null }, function(event) { + event.stopPropagation(); + event.preventDefault(); + + if ($actionEl.hasClass('open')) { + return; + } + var $file = $(event.target).closest('tr'); + if ($file.hasClass('busy')) { + return; + } var currentFile = $file.find('td.filename'); var fileName = $file.attr('data-file'); - event.stopPropagation(); - event.preventDefault(); context.fileActions.currentFile = currentFile; // also set on global object for legacy apps @@ -346,6 +411,7 @@ ); return $actionEl; }, + /** * Display file actions for the given element * @param parent "td" element of the file for which to display actions @@ -376,36 +442,29 @@ nameLinks = parent.children('a.name'); nameLinks.find('.fileactions, .nametext .action').remove(); nameLinks.append('<span class="fileactions" />'); - var defaultAction = this.getDefault( + var defaultAction = this.getDefaultFileAction( this.getCurrentMimeType(), this.getCurrentType(), this.getCurrentPermissions() ); + var context = { + $file: $tr, + fileActions: this, + fileList: fileList + }; + $.each(actions, function (name, actionSpec) { - if (name !== 'Share') { - self._renderAction( + if (actionSpec.type === FileActions.TYPE_INLINE) { + self._renderInlineAction( actionSpec, - actionSpec.action === defaultAction, { - $file: $tr, - fileActions: this, - fileList : fileList - } + defaultAction && actionSpec.name === defaultAction.name, + context ); } }); - // added here to make sure it's always the last action - var shareActionSpec = actions.Share; - if (shareActionSpec){ - this._renderAction( - shareActionSpec, - shareActionSpec.action === defaultAction, { - $file: $tr, - fileActions: this, - fileList: fileList - } - ); - } + + this._renderMenuTrigger($tr, context); if (triggerEvent){ fileList.$fileList.trigger(jQuery.Event("fileActionsReady", {fileList: fileList, $files: $tr})); @@ -429,35 +488,42 @@ */ registerDefaultActions: function() { this.registerAction({ - name: 'Delete', - displayName: '', + name: 'Download', + displayName: t('files', 'Download'), mime: 'all', - // permission is READ because we show a hint instead if there is no permission permissions: OC.PERMISSION_READ, - icon: function() { - return OC.imagePath('core', 'actions/delete'); + icon: function () { + return OC.imagePath('core', 'actions/download'); }, - render: _.bind(this._renderDeleteAction, this), - actionHandler: function(fileName, context) { - // if there is no permission to delete do nothing - if((context.$file.data('permissions') & OC.PERMISSION_DELETE) === 0) { + actionHandler: function (filename, context) { + var dir = context.dir || context.fileList.getCurrentDirectory(); + var url = context.fileList.getDownloadUrl(filename, dir); + + var downloadFileaction = $(context.$file).find('.fileactions .action-download'); + + // don't allow a second click on the download action + if(downloadFileaction.hasClass('disabled')) { return; } - context.fileList.do_delete(fileName, context.dir); - $('.tipsy').remove(); + + if (url) { + var disableLoadingState = function() { + context.fileList.showFileBusyState(filename, false); + }; + + context.fileList.showFileBusyState(downloadFileaction, true); + OCA.Files.Files.handleDownload(url, disableLoadingState); + } } }); - // t('files', 'Rename') this.registerAction({ name: 'Rename', - displayName: '', mime: 'all', permissions: OC.PERMISSION_UPDATE, icon: function() { return OC.imagePath('core', 'actions/rename'); }, - render: _.bind(this._renderRenameAction, this), actionHandler: function (filename, context) { context.fileList.rename(filename); } @@ -471,30 +537,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..623ebde5442 --- /dev/null +++ b/apps/files/js/fileactionsmenu.js @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2014 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function() { + + var TEMPLATE_MENU = + '<ul>' + + '{{#each items}}' + + '<li>' + + '<a href="#" class="action action-{{nameLowerCase}} permanent" data-action="{{name}}">{{#if icon}}<img src="{{icon}}"/>{{else}}<span class="no-icon"></span>{{/if}}<span>{{displayName}}</span></a>' + + '</li>' + + '{{/each}}' + + '</ul>'; + + /** + * Construct a new FileActionsMenu instance + * @constructs FileActionsMenu + * @memberof OCA.Files + */ + var FileActionsMenu = OC.Backbone.View.extend({ + tagName: 'div', + className: 'fileActionsMenu bubble hidden open menu', + + /** + * Current context + * + * @type OCA.Files.FileActionContext + */ + _context: null, + + events: { + 'click a.action': '_onClickAction' + }, + + template: function(data) { + if (!OCA.Files.FileActionsMenu._TEMPLATE) { + OCA.Files.FileActionsMenu._TEMPLATE = Handlebars.compile(TEMPLATE_MENU); + } + return OCA.Files.FileActionsMenu._TEMPLATE(data); + }, + + /** + * Event handler whenever an action has been clicked within the menu + * + * @param {Object} event event object + */ + _onClickAction: function(event) { + var $target = $(event.target); + if (!$target.is('a')) { + $target = $target.closest('a'); + } + var fileActions = this._context.fileActions; + var actionName = $target.attr('data-action'); + var actions = fileActions.getActions( + fileActions.getCurrentMimeType(), + fileActions.getCurrentType(), + fileActions.getCurrentPermissions() + ); + var actionSpec = actions[actionName]; + var fileName = this._context.$file.attr('data-file'); + + event.stopPropagation(); + event.preventDefault(); + + OC.hideMenus(); + + actionSpec.action( + fileName, + this._context + ); + }, + + /** + * Renders the menu with the currently set items + */ + render: function() { + var fileActions = this._context.fileActions; + var actions = fileActions.getActions( + fileActions.getCurrentMimeType(), + fileActions.getCurrentType(), + fileActions.getCurrentPermissions() + ); + + var defaultAction = fileActions.getDefaultFileAction( + fileActions.getCurrentMimeType(), + fileActions.getCurrentType(), + fileActions.getCurrentPermissions() + ); + + var items = _.filter(actions, function(actionSpec) { + return ( + actionSpec.type === OCA.Files.FileActions.TYPE_DROPDOWN && + (!defaultAction || actionSpec.name !== defaultAction.name) + ); + }); + items = _.map(items, function(item) { + item.nameLowerCase = item.name.toLowerCase(); + return item; + }); + + this.$el.html(this.template({ + items: items + })); + }, + + /** + * Displays the menu under the given element + * + * @param {OCA.Files.FileActionContext} context context + * @param {Object} $trigger trigger element + */ + show: function(context) { + this._context = context; + + this.render(); + this.$el.removeClass('hidden'); + + OC.showMenu(null, this.$el); + } + }); + + OCA.Files.FileActionsMenu = FileActionsMenu; + +})(); + diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index 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.<string>} files file name or array of file names + * @param {bool} [busy=true] busy state, true for busy, false to remove busy state + * + * @since 8.2 + */ + showFileBusyState: function(files, state) { + var self = this; + if (!_.isArray(files)) { + files = [files]; + } + + if (_.isUndefined(state)) { + state = true; + } + + _.each(files, function($tr) { + // jquery element already ? + if (!$tr.is) { + $tr = self.findFileEl($tr); + } + + var $thumbEl = $tr.find('.thumbnail'); + $tr.toggleClass('busy', state); + + if (state) { + $thumbEl.attr('data-oldimage', $thumbEl.css('background-image')); + $thumbEl.css('background-image', 'url('+ OC.imagePath('core', 'loading.gif') + ')'); + } else { + $thumbEl.css('background-image', $thumbEl.attr('data-oldimage')); + $thumbEl.removeAttr('data-oldimage'); + } + }); + }, + /** * Delete the given files from the given dir * @param files file names list (without path) @@ -1649,9 +1683,8 @@ files=[files]; } if (files) { + this.showFileBusyState(files, true); for (var i=0; i<files.length; i++) { - var deleteAction = this.findFileEl(files[i]).children("td.date").children(".action.delete"); - deleteAction.removeClass('icon-delete').addClass('icon-loading-small'); } } // Finish any existing actions @@ -1669,7 +1702,7 @@ // no files passed, delete all in current dir params.allfiles = true; // show spinner for all files - this.$fileList.find('tr>td.date .action.delete').removeClass('icon-delete').addClass('icon-loading-small'); + this.$fileList.find('tr').addClass('busy'); } $.post(OC.filePath('files', 'ajax', 'delete.php'), @@ -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/tests/js/fileactionsSpec.js b/apps/files/tests/js/fileactionsSpec.js index e420ab828af..236cff6cafd 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,191 @@ describe('OCA.Files.FileActions tests', function() { $body.append('<input type="hidden" id="dir" value="/subdir"></input>'); $body.append('<input type="hidden" id="permissions" value="31"></input>'); // dummy files table - $filesTable = $body.append('<table id="filestable"></table>'); - 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' - }; - - // 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(); - }); - 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'); + }); + 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() { var $tr; @@ -251,10 +230,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 +246,13 @@ describe('OCA.Files.FileActions tests', function() { expect(context.fileList).toEqual(fileList); expect(context.$file[0]).toEqual($tr[0]); - var $customEl = $('<a href="#"><span>blabli</span><span>blabla</span></a>'); + var $customEl = $('<a class="action action-test" href="#"><span>blabli</span><span>blabla</span></a>'); $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 +286,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 +324,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 +355,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 +389,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 +432,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..0cfd12a2d04 --- /dev/null +++ b/apps/files/tests/js/fileactionsmenuSpec.js @@ -0,0 +1,273 @@ +/** +* ownCloud +* +* @author Vincent Petry +* @copyright 2015 Vincent Petry <pvince81@owncloud.com> +* +* 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 <http://www.gnu.org/licenses/>. +* +*/ + +describe('OCA.Files.FileActionsMenu tests', function() { + var fileList, fileActions, menu, actionStub, $tr; + + beforeEach(function() { + // init horrible parameters + var $body = $('#testArea'); + $body.append('<input type="hidden" id="dir" value="/subdir"></input>'); + $body.append('<input type="hidden" id="permissions" value="31"></input>'); + // 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.show(menuContext); + }); + afterEach(function() { + fileActions = null; + fileList.destroy(); + fileList = undefined; + menu.remove(); + $('#dir, #permissions, #filestable').remove(); + }); + + describe('rendering', function() { + 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.show(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.show(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.show(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(); + }); + }); +}); + 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); + }); + }); + }); }); diff --git a/apps/files_sharing/js/share.js b/apps/files_sharing/js/share.js index 12bec0e8c9a..04700b84011 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: '', + 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_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/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/css/apps.css b/core/css/apps.css index 5769120c5ed..300b186bba2 100644 --- a/core/css/apps.css +++ b/core/css/apps.css @@ -292,13 +292,13 @@ list-style-type: none; } +.bubble, #app-navigation .app-navigation-entry-menu { - display: none; position: absolute; 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; @@ -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; 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", diff --git a/core/js/js.js b/core/js/js.js index 72d4edd28dd..89bb9a71430 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,15 +598,56 @@ 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) { + var lastMenu = OC._currentMenu; + OC._currentMenu.trigger(new $.Event('beforeHide')); + OC._currentMenu.slideUp(OC.menuSpeed, function() { + lastMenu.trigger(new $.Event('afterHide')); + if (complete) { + complete.apply(this, arguments); + } + }); + } + 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 * * This is makes it possible for unit tests to @@ -1256,11 +1296,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(); }); 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(' <span>'+ escapeHTML(t('core', 'Share'))+'</span>').prepend(img); + action.html('<span></span>').prepend(img); } if (hasLink) { image = OC.imagePath('core', 'actions/public'); |