Files
2024-09-01 18:54:23 +05:00

190 lines
3.9 KiB
Vue

<template>
<teleport to="body">
<template v-if="show">
<div
v-bind="defaultAttributes"
class="modal fixed inset-0 z-[60]"
:class="{
'px-3 md:px-0 py-3 md:py-6 overflow-x-hidden overflow-y-auto':
modalStyle === 'window',
'h-full': modalStyle === 'fullscreen',
}"
:role="role"
:data-modal-open="show"
:aria-modal="show"
>
<div
class="@container/modal relative mx-auto z-20"
:class="contentClasses"
ref="modalContent"
>
<slot />
</div>
</div>
<div
class="fixed inset-0 z-[55] bg-gray-500/75 dark:bg-gray-900/75"
dusk="modal-backdrop"
/>
</template>
</teleport>
</template>
<script setup>
import { useStore } from 'vuex'
import filter from 'lodash/filter'
import omit from 'lodash/omit'
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
import { useEventListener } from '@vueuse/core'
import {
computed,
nextTick,
onBeforeUnmount,
onMounted,
ref,
useAttrs,
watch,
} from 'vue'
const modalContent = ref(null)
const attrs = useAttrs()
const emit = defineEmits(['showing', 'closing', 'close-via-escape'])
defineOptions({ inheritAttrs: false })
const props = defineProps({
show: { type: Boolean, default: false },
size: {
type: String,
default: 'xl',
validator: v =>
[
'sm',
'md',
'lg',
'xl',
'2xl',
'3xl',
'4xl',
'5xl',
'6xl',
'7xl',
].includes(v),
},
modalStyle: { type: String, default: 'window' },
role: { type: String, default: 'dialog' },
useFocusTrap: { type: Boolean, default: true },
})
const usesFocusTrap = ref(true)
const hasTrapFocus = computed(() => {
return props.useFocusTrap && usesFocusTrap.value === true
})
const { activate, deactivate } = useFocusTrap(modalContent, {
immediate: false,
allowOutsideClick: true,
escapeDeactivates: false,
})
watch(
() => props.show,
v => handleVisibilityChange(v)
)
watch(hasTrapFocus, enable => {
try {
if (enable) {
nextTick(() => activate())
} else {
deactivate()
}
} catch (e) {
//
}
})
useEventListener(document, 'keydown', e => {
if (e.key === 'Escape' && props.show === true) {
emit('close-via-escape', e)
}
})
const disableModalFocusTrap = () => {
usesFocusTrap.value = false
}
const enableModalFocusTrap = () => {
usesFocusTrap.value = true
}
onMounted(() => {
Nova.$on('disable-focus-trap', disableModalFocusTrap)
Nova.$on('enable-focus-trap', enableModalFocusTrap)
if (props.show === true) handleVisibilityChange(true)
})
onBeforeUnmount(() => {
document.body.classList.remove('overflow-hidden')
Nova.resumeShortcuts()
Nova.$off('disable-focus-trap', disableModalFocusTrap)
Nova.$off('enable-focus-trap', enableModalFocusTrap)
usesFocusTrap.value = false
})
const store = useStore()
async function handleVisibilityChange(showing) {
if (showing === true) {
emit('showing')
document.body.classList.add('overflow-hidden')
Nova.pauseShortcuts()
usesFocusTrap.value = true
} else {
usesFocusTrap.value = false
emit('closing')
document.body.classList.remove('overflow-hidden')
Nova.resumeShortcuts()
}
store.commit('allowLeavingModal')
}
const defaultAttributes = computed(() => {
return omit(attrs, ['class'])
})
const sizeClasses = computed(() => {
return {
sm: 'max-w-sm',
md: 'max-w-md',
lg: 'max-w-lg',
xl: 'max-w-xl',
'2xl': 'max-w-2xl',
'3xl': 'max-w-3xl',
'4xl': 'max-w-4xl',
'5xl': 'max-w-5xl',
'6xl': 'max-w-6xl',
'7xl': 'max-w-7xl',
}
})
const contentClasses = computed(() => {
let windowClasses = props.modalStyle === 'window' ? sizeClasses.value : {}
return filter([
windowClasses[props.size] ?? null,
props.modalStyle === 'fullscreen' ? 'h-full' : '',
attrs.class,
])
})
</script>