refactor: modal dialog (#277)

This commit is contained in:
Ayaka Rizumu 2022-12-02 15:02:44 +08:00 committed by GitHub
parent 585b9e0229
commit feb8872f5f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 271 additions and 192 deletions

View file

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

View file

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

View file

@ -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">
<div <!-- Dialog component -->
v-if="isVisible" <Transition name="dialog-visible">
class="scrollbar-hide"
fixed top-0 bottom-0 left-0 right-0 z-10 overscroll-none overflow-y-scroll
:class="modelValue ? '' : 'pointer-events-none'"
>
<!-- 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. -->
<div <div
bg-base bottom-0 left-0 right-0 top-0 absolute transition-opacity duration-500 ease-out v-if="isVIf"
h="[calc(100%+0.5px)]" v-show="isVShow"
:class="isOut ? 'opacity-0' : 'opacity-85'" ref="elDialogRoot"
@click="close" :style="{
/> 'z-index': zIndex,
<div }"
ref="target" class="scrollbar-hide" fixed inset-0 overflow-y-auto overscroll-none
bg-base border-base absolute transition-all duration-200 ease-out transform
:class="`${positionClass} ${transformClass}`"
@transitionend="onTransitionEnd"
> >
<slot /> <!-- 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>
</div> </div>
<button </Transition>
v-if="closeButton"
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>
</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;
} }

View file

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

View file

@ -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,71 +202,73 @@ const { isOverDropZone } = useDropZone(dropZoneRef, onDrop)
@remove="removeAttachment(idx)" @remove="removeAttachment(idx)"
/> />
</div> </div>
</div>
<div </div>
v-if="shouldExpanded" flex="~ gap-2" m="l--1" pt-2 <div flex gap-4>
border="t base" <div w-12 h-full sm:block hidden />
> <div
<CommonTooltip placement="bottom" :content="$t('tooltip.add_media')"> v-if="shouldExpanded" flex="~ gap-2 1" m="l--1" pt-2 justify="between" max-full
<button btn-action-icon :aria-label="$t('tooltip.add_media')" @click="pickAttachments"> border="t base"
<div i-ri:image-add-line /> >
</button> <CommonTooltip placement="bottom" :content="$t('tooltip.add_media')">
</CommonTooltip> <button btn-action-icon :aria-label="$t('tooltip.add_media')" @click="pickAttachments">
<div i-ri:image-add-line />
<template v-if="editor">
<CommonTooltip placement="bottom" :content="$t('tooltip.toggle_code_block')">
<button
btn-action-icon
:aria-label="$t('tooltip.toggle_code_block')"
:class="editor.isActive('codeBlock') ? 'op100' : 'op50'"
@click="editor?.chain().focus().toggleCodeBlock().run()"
>
<div i-ri:code-s-slash-line />
</button>
</CommonTooltip>
</template>
<div flex-auto />
<CommonTooltip placement="bottom" :content="$t('tooltip.add_content_warning')">
<button btn-action-icon :aria-label="$t('tooltip.add_content_warning')" @click="toggleSensitive">
<div v-if="draft.params.sensitive" i-ri:alarm-warning-fill text-orange />
<div v-else i-ri:alarm-warning-line />
</button>
</CommonTooltip>
<CommonTooltip placement="bottom" :content="$t('tooltip.change_content_visibility')">
<CommonDropdown>
<button :aria-label="$t('tooltip.change_content_visibility')" btn-action-icon w-12>
<div :class="currentVisibility.icon" />
<div i-ri:arrow-down-s-line text-sm text-secondary mr--1 />
</button>
<template #popper>
<CommonDropdownItem
v-for="visibility in STATUS_VISIBILITIES"
:key="visibility.value"
:icon="visibility.icon"
:checked="visibility.value === draft.params.visibility"
@click="chooseVisibility(visibility.value)"
>
{{ $t(`visibility.${visibility.value}`) }}
<template #description>
{{ $t(`visibility.${visibility.value}_desc`) }}
</template>
</CommonDropdownItem>
</template>
</CommonDropdown>
</CommonTooltip>
<button
btn-solid rounded-full text-sm
:disabled="isEmpty || isUploading || (draft.attachments.length === 0 && !draft.params.status)"
@click="publish"
>
{{ !draft.editingStatus ? $t('action.publish') : $t('action.save_changes') }}
</button> </button>
</div> </CommonTooltip>
<template v-if="editor">
<CommonTooltip placement="bottom" :content="$t('tooltip.toggle_code_block')">
<button
btn-action-icon
:aria-label="$t('tooltip.toggle_code_block')"
:class="editor.isActive('codeBlock') ? 'op100' : 'op50'"
@click="editor?.chain().focus().toggleCodeBlock().run()"
>
<div i-ri:code-s-slash-line />
</button>
</CommonTooltip>
</template>
<div flex-auto />
<CommonTooltip placement="bottom" :content="$t('tooltip.add_content_warning')">
<button btn-action-icon :aria-label="$t('tooltip.add_content_warning')" @click="toggleSensitive">
<div v-if="draft.params.sensitive" i-ri:alarm-warning-fill text-orange />
<div v-else i-ri:alarm-warning-line />
</button>
</CommonTooltip>
<CommonTooltip placement="bottom" :content="$t('tooltip.change_content_visibility')">
<CommonDropdown>
<button :aria-label="$t('tooltip.change_content_visibility')" btn-action-icon w-12>
<div :class="currentVisibility.icon" />
<div i-ri:arrow-down-s-line text-sm text-secondary mr--1 />
</button>
<template #popper>
<CommonDropdownItem
v-for="visibility in STATUS_VISIBILITIES"
:key="visibility.value"
:icon="visibility.icon"
:checked="visibility.value === draft.params.visibility"
@click="chooseVisibility(visibility.value)"
>
{{ $t(`visibility.${visibility.value}`) }}
<template #description>
{{ $t(`visibility.${visibility.value}_desc`) }}
</template>
</CommonDropdownItem>
</template>
</CommonDropdown>
</CommonTooltip>
<button
btn-solid rounded-full text-sm
:disabled="isEmpty || isUploading || (draft.attachments.length === 0 && !draft.params.status)"
@click="publish"
>
{{ !draft.editingStatus ? $t('action.publish') : $t('action.save_changes') }}
</button>
</div> </div>
</div> </div>
</div> </div>

View file

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