summaryrefslogtreecommitdiffstats
path: root/core
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
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')
-rw-r--r--core/css/header.scss301
-rw-r--r--core/src/components/AppMenu.vue268
-rw-r--r--core/src/components/MainMenu.js98
-rw-r--r--core/src/init.js81
-rw-r--r--core/templates/layout.user.php65
5 files changed, 284 insertions, 529 deletions
diff --git a/core/css/header.scss b/core/css/header.scss
index 5f3c4356d32..8ab0b019437 100644
--- a/core/css/header.scss
+++ b/core/css/header.scss
@@ -16,7 +16,6 @@
/* prevent ugly selection effect on accidental selection */
#header,
-#navigation,
#expanddiv {
-webkit-user-select: none;
-moz-user-select: none;
@@ -72,7 +71,6 @@
/* Header menu */
$header-menu-entry-height: 44px;
- .header-left > nav > .menu,
.header-right > div > .menu {
background-color: var(--color-main-background);
filter: drop-shadow(0 1px 5px var(--color-box-shadow));
@@ -103,7 +101,6 @@
right: 10px;
}
- #apps > ul,
& > div,
& > ul {
overflow-y: auto;
@@ -111,8 +108,7 @@
@include header-menu-height();
}
- /* Use by the apps menu and the settings right menu */
- #apps > ul,
+ /* Use by the settings right menu */
&.settings-menu > ul {
li {
a {
@@ -182,16 +178,6 @@
padding-right: 10px;
flex-shrink: 0;
}
- /* show caret indicator next to logo to make clear it is tappable */
- .icon-caret {
- display: inline-block;
- width: 12px;
- height: 12px;
- margin: 0;
- margin-top: -21px;
- padding: 0;
- vertical-align: middle;
- }
#header-left, .header-left,
#header-right, .header-right {
@@ -245,27 +231,6 @@
opacity: .75;
}
-.menutoggle {
- .icon-caret {
- opacity: .75;
- }
- &:hover {
- .header-appname, .icon-caret {
- opacity: 1;
- }
- }
- &:focus {
- .header-appname, .icon-caret {
- opacity: 1;
- }
- }
- &.active {
- .header-appname, .icon-caret {
- opacity: 1;
- }
- }
-}
-
/* TODO: move into minimal css file for public shared template */
/* only used for public share pages now as we have the app icons when logged in */
.header-appname {
@@ -291,56 +256,6 @@
text-overflow: ellipsis;
}
-/* do not show menu toggle on public share links as there is no menu */
-#body-public #header .icon-caret {
- display: none;
-}
-
-/* NAVIGATION --------------------------------------------------------------- */
-nav[role='navigation'] {
- display: inline-block;
- width: variables.$header-height;
- height: variables.$header-height;
- margin-left: -#{variables.$header-height};
- position: relative;
-}
-
-#header .header-left > nav > #navigation {
- position: relative;
- left: 25px; /* half the togglemenu */
- transform: translateX(-50%);
- width: 160px;
-}
-
-#header .header-left > nav > #navigation,
-.ui-datepicker,
-.ui-timepicker.ui-widget {
- background-color: var(--color-main-background);
- filter: drop-shadow(0 1px 10px var(--color-box-shadow));
- &:after {
- /* position of dropdown arrow */
- left: 50%;
- bottom: 100%;
- border: solid transparent;
- content: ' ';
- height: 0;
- width: 0;
- position: absolute;
- pointer-events: none;
- border-color: rgba(0, 0, 0, 0);
- border-bottom-color: var(--color-main-background);
- border-width: 10px;
- margin-left: -10px; /* border width */
- }
-}
-
-#navigation {
- box-sizing: border-box;
- .in-header {
- display: none;
- }
-}
-
/* USER MENU -----------------------------------------------------------------*/
#settings {
display: inline-block;
@@ -420,220 +335,6 @@ nav[role='navigation'] {
}
}
-/* Apps menu */
-#appmenu {
- display: inline-flex;
- min-width: variables.$header-height;
- z-index: 2;
-
- li {
- position: relative;
- cursor: pointer;
- padding: 0 2px;
- display: flex;
- justify-content: center;
-
- a {
- position: relative;
- display: flex;
- margin: 0;
- height: calc(variables.$header-height - 6px);
- width: variables.$header-height;
- align-items: center;
- justify-content: center;
- opacity: .85;
- // Make sure most app names don’t ellipsize
- letter-spacing: -0.5px;
- font-size: 12px;
- margin: 2px;
- }
-
- /* focused app visual feedback */
- &:hover a,
- a:focus,
- a.active {
- opacity: 1;
- font-weight: bold;
- }
-
- // Text size back to normal for hover/focus
- &:hover a,
- a:focus {
- font-size: 14px;
- }
-
- &:hover a + span,
- a:focus + span,
- &:hover span,
- &:focus span,
- a:focus span,
- a.active span {
- display: inline-block;
- text-overflow: initial;
- width: auto;
- overflow: hidden;
- padding: 0 5px;
- z-index: 2;
- }
-
- /* hidden apps menu */
- img,
- .icon-more-white {
- display: inline-block;
- width: 20px;
- height: 20px;
- }
-
- .icon-more-white {
- background-image: url('../img/actions/more-white.svg?v=1');
- }
-
- /* App title */
- span {
- opacity: 0;
- position: absolute;
- color: var(--color-primary-text);
- bottom: 2px;
- width: 100%;
- text-align: center;
- overflow: hidden;
- text-overflow: ellipsis;
- transition: all var(--animation-quick) ease;
- pointer-events: none;
- }
-
- /* Set up transitions for showing app titles on hover */
- /* App icon */
- svg,
- .icon-more-white {
- transition: transform var(--animation-quick) ease;
- // If the primary is too bright, invert the app icons
- filter: var(--primary-invert-if-bright);
- }
-
- /* Triangle */
- a::before {
- transition: border var(--animation-quick) ease;
- }
- }
-
- /* Show all app titles on hovering app menu area */
- &:hover {
- li {
- /* Move up app icon */
- svg,
- .icon-more,
- .icon-more-white,
- .icon-loading-small,
- .icon-loading-small-dark {
- transform: translateY(-7px);
- }
-
- /* Show app title */
- span {
- opacity: 1;
- bottom: 2px;
- z-index: -1; /* fix clickability issue - otherwise we need to move the span into the link */
- }
-
- /* Prominent app title for current and hovered/focused app */
- &:hover span,
- &:focus span,
- .active + span {
- opacity: 1;
- }
-
- /* Smaller triangle because of limited space */
- a::before {
- border-width: 5px;
- }
- }
- }
-
- /* Also show app title on focusing single entry (showing all on focus is only possible with CSS4 and parent selectors) */
- li a:focus {
- /* Move up app icon */
- svg,
- .icon-more,
- .icon-more-white,
- .icon-loading-small,
- .icon-loading-small-dark {
- transform: translateY(-7px);
- }
-
- /* Show app title */
- & + span,
- span {
- opacity: 1;
- bottom: 2px;
- }
-
- /* Smaller triangle because of limited space */
- &::before {
- border-width: 5px;
- }
- }
-
- /* show triangle below active app */
- li a::before {
- content: ' ';
- height: 0;
- width: 0;
- position: absolute;
- pointer-events: none;
- border: 0 solid transparent;
- border-bottom-color: var(--color-main-background);
- border-width: 10px;
- transform: translateX(-50%);
- left: 50%;
- bottom: -5px;
- display: none;
- }
-
- /* triangle focus feedback */
- li a.active::before,
- li:hover a::before,
- li:hover a.active::before,
- li a:focus::before {
- display: block;
- }
- li a.active::before {
- z-index: 99;
- }
- li:hover a::before,
- li a.active:hover::before,
- li a:focus::before {
- z-index: 101;
- }
-
- li.hidden {
- display: none;
- }
-
- #more-apps {
- z-index: 3;
- }
-}
-
-.unread-counter {
- display: none;
-}
-#apps .app-icon-notification,
-#appmenu .app-icon-notification {
- fill: var(--color-error);
-}
-
-#apps svg:not(.has-unread),
-#appmenu svg:not(.has-unread) {
- .app-icon-notification-mask {
- display: none;
- }
- .app-icon-notification {
- display: none;
- }
-}
-
-
/* Skip navigation links – show only on keyboard focus */
#skip-actions {
position: absolute;
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')
diff --git a/core/templates/layout.user.php b/core/templates/layout.user.php
index c7eb2fde5ad..05698b3aa67 100644
--- a/core/templates/layout.user.php
+++ b/core/templates/layout.user.php
@@ -65,70 +65,7 @@ $getUserAvatar = static function (int $size) use ($_): string {
</div>
</a>
- <ul id="appmenu">
- <?php foreach ($_['navigation'] as $entry): ?>
- <li data-id="<?php p($entry['id']); ?>" class="hidden" tabindex="-1">
- <a href="<?php print_unescaped($entry['href']); ?>"
- <?php if (isset($entry['target']) && $entry['target']): ?> target="_blank" rel="noreferrer noopener"<?php endif; ?>
- <?php if ($entry['active']): ?> class="active"<?php endif; ?>
- aria-label="<?php p($entry['name']); ?>">
- <svg width="24" height="20" viewBox="0 0 24 20"<?php if ($entry['unread'] !== 0) { ?> class="has-unread"<?php } ?>>
- <defs>
- <mask id="hole-appmenu-<?php p($entry['id']); ?>">
- <rect width="100%" height="100%" fill="white"/>
- <circle r="4.5" cx="21" cy="3" fill="black"/>
- </mask>
- </defs>
- <image x="2" y="0" width="20" height="20" preserveAspectRatio="xMinYMin meet" xlink:href="<?php print_unescaped($entry['icon'] . '?v=' . $_['versionHash']); ?>" style="<?php if ($entry['unread'] !== 0) { ?>mask: url("#hole");<?php } ?>" class="app-icon"></image>
- <circle class="app-icon-notification" r="3" cx="21" cy="3" fill="red"/>
- </svg>
- <div class="unread-counter" aria-hidden="true"><?php p($entry['unread']); ?></div>
- <span>
- <?php p($entry['name']); ?>
- </span>
- </a>
- </li>
- <?php endforeach; ?>
- <li id="more-apps" class="menutoggle"
- aria-haspopup="true" aria-controls="navigation" aria-expanded="false">
- <a href="#" aria-label="<?php p($l->t('More apps')); ?>">
- <div class="icon-more-white"></div>
- <span><?php p($l->t('More')); ?></span>
- </a>
- </li>
- </ul>
-
- <nav role="navigation">
- <div id="navigation" style="display: none;" aria-label="<?php p($l->t('More apps menu')); ?>">
- <div id="apps">
- <ul>
- <?php foreach ($_['navigation'] as $entry): ?>
- <li data-id="<?php p($entry['id']); ?>">
- <a href="<?php print_unescaped($entry['href']); ?>"
- <?php if (isset($entry['target']) && $entry['target']): ?> target="_blank" rel="noreferrer noopener"<?php endif; ?>
- <?php if ($entry['active']): ?> class="active"<?php endif; ?>
- aria-label="<?php p($entry['name']); ?>">
- <svg width="20" height="20" viewBox="0 0 16 16"<?php if ($entry['unread'] !== 0) { ?> class="has-unread"<?php } ?>>
- <defs>
- <filter id="invertMenuMore-<?php p($entry['id']); ?>"><feColorMatrix in="SourceGraphic" type="matrix" values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0"></feColorMatrix></filter>
- <mask id="hole-<?php p($entry['id']); ?>">
- <rect width="100%" height="100%" fill="white"/>
- <circle r="4.5" cx="17" cy="3" fill="black"/>
- </mask>
- </defs>
- <image x="0" y="0" width="16" height="16" preserveAspectRatio="xMinYMin meet" filter="url(#invertMenuMore-<?php p($entry['id']); ?>)" xlink:href="<?php print_unescaped($entry['icon'] . '?v=' . $_['versionHash']); ?>" style="<?php if ($entry['unread'] !== 0) { ?>mask: url("#hole");<?php } ?>" class="app-icon"></image>
- <circle class="app-icon-notification" r="3" cx="17" cy="3" fill="red"/>
- </svg>
- <div class="unread-counter" aria-hidden="true"><?php p($entry['unread']); ?></div>
- <span class="app-title"><?php p($entry['name']); ?></span>
- </a>
- </li>
- <?php endforeach; ?>
- </ul>
- </div>
- </div>
- </nav>
-
+ <nav id="header-left__appmenu"></nav>
</div>
<div class="header-right">