2022-12-02 16:02:44 +09:00
|
|
|
<!-- 对话框组件 -->
|
|
|
|
<script lang="ts" setup>
|
2022-11-27 10:52:46 +09:00
|
|
|
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
|
2022-12-02 16:02:44 +09:00
|
|
|
import { useDeactivated } from '~/composables/lifecycle'
|
2022-11-27 10:52:46 +09:00
|
|
|
|
2022-12-02 16:02:44 +09:00
|
|
|
export interface Props {
|
|
|
|
/** v-model dislog visibility */
|
|
|
|
modelValue: boolean
|
2022-11-27 10:52:46 +09:00
|
|
|
|
2022-12-02 16:02:44 +09:00
|
|
|
/**
|
|
|
|
* level of depth
|
|
|
|
*
|
|
|
|
* @default 100
|
|
|
|
*/
|
|
|
|
zIndex?: number
|
|
|
|
|
|
|
|
/**
|
|
|
|
* whether to allow close dialog by clicking mask layer
|
|
|
|
*
|
|
|
|
* @default true
|
|
|
|
*/
|
|
|
|
closeByMask?: boolean
|
|
|
|
|
|
|
|
/**
|
|
|
|
* use v-if, destroy all the internal elements after closed
|
|
|
|
*
|
|
|
|
* @default true
|
|
|
|
*/
|
|
|
|
useVIf?: boolean
|
|
|
|
|
|
|
|
/**
|
|
|
|
* keep the dialog opened even when in other views
|
|
|
|
*
|
|
|
|
* @default false
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
keepAlive?: boolean
|
|
|
|
|
|
|
|
/** customizable class for the div outside of slot */
|
|
|
|
customClass?: string
|
|
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
|
|
zIndex: 100,
|
|
|
|
closeByMask: true,
|
|
|
|
useVIf: true,
|
|
|
|
keepAlive: false,
|
|
|
|
})
|
2022-11-27 10:52:46 +09:00
|
|
|
|
2022-12-02 16:02:44 +09:00
|
|
|
const emits = defineEmits<{
|
|
|
|
/** v-model dislog visibility */
|
|
|
|
(event: 'update:modelValue', value: boolean): void
|
2022-11-23 12:48:01 +09:00
|
|
|
}>()
|
2022-11-24 17:04:53 +09:00
|
|
|
|
2022-12-02 16:02:44 +09:00
|
|
|
const visible = useVModel(props, 'modelValue', emits, { passive: true })
|
2022-11-27 10:52:46 +09:00
|
|
|
|
2022-12-02 16:02:44 +09:00
|
|
|
const deactivated = useDeactivated()
|
|
|
|
const route = useRoute()
|
2022-11-27 10:52:46 +09:00
|
|
|
|
2022-12-02 16:02:44 +09:00
|
|
|
/** scrollable HTML element */
|
|
|
|
const elDialogScroll = ref<HTMLDivElement>()
|
|
|
|
const elDialogMain = ref<HTMLDivElement>()
|
|
|
|
const elDialogRoot = ref<HTMLDivElement>()
|
2022-11-27 10:52:46 +09:00
|
|
|
|
2022-12-02 16:02:44 +09:00
|
|
|
defineExpose({
|
|
|
|
elDialogRoot,
|
|
|
|
elDialogMain,
|
|
|
|
elDialogScroll,
|
|
|
|
})
|
|
|
|
|
|
|
|
/** close the dialog */
|
2022-11-27 12:13:39 +09:00
|
|
|
function close() {
|
2022-12-02 16:02:44 +09:00
|
|
|
visible.value = false
|
|
|
|
}
|
|
|
|
|
|
|
|
function clickMask() {
|
|
|
|
if (props.closeByMask)
|
|
|
|
close()
|
2022-11-27 12:13:39 +09:00
|
|
|
}
|
|
|
|
|
2022-12-02 16:02:44 +09:00
|
|
|
const routePath = ref(route.path)
|
|
|
|
watch(visible, (value) => {
|
|
|
|
if (value)
|
|
|
|
routePath.value = route.path
|
|
|
|
})
|
|
|
|
|
|
|
|
const notInCurrentPage = computed(() => deactivated.value || routePath.value !== route.path)
|
|
|
|
watch(notInCurrentPage, (value) => {
|
|
|
|
if (props.keepAlive)
|
|
|
|
return
|
|
|
|
if (value)
|
|
|
|
close()
|
|
|
|
})
|
|
|
|
|
|
|
|
// controls the state of v-if.
|
|
|
|
// when useVIf is toggled, v-if has the same state as modelValue, otherwise v-if is true
|
|
|
|
const isVIf = computed(() => {
|
|
|
|
return props.useVIf
|
|
|
|
? visible.value
|
|
|
|
: true
|
|
|
|
})
|
|
|
|
|
|
|
|
// controls the state of v-show.
|
|
|
|
// when useVIf is toggled, v-show is true, otherwise it has the same state as modelValue
|
|
|
|
const isVShow = computed(() => {
|
|
|
|
return !props.useVIf
|
|
|
|
? visible.value
|
|
|
|
: true
|
|
|
|
})
|
|
|
|
|
|
|
|
const bindTypeToAny = ($attrs: any) => $attrs as any
|
|
|
|
|
|
|
|
const { activate, deactivate } = useFocusTrap(elDialogRoot)
|
|
|
|
watch(visible, async (value) => {
|
|
|
|
await nextTick()
|
|
|
|
if (value)
|
2022-11-27 10:52:46 +09:00
|
|
|
activate()
|
|
|
|
else
|
|
|
|
deactivate()
|
|
|
|
})
|
2022-11-27 12:13:39 +09:00
|
|
|
useEventListener('keydown', (e: KeyboardEvent) => {
|
2022-12-02 16:02:44 +09:00
|
|
|
if (!visible.value)
|
2022-11-27 12:13:39 +09:00
|
|
|
return
|
|
|
|
if (e.key === 'Escape') {
|
|
|
|
close()
|
|
|
|
e.preventDefault()
|
|
|
|
}
|
2022-11-24 17:04:53 +09:00
|
|
|
})
|
2022-12-02 16:02:44 +09:00
|
|
|
</script>
|
2022-11-27 12:13:39 +09:00
|
|
|
|
2022-12-02 16:02:44 +09:00
|
|
|
<script lang="ts">
|
|
|
|
export default {
|
|
|
|
inheritAttrs: false,
|
2022-11-27 12:13:39 +09:00
|
|
|
}
|
2022-11-23 12:48:01 +09:00
|
|
|
</script>
|
|
|
|
|
|
|
|
<template>
|
2022-11-27 21:29:10 +09:00
|
|
|
<SafeTeleport to="#teleport-end">
|
2022-12-02 16:02:44 +09:00
|
|
|
<!-- Dialog component -->
|
|
|
|
<Transition name="dialog-visible">
|
2022-11-27 21:29:10 +09:00
|
|
|
<div
|
2022-12-02 16:02:44 +09:00
|
|
|
v-if="isVIf"
|
|
|
|
v-show="isVShow"
|
|
|
|
ref="elDialogRoot"
|
|
|
|
:style="{
|
|
|
|
'z-index': zIndex,
|
|
|
|
}"
|
|
|
|
class="scrollbar-hide" fixed inset-0 overflow-y-auto overscroll-none
|
2022-11-27 21:29:10 +09:00
|
|
|
>
|
2022-12-02 16:02:44 +09:00
|
|
|
<!-- The style `scrollbar-hide overscroll-none overflow-y-scroll` and `h="[calc(100%+0.5px)]"` is used to implement scroll locking, -->
|
|
|
|
<!-- corresponding to issue: #106, so please don't remove it. -->
|
|
|
|
|
|
|
|
<!-- Mask layer: blur -->
|
|
|
|
<div class="dialog-mask" absolute inset-0 z-0 bg-transparent opacity-100 backdrop-filter backdrop-blur-sm touch-none />
|
|
|
|
<!-- Mask layer: dimming -->
|
|
|
|
<div class="dialog-mask" absolute inset-0 z-0 bg-black opacity-48 touch-none h="[calc(100%+0.5px)]" @click="clickMask" />
|
|
|
|
<!-- Dialog container -->
|
|
|
|
<div class="p-safe-area" absolute inset-0 z-1 pointer-events-none opacity-100 flex>
|
|
|
|
<div class="flex-1 flex items-center justify-center p-4">
|
|
|
|
<!-- Dialog it self -->
|
|
|
|
<div
|
|
|
|
ref="elDialogMain"
|
|
|
|
class="dialog-main w-full rounded shadow-lg pointer-events-auto isolate bg-base border-base border-1px border-solid w-full max-w-125 max-h-full flex flex-col"
|
|
|
|
v-bind="bindTypeToAny($attrs)"
|
|
|
|
>
|
|
|
|
<!-- header -->
|
|
|
|
<slot name="header" />
|
|
|
|
<!-- main -->
|
|
|
|
<div ref="elDialogScroll" class="overflow-y-auto touch-pan-y touch-pan-x overscroll-none flex-1" :class="customClass">
|
|
|
|
<slot />
|
|
|
|
</div>
|
|
|
|
<!-- footer -->
|
|
|
|
<slot name="footer" />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
2022-11-27 21:29:10 +09:00
|
|
|
</div>
|
2022-12-02 16:02:44 +09:00
|
|
|
</Transition>
|
2022-11-27 21:29:10 +09:00
|
|
|
</SafeTeleport>
|
2022-11-23 12:48:01 +09:00
|
|
|
</template>
|
2022-11-27 17:02:09 +09:00
|
|
|
|
2022-12-02 16:02:44 +09:00
|
|
|
<style lang="postcss" scoped>
|
|
|
|
.dialog-visible-enter-active,
|
|
|
|
.dialog-visible-leave-active {
|
|
|
|
transition-duration: 0.25s;
|
|
|
|
|
|
|
|
.dialog-mask {
|
|
|
|
transition: opacity 0.25s ease;
|
|
|
|
}
|
|
|
|
|
|
|
|
.dialog-main {
|
|
|
|
transition: opacity 0.25s ease, transform 0.25s ease;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.dialog-visible-enter-from,
|
|
|
|
.dialog-visible-leave-to {
|
|
|
|
.dialog-mask {
|
|
|
|
opacity: 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
.dialog-main {
|
|
|
|
transform: translateY(50px);
|
|
|
|
opacity: 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.p-safe-area {
|
|
|
|
padding-top: env(safe-area-inset-top);
|
|
|
|
padding-right: env(safe-area-inset-right);
|
|
|
|
padding-bottom: env(safe-area-inset-bottom);
|
|
|
|
padding-left: env(safe-area-inset-left);
|
|
|
|
}
|
|
|
|
|
2022-11-27 17:02:09 +09:00
|
|
|
.scrollbar-hide {
|
|
|
|
scrollbar-width: none;
|
|
|
|
}
|
2022-11-30 05:37:45 +09:00
|
|
|
|
2022-11-27 17:02:09 +09:00
|
|
|
.scrollbar-hide::-webkit-scrollbar {
|
|
|
|
display: none;
|
|
|
|
}
|
|
|
|
</style>
|