Skip to content

Commit

Permalink
feat: add overlayBehavior prop to VueFinalModal #405
Browse files Browse the repository at this point in the history
  • Loading branch information
hunterliu1003 committed Dec 1, 2023
1 parent 98a1e6f commit dafa559
Show file tree
Hide file tree
Showing 15 changed files with 644 additions and 334 deletions.
12 changes: 1 addition & 11 deletions packages/vue-final-modal/src/Modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,20 +64,10 @@ export type Vfm = {
closeAll: () => Promise<PromiseSettledResult<string>[]>
}

export type InternalVfm = {
openLastOverlay: () => Promise<void>
moveToLastOpenedModals: (modal: ComputedRef<Modal>) => void
deleteFromOpenedModals: (modal: ComputedRef<Modal>) => void
moveToLastOpenedModalOverlays: (modal: ComputedRef<Modal>) => void
deleteFromOpenedModalOverlays: (modal: ComputedRef<Modal>) => void
deleteFromModals: (modal: ComputedRef<Modal>) => void
resolvedClosed: (index: number) => void
resolvedOpened: (index: number) => void
}

export type Modal = {
modalId?: ModalId
hideOverlay: Ref<boolean | undefined> | undefined
overlayBehavior: Ref<'auto' | 'persist'>
overlayVisible: Ref<boolean>
toggle: (show?: boolean) => Promise<string>
}
13 changes: 11 additions & 2 deletions packages/vue-final-modal/src/components/ModalsContainer.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
<script setup lang="ts">
import { computed, onBeforeUnmount } from 'vue'
import { isString } from '~/utils'
import { useInternalVfm, useVfm } from '~/useApi'
import { useVfm } from '~/useApi'
const { modalsContainers, dynamicModals } = useVfm()
const { resolvedClosed, resolvedOpened } = useInternalVfm()
const uid = Symbol('ModalsContainer')
const shouldMount = computed(() => uid === modalsContainers.value?.[0])
Expand All @@ -13,6 +12,16 @@ modalsContainers.value.push(uid)
onBeforeUnmount(() => {
modalsContainers.value = modalsContainers.value.filter(i => i !== uid)
})
function resolvedClosed(index: number) {
dynamicModals[index]?.resolveClosed?.()
if (!dynamicModals[index]?.keepAlive)
dynamicModals.splice(index, 1)
}
function resolvedOpened(index: number) {
dynamicModals[index]?.resolveOpened?.()
}
</script>

