add top menu (StandardMenu)

This commit is contained in:
Simon Vieille 2025-04-11 19:41:23 +02:00
commit 502cc4f3f0
Signed by: deblan
GPG key ID: 579388D585F70417
5 changed files with 143 additions and 141 deletions

View file

@ -17,15 +17,19 @@
const waitContainer = async (selector) => {
return new Promise((resolve) => {
const container = document.querySelector(selector)
const execute = () => {
const container = document.querySelector(selector)
if (container) {
return resolve(selector, container)
if (container) {
resolve(container)
} else {
setTimeout(() => {
execute(selector)
}, 50)
}
}
setTimeout(() => {
waitContainer(selector)
}, 50)
execute(selector)
})
}

View file

@ -19,12 +19,10 @@ import './scss/menu.scss'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { createElement } from './lib/dom.js'
import { createElement, waitContainer } from './lib/dom.js'
import MenuContainer from './menus/MenuContainer.vue'
// import PageLoader from './components/PageLoader.vue'
// window.PageLoader = PageLoader
import StandardMenu from './menus/StandardMenu'
import MenuContainer from './menus/MenuContainer'
const pinia = createPinia()
const body = document.querySelector('body')
@ -39,9 +37,16 @@ app.use(pinia)
app.mixin({ methods: { t, n } })
app.mount(container)
// waitContainer('#header .app-menu').then((selector) => {
// const app = createApp(AppMenu)
// app.use(pinia)
// app.mixin({ methods: { t, n }})
// app.mount(selector)
// })
waitContainer('#header .app-menu').then((container) => {
const menu = createElement('div', {
id: 'app-menu-container',
})
container.parentNode.insertBefore(menu, container.nextSibling)
container.remove()
const app = createApp(StandardMenu)
app.use(pinia)
app.mixin({ methods: { t, n } })
app.mount(menu)
})

View file

@ -25,28 +25,35 @@
:aria-label="t('core', 'Applications menu')"
>
<ul
v-if="appList.length"
v-if="ready"
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
v-for="app in mainAppList(state)"
v-for="app in mainAppList"
:key="app.id"
:data-app-id="app.id"
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)"
>
<a
:href="app.href"
:class="{ 'has-unread': app.unread > 0 }"
:aria-label="appLabel(app)"
:aria-label="app.name"
:target="targetBlankApps.indexOf(app.id) !== -1 ? '_blank' : undefined"
:aria-current="app.active ? 'page' : false"
>
<img
:src="app.icon"
alt=""
:alt="app.name"
/>
<div class="app-menu-entry--label">
{{ app.name }}
@ -64,9 +71,9 @@
:aria-label="t('core', 'More apps')"
>
<NcActionLink
v-for="app in popoverAppList(state)"
v-for="app in popoverAppList"
:key="app.id"
:aria-label="appLabel(app)"
:aria-label="app.name"
:aria-current="app.active ? 'page' : false"
:href="app.href"
:style="makeStyle(app)"
@ -79,7 +86,7 @@
>
<img
:src="app.icon"
alt=""
:alt="app.name"
/>
</div>
</template>
@ -94,134 +101,94 @@
</nav>
</template>
<script>
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
import { loadState } from '@nextcloud/initial-state'
import { n, t } from '@nextcloud/l10n'
import { useElementSize } from '@vueuse/core'
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'
<script setup>
import { ref, onMounted } from 'vue'
import { useConfigStore } from '../store/config.js'
import { useNavStore } from '../store/nav.js'
import { NcActions, NcActionLink } from '@nextcloud/vue'
export default defineComponent({
name: 'AppMenu',
const navStore = useNavStore()
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: {
NcActions,
NcActionLink,
},
setup() {
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
this.hiddenLabels = window.topMenuAppsMouseOverHiddenLabel
let timeout = null
window.addEventListener('resize', () => {
timeout = window.setTimeout(() => {
this.update()
}, 300)
const setApps = (value) => {
value.forEach((app) => {
Array.from(topMenuApps.value).forEach((id) => {
if (app.id === id) {
app.order = appsOrder.value.findIndex((element) => element === app.id) || null
appList.value.push(app)
}
})
},
})
methods: {
update() {
++this.state
},
computeLists()
}
mainAppList() {
return this.appList.slice(0, this.appLimit())
},
const appLimit = () => {
const headerStart = document.querySelector('#header .header-start')
const headerEnd = document.querySelector('#header .header-end')
const body = document.querySelector('body')
popoverAppList() {
return this.appList.slice(this.appLimit())
},
let size = (headerEnd ? headerEnd.offsetWidth : 0) + 70
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)
if (headerStart) {
Array.from(headerStart.children).forEach((child) => {
if (child.id !== 'app-menu-container') {
size += child.offsetWidth
}
})
}
return maxApps
},
return Math.floor((body.offsetWidth - size) / 70)
}
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`)
}
},
const makeStyle = (app) => {
if (app.order !== null) {
return { order: app.order }
}
setApps(apps) {
this.appList = []
let orders = {}
return {}
}
window.menuAppsOrder.forEach((app, order) => {
orders[app] = order + 1
})
const computeLists = () => {
mainAppList.value = appList.value.slice(0, appLimit())
popoverAppList.value = appList.value.slice(appLimit()).sort((a, b) => a.order - b.order)
}
apps.forEach((app) => {
Array.from(window.topMenuApps).forEach((id) => {
if (app.id === id) {
app.order = orders[id] || null
this.appList.push(app)
}
})
})
},
onMounted(async () => {
const config = await configStore.getConfig()
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 }) + ')' : '')
)
},
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
makeStyle(app) {
if (app.order !== null) {
return `order: ${app.order}`
}
},
},
setApps(await navStore.getCoreApps())
window.addEventListener('resize', () => {
window.clearTimeout(resizeTimeout)
resizeTimeout = window.setTimeout(computeLists, 100)
})
})
</script>
<script>
export default {
compatConfig: {
GLOBAL_MOUNT_CONTAINER: false,
},
}
</script>
<style lang="scss" scoped>
$header-icon-size: 20px;

View file

@ -15,6 +15,17 @@
* 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 {
position: fixed;
top: 0;

View file

@ -18,11 +18,12 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
import { generateUrl, generateOcsUrl } from '@nextcloud/router'
export const useNavStore = defineStore('nav', () => {
const categories = ref(null)
const apps = ref(null)
const coreApps = ref(null)
async function getApps() {
if (apps.value !== null) {
@ -41,6 +42,19 @@ export const useNavStore = defineStore('nav', () => {
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() {
if (categories.value !== null) {
return categories.value
@ -53,6 +67,7 @@ export const useNavStore = defineStore('nav', () => {
return {
getApps,
getCoreApps,
getCategories,
}
})