You've already forked AstralRinth
forked from didirus/AstralRinth
* New project page * fix silly icon tailwind classes * Start new versions page, add new ButtonStyled component * Pagination and finish mocking up versions page functionality * green download button * hover animation * New Modal, Avatar refactor, subpages in NavTabs * lint * Download modal * New user page + fix lint * fix ui lint * Download animation fix * Versions filter + finish project page * Improve consistency of buttons on home page * Fix ButtonStyled breaking * Fix margin on version summary * finish search, new modals, user + project page mobile * fix gallery image pages * New project header * Fix gallery tab showing improperly * Use auto direction + position for all popouts * Preliminary user page * test to see if this fixes login stuff * remove extra slash * Add version actions, move download button on versions page * Listed -> public * Shorten download modal selector height * Fix user menu open direction * Change breakpoint for header collapse * Only underline title * Tighten padding on stats a little * New nav * Make mobile breakpoint more consistent * fix header breakpoint regression * Add sign in button * Fix edit icon color * Fix margin at top of screen * Fix user bios and ad width * Fix user nav showing when there's only one type of project * Fix plural projects on user page & extract i18n * Remove ads on mobile for now * Fix overflow menu showing hidden items * NavTabs on mobile * Fix navbar z index * Search filter overhaul + negative filters * fix no-max-height * port version filters, fix following/collections, lint * hide promos * ui lint * Disable modal background animation to reduce reported motion sickness * Hide install with modrinth app button on mobile --------- Signed-off-by: Geometrically <18202329+Geometrically@users.noreply.github.com> Co-authored-by: Prospector <prospectordev@gmail.com>
249 lines
5.3 KiB
Vue
249 lines
5.3 KiB
Vue
<template>
|
|
<div ref="dropdown" class="popup-container" tabindex="-1" :aria-expanded="dropdownVisible">
|
|
<button
|
|
v-bind="$attrs"
|
|
ref="dropdownButton"
|
|
:class="{ 'popout-open': dropdownVisible }"
|
|
tabindex="-1"
|
|
@click="toggleDropdown"
|
|
>
|
|
<slot></slot>
|
|
</button>
|
|
<div
|
|
class="popup-menu"
|
|
:class="`position-${computedPosition}-${computedDirection} ${dropdownVisible ? 'visible' : ''}`"
|
|
>
|
|
<slot name="menu"> </slot>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
|
|
|
const props = defineProps({
|
|
disabled: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
position: {
|
|
type: String,
|
|
default: 'auto',
|
|
},
|
|
direction: {
|
|
type: String,
|
|
default: 'auto',
|
|
},
|
|
})
|
|
defineOptions({
|
|
inheritAttrs: false,
|
|
})
|
|
|
|
const dropdownVisible = ref(false)
|
|
const dropdown = ref(null)
|
|
const dropdownButton = ref(null)
|
|
const computedPosition = ref('bottom')
|
|
const computedDirection = ref('left')
|
|
|
|
function updateDirection() {
|
|
if (props.direction === 'auto') {
|
|
if (dropdownButton.value) {
|
|
const x = dropdownButton.value.getBoundingClientRect().left
|
|
computedDirection.value = x < window.innerWidth / 2 ? 'right' : 'left'
|
|
} else {
|
|
computedDirection.value = 'left'
|
|
}
|
|
} else {
|
|
computedDirection.value = props.direction
|
|
}
|
|
if (props.position === 'auto') {
|
|
if (dropdownButton.value) {
|
|
const y = dropdownButton.value.getBoundingClientRect().top
|
|
computedPosition.value = y < window.innerHeight / 2 ? 'bottom' : 'top'
|
|
} else {
|
|
computedPosition.value = 'bottom'
|
|
}
|
|
} else {
|
|
computedPosition.value = props.position
|
|
}
|
|
}
|
|
|
|
const toggleDropdown = () => {
|
|
if (!props.disabled) {
|
|
dropdownVisible.value = !dropdownVisible.value
|
|
if (!dropdownVisible.value) {
|
|
dropdownButton.value.focus()
|
|
}
|
|
}
|
|
}
|
|
|
|
const hide = () => {
|
|
dropdownVisible.value = false
|
|
dropdownButton.value.focus()
|
|
}
|
|
|
|
const show = () => {
|
|
dropdownVisible.value = true
|
|
}
|
|
|
|
defineExpose({
|
|
show,
|
|
hide,
|
|
})
|
|
|
|
const handleClickOutside = (event) => {
|
|
const elements = document.elementsFromPoint(event.clientX, event.clientY)
|
|
if (
|
|
dropdown.value.$el !== event.target &&
|
|
!elements.includes(dropdown.value.$el) &&
|
|
!dropdown.value.contains(event.target)
|
|
) {
|
|
dropdownVisible.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
window.addEventListener('click', handleClickOutside)
|
|
window.addEventListener('resize', updateDirection)
|
|
window.addEventListener('scroll', updateDirection)
|
|
updateDirection()
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
window.removeEventListener('click', handleClickOutside)
|
|
window.removeEventListener('resize', updateDirection)
|
|
window.removeEventListener('scroll', updateDirection)
|
|
})
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.popup-container {
|
|
position: relative;
|
|
|
|
.popup-menu {
|
|
--_animation-offset: -1rem;
|
|
position: absolute;
|
|
scale: 0.75;
|
|
border: 1px solid var(--color-button-bg);
|
|
padding: var(--gap-sm);
|
|
width: fit-content;
|
|
border-radius: var(--radius-md);
|
|
background-color: var(--color-raised-bg);
|
|
box-shadow: var(--shadow-floating);
|
|
z-index: 10;
|
|
opacity: 0;
|
|
transition:
|
|
bottom 0.125s ease-in-out,
|
|
top 0.125s ease-in-out,
|
|
left 0.125s ease-in-out,
|
|
right 0.125s ease-in-out,
|
|
opacity 0.125s ease-in-out,
|
|
scale 0.125s ease-in-out;
|
|
|
|
@media (prefers-reduced-motion) {
|
|
transition: none !important;
|
|
}
|
|
|
|
&.position-bottom-left {
|
|
top: calc(100% + var(--gap-sm) - 1rem);
|
|
right: -1rem;
|
|
}
|
|
|
|
&.position-bottom-right {
|
|
top: calc(100% + var(--gap-sm) - 1rem);
|
|
left: -1rem;
|
|
}
|
|
|
|
&.position-top-left {
|
|
bottom: calc(100% + var(--gap-sm) - 1rem);
|
|
right: -1rem;
|
|
}
|
|
|
|
&.position-top-right {
|
|
bottom: calc(100% + var(--gap-sm) - 1rem);
|
|
left: -1rem;
|
|
}
|
|
|
|
&.position-left-up {
|
|
bottom: -1rem;
|
|
right: calc(100% + var(--gap-sm) - 1rem);
|
|
}
|
|
|
|
&.position-left-down {
|
|
top: -1rem;
|
|
right: calc(100% + var(--gap-sm) - 1rem);
|
|
}
|
|
|
|
&.position-right-up {
|
|
bottom: -1rem;
|
|
left: calc(100% + var(--gap-sm) - 1rem);
|
|
}
|
|
|
|
&.position-right-down {
|
|
top: -1rem;
|
|
left: calc(100% + var(--gap-sm) - 1rem);
|
|
}
|
|
|
|
&:not(.visible):not(:focus-within) {
|
|
pointer-events: none;
|
|
|
|
*,
|
|
::before,
|
|
::after {
|
|
pointer-events: none;
|
|
}
|
|
}
|
|
|
|
&.visible,
|
|
&:focus-within {
|
|
opacity: 1;
|
|
scale: 1;
|
|
|
|
&.position-bottom-left {
|
|
top: calc(100% + var(--gap-sm));
|
|
right: 0;
|
|
}
|
|
|
|
&.position-bottom-right {
|
|
top: calc(100% + var(--gap-sm));
|
|
left: 0;
|
|
}
|
|
|
|
&.position-top-left {
|
|
bottom: calc(100% + var(--gap-sm));
|
|
right: 0;
|
|
}
|
|
|
|
&.position-top-right {
|
|
bottom: calc(100% + var(--gap-sm));
|
|
left: 0;
|
|
}
|
|
|
|
&.position-left-up {
|
|
bottom: 0rem;
|
|
right: calc(100% + var(--gap-sm));
|
|
}
|
|
|
|
&.position-left-down {
|
|
top: 0rem;
|
|
right: calc(100% + var(--gap-sm));
|
|
}
|
|
|
|
&.position-right-up {
|
|
bottom: 0rem;
|
|
left: calc(100% + var(--gap-sm));
|
|
}
|
|
|
|
&.position-right-down {
|
|
top: 0rem;
|
|
left: calc(100% + var(--gap-sm));
|
|
}
|
|
}
|
|
|
|
.btn {
|
|
white-space: nowrap;
|
|
}
|
|
}
|
|
}
|
|
</style>
|