messageBox.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. import { createVNode, render } from 'vue'
  2. import {
  3. debugWarn,
  4. hasOwn,
  5. isClient,
  6. isElement,
  7. isFunction,
  8. isObject,
  9. isString,
  10. isUndefined,
  11. isVNode,
  12. } from '@kankan-components/utils'
  13. import MessageBoxConstructor from './index.vue'
  14. import type { AppContext, ComponentPublicInstance, VNode } from 'vue'
  15. import type {
  16. Action,
  17. Callback,
  18. ElMessageBoxOptions,
  19. ElMessageBoxShortcutMethod,
  20. IElMessageBox,
  21. MessageBoxData,
  22. MessageBoxState,
  23. } from './message-box.type'
  24. // component default merge props & data
  25. const messageInstance = new Map<
  26. ComponentPublicInstance<{ doClose: () => void }>, // marking doClose as function
  27. {
  28. options: any
  29. callback: Callback | undefined
  30. resolve: (res: any) => void
  31. reject: (reason?: any) => void
  32. }
  33. >()
  34. const getAppendToElement = (props: any): HTMLElement => {
  35. let appendTo: HTMLElement | null = document.body
  36. if (props.appendTo) {
  37. if (isString(props.appendTo)) {
  38. appendTo = document.querySelector<HTMLElement>(props.appendTo)
  39. }
  40. if (isElement(props.appendTo)) {
  41. appendTo = props.appendTo
  42. }
  43. // should fallback to default value with a warning
  44. if (!isElement(appendTo)) {
  45. debugWarn(
  46. 'KkMessageBox',
  47. 'the appendTo option is not an HTMLElement. Falling back to document.body.'
  48. )
  49. appendTo = document.body
  50. }
  51. }
  52. return appendTo
  53. }
  54. const initInstance = (
  55. props: any,
  56. container: HTMLElement,
  57. appContext: AppContext | null = null
  58. ) => {
  59. const vnode = createVNode(
  60. MessageBoxConstructor,
  61. props,
  62. isFunction(props.message) || isVNode(props.message)
  63. ? {
  64. default: isFunction(props.message)
  65. ? props.message
  66. : () => props.message,
  67. }
  68. : null
  69. )
  70. vnode.appContext = appContext
  71. render(vnode, container)
  72. getAppendToElement(props).appendChild(container.firstElementChild!)
  73. return vnode.component
  74. }
  75. const genContainer = () => {
  76. return document.createElement('div')
  77. }
  78. const showMessage = (options: any, appContext?: AppContext | null) => {
  79. const container = genContainer()
  80. // Adding destruct method.
  81. // when transition leaves emitting `vanish` evt. so that we can do the clean job.
  82. options.onVanish = () => {
  83. // not sure if this causes mem leak, need proof to verify that.
  84. // maybe calling out like 1000 msg-box then close them all.
  85. render(null, container)
  86. messageInstance.delete(vm) // Remove vm to avoid mem leak.
  87. // here we were suppose to call document.body.removeChild(container.firstElementChild)
  88. // but render(null, container) did that job for us. so that we do not call that directly
  89. }
  90. options.onAction = (action: Action) => {
  91. const currentMsg = messageInstance.get(vm)!
  92. let resolve: Action | { value: string; action: Action }
  93. if (options.showInput) {
  94. resolve = { value: vm.inputValue, action }
  95. } else {
  96. resolve = action
  97. }
  98. if (options.callback) {
  99. options.callback(resolve, instance.proxy)
  100. } else {
  101. if (action === 'cancel' || action === 'close') {
  102. if (options.distinguishCancelAndClose && action !== 'cancel') {
  103. currentMsg.reject('close')
  104. } else {
  105. currentMsg.reject('cancel')
  106. }
  107. } else {
  108. currentMsg.resolve(resolve)
  109. }
  110. }
  111. }
  112. const instance = initInstance(options, container, appContext)!
  113. // This is how we use message box programmably.
  114. // Maybe consider releasing a template version?
  115. // get component instance like v2.
  116. const vm = instance.proxy as ComponentPublicInstance<
  117. {
  118. visible: boolean
  119. doClose: () => void
  120. } & MessageBoxState
  121. >
  122. for (const prop in options) {
  123. if (hasOwn(options, prop) && !hasOwn(vm.$props, prop)) {
  124. vm[prop as keyof ComponentPublicInstance] = options[prop]
  125. }
  126. }
  127. // change visibility after everything is settled
  128. vm.visible = true
  129. return vm
  130. }
  131. async function MessageBox(
  132. options: ElMessageBoxOptions,
  133. appContext?: AppContext | null
  134. ): Promise<MessageBoxData>
  135. function MessageBox(
  136. options: ElMessageBoxOptions | string | VNode,
  137. appContext: AppContext | null = null
  138. ): Promise<{ value: string; action: Action } | Action> {
  139. if (!isClient) return Promise.reject()
  140. let callback: Callback | undefined
  141. if (isString(options) || isVNode(options)) {
  142. options = {
  143. message: options,
  144. }
  145. } else {
  146. callback = options.callback
  147. }
  148. return new Promise((resolve, reject) => {
  149. const vm = showMessage(
  150. options,
  151. appContext ?? (MessageBox as IElMessageBox)._context
  152. )
  153. // collect this vm in order to handle upcoming events.
  154. messageInstance.set(vm, {
  155. options,
  156. callback,
  157. resolve,
  158. reject,
  159. })
  160. })
  161. }
  162. const MESSAGE_BOX_VARIANTS = ['alert', 'confirm', 'prompt'] as const
  163. const MESSAGE_BOX_DEFAULT_OPTS: Record<
  164. typeof MESSAGE_BOX_VARIANTS[number],
  165. Partial<ElMessageBoxOptions>
  166. > = {
  167. alert: { closeOnPressEscape: false, closeOnClickModal: false },
  168. confirm: { showCancelButton: true },
  169. prompt: { showCancelButton: true, showInput: true },
  170. }
  171. MESSAGE_BOX_VARIANTS.forEach((boxType) => {
  172. ;(MessageBox as IElMessageBox)[boxType] = messageBoxFactory(
  173. boxType
  174. ) as ElMessageBoxShortcutMethod
  175. })
  176. function messageBoxFactory(boxType: typeof MESSAGE_BOX_VARIANTS[number]) {
  177. return (
  178. message: string | VNode,
  179. title: string | ElMessageBoxOptions,
  180. options?: ElMessageBoxOptions,
  181. appContext?: AppContext | null
  182. ) => {
  183. let titleOrOpts = ''
  184. if (isObject(title)) {
  185. options = title as ElMessageBoxOptions
  186. titleOrOpts = ''
  187. } else if (isUndefined(title)) {
  188. titleOrOpts = ''
  189. } else {
  190. titleOrOpts = title as string
  191. }
  192. return MessageBox(
  193. Object.assign(
  194. {
  195. title: titleOrOpts,
  196. message,
  197. type: '',
  198. ...MESSAGE_BOX_DEFAULT_OPTS[boxType],
  199. },
  200. options,
  201. {
  202. boxType,
  203. }
  204. ),
  205. appContext
  206. )
  207. }
  208. }
  209. MessageBox.close = () => {
  210. // instance.setupInstall.doClose()
  211. // instance.setupInstall.state.visible = false
  212. messageInstance.forEach((_, vm) => {
  213. vm.doClose()
  214. })
  215. messageInstance.clear()
  216. }
  217. ;(MessageBox as IElMessageBox)._context = null
  218. export default MessageBox as IElMessageBox