mirror of https://github.com/nocodb/nocodb
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
124 lines
3.6 KiB
124 lines
3.6 KiB
import type { AppContext, VNode } from '@vue/runtime-dom' |
|
import { Suspense, isVNode, render } from '@vue/runtime-dom' |
|
import type { ComponentPublicInstance } from '@vue/runtime-core' |
|
import type { MaybeRef } from '@vueuse/core' |
|
import { isClient } from '@vueuse/core' |
|
|
|
interface UseDialogOptions { |
|
target: MaybeRef<HTMLElement | ComponentPublicInstance> |
|
context: Partial<AppContext> |
|
} |
|
|
|
/** |
|
* Programmatically create a component and attach it to the body (or a specific mount target), like a dialog or modal. |
|
* This composable is not SSR friendly - it should be used only on the client. |
|
* |
|
* @param componentOrVNode The component to create and attach. Can be a VNode or a component definition. |
|
* @param props The props to pass to the component. |
|
* @param options Additional options to use {@see UseDialogOptions} |
|
* |
|
* @example |
|
* |
|
* import DlgQuickImport from '~/components/dlg/QuickImport.vue' |
|
* |
|
* function openQuickImportDialog(type: string) { |
|
* // create a ref for showing/hiding the modal |
|
* const isOpen = ref(true) |
|
* |
|
* const { close, vNode } = useDialog(DlgQuickImport, { |
|
* 'modelValue': isOpen, |
|
* 'importType': type, |
|
* 'onUpdate:modelValue': closeDialog, |
|
* }) |
|
* |
|
* function closeDialog() { |
|
* // hide the modal |
|
* isOpen.value = false |
|
* |
|
* // debounce destroying the component, so the modal transition can finish |
|
* close(1000) |
|
* } |
|
* } |
|
*/ |
|
export function useDialog( |
|
componentOrVNode: any, |
|
props: NonNullable<Parameters<typeof h>[1]> = {}, |
|
{ target, context }: Partial<UseDialogOptions> = {}, |
|
) { |
|
if (typeof document === 'undefined' || !isClient) { |
|
console.warn('[useDialog]: Cannot use outside of browser!') |
|
} |
|
|
|
const closeHook = createEventHook<void>() |
|
const mountedHook = createEventHook<void>() |
|
|
|
const isMounted = ref(false) |
|
|
|
const domNode = document.createElement('div') |
|
|
|
const vNodeRef = ref<VNode>() |
|
|
|
const mountTarget = ref<HTMLElement>() |
|
|
|
/** When props change, we want to re-render the element with the new prop values */ |
|
const stop = watch( |
|
toReactive(props), |
|
(reactiveProps) => { |
|
const _mountTarget = unref(target) |
|
|
|
/** |
|
* If it's a component instance, use the instance's root element (`$el`), otherwise use the element itself |
|
* If no target is specified, use the document body |
|
*/ |
|
mountTarget.value = _mountTarget |
|
? '$el' in _mountTarget |
|
? (_mountTarget.$el as HTMLElement) |
|
: _mountTarget |
|
: document.body |
|
|
|
/** if specified, append vnode to mount target instead of document.body */ |
|
mountTarget.value.appendChild(domNode) |
|
|
|
// if it's a vnode, just render it, otherwise wrap in `h` to create a vnode |
|
const vNode = isVNode(componentOrVNode) ? componentOrVNode : h(componentOrVNode, reactiveProps) |
|
|
|
vNode.appContext = { ...useNuxtApp().vueApp._context, ...context } |
|
|
|
vNodeRef.value = vNode |
|
|
|
// wrap in suspense to resolve potential promises |
|
render(h(Suspense, vNode), domNode) |
|
|
|
if (!isMounted.value) mountedHook.trigger() |
|
}, |
|
{ deep: true, immediate: true, flush: 'post' }, |
|
) |
|
|
|
/** When calling scope is disposed, destroy component */ |
|
tryOnScopeDispose(close) |
|
|
|
/** destroy component, can be debounced */ |
|
function close(debounce = 0) { |
|
setTimeout(() => { |
|
stop() |
|
|
|
render(null, domNode) |
|
|
|
setTimeout(() => { |
|
try { |
|
;(mountTarget.value as HTMLElement)?.removeChild(domNode) |
|
} catch (e) {} |
|
}, 100) |
|
|
|
closeHook.trigger() |
|
}, debounce) |
|
} |
|
|
|
return { |
|
close, |
|
onClose: closeHook.on, |
|
onMounted: mountedHook.on, |
|
domNode, |
|
vNode: vNodeRef, |
|
} |
|
}
|
|
|