aboutsummaryrefslogtreecommitdiffstats
path: root/core/src
diff options
context:
space:
mode:
authorJulius Härtl <jus@bitgrid.net>2022-08-27 10:57:13 +0200
committerJulius Härtl <jus@bitgrid.net>2022-08-31 10:24:03 +0200
commit5b4708c5be100c3a4bbb2fd32151ae2a7420df2d (patch)
tree181dc1020643ecc5434fa77bfac5606be3826ed4 /core/src
parent23bb4f16f9056e7a79116129c7de5b59cf84f8be (diff)
downloadnextcloud-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.vue268
-rw-r--r--core/src/components/MainMenu.js98
-rw-r--r--core/src/init.js81
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')