import { createVNode, render } from 'vue' import { isClient } from '@vueuse/core' import { debugWarn, hasOwn, isElement, isFunction, isObject, isString, isUndefined, isVNode, } from '@kankan-components/utils' import MessageBoxConstructor from './index.vue' import type { AppContext, ComponentPublicInstance, VNode } from 'vue' import type { Action, Callback, IKkMessageBox, KkMessageBoxOptions, KkMessageBoxShortcutMethod, MessageBoxData, MessageBoxState, } from './message-box.type' // component default merge props & data const messageInstance = new Map< ComponentPublicInstance<{ doClose: () => void }>, // marking doClose as function { options: any callback: Callback | undefined resolve: (res: any) => void reject: (reason?: any) => void } >() const getAppendToElement = (props: any): HTMLElement => { let appendTo: HTMLElement | null = document.body if (props.appendTo) { if (isString(props.appendTo)) { appendTo = document.querySelector(props.appendTo) } if (isElement(props.appendTo)) { appendTo = props.appendTo } // should fallback to default value with a warning if (!isElement(appendTo)) { debugWarn( 'ElMessageBox', 'the appendTo option is not an HTMLElement. Falling back to document.body.' ) appendTo = document.body } } return appendTo } const initInstance = ( props: any, container: HTMLElement, appContext: AppContext | null = null ) => { const vnode = createVNode( MessageBoxConstructor, props, isFunction(props.message) || isVNode(props.message) ? { default: isFunction(props.message) ? props.message : () => props.message, } : null ) vnode.appContext = appContext render(vnode, container) getAppendToElement(props).appendChild(container.firstElementChild!) return vnode.component } const genContainer = () => { return document.createElement('div') } const showMessage = (options: any, appContext?: AppContext | null) => { const container = genContainer() // Adding destruct method. // when transition leaves emitting `vanish` evt. so that we can do the clean job. options.onVanish = () => { // not sure if this causes mem leak, need proof to verify that. // maybe calling out like 1000 msg-box then close them all. render(null, container) messageInstance.delete(vm) // Remove vm to avoid mem leak. // here we were suppose to call document.body.removeChild(container.firstElementChild) // but render(null, container) did that job for us. so that we do not call that directly } options.onAction = (action: Action) => { const currentMsg = messageInstance.get(vm)! let resolve: Action | { value: string; action: Action } if (options.showInput) { resolve = { value: vm.inputValue, action } } else { resolve = action } if (options.callback) { options.callback(resolve, instance.proxy) } else { if (action === 'cancel' || action === 'close') { if (options.distinguishCancelAndClose && action !== 'cancel') { currentMsg.reject('close') } else { currentMsg.reject('cancel') } } else { currentMsg.resolve(resolve) } } } const instance = initInstance(options, container, appContext)! // This is how we use message box programmably. // Maybe consider releasing a template version? // get component instance like v2. const vm = instance.proxy as ComponentPublicInstance< { visible: boolean doClose: () => void } & MessageBoxState > for (const prop in options) { if (hasOwn(options, prop) && !hasOwn(vm.$props, prop)) { vm[prop as keyof ComponentPublicInstance] = options[prop] } } // change visibility after everything is settled vm.visible = true return vm } async function MessageBox( options: KkMessageBoxOptions, appContext?: AppContext | null ): Promise function MessageBox( options: KkMessageBoxOptions | string | VNode, appContext: AppContext | null = null ): Promise<{ value: string; action: Action } | Action> { if (!isClient) return Promise.reject() let callback: Callback | undefined if (isString(options) || isVNode(options)) { options = { message: options, } } else { callback = options.callback } return new Promise((resolve, reject) => { const vm = showMessage( options, appContext ?? (MessageBox as IKkMessageBox)._context ) // collect this vm in order to handle upcoming events. messageInstance.set(vm, { options, callback, resolve, reject, }) }) } const MESSAGE_BOX_VARIANTS = ['alert', 'confirm', 'prompt'] as const const MESSAGE_BOX_DEFAULT_OPTS: Record< (typeof MESSAGE_BOX_VARIANTS)[number], Partial > = { alert: { closeOnPressEscape: false, closeOnClickModal: false }, confirm: { showCancelButton: true }, prompt: { showCancelButton: true, showInput: true }, } MESSAGE_BOX_VARIANTS.forEach((boxType) => { ;(MessageBox as IKkMessageBox)[boxType] = messageBoxFactory( boxType ) as KkMessageBoxShortcutMethod }) function messageBoxFactory(boxType: (typeof MESSAGE_BOX_VARIANTS)[number]) { return ( message: string | VNode, title: string | KkMessageBoxOptions, options?: KkMessageBoxOptions, appContext?: AppContext | null ) => { let titleOrOpts = '' if (isObject(title)) { options = title as KkMessageBoxOptions titleOrOpts = '' } else if (isUndefined(title)) { titleOrOpts = '' } else { titleOrOpts = title as string } return MessageBox( Object.assign( { title: titleOrOpts, message, type: '', ...MESSAGE_BOX_DEFAULT_OPTS[boxType], }, options, { boxType, } ), appContext ) } } MessageBox.close = () => { // instance.setupInstall.doClose() // instance.setupInstall.state.visible = false messageInstance.forEach((_, vm) => { vm.doClose() }) messageInstance.clear() } ;(MessageBox as IKkMessageBox)._context = null export default MessageBox as IKkMessageBox