diff --git a/apps/files/css/files.scss b/apps/files/css/files.scss index d1405517f13b3..1be3c9216f066 100644 --- a/apps/files/css/files.scss +++ b/apps/files/css/files.scss @@ -47,14 +47,6 @@ top: 44px; } -/* make sure there's enough room for the file actions */ -#body-user #filestable { - min-width: 688px; /* 768 (mobile break) - 80 (nav width) */ -} -#body-user #controls { - min-width: 688px; /* 768 (mobile break) - 80 (nav width) */ -} - #filestable tbody tr { height: 51px; } @@ -74,12 +66,16 @@ background-color: rgb(179, 230, 255)!important; } -.app-files #app-content.dir-drop, .file-drag #filestable tbody tr, .file-drag #filestable tbody tr:hover{ - background-color: rgba(0, 0, 0, 0)!important; +.app-files #app-content.dir-drop { + background-color: $color-main-background !important; +} + +.file-drag #filestable tbody tr, .file-drag #filestable tbody tr:hover{ + background-color: transparent !important; } .app-files #app-content.dir-drop #filestable tbody tr.dropping-to-dir{ - background-color: rgb(179, 230, 255)!important; + background-color: rgb(179, 230, 255) !important; } /* icons for sidebar */ @@ -740,9 +736,9 @@ table.dragshadow td.size { margin-bottom: 2px; } -.canDrop, +.breadcrumb .canDrop > a, #filestable tbody tr.canDrop { - background-color: rgba(255, 255, 140, 1); + background-color: rgb(179, 230, 255); } diff --git a/apps/files/js/breadcrumb.js b/apps/files/js/breadcrumb.js index 2a4c2bc8a5298..35aeb8d357d1b 100644 --- a/apps/files/js/breadcrumb.js +++ b/apps/files/js/breadcrumb.js @@ -33,6 +33,9 @@ */ var BreadCrumb = function(options){ this.$el = $(''); + this.$menu = $(''); + + this.crumbSelector = '.crumb:not(.hidden):not(.crumbhome):not(.crumbmenu)'; options = options || {}; if (options.onClick) { this.onClick = options.onClick; @@ -47,6 +50,7 @@ } this._detailViews = []; }; + /** * @memberof OCA.Files */ @@ -110,19 +114,32 @@ * Renders the breadcrumb elements */ render: function() { + // Menu is destroyed on every change, we need to init it + OC.unregisterMenu($('.crumbmenu'), $('.crumbmenu > .popovermenu')); + var parts = this._makeCrumbs(this.dir || '/'); var $crumb; + var $menuItem; this.$el.empty(); this.breadcrumbs = []; for (var i = 0; i < parts.length; i++) { var part = parts[i]; var $image; - var $link = $('').attr('href', this.getCrumbUrl(part, i)); - $link.text(part.name); + var $link = $(''); $crumb = $('
'); + if(part.dir) { + $link.attr('href', this.getCrumbUrl(part, i)); + } + if(part.name) { + $link.text(part.name); + } + $link.addClass(part.linkclass); $crumb.append($link); - $crumb.attr('data-dir', part.dir); + $crumb.data('dir', part.dir); + // Ignore menu button + $crumb.data('crumb-id', i - 1); + $crumb.addClass(part.class); if (part.img) { $image = $(''); @@ -132,12 +149,27 @@ } this.breadcrumbs.push($crumb); this.$el.append($crumb); - if (this.onClick) { - $crumb.on('click', this.onClick); + // Only add feedback if not menu + if (this.onClick && i !== 0) { + $link.on('click', this.onClick); } } - $crumb.addClass('last'); + // Menu creation + this._createMenu(); + for (var j = 0; j < parts.length; j++) { + var menuPart = parts[j]; + if(menuPart.dir) { + $menuItem = $('
  • '); + $menuItem.data('dir', menuPart.dir); + $menuItem.find('a').attr('href', this.getCrumbUrl(part, j)); + $menuItem.find('span:eq(1)').text(menuPart.name); + this.$menu.children('ul').append($menuItem); + if (this.onClick) { + $menuItem.on('click', this.onClick); + } + } + } _.each(this._detailViews, function(view) { view.render({ dirInfo: this.dirInfo @@ -152,16 +184,20 @@ // setup drag and drop if (this.onDrop) { - this.$el.find('.crumb:not(.last)').droppable({ + this.$el.find('.crumb:not(:last-child):not(.crumbmenu), .crumblist:not(:last-child)').droppable({ drop: this.onDrop, over: this.onOver, out: this.onOut, tolerance: 'pointer', - hoverClass: 'canDrop' + hoverClass: 'canDrop', + greedy: true }); } - this._updateTotalWidth(); + // Menu is destroyed on every change, we need to init it + OC.registerMenu($('.crumbmenu'), $('.crumbmenu > .popovermenu')); + + this._resize(); }, /** @@ -179,12 +215,17 @@ if (dir === '') { parts = []; } + // menu part + crumbs.push({ + class: 'crumbmenu hidden', + linkclass: 'icon-more' + }); // root part crumbs.push({ + name: t('core', 'Home'), dir: '/', - name: '', - alt: t('files', 'Home'), - img: OC.imagePath('core', 'places/home.svg') + class: 'crumbhome', + linkclass: 'icon-home' }); for (var i = 0; i < parts.length; i++) { var part = parts[i]; @@ -197,23 +238,10 @@ return crumbs; }, - /** - * Calculate the total breadcrumb width when - * all crumbs are expanded - */ - _updateTotalWidth: function () { - this.totalWidth = 0; - for (var i = 0; i < this.breadcrumbs.length; i++ ) { - var $crumb = $(this.breadcrumbs[i]); - $crumb.data('real-width', $crumb.width()); - this.totalWidth += $crumb.width(); - } - this._resize(); - }, - /** * Show/hide breadcrumbs to fit the given width - * + * Mostly used by tests + * * @param {int} availableWidth available width */ setMaxWidth: function (availableWidth) { @@ -223,74 +251,107 @@ } }, - _resize: function() { - var i, $crumb, $ellipsisCrumb; - - if (!this.availableWidth) { - this.availableWidth = this.$el.width(); + /** + * Calculate real width based on individual crumbs + * More accurate and works with tests + * + * @param {boolean} ignoreHidden ignore hidden crumbs + */ + getTotalWidth: function(ignoreHidden) { + var totalWidth = 0; + for (var i = 0; i < this.breadcrumbs.length; i++ ) { + var $crumb = $(this.breadcrumbs[i]); + if(!$crumb.hasClass('hidden') || ignoreHidden === true) { + totalWidth += $crumb.outerWidth(); + } } + return totalWidth; + }, - if (this.breadcrumbs.length <= 1) { - return; + /** + * Hide the middle crumb + */ + _hideCrumb: function() { + var length = this.$el.find(this.crumbSelector).length; + // Get the middle one floored down + var elmt = Math.floor(length / 2 - 0.5); + this.$el.find(this.crumbSelector+':eq('+elmt+')').addClass('hidden'); + }, + + /** + * Get the crumb to show + */ + _getCrumbElement: function() { + var hidden = this.$el.find('.crumb.hidden').length; + var shown = this.$el.find(this.crumbSelector).length; + // Get the outer one with priority to the highest + var elmt = (1 - shown % 2) * (hidden - 1); + return this.$el.find('.crumb.hidden:eq('+elmt+')'); + }, + + /** + * Show the middle crumb + */ + _showCrumb: function() { + if(this.$el.find('.crumb.hidden').length === 1) { + this.$el.find('.crumb.hidden').removeClass('hidden'); } + this._getCrumbElement().removeClass('hidden'); + }, + + /** + * Create and append the popovermenu + */ + _createMenu: function() { + this.$el.find('.crumbmenu').append(this.$menu); + this.$menu.children('ul').empty(); + }, + + /** + * Update the popovermenu + */ + _updateMenu: function() { + var menuItems = this.$el.find('.crumb.hidden'); + // Hide the crumb menu if no elements + this.$el.find('.crumbmenu').toggleClass('hidden', menuItems.length === 0); - // reset crumbs - this.$el.find('.crumb.ellipsized').remove(); + this.$menu.find('li').addClass('in-breadcrumb'); + for (var i = 0; i < menuItems.length; i++) { + var crumbId = $(menuItems[i]).data('crumb-id'); + this.$menu.find('li:eq('+crumbId+')').removeClass('in-breadcrumb'); + } + }, - // unhide all - this.$el.find('.crumb.hidden').removeClass('hidden'); + _resize: function() { - if (this.totalWidth <= this.availableWidth) { - // no need to compute breadcrumbs, there is enough space + if (this.breadcrumbs.length <= 2) { + // home & menu return; } - // running width, considering the hidden crumbs - var currentTotalWidth = $(this.breadcrumbs[0]).data('real-width'); - var firstHidden = true; - - // insert ellipsis after root part (root part is always visible) - $ellipsisCrumb = $('
    ...
    '); - $(this.breadcrumbs[0]).after($ellipsisCrumb); - currentTotalWidth += $ellipsisCrumb.width(); - - i = this.breadcrumbs.length - 1; - - // find the first section that would cause the overflow - // then hide everything in front of that - // - // this ensures that the last crumb section stays visible - // for most of the cases and is always the last one to be - // hidden when the screen becomes very narrow - while (i > 0) { - $crumb = $(this.breadcrumbs[i]); - // if the current breadcrumb would cause overflow - if (!firstHidden || currentTotalWidth + $crumb.data('real-width') > this.availableWidth) { - // hide it - $crumb.addClass('hidden'); - if (firstHidden) { - // set the path of this one as title for the ellipsis - this.$el.find('.crumb.ellipsized') - .attr('title', $crumb.attr('data-dir')) - .tooltip(); - this.$el.find('.ellipsis') - .wrap(''); - } - // and all the previous ones (going backwards) - firstHidden = false; - } else { - // add to total width - currentTotalWidth += $crumb.data('real-width'); - } - i--; + // Used for testing since this.$el.parent fails + if (!this.availableWidth) { + this.usedWidth = this.$el.parent().width() - (this.$el.parent().find('.button').length + 1) * 44; + } else { + this.usedWidth = this.availableWidth; } - if (!OC.Util.hasSVGSupport()) { - OC.Util.replaceSVG(this.$el); + // If container is smaller than content + // AND if there are crumbs left to hide + while (this.getTotalWidth() > this.usedWidth + && this.$el.find(this.crumbSelector).length > 0) { + this._hideCrumb(); } + // If container is bigger than content + element to be shown + // AND if there is at least one hidden crumb + while (this.$el.find('.crumb.hidden').length > 0 + && this.getTotalWidth() + this._getCrumbElement().width() < this.usedWidth) { + this._showCrumb(); + } + + this._updateMenu(); } }; OCA.Files.BreadCrumb = BreadCrumb; })(); - diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index 4790afcf4d0a8..23a2fb8bb4def 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -322,7 +322,7 @@ this.$el.find('thead th .columntitle').click(_.bind(this._onClickHeader, this)); - this._onResize = _.debounce(_.bind(this._onResize, this), 100); + this._onResize = _.debounce(_.bind(this._onResize, this), 250); $('#app-content').on('appresized', this._onResize); $(window).resize(this._onResize); @@ -555,7 +555,7 @@ // subtract app navigation toggle when visible containerWidth -= $('#app-navigation-toggle').width(); - this.breadcrumb.setMaxWidth(containerWidth - actionsWidth - 10); + this.breadcrumb._resize(); this.$table.find('>thead').width($('#app-content').width() - OC.Util.getScrollBarWidth()); }, @@ -836,7 +836,8 @@ * Event handler when clicking on a bread crumb */ _onClickBreadCrumb: function(e) { - var $el = $(e.target).closest('.crumb'), + // Select a crumb or a crumb in the menu + var $el = $(e.target).closest('.crumb, .crumblist'), $targetDir = $el.data('dir'); if ($targetDir !== undefined && e.which === 1) { @@ -862,8 +863,8 @@ _onDropOnBreadCrumb: function( event, ui ) { var self = this; var $target = $(event.target); - if (!$target.is('.crumb')) { - $target = $target.closest('.crumb'); + if (!$target.is('.crumb, .crumblist')) { + $target = $target.closest('.crumb, .crumblist'); } var targetPath = $(event.target).data('dir'); var dir = this.getCurrentDirectory(); @@ -1709,7 +1710,7 @@ if (status === 500) { // Go home this.changeDirectory('/'); - OC.Notification.show(t('files', 'This directory is unavailable, please check the logs or contact the administrator'), + OC.Notification.show(t('files', 'This directory is unavailable, please check the logs or contact the administrator'), {type: 'error'} ); return false; @@ -1720,7 +1721,7 @@ if (this.getCurrentDirectory() !== '/') { this.changeDirectory('/'); // TODO: read error message from exception - OC.Notification.show(t('files', 'Storage is temporarily not available'), + OC.Notification.show(t('files', 'Storage is temporarily not available'), {type: 'error'} ); } @@ -2036,11 +2037,11 @@ .fail(function(status) { if (status === 412) { // TODO: some day here we should invoke the conflict dialog - OC.Notification.show(t('files', 'Could not move "{file}", target exists', + OC.Notification.show(t('files', 'Could not move "{file}", target exists', {file: fileName}), {type: 'error'} ); } else { - OC.Notification.show(t('files', 'Could not move "{file}"', + OC.Notification.show(t('files', 'Could not move "{file}"', {file: fileName}), {type: 'error'} ); } @@ -2267,7 +2268,7 @@ // TODO: 409 means current folder does not exist, redirect ? if (status === 404) { // source not found, so remove it from the list - OC.Notification.show(t('files', 'Could not rename "{fileName}", it does not exist any more', + OC.Notification.show(t('files', 'Could not rename "{fileName}", it does not exist any more', {fileName: oldName}), {timeout: 7, type: 'error'} ); @@ -2287,7 +2288,7 @@ ); } else { // restore the item to its previous state - OC.Notification.show(t('files', 'Could not rename "{fileName}"', + OC.Notification.show(t('files', 'Could not rename "{fileName}"', {fileName: oldName}), {type: 'error'} ); } @@ -2372,18 +2373,18 @@ self.addAndFetchFileInfo(targetPath, '', {scrollTo: true}).then(function(status, data) { deferred.resolve(status, data); }, function() { - OC.Notification.show(t('files', 'Could not create file "{file}"', + OC.Notification.show(t('files', 'Could not create file "{file}"', {file: name}), {type: 'error'} ); }); }) .fail(function(status) { if (status === 412) { - OC.Notification.show(t('files', 'Could not create file "{file}" because it already exists', + OC.Notification.show(t('files', 'Could not create file "{file}" because it already exists', {file: name}), {type: 'error'} ); } else { - OC.Notification.show(t('files', 'Could not create file "{file}"', + OC.Notification.show(t('files', 'Could not create file "{file}"', {file: name}), {type: 'error'} ); } @@ -2422,7 +2423,7 @@ self.addAndFetchFileInfo(targetPath, '', {scrollTo:true}).then(function(status, data) { deferred.resolve(status, data); }, function() { - OC.Notification.show(t('files', 'Could not create folder "{dir}"', + OC.Notification.show(t('files', 'Could not create folder "{dir}"', {dir: name}), {type: 'error'} ); }); @@ -2433,20 +2434,20 @@ // add it to the list, for completeness self.addAndFetchFileInfo(targetPath, '', {scrollTo:true}) .done(function(status, data) { - OC.Notification.show(t('files', 'Could not create folder "{dir}" because it already exists', + OC.Notification.show(t('files', 'Could not create folder "{dir}" because it already exists', {dir: name}), {type: 'error'} ); // still consider a failure deferred.reject(createStatus, data); }) .fail(function() { - OC.Notification.show(t('files', 'Could not create folder "{dir}"', + OC.Notification.show(t('files', 'Could not create folder "{dir}"', {dir: name}), {type: 'error'} ); deferred.reject(status); }); } else { - OC.Notification.show(t('files', 'Could not create folder "{dir}"', + OC.Notification.show(t('files', 'Could not create folder "{dir}"', {dir: name}), {type: 'error'} ); deferred.reject(createStatus); @@ -2503,7 +2504,7 @@ deferred.resolve(status, data); }) .fail(function(status) { - OC.Notification.show(t('files', 'Could not create file "{file}"', + OC.Notification.show(t('files', 'Could not create file "{file}"', {file: name}), {type: 'error'} ); deferred.reject(status); @@ -2612,7 +2613,7 @@ removeFromList(file); } else { // only reset the spinner for that one file - OC.Notification.show(t('files', 'Error deleting file "{fileName}".', + OC.Notification.show(t('files', 'Error deleting file "{fileName}".', {fileName: file}), {type: 'error'} ); var deleteAction = self.findFileEl(file).find('.action.delete'); diff --git a/apps/files/js/files.js b/apps/files/js/files.js index e34d7fe2550c2..017bf7ecf41a6 100644 --- a/apps/files/js/files.js +++ b/apps/files/js/files.js @@ -117,32 +117,32 @@ ownerDisplayName = $('#ownerDisplayName').val(); if (usedSpacePercent > 98) { if (owner !== oc_current_user) { - OC.Notification.show(t('files', 'Storage of {owner} is full, files can not be updated or synced anymore!', + OC.Notification.show(t('files', 'Storage of {owner} is full, files can not be updated or synced anymore!', {owner: ownerDisplayName}), {type: 'error'} ); return; } - OC.Notification.show(t('files', - 'Your storage is full, files can not be updated or synced anymore!'), + OC.Notification.show(t('files', + 'Your storage is full, files can not be updated or synced anymore!'), {type : 'error'} ); return; } if (usedSpacePercent > 90) { if (owner !== oc_current_user) { - OC.Notification.show(t('files', 'Storage of {owner} is almost full ({usedSpacePercent}%)', + OC.Notification.show(t('files', 'Storage of {owner} is almost full ({usedSpacePercent}%)', { - usedSpacePercent: usedSpacePercent, + usedSpacePercent: usedSpacePercent, owner: ownerDisplayName }), - { + { type: 'error' } ); return; } OC.Notification.show(t('files', 'Your storage is almost full ({usedSpacePercent}%)', - {usedSpacePercent: usedSpacePercent}), + {usedSpacePercent: usedSpacePercent}), {type : 'error'} ); } @@ -396,6 +396,8 @@ var dragOptions={ } $selectedFiles.closest('tr').addClass('animate-opacity dragging'); $selectedFiles.closest('tr').filter('.ui-droppable').droppable( 'disable' ); + // Show breadcrumbs menu + $('.crumbmenu').addClass('canDropChildren'); }, stop: function(event, ui) { @@ -411,6 +413,8 @@ var dragOptions={ setTimeout(function() { $tr.removeClass('animate-opacity'); }, 300); + // Hide breadcrumbs menu + $('.crumbmenu').removeClass('canDropChildren'); }, drag: function(event, ui) { var scrollingArea = FileList.$container; diff --git a/apps/files/tests/js/breadcrumbSpec.js b/apps/files/tests/js/breadcrumbSpec.js index a26f0176f155c..dd3eac017ec2f 100644 --- a/apps/files/tests/js/breadcrumbSpec.js +++ b/apps/files/tests/js/breadcrumbSpec.js @@ -43,80 +43,93 @@ describe('OCA.Files.BreadCrumb tests', function() { var $crumbs; bc.render(); $crumbs = bc.$el.find('.crumb'); - expect($crumbs.length).toEqual(1); - expect($crumbs.eq(0).find('a').attr('href')).toEqual('/#0'); - expect($crumbs.eq(0).find('img').length).toEqual(1); - expect($crumbs.eq(0).attr('data-dir')).toEqual('/'); + // menu and home + expect($crumbs.length).toEqual(2); + expect($crumbs.eq(0).find('a').hasClass('icon-more')).toEqual(true); + expect($crumbs.eq(0).find('div.popovermenu').length).toEqual(1); + expect($crumbs.eq(0).data('dir')).not.toBeDefined(); + expect($crumbs.eq(1).find('a').attr('href')).toEqual('/#1'); + expect($crumbs.eq(1).find('a').hasClass('icon-home')).toEqual(true); + expect($crumbs.eq(1).data('dir')).toEqual('/'); }); it('Renders root when switching to root', function() { var $crumbs; bc.setDirectory('/somedir'); bc.setDirectory('/'); $crumbs = bc.$el.find('.crumb'); - expect($crumbs.length).toEqual(1); - expect($crumbs.eq(0).attr('data-dir')).toEqual('/'); - }); - it('Renders last crumb with "last" class', function() { - bc.setDirectory('/abc/def'); - expect(bc.$el.find('.crumb:last').hasClass('last')).toEqual(true); + expect($crumbs.length).toEqual(2); + expect($crumbs.eq(1).data('dir')).toEqual('/'); }); it('Renders single path section', function() { var $crumbs; bc.setDirectory('/somedir'); $crumbs = bc.$el.find('.crumb'); - expect($crumbs.length).toEqual(2); - expect($crumbs.eq(0).find('a').attr('href')).toEqual('/#0'); - expect($crumbs.eq(0).find('img').length).toEqual(1); - expect($crumbs.eq(0).attr('data-dir')).toEqual('/'); - expect($crumbs.eq(1).find('a').attr('href')).toEqual('/somedir#1'); - expect($crumbs.eq(1).find('img').length).toEqual(0); - expect($crumbs.eq(1).attr('data-dir')).toEqual('/somedir'); + expect($crumbs.length).toEqual(3); + expect($crumbs.eq(0).find('a').hasClass('icon-more')).toEqual(true); + expect($crumbs.eq(0).find('div.popovermenu').length).toEqual(1); + expect($crumbs.eq(0).data('dir')).not.toBeDefined(); + + expect($crumbs.eq(1).find('a').attr('href')).toEqual('/#1'); + expect($crumbs.eq(1).find('a').hasClass('icon-home')).toEqual(true); + expect($crumbs.eq(1).data('dir')).toEqual('/'); + + expect($crumbs.eq(2).find('a').attr('href')).toEqual('/somedir#2'); + expect($crumbs.eq(2).find('img').length).toEqual(0); + expect($crumbs.eq(2).data('dir')).toEqual('/somedir'); }); it('Renders multiple path sections and special chars', function() { var $crumbs; bc.setDirectory('/somedir/with space/abc'); $crumbs = bc.$el.find('.crumb'); - expect($crumbs.length).toEqual(4); - expect($crumbs.eq(0).find('a').attr('href')).toEqual('/#0'); - expect($crumbs.eq(0).find('img').length).toEqual(1); - expect($crumbs.eq(0).attr('data-dir')).toEqual('/'); + expect($crumbs.length).toEqual(5); + expect($crumbs.eq(0).find('a').hasClass('icon-more')).toEqual(true); + expect($crumbs.eq(0).find('div.popovermenu').length).toEqual(1); + expect($crumbs.eq(0).data('dir')).not.toBeDefined(); - expect($crumbs.eq(1).find('a').attr('href')).toEqual('/somedir#1'); - expect($crumbs.eq(1).find('img').length).toEqual(0); - expect($crumbs.eq(1).attr('data-dir')).toEqual('/somedir'); + expect($crumbs.eq(1).find('a').attr('href')).toEqual('/#1'); + expect($crumbs.eq(1).find('a').hasClass('icon-home')).toEqual(true); + expect($crumbs.eq(1).data('dir')).toEqual('/'); - expect($crumbs.eq(2).find('a').attr('href')).toEqual('/somedir/with space#2'); + expect($crumbs.eq(2).find('a').attr('href')).toEqual('/somedir#2'); expect($crumbs.eq(2).find('img').length).toEqual(0); - expect($crumbs.eq(2).attr('data-dir')).toEqual('/somedir/with space'); + expect($crumbs.eq(2).data('dir')).toEqual('/somedir'); - expect($crumbs.eq(3).find('a').attr('href')).toEqual('/somedir/with space/abc#3'); + expect($crumbs.eq(3).find('a').attr('href')).toEqual('/somedir/with space#3'); expect($crumbs.eq(3).find('img').length).toEqual(0); - expect($crumbs.eq(3).attr('data-dir')).toEqual('/somedir/with space/abc'); + expect($crumbs.eq(3).data('dir')).toEqual('/somedir/with space'); + + expect($crumbs.eq(4).find('a').attr('href')).toEqual('/somedir/with space/abc#4'); + expect($crumbs.eq(4).find('img').length).toEqual(0); + expect($crumbs.eq(4).data('dir')).toEqual('/somedir/with space/abc'); }); it('Renders backslashes as regular directory separator', function() { var $crumbs; bc.setDirectory('/somedir\\with/mixed\\separators'); $crumbs = bc.$el.find('.crumb'); - expect($crumbs.length).toEqual(5); - expect($crumbs.eq(0).find('a').attr('href')).toEqual('/#0'); - expect($crumbs.eq(0).find('img').length).toEqual(1); - expect($crumbs.eq(0).attr('data-dir')).toEqual('/'); + expect($crumbs.length).toEqual(6); + expect($crumbs.eq(0).find('a').hasClass('icon-more')).toEqual(true); + expect($crumbs.eq(0).find('div.popovermenu').length).toEqual(1); + expect($crumbs.eq(0).data('dir')).not.toBeDefined(); - expect($crumbs.eq(1).find('a').attr('href')).toEqual('/somedir#1'); - expect($crumbs.eq(1).find('img').length).toEqual(0); - expect($crumbs.eq(1).attr('data-dir')).toEqual('/somedir'); + expect($crumbs.eq(1).find('a').attr('href')).toEqual('/#1'); + expect($crumbs.eq(1).find('a').hasClass('icon-home')).toEqual(true); + expect($crumbs.eq(1).data('dir')).toEqual('/'); - expect($crumbs.eq(2).find('a').attr('href')).toEqual('/somedir/with#2'); + expect($crumbs.eq(2).find('a').attr('href')).toEqual('/somedir#2'); expect($crumbs.eq(2).find('img').length).toEqual(0); - expect($crumbs.eq(2).attr('data-dir')).toEqual('/somedir/with'); + expect($crumbs.eq(2).data('dir')).toEqual('/somedir'); - expect($crumbs.eq(3).find('a').attr('href')).toEqual('/somedir/with/mixed#3'); + expect($crumbs.eq(3).find('a').attr('href')).toEqual('/somedir/with#3'); expect($crumbs.eq(3).find('img').length).toEqual(0); - expect($crumbs.eq(3).attr('data-dir')).toEqual('/somedir/with/mixed'); + expect($crumbs.eq(3).data('dir')).toEqual('/somedir/with'); - expect($crumbs.eq(4).find('a').attr('href')).toEqual('/somedir/with/mixed/separators#4'); + expect($crumbs.eq(4).find('a').attr('href')).toEqual('/somedir/with/mixed#4'); expect($crumbs.eq(4).find('img').length).toEqual(0); - expect($crumbs.eq(4).attr('data-dir')).toEqual('/somedir/with/mixed/separators'); + expect($crumbs.eq(4).data('dir')).toEqual('/somedir/with/mixed'); + + expect($crumbs.eq(5).find('a').attr('href')).toEqual('/somedir/with/mixed/separators#5'); + expect($crumbs.eq(5).find('img').length).toEqual(0); + expect($crumbs.eq(5).data('dir')).toEqual('/somedir/with/mixed/separators'); }); }); describe('Events', function() { @@ -126,14 +139,15 @@ describe('OCA.Files.BreadCrumb tests', function() { onClick: handler }); bc.setDirectory('/one/two/three/four'); - bc.$el.find('.crumb:eq(3)').click(); - expect(handler.calledOnce).toEqual(true); - expect(handler.getCall(0).thisValue).toEqual(bc.$el.find('.crumb').get(3)); + // Click on crumb does not work, only link + bc.$el.find('.crumb:eq(4)').click(); + expect(handler.calledOnce).toEqual(false); handler.reset(); - bc.$el.find('.crumb:eq(0) a').click(); + // Click on crumb link works + bc.$el.find('.crumb:eq(1) a').click(); expect(handler.calledOnce).toEqual(true); - expect(handler.getCall(0).thisValue).toEqual(bc.$el.find('.crumb').get(0)); + expect(handler.getCall(0).thisValue).toEqual(bc.$el.find('.crumb > a').get(1)); }); it('Calls onDrop handler when dropping on a crumb', function() { var droppableStub = sinon.stub($.fn, 'droppable'); @@ -154,8 +168,75 @@ describe('OCA.Files.BreadCrumb tests', function() { droppableStub.restore(); }); }); + + describe('Menu tests', function() { + var bc, dummyDir, $crumbmenuLink, $popovermenu; + + beforeEach(function() { + dummyDir = '/one/two/three/four/five' + + $('div.crumb').each(function(index){ + $(this).css('width', 50); + }); + + bc = new BreadCrumb(); + // append dummy navigation and controls + // as they are currently used for measurements + $('#testArea').append( + '
    ' + ); + $('#controls').append(bc.$el); + + // Shrink to show popovermenu + bc.setMaxWidth(300); + + // triggers resize implicitly + bc.setDirectory(dummyDir); + + $crumbmenuLink = bc.$el.find('.crumbmenu > a'); + $popovermenu = $crumbmenuLink.next('.popovermenu'); + }); + afterEach(function() { + bc = null; + }); + + it('Opens and closes the menu on click', function() { + // Menu exists + expect($popovermenu.length).toEqual(1); + + // Disable jQuery delay + jQuery.fx.off = true + + // Click on menu + $crumbmenuLink.click(); + expect($popovermenu.is(':visible')).toEqual(true); + + // Click on home + $(document).mouseup(); + expect($popovermenu.is(':visible')).toEqual(false); + + // Change directory and reset elements + bc.setDirectory('/one/two/three/four/five/six/seven/eight/nine/ten'); + $crumbmenuLink = bc.$el.find('.crumbmenu > a'); + $popovermenu = $crumbmenuLink.next('.popovermenu'); + + // Click on menu again + $crumbmenuLink.click(); + expect($popovermenu.is(':visible')).toEqual(true); + + // Click on home again + $(document).mouseup(); + expect($popovermenu.is(':visible')).toEqual(false); + + }); + it('Shows only items not in the breadcrumb', function() { + var hiddenCrumbs = bc.$el.find('.crumb:not(.crumbmenu).hidden'); + expect($popovermenu.find('li:not(.in-breadcrumb)').length).toEqual(hiddenCrumbs.length); + }); + }); + describe('Resizing', function() { - var bc, dummyDir, widths, oldUpdateTotalWidth; + var bc, dummyDir, widths; beforeEach(function() { dummyDir = '/short name/longer name/looooooooooooonger/' + @@ -163,17 +244,12 @@ describe('OCA.Files.BreadCrumb tests', function() { // using hard-coded widths (pre-measured) to avoid getting different // results on different browsers due to font engine differences - widths = [41, 106, 112, 160, 257, 251, 91]; + // 51px is default size for menu and home + widths = [51, 51, 106, 112, 160, 257, 251, 91]; - oldUpdateTotalWidth = BreadCrumb.prototype._updateTotalWidth; - BreadCrumb.prototype._updateTotalWidth = function() { - // pre-set a width to simulate consistent measurement - $('div.crumb').each(function(index){ - $(this).css('width', widths[index]); - }); - - return oldUpdateTotalWidth.apply(this, arguments); - }; + $('div.crumb').each(function(index){ + $(this).css('width', widths[index]); + }); bc = new BreadCrumb(); // append dummy navigation and controls @@ -184,30 +260,26 @@ describe('OCA.Files.BreadCrumb tests', function() { $('#controls').append(bc.$el); }); afterEach(function() { - BreadCrumb.prototype._updateTotalWidth = oldUpdateTotalWidth; bc = null; }); it('Hides breadcrumbs to fit max allowed width', function() { var $crumbs; bc.setMaxWidth(500); + // triggers resize implicitly bc.setDirectory(dummyDir); $crumbs = bc.$el.find('.crumb'); - // first one is always visible + // Menu and home are always visible expect($crumbs.eq(0).hasClass('hidden')).toEqual(false); - // second one has ellipsis expect($crumbs.eq(1).hasClass('hidden')).toEqual(false); - expect($crumbs.eq(1).find('.ellipsis').length).toEqual(1); - // there is only one ellipsis in total - expect($crumbs.find('.ellipsis').length).toEqual(1); - // subsequent elements are hidden - expect($crumbs.eq(2).hasClass('hidden')).toEqual(true); + + expect($crumbs.eq(2).hasClass('hidden')).toEqual(false); expect($crumbs.eq(3).hasClass('hidden')).toEqual(true); expect($crumbs.eq(4).hasClass('hidden')).toEqual(true); expect($crumbs.eq(5).hasClass('hidden')).toEqual(true); - expect($crumbs.eq(6).hasClass('hidden')).toEqual(false); + expect($crumbs.eq(6).hasClass('hidden')).toEqual(true); expect($crumbs.eq(7).hasClass('hidden')).toEqual(false); }); it('Updates the breadcrumbs when reducing max allowed width', function() { @@ -215,56 +287,27 @@ describe('OCA.Files.BreadCrumb tests', function() { // enough space bc.setMaxWidth(1800); + $crumbs = bc.$el.find('.crumb'); - expect(bc.$el.find('.ellipsis').length).toEqual(0); + // Menu is hidden + expect($crumbs.eq(0).hasClass('hidden')).toEqual(false); // triggers resize implicitly bc.setDirectory(dummyDir); - // simulate increase + // simulate decrease bc.setMaxWidth(950); - $crumbs = bc.$el.find('.crumb'); - // first one is always visible + // Menu and home are always visible expect($crumbs.eq(0).hasClass('hidden')).toEqual(false); - // second one has ellipsis expect($crumbs.eq(1).hasClass('hidden')).toEqual(false); - expect($crumbs.eq(1).find('.ellipsis').length).toEqual(1); - // there is only one ellipsis in total - expect($crumbs.find('.ellipsis').length).toEqual(1); - // subsequent elements are hidden - expect($crumbs.eq(2).hasClass('hidden')).toEqual(true); - expect($crumbs.eq(3).hasClass('hidden')).toEqual(true); - // the rest is visible - expect($crumbs.eq(4).hasClass('hidden')).toEqual(false); - expect($crumbs.eq(5).hasClass('hidden')).toEqual(false); - expect($crumbs.eq(6).hasClass('hidden')).toEqual(false); - }); - it('Removes the ellipsis when there is enough space', function() { - var $crumbs; - - bc.setMaxWidth(500); - // triggers resize implicitly - bc.setDirectory(dummyDir); - $crumbs = bc.$el.find('.crumb'); - - // ellipsis - expect(bc.$el.find('.ellipsis').length).toEqual(1); - - // simulate increase - bc.setMaxWidth(1800); - // no ellipsis - expect(bc.$el.find('.ellipsis').length).toEqual(0); - - // all are visible - expect($crumbs.eq(0).hasClass('hidden')).toEqual(false); - expect($crumbs.eq(1).hasClass('hidden')).toEqual(false); expect($crumbs.eq(2).hasClass('hidden')).toEqual(false); expect($crumbs.eq(3).hasClass('hidden')).toEqual(false); expect($crumbs.eq(4).hasClass('hidden')).toEqual(false); expect($crumbs.eq(5).hasClass('hidden')).toEqual(false); expect($crumbs.eq(6).hasClass('hidden')).toEqual(false); + expect($crumbs.eq(7).hasClass('hidden')).toEqual(false); }); }); }); diff --git a/apps/files/tests/js/filelistSpec.js b/apps/files/tests/js/filelistSpec.js index 8bb188e3602ae..5d37023f17ea0 100644 --- a/apps/files/tests/js/filelistSpec.js +++ b/apps/files/tests/js/filelistSpec.js @@ -1656,7 +1656,7 @@ describe('OCA.Files.FileList tests', function() { fileList.changeDirectory('/subdir/two/three with space/four/five'); deferredList.resolve(200, [testRoot].concat(testFiles)); var changeDirStub = sinon.stub(fileList, 'changeDirectory'); - fileList.breadcrumb.$el.find('.crumb:eq(0)').trigger({type: 'click', which: 1}); + fileList.breadcrumb.$el.find('.crumb:eq(1) > a').trigger({type: 'click', which: 1}); expect(changeDirStub.calledOnce).toEqual(true); expect(changeDirStub.getCall(0).args[0]).toEqual('/'); @@ -1666,7 +1666,7 @@ describe('OCA.Files.FileList tests', function() { fileList.changeDirectory('/subdir/two/three with space/four/five'); deferredList.resolve(200, [testRoot].concat(testFiles)); var changeDirStub = sinon.stub(fileList, 'changeDirectory'); - fileList.breadcrumb.$el.find('.crumb:eq(3)').trigger({type: 'click', which: 1}); + fileList.breadcrumb.$el.find('.crumb:eq(4) > a').trigger({type: 'click', which: 1}); expect(changeDirStub.calledOnce).toEqual(true); expect(changeDirStub.getCall(0).args[0]).toEqual('/subdir/two/three with space'); @@ -1677,7 +1677,7 @@ describe('OCA.Files.FileList tests', function() { var moveStub = sinon.stub(filesClient, 'move').returns($.Deferred().promise()); fileList.changeDirectory(testDir); deferredList.resolve(200, [testRoot].concat(testFiles)); - var $crumb = fileList.breadcrumb.$el.find('.crumb:eq(3)'); + var $crumb = fileList.breadcrumb.$el.find('.crumb:eq(4)'); // no idea what this is but is required by the handler var ui = { helper: { @@ -2990,7 +2990,7 @@ describe('OCA.Files.FileList tests', function() { it('drop on a breadcrumb inside the table triggers upload to target folder', function() { var ev; fileList.changeDirectory('a/b/c/d'); - ev = dropOn(fileList.$el.find('.crumb:eq(2)'), uploadData); + ev = dropOn(fileList.$el.find('.crumb:eq(3)'), uploadData); expect(ev).not.toEqual(false); expect(uploadData.targetDir).toEqual('/a/b'); diff --git a/apps/files_trashbin/tests/js/filelistSpec.js b/apps/files_trashbin/tests/js/filelistSpec.js index 5e9a4cf27d1c4..04ff243d07b70 100644 --- a/apps/files_trashbin/tests/js/filelistSpec.js +++ b/apps/files_trashbin/tests/js/filelistSpec.js @@ -132,12 +132,12 @@ describe('OCA.Trashbin.FileList tests', function() { fileList.changeDirectory('/subdir', false, true); fakeServer.respond(); var $crumbs = fileList.$el.find('#controls .crumb'); - expect($crumbs.length).toEqual(2); - expect($crumbs.eq(0).find('a').text()).toEqual(''); - expect($crumbs.eq(0).find('a').attr('href')) - .toEqual(OC.webroot + '/index.php/apps/files?view=trashbin&dir=/'); - expect($crumbs.eq(1).find('a').text()).toEqual('subdir'); + expect($crumbs.length).toEqual(3); + expect($crumbs.eq(1).find('a').text()).toEqual('Home'); expect($crumbs.eq(1).find('a').attr('href')) + .toEqual(OC.webroot + '/index.php/apps/files?view=trashbin&dir=/'); + expect($crumbs.eq(2).find('a').text()).toEqual('subdir'); + expect($crumbs.eq(2).find('a').attr('href')) .toEqual(OC.webroot + '/index.php/apps/files?view=trashbin&dir=/subdir'); }); }); diff --git a/core/css/apps.scss b/core/css/apps.scss index 5493375b5ae56..8f49df7fdaf8c 100644 --- a/core/css/apps.scss +++ b/core/css/apps.scss @@ -822,6 +822,7 @@ kbd { } li { display: flex; + flex: 0 0 auto; > button, > a, > .menuitem { diff --git a/core/css/header.scss b/core/css/header.scss index 952389882c342..1844022e8a3ac 100644 --- a/core/css/header.scss +++ b/core/css/header.scss @@ -22,30 +22,6 @@ -ms-user-select: none; } -/* Header menu */ -.menu { - position: absolute; - top: 45px; - background-color: $color-main-background; - box-shadow: 0 1px 10px $color-box-shadow; - border-radius: 0 0 3px 3px; - display: none; - box-sizing: border-box; - z-index: 2000; - - /* Dropdown arrow */ - &:after { - border: 10px solid transparent; - border-bottom-color: $color-main-background; - bottom: 100%; - content: ' '; - height: 0; - width: 0; - position: absolute; - pointer-events: none; - } -} - /* removed until content-focusing issue is fixed */ #skip-to-content a { position: absolute; @@ -96,6 +72,29 @@ } #header { + /* Header menu */ + .menu { + top: 45px; + background-color: $color-main-background; + filter: drop-shadow(0 1px 3px $color-box-shadow); + border-radius: 0 0 3px 3px; + display: none; + box-sizing: border-box; + z-index: 2000; + position: absolute; + + /* Dropdown arrow */ + &:after { + border: 10px solid transparent; + border-bottom-color: $color-main-background; + bottom: 100%; + content: ' '; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + } + } .logo { display: inline-flex; background-image: url($image-logo); @@ -204,14 +203,14 @@ nav { margin-left: -54px; } -#navigation, +.header-left #navigation, .ui-datepicker, .ui-timepicker.ui-widget { position: relative; left: -100%; width: 160px; background-color: $color-main-background; - box-shadow: 0 1px 10px $color-box-shadow; + filter: drop-shadow(0 1px 3px $color-box-shadow); &:after { /* position of dropdown arrow */ left: 47%; diff --git a/core/css/mobile.scss b/core/css/mobile.scss index aad3070e79ad3..1951847998740 100644 --- a/core/css/mobile.scss +++ b/core/css/mobile.scss @@ -81,16 +81,11 @@ opacity: 1; } - -/* controls bar for mobile */ -#controls { - min-width: initial !important; - left: 0 !important; - padding-left: 0; -} /* position controls for apps with app-navigation */ #app-navigation+#app-content #controls { + left: 0 !important; padding-left: 44px; + width: 100%; } /* .viewer-mode is when text editor, PDF viewer, etc is open */ diff --git a/core/css/styles.scss b/core/css/styles.scss index e5e0145cddbb9..ed72b32113f2b 100644 --- a/core/css/styles.scss +++ b/core/css/styles.scss @@ -228,7 +228,7 @@ body { right: 0; left: 0; height: 44px; - width: 100%; + width: calc(100% - 250px); padding: 0; margin: 0; background-color: rgba($color-main-background, 0.95); @@ -237,6 +237,7 @@ body { -moz-user-select: none; -ms-user-select: none; user-select: none; + display: inline-flex; } /* position controls for apps with app-navigation */ @@ -1144,9 +1145,7 @@ code { #oc-dialog-filepicker-content { .dirtree { width: 92%; - float: left; - margin-left: 15px; - overflow: hidden; + flex-wrap: wrap; div:first-child a { background-image: url('../img/places/home.svg?v=1'); background-repeat: no-repeat; @@ -1181,7 +1180,7 @@ code { display: none; } .filelist { - background-color: white; + background-color: $color-main-background; width: 100%; } #filestable.filelist { @@ -1388,46 +1387,88 @@ span.ui-icon { } /* ---- BREADCRUMB ---- */ - +.breadcrumb { + display: inline-flex; +} div.crumb { - float: left; - display: block; + display: inline-flex; background-image: url('../img/breadcrumb.svg?v=1'); background-repeat: no-repeat; background-position: right center; height: 44px; background-size: auto 24px; + flex: 0 0 auto; + order: 1; + padding-right: 7px; + &.crumbmenu { + order: 2; + position: relative; + a { + opacity: 0.5 + } + &.canDropChildren, + &.canDrop { + .popovermenu { + display: block; + } + } + // Fix because of the display flex + .popovermenu { + top: 100%; + margin-right: 3px; + ul { + max-height: 345px; + overflow-y: auto; + overflow-x: hidden; + padding-right: 5px; + li.canDrop span:first-child { + background-image: url('../img/filetypes/folder-drag-accept.svg?v=1') !important; + } + } + .in-breadcrumb { + display: none; + } + } + } &.hidden { display: none; + ~ .crumb { + order: 3; + } } - a, > span { + > a, + > span { position: relative; - top: 12px; - padding: 14px 24px 14px 17px; - color: nc-lighten($color-main-text, 33%); + padding: 12px; + opacity: 0.5; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + flex: 0 0 auto; + &.icon-home { + // Hide home text + text-indent: -9999px; + } } - &.last a { - padding-right: 0; + > a[class^='icon-'] { + padding: 0; + width: 44px; } - &:first-child a { - position: relative; - top: 13px; - padding-right: 14px; + &:not(:first-child) a { } - &.last { + &:last-child { font-weight: 600; margin-right: 10px; - } - &.ellipsized { - padding: 0 10px 0 5px; - } - a.ellipsislink { - padding: 0 !important; - position: relative; - top: 8px !important; + // Allow multiple span next to the main 'a' + a ~ span { + padding-left: 0; + } } &:hover, &:focus, a:focus, &:active { - opacity: .7; + > a, + > span { + opacity: .7; + } } } diff --git a/core/img/places/home.svg b/core/img/places/home.svg index 30ef3e31ad4ee..7ef6db0fde19a 100644 --- a/core/img/places/home.svg +++ b/core/img/places/home.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/core/js/js.js b/core/js/js.js index 1f84b40a35add..730586713a0f5 100644 --- a/core/js/js.js +++ b/core/js/js.js @@ -1625,39 +1625,6 @@ function initCore() { // initial call toggleSnapperOnSize(); - // adjust controls bar width - var adjustControlsWidth = function() { - if($('#controls').length) { - var controlsWidth; - // if there is a scrollbar … - if($('#app-content').get(0).scrollHeight > $('#app-content').height()) { - if($(window).width() > 768) { - controlsWidth = $('#content').width() - $('#app-navigation').width() - getScrollBarWidth(); - if (!$('#app-sidebar').hasClass('hidden') && !$('#app-sidebar').hasClass('disappear')) { - controlsWidth -= $('#app-sidebar').width(); - } - } else { - controlsWidth = $('#content').width() - getScrollBarWidth(); - } - } else { // if there is none - if($(window).width() > 768) { - controlsWidth = $('#content').width() - $('#app-navigation').width(); - if (!$('#app-sidebar').hasClass('hidden') && !$('#app-sidebar').hasClass('disappear')) { - controlsWidth -= $('#app-sidebar').width(); - } - } else { - controlsWidth = $('#content').width(); - } - } - $('#controls').css('width', controlsWidth); - $('#controls').css('min-width', controlsWidth); - } - }; - - $(window).resize(_.debounce(adjustControlsWidth, 250)); - - $('body').delegate('#app-content', 'apprendered appresized', _.debounce(adjustControlsWidth, 150)); - } // Update live timestamps every 30 seconds diff --git a/core/js/oc-dialogs.js b/core/js/oc-dialogs.js index 1bc1399466db8..9cd806720b881 100644 --- a/core/js/oc-dialogs.js +++ b/core/js/oc-dialogs.js @@ -929,7 +929,7 @@ var OCdialogs = { */ _handleTreeListSelect:function(event, type) { var self = event.data; - var dir = $(event.target).parent().data('dir'); + var dir = $(event.target).closest('.crumb').data('dir'); self._fillFilePicker(dir); var getOcDialog = (event.target).closest('.oc-dialog'); var buttonEnableDisable = $('.primary', getOcDialog);