refactor: modal dialog (#277)
parent
585b9e0229
commit
feb8872f5f
|
@ -39,7 +39,7 @@ const teams: Team[] = [
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div p8 flex="~ col gap-4" relative max-h-screen of-auto>
|
<div my-8 px-3 sm-px-8 flex="~ col gap-4" relative max-h-screen>
|
||||||
<button btn-action-icon absolute top-0 right-0 m1 aria-label="Close" @click="emit('close')">
|
<button btn-action-icon absolute top-0 right-0 m1 aria-label="Close" @click="emit('close')">
|
||||||
<div i-ri:close-fill />
|
<div i-ri:close-fill />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -9,16 +9,16 @@ import {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModalDialog v-model="isSigninDialogOpen">
|
<ModalDialog v-model="isSigninDialogOpen" py-6 px-3 sm-px-6>
|
||||||
<UserSignIn m6 />
|
<UserSignIn />
|
||||||
</ModalDialog>
|
</ModalDialog>
|
||||||
<ModalDialog v-model="isPreviewHelpOpen" :type="isSmallScreen ? 'bottom' : 'dialog'">
|
<ModalDialog v-model="isPreviewHelpOpen">
|
||||||
<HelpPreview @close="closePreviewHelp()" />
|
<HelpPreview @close="closePreviewHelp()" />
|
||||||
</ModalDialog>
|
</ModalDialog>
|
||||||
<ModalDialog v-model="isPublishDialogOpen">
|
<ModalDialog v-model="isPublishDialogOpen" max-w-180>
|
||||||
<PublishWidget :draft-key="dialogDraftKey" expanded min-w-180 />
|
<PublishWidget :draft-key="dialogDraftKey" expanded />
|
||||||
</ModalDialog>
|
</ModalDialog>
|
||||||
<ModalDialog v-model="isMediaPreviewOpen" close-button>
|
<ModalDialog v-model="isMediaPreviewOpen" w-full max-w-full h-full max-h-full bg-transparent border-0 pointer-events-none>
|
||||||
<ModalMediaPreview v-if="isMediaPreviewOpen" @close="closeMediaPreview()" />
|
<ModalMediaPreview v-if="isMediaPreviewOpen" @close="closeMediaPreview()" />
|
||||||
</ModalDialog>
|
</ModalDialog>
|
||||||
<ModalDialog v-model="isEditHistoryDialogOpen">
|
<ModalDialog v-model="isEditHistoryDialogOpen">
|
||||||
|
|
|
@ -1,148 +1,216 @@
|
||||||
<script setup lang='ts'>
|
<!-- 对话框组件 -->
|
||||||
|
<script lang="ts" setup>
|
||||||
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
|
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
|
||||||
|
import { useDeactivated } from '~/composables/lifecycle'
|
||||||
|
|
||||||
type DialogType = 'top' | 'right' | 'bottom' | 'left' | 'dialog'
|
export interface Props {
|
||||||
|
/** v-model dislog visibility */
|
||||||
const {
|
|
||||||
type = 'dialog',
|
|
||||||
closeButton = false,
|
|
||||||
} = defineProps<{
|
|
||||||
type?: DialogType
|
|
||||||
closeButton?: boolean
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const { modelValue } = defineModel<{
|
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
closeButton?: boolean
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emits = defineEmits<{
|
||||||
|
/** v-model dislog visibility */
|
||||||
|
(event: 'update:modelValue', value: boolean): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
let isVisible = $ref(modelValue.value)
|
const visible = useVModel(props, 'modelValue', emits, { passive: true })
|
||||||
let isOut = $ref(!modelValue.value)
|
|
||||||
|
|
||||||
const positionClass = computed(() => {
|
const deactivated = useDeactivated()
|
||||||
switch (type) {
|
const route = useRoute()
|
||||||
case 'dialog':
|
|
||||||
return 'border rounded top-1/2 -translate-y-1/2 left-1/2 -translate-x-1/2'
|
/** scrollable HTML element */
|
||||||
case 'bottom':
|
const elDialogScroll = ref<HTMLDivElement>()
|
||||||
return 'bottom-0 left-0 right-0 border-t'
|
const elDialogMain = ref<HTMLDivElement>()
|
||||||
case 'top':
|
const elDialogRoot = ref<HTMLDivElement>()
|
||||||
return 'top-0 left-0 right-0 border-b'
|
|
||||||
case 'left':
|
defineExpose({
|
||||||
return 'bottom-0 left-0 top-0 border-r'
|
elDialogRoot,
|
||||||
case 'right':
|
elDialogMain,
|
||||||
return 'bottom-0 top-0 right-0 border-l'
|
elDialogScroll,
|
||||||
default:
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const transformClass = computed(() => {
|
/** close the dialog */
|
||||||
if (isOut) {
|
|
||||||
switch (type) {
|
|
||||||
case 'dialog':
|
|
||||||
return 'op0'
|
|
||||||
case 'bottom':
|
|
||||||
return 'translate-y-[100%]'
|
|
||||||
case 'top':
|
|
||||||
return 'translate-y-[100%]'
|
|
||||||
case 'left':
|
|
||||||
return 'translate-x-[-100%]'
|
|
||||||
case 'right':
|
|
||||||
return 'translate-x-[100%]'
|
|
||||||
default:
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const target = ref<HTMLElement | null>(null)
|
|
||||||
const { activate, deactivate } = useFocusTrap(target)
|
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
modelValue.value = false
|
visible.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
watchEffect(() => {
|
function clickMask() {
|
||||||
if (modelValue)
|
if (props.closeByMask)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
activate()
|
activate()
|
||||||
else
|
else
|
||||||
deactivate()
|
deactivate()
|
||||||
})
|
})
|
||||||
|
|
||||||
useEventListener('keydown', (e: KeyboardEvent) => {
|
useEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
if (!modelValue.value)
|
if (!visible.value)
|
||||||
return
|
return
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
close()
|
close()
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
let unsubscribe: () => void
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
watch(modelValue, async (v) => {
|
inheritAttrs: false,
|
||||||
if (v) {
|
|
||||||
isOut = true
|
|
||||||
isVisible = true
|
|
||||||
setTimeout(() => {
|
|
||||||
isOut = false
|
|
||||||
}, 10)
|
|
||||||
|
|
||||||
unsubscribe = useRouter().beforeEach(() => {
|
|
||||||
unsubscribe()
|
|
||||||
close()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
unsubscribe?.()
|
|
||||||
isOut = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function onTransitionEnd() {
|
|
||||||
if (!modelValue.value)
|
|
||||||
isVisible = false
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SafeTeleport to="#teleport-end">
|
<SafeTeleport to="#teleport-end">
|
||||||
|
<!-- Dialog component -->
|
||||||
|
<Transition name="dialog-visible">
|
||||||
<div
|
<div
|
||||||
v-if="isVisible"
|
v-if="isVIf"
|
||||||
class="scrollbar-hide"
|
v-show="isVShow"
|
||||||
fixed top-0 bottom-0 left-0 right-0 z-10 overscroll-none overflow-y-scroll
|
ref="elDialogRoot"
|
||||||
:class="modelValue ? '' : 'pointer-events-none'"
|
:style="{
|
||||||
|
'z-index': zIndex,
|
||||||
|
}"
|
||||||
|
class="scrollbar-hide" fixed inset-0 overflow-y-auto overscroll-none
|
||||||
>
|
>
|
||||||
<!-- The style `scrollbar-hide overscroll-none overflow-y-scroll` and `h="[calc(100%+0.5px)]"` is used to implement scroll locking, -->
|
<!-- 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. -->
|
<!-- 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
|
<div
|
||||||
bg-base bottom-0 left-0 right-0 top-0 absolute transition-opacity duration-500 ease-out
|
ref="elDialogMain"
|
||||||
h="[calc(100%+0.5px)]"
|
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"
|
||||||
:class="isOut ? 'opacity-0' : 'opacity-85'"
|
v-bind="bindTypeToAny($attrs)"
|
||||||
@click="close"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
ref="target"
|
|
||||||
bg-base border-base absolute transition-all duration-200 ease-out transform
|
|
||||||
:class="`${positionClass} ${transformClass}`"
|
|
||||||
@transitionend="onTransitionEnd"
|
|
||||||
>
|
>
|
||||||
|
<!-- 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 />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
<button
|
<!-- footer -->
|
||||||
v-if="closeButton"
|
<slot name="footer" />
|
||||||
btn-action-icon bg="black/20" aria-label="Close"
|
|
||||||
hover:bg="black/40" dark:bg="white/10" dark:hover:bg="white/20"
|
|
||||||
absolute top-0 right-0 m1
|
|
||||||
@click="close"
|
|
||||||
>
|
|
||||||
<div i-ri:close-fill text-white />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
</SafeTeleport>
|
</SafeTeleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style socped>
|
<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);
|
||||||
|
}
|
||||||
|
|
||||||
.scrollbar-hide {
|
.scrollbar-hide {
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,16 +29,25 @@ function onClick(e: MouseEvent) {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div relative h-screen w-screen flex select-none @click="onClick">
|
<div relative h-full w-full flex select-none pointer-events-none>
|
||||||
<div absolute top-0 left-0 right-0 text-center>
|
<div absolute top-0 left-0 right-0 text-center>
|
||||||
{{ mediaPreviewIndex + 1 }} / {{ mediaPreviewList.length }}
|
{{ mediaPreviewIndex + 1 }} / {{ mediaPreviewList.length }}
|
||||||
</div>
|
</div>
|
||||||
<button v-if="hasNext" btn-action-icon absolute top="1/2" right-1 title="Next" @click="next">
|
<button v-if="hasNext" btn-action-icon absolute pointer-events-auto top="1/2" right-1 title="Next" @click="next">
|
||||||
<div i-ri:arrow-right-s-line />
|
<div i-ri:arrow-right-s-line />
|
||||||
</button>
|
</button>
|
||||||
<button v-if="hasPrev" btn-action-icon absolute top="1/2" left-1 title="Next" @click="prev">
|
<button v-if="hasPrev" btn-action-icon absolute pointer-events-auto top="1/2" left-1 title="Next" @click="prev">
|
||||||
<div i-ri:arrow-left-s-line />
|
<div i-ri:arrow-left-s-line />
|
||||||
</button>
|
</button>
|
||||||
<img :src="current.url || current.previewUrl" :alt="current.description || ''" max-w-95vw max-h-95vh ma>
|
<img :src="current.url || current.previewUrl" :alt="current.description || ''" w="max-[95%]" h="max-[95%]" ma>
|
||||||
|
|
||||||
|
<button
|
||||||
|
btn-action-icon bg="black/20" aria-label="Close"
|
||||||
|
hover:bg="black/40" dark:bg="white/30" dark:hover:bg="white/20"
|
||||||
|
absolute top-0 right-0 m1 pointer-events-auto
|
||||||
|
@click="emit('close')"
|
||||||
|
>
|
||||||
|
<div i-ri:close-fill text-white />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -148,20 +148,20 @@ const { isOverDropZone } = useDropZone(dropZoneRef, onDrop)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="currentUser" flex="~ col gap-1">
|
<div v-if="currentUser" flex="~ col gap-4" py4 px2 sm:px4>
|
||||||
<template v-if="draft.editingStatus">
|
<template v-if="draft.editingStatus">
|
||||||
<div flex="~ col gap-1">
|
<div flex="~ col gap-1">
|
||||||
<div text-secondary self-center>
|
<div text-secondary self-center>
|
||||||
{{ $t('state.editing') }}
|
{{ $t('state.editing') }}
|
||||||
</div>
|
</div>
|
||||||
<StatusCard :status="draft.editingStatus" :actions="false" :hover="false" />
|
<StatusCard :status="draft.editingStatus" :actions="false" :hover="false" px-0 />
|
||||||
</div>
|
</div>
|
||||||
<div border="b dashed gray/40" />
|
<div border="b dashed gray/40" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div p4 flex gap-4>
|
<div flex gap-4>
|
||||||
<NuxtLink w-12 h-12 :to="getAccountRoute(currentUser.account)">
|
<NuxtLink w-12 h-12 :to="getAccountRoute(currentUser.account)">
|
||||||
<AccountAvatar :account="currentUser.account" w-12 h-12 />
|
<AccountAvatar :account="currentUser.account" f-full h-full />
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<div
|
<div
|
||||||
ref="dropZoneRef"
|
ref="dropZoneRef"
|
||||||
|
@ -202,9 +202,12 @@ const { isOverDropZone } = useDropZone(dropZoneRef, onDrop)
|
||||||
@remove="removeAttachment(idx)"
|
@remove="removeAttachment(idx)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div flex gap-4>
|
||||||
|
<div w-12 h-full sm:block hidden />
|
||||||
<div
|
<div
|
||||||
v-if="shouldExpanded" flex="~ gap-2" m="l--1" pt-2
|
v-if="shouldExpanded" flex="~ gap-2 1" m="l--1" pt-2 justify="between" max-full
|
||||||
border="t base"
|
border="t base"
|
||||||
>
|
>
|
||||||
<CommonTooltip placement="bottom" :content="$t('tooltip.add_media')">
|
<CommonTooltip placement="bottom" :content="$t('tooltip.add_media')">
|
||||||
|
@ -269,5 +272,4 @@ const { isOverDropZone } = useDropZone(dropZoneRef, onDrop)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -21,7 +21,7 @@ onMounted(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<form text-center justify-center items-center w-150 py6 flex="~ col gap-3" @submit.prevent="oauth">
|
<form text-center justify-center items-center max-w-150 py6 flex="~ col gap-3" @submit.prevent="oauth">
|
||||||
<div flex="~ center" mb2>
|
<div flex="~ center" mb2>
|
||||||
<img src="/logo.svg" w-12 h-12 mxa alt="logo">
|
<img src="/logo.svg" w-12 h-12 mxa alt="logo">
|
||||||
<div text-3xl>
|
<div text-3xl>
|
||||||
|
@ -31,7 +31,7 @@ onMounted(() => {
|
||||||
<div>{{ $t('user.server_address_label') }}</div>
|
<div>{{ $t('user.server_address_label') }}</div>
|
||||||
<div flex bg-gray:10 px4 py2 mxa rounded border="~ base" items-center font-mono focus:outline-none focus:ring="2 primary inset">
|
<div flex bg-gray:10 px4 py2 mxa rounded border="~ base" items-center font-mono focus:outline-none focus:ring="2 primary inset">
|
||||||
<span text-secondary-light mr1>https://</span>
|
<span text-secondary-light mr1>https://</span>
|
||||||
<input ref="input" v-model="server" outline-none bg-transparent @input="handleInput">
|
<input ref="input" v-model="server" outline-none bg-transparent w-full max-w-50 @input="handleInput">
|
||||||
</div>
|
</div>
|
||||||
<div text-secondary text-sm flex>
|
<div text-secondary text-sm flex>
|
||||||
<div i-ri:lightbulb-line mr-1 />
|
<div i-ri:lightbulb-line mr-1 />
|
||||||
|
|
Loading…
Reference in New Issue