diff options
author | Julius Härtl <jus@bitgrid.net> | 2022-08-27 10:57:13 +0200 |
---|---|---|
committer | Julius Härtl <jus@bitgrid.net> | 2022-08-31 10:24:03 +0200 |
commit | 5b4708c5be100c3a4bbb2fd32151ae2a7420df2d (patch) | |
tree | 181dc1020643ecc5434fa77bfac5606be3826ed4 /core/src | |
parent | 23bb4f16f9056e7a79116129c7de5b59cf84f8be (diff) | |
download | nextcloud-server-5b4708c5be100c3a4bbb2fd32151ae2a7420df2d.tar.gz nextcloud-server-5b4708c5be100c3a4bbb2fd32151ae2a7420df2d.zip |
Move app menu to vue
Signed-off-by: Julius Härtl <jus@bitgrid.net>
Diffstat (limited to 'core/src')
-rw-r--r-- | core/src/components/AppMenu.vue | 268 | ||||
-rw-r--r-- | core/src/components/MainMenu.js | 98 | ||||
-rw-r--r-- | core/src/init.js | 81 |
3 files changed, 282 insertions, 165 deletions
diff --git a/core/src/components/AppMenu.vue b/core/src/components/AppMenu.vue new file mode 100644 index 00000000000..a8ea7250852 --- /dev/null +++ b/core/src/components/AppMenu.vue @@ -0,0 +1,268 @@ +<!-- + - @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net> + - + - @author Julius Härtl <jus@bitgrid.net> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program 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 program. If not, see <http://www.gnu.org/licenses/>. + --> + +<template> + <nav class="app-menu"> + <ul class="app-menu-main"> + <li v-for="app in mainAppList" + :key="app.id" + :data-app-id="app.id" + class="app-menu-entry" + :class="{ 'app-menu-entry__active': app.active }"> + <a :href="app.href" :class="{ 'has-unread': app.unread > 0 }" :aria-label="appLabel(app)"> + <img :src="app.icon" alt=""> + <div class="app-menu-entry--label"> + {{ app.name }} + <span v-if="app.unread > 0" class="hidden-visually unread-counter">{{ app.unread }}</span> + </div> + </a> + </li> + </ul> + <NcActions class="app-menu-more" :aria-label="t('core', 'More apps')"> + <NcActionLink v-for="app in popoverAppList" + :key="app.id" + :aria-label="appLabel(app)" + :href="app.href" + class="app-menu-popover-entry"> + <template #icon> + <div class="app-icon" :class="{ 'has-unread': app.unread > 0 }"> + <img :src="app.icon" alt=""> + </div> + </template> + {{ app.name }} + <span v-if="app.unread > 0" class="hidden-visually unread-counter">{{ app.unread }}</span> + </NcActionLink> + </NcActions> + </nav> +</template> + +<script> +import { loadState } from '@nextcloud/initial-state' +import NcActions from '@nextcloud/vue/dist/Components/NcActions' +import NcActionLink from '@nextcloud/vue/dist/Components/NcActionLink' + +export default { + name: 'AppMenu', + components: { + NcActions, NcActionLink, + }, + data() { + return { + apps: loadState('core', 'apps', {}), + appLimit: 0, + } + }, + computed: { + appList() { + return Object.values(this.apps) + }, + mainAppList() { + return this.appList.slice(0, this.appLimit) + }, + popoverAppList() { + return this.appList.slice(this.appLimit) + }, + appLabel() { + return (app) => app.name + + (app.active ? ' (' + t('core', 'Currently open') + ')' : '') + + (app.unread > 0 ? ' (' + n('core', '{count} notification', '{count} notifications', app.unread, { count: app.unread }) + ')' : '') + }, + }, + mounted() { + window.addEventListener('resize', this.resize) + this.resize() + }, + methods: { + setNavigationCounter(id, counter) { + this.$set(this.apps[id], 'unread', counter) + }, + resize() { + const availableWidth = this.$el.offsetWidth + let appCount = Math.floor(availableWidth / 50) - 1 + const popoverAppCount = this.appList.length - appCount + if (popoverAppCount === 1) { + appCount-- + } + if (appCount < 1) { + appCount = 0 + } + this.appLimit = appCount + }, + }, +} +</script> + +<style lang="scss" scoped> +$header-icon-size: 20px; + +.app-menu { + width: 100%; + display: flex; + flex-shrink: 1; + flex-wrap: wrap; +} +.app-menu-main { + display: flex; + flex-wrap: nowrap; + + .app-menu-entry { + width: 50px; + height: 50px; + position: relative; + display: flex; + opacity: .7; + + &.app-menu-entry__active { + opacity: 1; + + &::before { + content: " "; + position: absolute; + pointer-events: none; + border: 8px solid transparent; + border-bottom-color: var(--color-main-background); + transform: translateX(-50%); + left: 50%; + bottom: 0; + display: block; + transition: all 0.1s ease-in-out; + opacity: 1; + } + + .app-menu-entry--label { + font-weight: bold; + } + } + + a { + width: 100%; + height: 100%; + color: var(--color-primary-text); + position: relative; + } + + img { + transition: margin 0.1s ease-in-out; + width: $header-icon-size; + height: $header-icon-size; + padding: calc((100% - $header-icon-size) / 2); + filter: var(--primary-invert-if-bright); + } + + .app-menu-entry--label { + opacity: 0; + position: absolute; + font-size: 12px; + color: var(--color-primary-text); + text-align: center; + bottom: -5px; + left: 50%; + display: block; + min-width: 100%; + transform: translateX(-50%); + transition: all 0.1s ease-in-out; + width: 100%; + text-overflow: ellipsis; + overflow: hidden; + } + + &:hover, + &:focus-within { + opacity: 1; + .app-menu-entry--label { + opacity: 1; + font-weight: bold; + font-size: 14px; + bottom: 0; + width: auto; + overflow: visible; + } + } + + } + + // Show labels + &:hover, + &:focus-within, + .app-menu-entry:hover, + .app-menu-entry:focus { + opacity: 1; + + img { + margin-top: -6px; + } + + .app-menu-entry--label { + opacity: 1; + bottom: 0; + } + + &::before, .app-menu-entry::before { + border-width: 3px; + } + } +} + +::v-deep .app-menu-more .button-vue--vue-tertiary { + color: var(--color-primary-text); + opacity: .7; + margin: 3px; + + &:hover { + opacity: 1; + background-color: transparent !important; + } +} + +.app-menu-popover-entry { + .app-icon { + position: relative; + height: 44px; + + &.has-unread::after { + background-color: var(--color-main-text); + } + + img { + filter: var(--background-invert-if-bright); + width: $header-icon-size; + height: $header-icon-size; + padding: calc((50px - $header-icon-size) / 2); + } + } +} + +.has-unread::after { + content: ""; + width: 8px; + height: 8px; + background-color: var(--color-primary-text); + border-radius: 50%; + position: absolute; + display: block; + top: 10px; + right: 10px; +} + +.unread-counter { + display: none; +} +</style> diff --git a/core/src/components/MainMenu.js b/core/src/components/MainMenu.js index 603338d05b3..267a3d9a361 100644 --- a/core/src/components/MainMenu.js +++ b/core/src/components/MainMenu.js @@ -22,99 +22,27 @@ * */ -import $ from 'jquery' +import { translate as t, translatePlural as n } from '@nextcloud/l10n' +import Vue from 'vue' -import OC from '../OC' +import AppMenu from './AppMenu.vue' -/** - * Set up the main menu toggle to react to media query changes. - * If the screen is small enough, the main menu becomes a toggle. - * If the screen is bigger, the main menu is not a toggle any more. - */ export const setUp = () => { - Object.assign(OC, { - setNavigationCounter(id, counter) { - const appmenuElement = document.getElementById('appmenu').querySelector('[data-id="' + id + '"] svg') - const appsElement = document.getElementById('apps').querySelector('[data-id="' + id + '"] svg') - if (counter === 0) { - appmenuElement.classList.remove('has-unread') - appsElement.classList.remove('has-unread') - appmenuElement.getElementsByTagName('image')[0].style.mask = '' - appsElement.getElementsByTagName('image')[0].style.mask = '' - } else { - appmenuElement.classList.add('has-unread') - appsElement.classList.add('has-unread') - appmenuElement.getElementsByTagName('image')[0].style.mask = 'url(#hole-appmenu-' + id + ')' - appsElement.getElementsByTagName('image')[0].style.mask = 'url(#hole-' + id + ')' - } - document.getElementById('appmenu').querySelector('[data-id="' + id + '"] .unread-counter').textContent = counter - document.getElementById('apps').querySelector('[data-id="' + id + '"] .unread-counter').textContent = counter + Vue.mixin({ + methods: { + t, + n, }, }) - // init the more-apps menu - OC.registerMenu($('#more-apps > a'), $('#navigation')) - - // toggle the navigation - const $toggle = $('#header .header-appname-container') - const $navigation = $('#navigation') - const $appmenu = $('#appmenu') - - // init the menu - OC.registerMenu($toggle, $navigation) - $toggle.data('oldhref', $toggle.attr('href')) - $toggle.attr('href', '#') - $navigation.hide() - // show loading feedback on more apps list - $navigation.delegate('a', 'click', event => { - let $app = $(event.target) - if (!$app.is('a')) { - $app = $app.closest('a') - } - if (event.which === 1 && !event.ctrlKey && !event.metaKey && $app.attr('target') !== '_blank') { - $app.find('svg').remove() - $app.find('div').remove() // prevent odd double-clicks - // no need for theming, loader is already inverted on dark mode - // but we need it over the primary colour - $app.prepend($('<div></div>').addClass('icon-loading-small')) - } else { - // Close navigation when opening app in - // a new tab - OC.hideMenus(() => false) - } - }) + const AppMenuApp = Vue.extend(AppMenu) + const appMenu = new AppMenuApp({}).$mount('#header-left__appmenu') - $navigation.delegate('a', 'mouseup', event => { - if (event.which === 2) { - // Close navigation when opening app in - // a new tab via middle click - OC.hideMenus(() => false) - } + Object.assign(OC, { + setNavigationCounter(id, counter) { + appMenu.setNavigationCounter(id, counter) + }, }) - // show loading feedback on visible apps list - $appmenu.delegate('li:not(#more-apps) > a', 'click', event => { - let $app = $(event.target) - if (!$app.is('a')) { - $app = $app.closest('a') - } - - if (event.which === 1 && !event.ctrlKey && !event.metaKey && $app.parent('#more-apps').length === 0 && $app.attr('target') !== '_blank') { - $app.find('svg').remove() - $app.find('div').remove() // prevent odd double-clicks - $app.prepend($('<div></div>').addClass( - OCA.Theming && OCA.Theming.inverted - ? 'icon-loading-small' - : 'icon-loading-small-dark' - )) - // trigger redirect - // needed for ie, but also works for every browser - window.location = $app.attr('href') - } else { - // Close navigation when opening app in - // a new tab - OC.hideMenus(() => false) - } - }) } diff --git a/core/src/init.js b/core/src/init.js index 507f5bbb35f..487114051ae 100644 --- a/core/src/init.js +++ b/core/src/init.js @@ -36,62 +36,7 @@ import { setUp as setUpMainMenu } from './components/MainMenu' import { setUp as setUpUserMenu } from './components/UserMenu' import PasswordConfirmation from './OC/password-confirmation' -// keep in sync with core/css/variables.scss -const breakpointMobileWidth = 1024 - -const resizeMenu = () => { - const appList = $('#appmenu li') - const rightHeaderWidth = $('.header-right').outerWidth() - const headerWidth = $('header').outerWidth() - const usePercentualAppMenuLimit = 0.67 - const minAppsDesktop = 12 - let availableWidth = headerWidth - $('#nextcloud').outerWidth() - (rightHeaderWidth > 210 ? rightHeaderWidth : 210) - const isMobile = $(window).width() < breakpointMobileWidth - if (!isMobile) { - availableWidth = availableWidth * usePercentualAppMenuLimit - } - let appCount = Math.floor((availableWidth / $(appList).width())) - if (isMobile && appCount > minAppsDesktop) { - appCount = minAppsDesktop - } - if (!isMobile && appCount < minAppsDesktop) { - appCount = minAppsDesktop - } - - // show at least 2 apps in the popover - if (appList.length - 1 - appCount >= 1) { - appCount-- - } - - $('#more-apps a').removeClass('active') - let lastShownApp - for (let k = 0; k < appList.length - 1; k++) { - const name = $(appList[k]).data('id') - if (k < appCount) { - $(appList[k]).removeClass('hidden') - $('#apps li[data-id=' + name + ']').addClass('in-header') - lastShownApp = appList[k] - } else { - $(appList[k]).addClass('hidden') - $('#apps li[data-id=' + name + ']').removeClass('in-header') - // move active app to last position if it is active - if (appCount > 0 && $(appList[k]).children('a').hasClass('active')) { - $(lastShownApp).addClass('hidden') - $('#apps li[data-id=' + $(lastShownApp).data('id') + ']').removeClass('in-header') - $(appList[k]).removeClass('hidden') - $('#apps li[data-id=' + name + ']').addClass('in-header') - } - } - } - - // show/hide more apps icon - if ($('#apps li:not(.in-header)').length === 0) { - $('#more-apps').hide() - $('#navigation').hide() - } else { - $('#more-apps').show() - } -} +const breakpointMobileWidth = getComputedStyle(document.documentElement).getPropertyValue('--breakpoint-mobile') const initLiveTimestamps = () => { // Update live timestamps every 30 seconds @@ -179,30 +124,6 @@ export const initCore = () => { setUpUserMenu() setUpContactsMenu() - // move triangle of apps dropdown to align with app name triangle - // 2 is the additional offset between the triangles - if ($('#navigation').length) { - $('#header #nextcloud + .menutoggle').on('click', () => { - $('#menu-css-helper').remove() - const caretPosition = $('.header-appname + .icon-caret').offset().left - 2 - if (caretPosition > 255) { - // if the app name is longer than the menu, just put the triangle in the middle - - } else { - $('head').append('<style id="menu-css-helper">#navigation:after { left: ' + caretPosition + 'px }</style>') - } - }) - $('#header #appmenu .menutoggle').on('click', () => { - $('#appmenu').toggleClass('menu-open') - if ($('#appmenu').is(':visible')) { - $('#menu-css-helper').remove() - } - }) - } - - $(window).resize(resizeMenu) - setTimeout(resizeMenu, 0) - // just add snapper for logged in users // and if the app doesn't handle the nav slider itself if ($('#app-navigation').length && !$('html').hasClass('lte9') |