Fix dropdown on mobile
This commit is contained in:
parent
fb22218016
commit
a1c65572d5
5 changed files with 104 additions and 35 deletions
|
|
@ -11,15 +11,21 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div class="dropdown-menu" role="menu" id="dropdown-menu">
|
||||
<div class="dropdown-content" @click="handle_slot_click">
|
||||
<slot />
|
||||
<Teleport to="body">
|
||||
<div v-if="isOpen" class="dropdown is-active dropdown-portal" ref="menu" :style="menuStyle">
|
||||
<div class="dropdown-menu" role="menu">
|
||||
<div class="dropdown-content" @click="handle_slot_click">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, nextTick, watchEffect, useTemplateRef } from 'vue'
|
||||
|
||||
const emitter = defineEmits(['open_state'])
|
||||
defineProps({
|
||||
label: {
|
||||
|
|
@ -39,17 +45,56 @@ defineProps({
|
|||
const isOpen = ref(false)
|
||||
const dropUp = ref(false)
|
||||
const dropdown = useTemplateRef<HTMLDivElement>('dropdown')
|
||||
const menu = useTemplateRef<HTMLDivElement>('menu')
|
||||
const menuStyle = ref<Record<string, string>>({})
|
||||
|
||||
const updatePosition = () => {
|
||||
if (!dropdown.value || !isOpen.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const triggerRect = dropdown.value.getBoundingClientRect()
|
||||
const menuHeight = menu.value?.offsetHeight || 300
|
||||
const spaceBelow = window.innerHeight - triggerRect.bottom
|
||||
const spaceAbove = triggerRect.top
|
||||
|
||||
// Determine if dropdown should appear above or below
|
||||
const shouldDropUp = spaceBelow < menuHeight + 24 && spaceAbove > spaceBelow
|
||||
dropUp.value = shouldDropUp
|
||||
|
||||
// Calculate position
|
||||
const left = triggerRect.left
|
||||
const width = triggerRect.width
|
||||
|
||||
if (shouldDropUp) {
|
||||
// Position above the trigger
|
||||
const bottom = window.innerHeight - triggerRect.top
|
||||
menuStyle.value = {
|
||||
position: 'fixed',
|
||||
left: `${left}px`,
|
||||
bottom: `${bottom}px`,
|
||||
width: `${width}px`,
|
||||
top: 'auto'
|
||||
}
|
||||
} else {
|
||||
// Position below the trigger
|
||||
const top = triggerRect.bottom
|
||||
menuStyle.value = {
|
||||
position: 'fixed',
|
||||
left: `${left}px`,
|
||||
top: `${top}px`,
|
||||
width: `${width}px`,
|
||||
bottom: 'auto'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toggle = async () => {
|
||||
isOpen.value = !isOpen.value
|
||||
|
||||
if (isOpen.value && dropdown.value) {
|
||||
if (isOpen.value) {
|
||||
await nextTick()
|
||||
const rect = dropdown.value.getBoundingClientRect()
|
||||
const menu = dropdown.value.querySelector('.dropdown-menu') as HTMLElement
|
||||
const menuHeight = menu?.offsetHeight || 0
|
||||
const spaceBelow = window.innerHeight - rect.bottom
|
||||
dropUp.value = spaceBelow < menuHeight + 24
|
||||
updatePosition()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -67,14 +112,36 @@ const handle_event = (event: MouseEvent) => {
|
|||
|
||||
const target = event.target as HTMLElement
|
||||
|
||||
if (!dropdown.value.contains(target)) {
|
||||
if (!dropdown.value.contains(target) && !menu.value?.contains(target)) {
|
||||
isOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleScroll = () => {
|
||||
if (isOpen.value) {
|
||||
updatePosition()
|
||||
}
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
if (isOpen.value) {
|
||||
updatePosition()
|
||||
}
|
||||
}
|
||||
|
||||
watchEffect(() => emitter('open_state', isOpen.value))
|
||||
onMounted(() => document.addEventListener('click', handle_event))
|
||||
onBeforeUnmount(() => document.removeEventListener('click', handle_event))
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handle_event)
|
||||
window.addEventListener('scroll', handleScroll, true) // Use capture to catch all scroll events
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', handle_event)
|
||||
window.removeEventListener('scroll', handleScroll, true)
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -86,22 +153,27 @@ onBeforeUnmount(() => document.removeEventListener('click', handle_event))
|
|||
.dropdown-trigger {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
.dropdown-menu {
|
||||
width: 100%;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
<style>
|
||||
.dropdown.dropdown-portal {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
z-index: 99;
|
||||
width: 100%;
|
||||
.dropdown.dropdown-portal .dropdown-menu {
|
||||
display: block !important; /* Override Bulma's display: none */
|
||||
position: static; /* Don't use absolute positioning inside fixed container */
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.dropdown.drop-up .dropdown-menu {
|
||||
bottom: 100%;
|
||||
top: auto;
|
||||
position: absolute;
|
||||
.dropdown.dropdown-portal .dropdown-content {
|
||||
background-color: var(--bulma-dropdown-content-background-color, var(--bulma-scheme-main, #fff));
|
||||
border-radius: var(--bulma-dropdown-content-radius, var(--bulma-radius, 4px));
|
||||
box-shadow: var(--bulma-dropdown-content-shadow, var(--bulma-shadow, 0 0.5em 1em -0.125em rgba(10, 10, 10, 0.1), 0 0px 0 1px rgba(10, 10, 10, 0.02)));
|
||||
padding-top: var(--bulma-dropdown-content-padding-top, 0.5rem);
|
||||
padding-bottom: var(--bulma-dropdown-content-padding-bottom, 0.5rem);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -174,8 +174,7 @@
|
|||
</button>
|
||||
</div>
|
||||
<div class="control is-expanded" v-if="item.url">
|
||||
<Dropdown icons="fa-solid fa-cogs" @open_state="(s: boolean) => table_container = !s"
|
||||
:button_classes="'is-small'" label="Actions">
|
||||
<Dropdown icons="fa-solid fa-cogs" :button_classes="'is-small'" label="Actions">
|
||||
<template v-if="'finished' === item.status && item.filename">
|
||||
<NuxtLink @click="playVideo(item)" class="dropdown-item">
|
||||
<span class="icon"><i class="fa-solid fa-play" /></span>
|
||||
|
|
@ -450,7 +449,7 @@
|
|||
import moment from 'moment'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import type { StoreItem } from '~/types/store'
|
||||
import {useConfirm} from '~/composables/useConfirm'
|
||||
import { useConfirm } from '~/composables/useConfirm'
|
||||
|
||||
const emitter = defineEmits<{
|
||||
(e: 'getInfo', url: string, preset: string, cli: string): void
|
||||
|
|
|
|||
|
|
@ -115,8 +115,7 @@
|
|||
:data-datetime="item.datetime" v-rtime="item.datetime" />
|
||||
</td>
|
||||
<td class="is-vcentered is-items-center">
|
||||
<Dropdown icons="fa-solid fa-cogs" @open_state="(s: boolean) => table_container = !s"
|
||||
:button_classes="'is-small'" label="Actions">
|
||||
<Dropdown icons="fa-solid fa-cogs" :button_classes="'is-small'" label="Actions">
|
||||
<template v-if="isEmbedable(item.url)">
|
||||
<NuxtLink class="dropdown-item has-text-danger"
|
||||
@click="embed_url = getEmbedable(item.url) as string">
|
||||
|
|
@ -263,7 +262,7 @@
|
|||
</button>
|
||||
</div>
|
||||
<div class="column is-half-mobile">
|
||||
<Dropdown icons="fa-solid fa-cogs" @open_state="(s: boolean) => table_container = !s" label="Actions">
|
||||
<Dropdown icons="fa-solid fa-cogs" label="Actions">
|
||||
<template v-if="isEmbedable(item.url)">
|
||||
<NuxtLink class="dropdown-item has-text-danger"
|
||||
@click="embed_url = getEmbedable(item.url) as string">
|
||||
|
|
@ -313,7 +312,7 @@
|
|||
import moment from 'moment'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import type { StoreItem } from '~/types/store'
|
||||
import {useConfirm} from '~/composables/useConfirm'
|
||||
import { useConfirm } from '~/composables/useConfirm'
|
||||
|
||||
const emitter = defineEmits<{
|
||||
(e: 'getInfo', url: string, preset: string, cli: string): void
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@
|
|||
</span>
|
||||
</td>
|
||||
<td class="is-vcentered" v-if="config.app.browser_control_enabled">
|
||||
<Dropdown icons="fa-solid fa-cogs" @open_state="s => table_container = !s" label="Actions">
|
||||
<Dropdown icons="fa-solid fa-cogs" label="Actions">
|
||||
<template v-if="'file' === item.type">
|
||||
<a :href="makeDownload({}, { filename: item.path, folder: '' })"
|
||||
:download="item.name.split('/').reverse()[0]" class="dropdown-item">
|
||||
|
|
@ -252,7 +252,7 @@ const path = ref<string>((() => {
|
|||
}
|
||||
return '/'
|
||||
})())
|
||||
const table_container = ref<boolean>(false)
|
||||
const table_container = ref<boolean>(true)
|
||||
const search = ref<string>('')
|
||||
const show_filter = ref<boolean>(false)
|
||||
|
||||
|
|
|
|||
|
|
@ -193,8 +193,7 @@
|
|||
</div>
|
||||
|
||||
<div class="control is-expanded">
|
||||
<Dropdown icons="fa-solid fa-cogs" label="Actions" button_classes="is-small"
|
||||
@open_state="s => table_container = !s">
|
||||
<Dropdown icons="fa-solid fa-cogs" label="Actions" button_classes="is-small">
|
||||
<NuxtLink class="dropdown-item has-text-purple" @click="runNow(item)">
|
||||
<span class="icon"><i class="fa-solid fa-up-right-from-square" /></span>
|
||||
<span>Run now</span>
|
||||
|
|
|
|||
Loading…
Reference in a new issue