Fix dropdown on mobile

This commit is contained in:
arabcoders 2025-10-02 16:21:41 +03:00
parent fb22218016
commit a1c65572d5
5 changed files with 104 additions and 35 deletions

View file

@ -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>

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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>