<template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import { useFocusTrap } from './useFocusTrap'
import { useLockScroll } from './useBodyScrollLock'
import { useZIndex } from './useZIndex'
import { vVisible } from './vVisible'
import { noop, once } from '~/utils'
import { arrayMoveItemToLast, arrayRemoveItem, noop, once } from '~/utils'
import { type Modal } from '~/Modal'
import { useSwipeToClose } from '~/useSwipeToClose'
import { useInternalVfm, useVfm } from '~/useApi'
import { useVfm } from '~/useApi'
export interface VueFinalModalEmits {
(e: 'update:modelValue', modelValue: boolean): void
Expand All @@ -26,30 +26,17 @@ export interface VueFinalModalEmits {
}
const props = defineProps(vueFinalModalProps)
const emit = defineEmits<VueFinalModalEmits>()
const attrs = useAttrs()
defineOptions({
inheritAttrs: false,
})
defineOptions({ inheritAttrs: false })
defineSlots<{
'default'(): void
'swipe-banner'(): void
}>()
const { modals, openedModals } = useVfm()
const {
openLastOverlay,
moveToLastOpenedModals,
deleteFromOpenedModals,
moveToLastOpenedModalOverlays,
deleteFromOpenedModalOverlays,
deleteFromModals,
} = useInternalVfm()
const { modals, openedModals, openedModalOverlays } = useVfm()
const vfmRootEl = ref<HTMLDivElement>()
const vfmContentEl = ref<HTMLDivElement>()
Expand Down Expand Up @@ -90,7 +77,7 @@ const {
resolveToggle('opened')
},
onLeave() {
deleteFromOpenedModals(getModalInstance())
arrayRemoveItem(openedModals, modalInstance)
resetZIndex()
enableBodyScroll()
emit('closed')
Expand All @@ -107,35 +94,32 @@ const {
} = useSwipeToClose(props, { vfmContentEl, modelValueLocal })
const hideOverlay = toRef(props, 'hideOverlay')
const overlayBehavior = toRef(props, 'overlayBehavior')
const modalInstance = computed<Modal>(() => ({
modalId: props.modalId,
hideOverlay,
overlayBehavior,
overlayVisible,
focus,
toggle(show?: boolean): Promise<string> {
return new Promise((resolve) => {
resolveToggle = once((res: string) => resolve(res))
const value = typeof show === 'boolean' ? show : !modelValueLocal.value
modelValueLocal.value = value
emit('update:modelValue', value)
})
},
}))
function getModalInstance() {
return modalInstance
}
const index = computed(() => openedModals.indexOf(modalInstance))
watch([() => props.zIndexFn, index], () => {
if (visible.value)
refreshZIndex(index.value)
if (!visible.value)
return
refreshZIndex(index.value)
})
onMounted(() => {
modals.push(modalInstance)
arrayMoveItemToLast(modals, modalInstance)
})
if (props.modelValue)
Expand All @@ -146,9 +130,8 @@ function open(): boolean {
emit('beforeOpen', { stop: () => shouldStop = true })
if (shouldStop)
return false
moveToLastOpenedModals(modalInstance)
moveToLastOpenedModalOverlays(modalInstance)
refreshZIndex(index.value)
arrayMoveItemToLast(openedModals, modalInstance)
arrayMoveItemToLast(openedModalOverlays, modalInstance)
openLastOverlay()
enterTransition()
return true
Expand All @@ -159,7 +142,7 @@ function close(): boolean {
emit('beforeClose', { stop: () => shouldStop = true })
if (shouldStop)
return false
deleteFromOpenedModalOverlays(getModalInstance())
arrayRemoveItem(openedModalOverlays, modalInstance)
openLastOverlay()
blur()
leaveTransition()
Expand All @@ -168,12 +151,21 @@ function close(): boolean {
onBeforeUnmount(() => {
enableBodyScroll()
deleteFromModals(modalInstance)
deleteFromOpenedModals(modalInstance)
deleteFromOpenedModalOverlays(modalInstance)
arrayRemoveItem(modals, modalInstance)
arrayRemoveItem(openedModals, modalInstance)
blur()
openLastOverlay()
})
async function openLastOverlay() {
await nextTick()
// Found the modals which has overlay and has `auto` overlayBehavior
const openedModalsOverlaysAuto = openedModalOverlays.filter(modal => modal.value.overlayBehavior.value === 'auto' && !modal.value.hideOverlay?.value)
// Only keep the last overlay open
openedModalsOverlaysAuto.forEach((modal, index) => {
modal.value.overlayVisible.value = index === openedModalsOverlaysAuto.length - 1
})
}
</script>

<template>
Expand All @@ -193,7 +185,7 @@ onBeforeUnmount(() => {
@mouseup.self="() => onMouseupRoot()"
@mousedown.self="e => onMousedown(e)"
>
<Transition v-if="!hideOverlay" v-bind="overlayTransition" :appear="true" v-on="overlayListeners">
<Transition v-if="!hideOverlay" v-bind="(overlayTransition as object)" v-on="overlayListeners">
<div
v-if="displayDirective !== 'if' || overlayVisible"
v-show="displayDirective !== 'show' || overlayVisible"
Expand All @@ -204,7 +196,7 @@ onBeforeUnmount(() => {
aria-hidden="true"
/>
</Transition>
<Transition v-bind="contentTransition" :appear="true" v-on="contentListeners">
<Transition v-bind="(contentTransition as object)" v-on="contentListeners">
<div
v-if="displayDirective !== 'if' || contentVisible"
v-show="displayDirective !== 'show' || contentVisible"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ export const vueFinalModalProps = {
type: Boolean as PropType<boolean>,
default: undefined,
},
/**
* @description Customize the overlay behavior.
*/
overlayBehavior: {
type: String as PropType<'auto' | 'persist'>,
default: 'auto',
validator: (prop: any) => ['auto', 'persist'].includes(prop),
},
/**
* @description Customize the overlay transition.
* @default `undefined`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,14 @@ export function useTransition(

const contentTransition = computed<TransitionProps>(() => {
if (typeof props.contentTransition === 'string')
return { name: props.contentTransition }
return { ...props.contentTransition }
return { name: props.contentTransition, appear: true }
return { appear: true, ...props.contentTransition }
})

const overlayTransition = computed<TransitionProps>(() => {
if (typeof props.overlayTransition === 'string')
return { name: props.overlayTransition }
return { ...props.overlayTransition }
return { name: props.overlayTransition, appear: true }
return { appear: true, ...props.overlayTransition }
})

const isReadyToBeDestroyed = computed(() =>
Expand Down
3 changes: 1 addition & 2 deletions packages/vue-final-modal/src/injectionSymbols.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { InjectionKey } from 'vue'
import type { InternalVfm, Vfm } from './Modal'
import type { Vfm } from './Modal'

export const vfmSymbol = Symbol(__DEV__ ? 'vfm' : '') as InjectionKey<Vfm>
export const internalVfmSymbol = Symbol(__DEV__ ? 'internalVfm' : '') as InjectionKey<InternalVfm>
59 changes: 3 additions & 56 deletions packages/vue-final-modal/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { App, ComputedRef } from 'vue'
import { getCurrentInstance, inject, markRaw, nextTick, ref, shallowReactive } from 'vue'
import { internalVfmSymbol, vfmSymbol } from './injectionSymbols'
import type { InternalVfm, Modal, ModalId, UseModalOptions, UseModalOptionsPrivate, Vfm } from './Modal'
import { getCurrentInstance, inject, markRaw, ref, shallowReactive } from 'vue'
import { vfmSymbol } from './injectionSymbols'
import type { Modal, ModalId, UseModalOptions, UseModalOptionsPrivate, Vfm } from './Modal'
import { noop } from './utils'

// eslint-disable-next-line import/no-mutable-exports
Expand Down Expand Up @@ -38,9 +38,6 @@ export function createVfm() {
install(app: App) {
app.provide(vfmSymbol, vfm)
app.config.globalProperties.$vfm = vfm

const internalVfm = createInternalVfm(vfm)
app.provide(internalVfmSymbol, internalVfm)
},
modals,
openedModals,
Expand Down Expand Up @@ -69,53 +66,3 @@ export function createVfm() {

return vfm
}

function createInternalVfm(vfm: Vfm) {
const { modals, openedModals, openedModalOverlays, dynamicModals } = vfm

const internalVfm: InternalVfm = {
deleteFromModals(modal: ComputedRef<Modal>) {
const index = modals.findIndex(_modal => _modal.value === modal.value)
if (index !== -1)
modals.splice(index, 1)
},
moveToLastOpenedModals(modal: ComputedRef<Modal>) {
internalVfm.deleteFromOpenedModals(modal)
openedModals.push(modal)
},
deleteFromOpenedModals(modal: ComputedRef<Modal>) {
const index = openedModals.findIndex(_modal => _modal.value === modal.value)
if (index !== -1)
openedModals.splice(index, 1)
},
moveToLastOpenedModalOverlays(modal: ComputedRef<Modal>) {
internalVfm.deleteFromOpenedModalOverlays(modal)
openedModalOverlays.push(modal)
},
deleteFromOpenedModalOverlays(modal: ComputedRef<Modal>) {
const index = openedModalOverlays.findIndex(_modal => _modal.value === modal.value)
if (index !== -1)
openedModalOverlays.splice(index, 1)
},
async openLastOverlay() {
await nextTick()
// Close all overlay first
openedModalOverlays.forEach(modal => modal.value.overlayVisible.value = false)
// Open the last overlay if it has overlay
if (openedModalOverlays.length > 0) {
const modal = openedModalOverlays[openedModalOverlays.length - 1]
!modal.value.hideOverlay?.value && (modal.value.overlayVisible.value = true)
}
},
resolvedClosed(index: number) {
dynamicModals[index]?.resolveClosed?.()
if (!dynamicModals[index]?.keepAlive)
dynamicModals.splice(index, 1)
},
resolvedOpened(index: number) {
dynamicModals[index]?.resolveOpened?.()
},
}

return internalVfm
}
22 changes: 3 additions & 19 deletions packages/vue-final-modal/src/useApi.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { computed, inject, markRaw, nextTick, reactive, useAttrs } from 'vue'
import { computed, markRaw, nextTick, reactive, useAttrs } from 'vue'
import { tryOnUnmounted } from '@vueuse/core'
import VueFinalModal from './components/VueFinalModal/VueFinalModal.vue'
import type { ComponentProps, ComponentType, InternalVfm, ModalSlot, ModalSlotOptions, UseModalOptions, UseModalOptionsPrivate, UseModalReturnType, Vfm } from './Modal'
import type { ComponentProps, ComponentType, ModalSlot, ModalSlotOptions, UseModalOptions, UseModalOptionsPrivate, UseModalReturnType, Vfm } from './Modal'
import { activeVfm, getActiveVfm } from './plugin'
import { internalVfmSymbol } from './injectionSymbols'
import { isString, noop, noopPromise } from '~/utils'
import { isString } from '~/utils'

/**
* Returns the vfm instance. Equivalent to using `$vfm` inside
Expand All @@ -24,21 +23,6 @@ export function useVfm(): Vfm {
return vfm!
}

export const defaultInternalVfm: InternalVfm = {
openLastOverlay: noopPromise,
moveToLastOpenedModals: noop,
deleteFromOpenedModals: noop,
moveToLastOpenedModalOverlays: noop,
deleteFromOpenedModalOverlays: noop,
deleteFromModals: noop,
resolvedClosed: noop,
resolvedOpened: noop,
}

export function useInternalVfm() {
return inject(internalVfmSymbol, defaultInternalVfm)
}

function withMarkRaw<T extends ComponentType>(options: Partial<UseModalOptions<T>>, DefaultComponent: ComponentType = VueFinalModal) {
const { component, slots: innerSlots, ...rest } = options

Expand Down
18 changes: 17 additions & 1 deletion packages/vue-final-modal/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,26 @@ export const once
}

export const noop = () => {}
export const noopPromise = async () => {}

export function clamp(val: number, min: number, max: number) {
return val > max ? max : val < min ? min : val
}

export const isString = (value: unknown): value is string => typeof value === 'string'

/**
* @example
* const arr = [1, 2, 6, 3, 4, 5]
* arrayMoveItemToLast(arr, 6)
* console.log(arr) // [1, 2, 3, 4, 5, 6]
*/
export function arrayMoveItemToLast<T>(arr: T[], item: T) {
const removedItem = arrayRemoveItem(arr, item)?.[0] || item
arr.push(removedItem)
}

export function arrayRemoveItem<T>(arr: T[], item: T) {
const index = arr.indexOf(item)
if (index !== -1)
return arr.splice(index, 1)
}
Loading

0 comments on commit dafa559

Please sign in to comment.