add top menu (StandardMenu)

This commit is contained in:
Simon Vieille
2025-04-11 19:41:23 +02:00
parent e74de11215
commit 502cc4f3f0
5 changed files with 139 additions and 137 deletions
+8 -4
View File
@@ -17,15 +17,19 @@
const waitContainer = async (selector) => { const waitContainer = async (selector) => {
return new Promise((resolve) => { return new Promise((resolve) => {
const execute = () => {
const container = document.querySelector(selector) const container = document.querySelector(selector)
if (container) { if (container) {
return resolve(selector, container) resolve(container)
} else {
setTimeout(() => {
execute(selector)
}, 50)
}
} }
setTimeout(() => { execute(selector)
waitContainer(selector)
}, 50)
}) })
} }
+16 -11
View File
@@ -19,12 +19,10 @@ import './scss/menu.scss'
import { createApp } from 'vue' import { createApp } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import { createElement } from './lib/dom.js' import { createElement, waitContainer } from './lib/dom.js'
import MenuContainer from './menus/MenuContainer.vue' import StandardMenu from './menus/StandardMenu'
import MenuContainer from './menus/MenuContainer'
// import PageLoader from './components/PageLoader.vue'
// window.PageLoader = PageLoader
const pinia = createPinia() const pinia = createPinia()
const body = document.querySelector('body') const body = document.querySelector('body')
@@ -39,9 +37,16 @@ app.use(pinia)
app.mixin({ methods: { t, n } }) app.mixin({ methods: { t, n } })
app.mount(container) app.mount(container)
// waitContainer('#header .app-menu').then((selector) => { waitContainer('#header .app-menu').then((container) => {
// const app = createApp(AppMenu) const menu = createElement('div', {
// app.use(pinia) id: 'app-menu-container',
// app.mixin({ methods: { t, n }}) })
// app.mount(selector)
// }) container.parentNode.insertBefore(menu, container.nextSibling)
container.remove()
const app = createApp(StandardMenu)
app.use(pinia)
app.mixin({ methods: { t, n } })
app.mount(menu)
})
@@ -25,28 +25,35 @@
:aria-label="t('core', 'Applications menu')" :aria-label="t('core', 'Applications menu')"
> >
<ul <ul
v-if="appList.length" v-if="ready"
class="app-menu-main" class="app-menu-main"
:class="{ 'app-menu-main__hidden-label': hiddenLabels === 1, 'app-menu-main__show-hovered': hiddenLabels === 2 }" :class="{
'app-menu-main__hidden-label': hiddenLabels === 1,
'app-menu-main__show-hovered': hiddenLabels === 2,
}"
> >
<li <li
v-for="app in mainAppList(state)" v-for="app in mainAppList"
:key="app.id" :key="app.id"
:data-app-id="app.id" :data-app-id="app.id"
class="app-menu-entry" class="app-menu-entry"
:class="{ 'app-menu-entry__active': app.active, 'app-menu-entry__hidden-label': hiddenLabels === 1, 'app-menu-main__show-hovered': hiddenLabels === 2 }" :class="{
'app-menu-entry__active': app.active,
'app-menu-entry__hidden-label': hiddenLabels === 1,
'app-menu-main__show-hovered': hiddenLabels === 2,
}"
:style="makeStyle(app)" :style="makeStyle(app)"
> >
<a <a
:href="app.href" :href="app.href"
:class="{ 'has-unread': app.unread > 0 }" :class="{ 'has-unread': app.unread > 0 }"
:aria-label="appLabel(app)" :aria-label="app.name"
:target="targetBlankApps.indexOf(app.id) !== -1 ? '_blank' : undefined" :target="targetBlankApps.indexOf(app.id) !== -1 ? '_blank' : undefined"
:aria-current="app.active ? 'page' : false" :aria-current="app.active ? 'page' : false"
> >
<img <img
:src="app.icon" :src="app.icon"
alt="" :alt="app.name"
/> />
<div class="app-menu-entry--label"> <div class="app-menu-entry--label">
{{ app.name }} {{ app.name }}
@@ -64,9 +71,9 @@
:aria-label="t('core', 'More apps')" :aria-label="t('core', 'More apps')"
> >
<NcActionLink <NcActionLink
v-for="app in popoverAppList(state)" v-for="app in popoverAppList"
:key="app.id" :key="app.id"
:aria-label="appLabel(app)" :aria-label="app.name"
:aria-current="app.active ? 'page' : false" :aria-current="app.active ? 'page' : false"
:href="app.href" :href="app.href"
:style="makeStyle(app)" :style="makeStyle(app)"
@@ -79,7 +86,7 @@
> >
<img <img
:src="app.icon" :src="app.icon"
alt="" :alt="app.name"
/> />
</div> </div>
</template> </template>
@@ -94,134 +101,94 @@
</nav> </nav>
</template> </template>
<script> <script setup>
import { subscribe, unsubscribe } from '@nextcloud/event-bus' import { ref, onMounted } from 'vue'
import { loadState } from '@nextcloud/initial-state' import { useConfigStore } from '../store/config.js'
import { n, t } from '@nextcloud/l10n' import { useNavStore } from '../store/nav.js'
import { useElementSize } from '@vueuse/core' import { NcActions, NcActionLink } from '@nextcloud/vue'
import { defineComponent, ref } from 'vue'
import axios from '@nextcloud/axios'
import { generateOcsUrl } from '@nextcloud/router'
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
import NcActionLink from '@nextcloud/vue/dist/Components/NcActionLink.js'
export default defineComponent({ const navStore = useNavStore()
name: 'AppMenu', const configStore = useConfigStore()
const ready = ref(false)
const appList = ref([])
const targetBlankApps = ref(null)
const hiddenLabels = ref(false)
const topMenuApps = ref([])
const appsOrder = ref([])
const mainAppList = ref([])
const popoverAppList = ref([])
let resizeTimeout = null
components: { const setApps = (value) => {
NcActions, value.forEach((app) => {
NcActionLink, Array.from(topMenuApps.value).forEach((id) => {
}, if (app.id === id) {
app.order = appsOrder.value.findIndex((element) => element === app.id) || null
setup() { appList.value.push(app)
return {
t,
n,
} }
}, })
data() {
return {
apps: null,
appList: [],
observer: null,
targetBlankApps: [],
hiddenLabels: true,
state: 1,
}
},
mounted() {
axios
.get(generateOcsUrl('core/navigation', 2) + '/apps?format=json')
.then((response) => response.data)
.then((data) => {
if (data.ocs.meta.statuscode !== 200) {
return
}
this.setApps(data.ocs.data)
}) })
this.targetBlankApps = window.targetBlankApps computeLists()
this.hiddenLabels = window.topMenuAppsMouseOverHiddenLabel }
let timeout = null
const appLimit = () => {
const headerStart = document.querySelector('#header .header-start')
const headerEnd = document.querySelector('#header .header-end')
const body = document.querySelector('body')
let size = (headerEnd ? headerEnd.offsetWidth : 0) + 70
if (headerStart) {
Array.from(headerStart.children).forEach((child) => {
if (child.id !== 'app-menu-container') {
size += child.offsetWidth
}
})
}
return Math.floor((body.offsetWidth - size) / 70)
}
const makeStyle = (app) => {
if (app.order !== null) {
return { order: app.order }
}
return {}
}
const computeLists = () => {
mainAppList.value = appList.value.slice(0, appLimit())
popoverAppList.value = appList.value.slice(appLimit()).sort((a, b) => a.order - b.order)
}
onMounted(async () => {
const config = await configStore.getConfig()
targetBlankApps.value = config['target-blank-apps']
hiddenLabels.value = config['top-menu-mouse-over-hidden-label']
topMenuApps.value = config['top-menu-apps']
appsOrder.value = config['apps-order']
ready.value = true
setApps(await navStore.getCoreApps())
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
timeout = window.setTimeout(() => { window.clearTimeout(resizeTimeout)
this.update() resizeTimeout = window.setTimeout(computeLists, 100)
}, 300)
}) })
},
methods: {
update() {
++this.state
},
mainAppList() {
return this.appList.slice(0, this.appLimit())
},
popoverAppList() {
return this.appList.slice(this.appLimit())
},
appLimit() {
const maxApps = Math.floor(this.$root.$el.offsetWidth / 60)
if (maxApps < this.appList.length) {
// Ensure there is space for the overflow menu
return Math.max(maxApps - 1, 0)
}
return maxApps
},
setNavigationCounter(id, counter) {
const app = this.appList.find(({ app }) => app === id)
if (app) {
this.$set(app, 'unread', counter)
} else {
logger.warn(`Could not find app "${id}" for setting navigation count`)
}
},
setApps(apps) {
this.appList = []
let orders = {}
window.menuAppsOrder.forEach((app, order) => {
orders[app] = order + 1
})
apps.forEach((app) => {
Array.from(window.topMenuApps).forEach((id) => {
if (app.id === id) {
app.order = orders[id] || null
this.appList.push(app)
}
})
})
},
appLabel(app) {
return (
app.name +
(app.active ? ' (' + t('core', 'Currently open') + ')' : '') +
(app.unread > 0 ? ' (' + n('core', '{count} notification', '{count} notifications', app.unread, { count: app.unread }) + ')' : '')
)
},
makeStyle(app) {
if (app.order !== null) {
return `order: ${app.order}`
}
},
},
}) })
</script> </script>
<script>
export default {
compatConfig: {
GLOBAL_MOUNT_CONTAINER: false,
},
}
</script>
<style lang="scss" scoped> <style lang="scss" scoped>
$header-icon-size: 20px; $header-icon-size: 20px;
+11
View File
@@ -15,6 +15,17 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
.__debug {
background: red;
padding: 5px;
color: #fff;
width: 50vw;
bottom: 0;
right: 0;
z-index: 3000;
position: fixed;
}
#side-menu { #side-menu {
position: fixed; position: fixed;
top: 0; top: 0;
+16 -1
View File
@@ -18,11 +18,12 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref } from 'vue' import { ref } from 'vue'
import axios from '@nextcloud/axios' import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router' import { generateUrl, generateOcsUrl } from '@nextcloud/router'
export const useNavStore = defineStore('nav', () => { export const useNavStore = defineStore('nav', () => {
const categories = ref(null) const categories = ref(null)
const apps = ref(null) const apps = ref(null)
const coreApps = ref(null)
async function getApps() { async function getApps() {
if (apps.value !== null) { if (apps.value !== null) {
@@ -41,6 +42,19 @@ export const useNavStore = defineStore('nav', () => {
return apps.value return apps.value
} }
async function getCoreApps() {
if (coreApps.value !== null) {
return coreApps.value
}
coreApps.value = await axios
.get(generateOcsUrl('core/navigation', 2) + '/apps?format=json')
.then((response) => response.data)
.then((value) => value.ocs.data)
return coreApps.value
}
async function getCategories() { async function getCategories() {
if (categories.value !== null) { if (categories.value !== null) {
return categories.value return categories.value
@@ -53,6 +67,7 @@ export const useNavStore = defineStore('nav', () => {
return { return {
getApps, getApps,
getCoreApps,
getCategories, getCategories,
} }
}) })