diff --git a/packages/components/message/__tests__/message-manager.spec.ts b/packages/components/message/__tests__/message-manager.spec.ts index cc37e88c59..d82cf83356 100644 --- a/packages/components/message/__tests__/message-manager.spec.ts +++ b/packages/components/message/__tests__/message-manager.spec.ts @@ -1,7 +1,7 @@ import { nextTick } from 'vue' import { getStyle } from '@element-plus/utils/dom' import { rAF } from '@element-plus/test-utils/tick' -import Message from '../src/message' +import Message from '../src/message-method' jest.useFakeTimers() const selector = '.el-message' diff --git a/packages/components/message/__tests__/message.spec.ts b/packages/components/message/__tests__/message.spec.ts index d545e728cf..730c028362 100644 --- a/packages/components/message/__tests__/message.spec.ts +++ b/packages/components/message/__tests__/message.spec.ts @@ -1,9 +1,8 @@ import { h, nextTick } from 'vue' -import * as domExports from '@element-plus/utils/dom' import makeMount from '@element-plus/test-utils/make-mount' import { rAF } from '@element-plus/test-utils/tick' import { EVENT_CODE } from '@element-plus/utils/aria' -import Message from '../src/index.vue' +import Message from '../src/message.vue' import type { ComponentPublicInstance, CSSProperties } from 'vue' @@ -76,19 +75,6 @@ describe('Message.vue', () => { }) }) - describe('lifecycle', () => { - test('should add keydown event lister on mount', () => { - jest.spyOn(domExports, 'on') - jest.spyOn(domExports, 'off') - const wrapper = _mount({ - slots: { default: AXIOM }, - }) - expect(domExports.on).toHaveBeenCalled() - wrapper.unmount() - expect(domExports.off).toHaveBeenCalled() - }) - }) - describe('Message.type', () => { test('should be able to render typed messages', () => { for (const type of ['success', 'warning', 'info', 'error'] as const) { diff --git a/packages/components/message/index.ts b/packages/components/message/index.ts index cef8d5ad11..91ac3af3e8 100644 --- a/packages/components/message/index.ts +++ b/packages/components/message/index.ts @@ -1,15 +1,8 @@ -import Message from './src/message' +import { withInstallFunction } from '@element-plus/utils/with-install' -import type { App } from 'vue' -import type { SFCWithInstall } from '@element-plus/utils/types' +import Message from './src/message-method' -const _Message = Message as SFCWithInstall +export const ElMessage = withInstallFunction(Message, '$message') +export default ElMessage -_Message.install = (app: App) => { - app.config.globalProperties.$message = _Message -} - -export default _Message -export const ElMessage = _Message - -export * from './src/types' +export * from './src/message' diff --git a/packages/components/message/src/message-method.ts b/packages/components/message/src/message-method.ts new file mode 100644 index 0000000000..74c795869f --- /dev/null +++ b/packages/components/message/src/message-method.ts @@ -0,0 +1,120 @@ +import { createVNode, render } from 'vue' +import { isVNode } from '@element-plus/utils/util' +import PopupManager from '@element-plus/utils/popup-manager' +import isServer from '@element-plus/utils/isServer' +import MessageConstructor from './message.vue' +import { messageTypes } from './message' + +import type { MessagePartial, MessageQueue, MessageProps } from './message' +import type { ComponentPublicInstance, VNode } from 'vue' + +const instances: MessageQueue = [] +let seed = 1 + +// TODO: Since Notify.ts is basically the same like this file. So we could do some encapsulation against them to reduce code duplication. + +const message: MessagePartial = function (options = {}) { + if (isServer) return { close: () => undefined } + + if (typeof options === 'string' || isVNode(options)) { + options = { message: options } + } + + let verticalOffset = options.offset || 20 + instances.forEach(({ vm }) => { + verticalOffset += (vm.el?.offsetHeight || 0) + 16 + }) + verticalOffset += 16 + + const id = `message_${seed++}` + const userOnClose = options.onClose + const props: Partial = { + zIndex: PopupManager.nextZIndex(), + offset: verticalOffset, + ...options, + id, + onClose: () => { + close(id, userOnClose) + }, + } + + const container = document.createElement('div') + + container.className = `container_${id}` + + const message = props.message + const vm = createVNode( + MessageConstructor, + props, + isVNode(props.message) ? { default: () => message } : null + ) + + // clean message element preventing mem leak + vm.props!.onDestroy = () => { + render(null, container) + // since the element is destroy, then the VNode should be collected by GC as well + // we do not want cause any mem leak because we have returned vm as a reference to users + // so that we manually set it to false. + } + + render(vm, container) + // instances will remove this item when close function gets called. So we do not need to worry about it. + instances.push({ vm }) + document.body.appendChild(container.firstElementChild!) + + return { + // instead of calling the onClose function directly, setting this value so that we can have the full lifecycle + // for out component, so that all closing steps will not be skipped. + close: () => + (( + vm.component!.proxy as ComponentPublicInstance<{ visible: boolean }> + ).visible = false), + } +} + +messageTypes.forEach((type) => { + message[type] = (options = {}) => { + if (typeof options === 'string' || isVNode(options)) { + options = { + message: options, + } + } + return message({ + ...options, + type, + }) + } +}) + +export function close(id: string, userOnClose?: (vm: VNode) => void): void { + const idx = instances.findIndex(({ vm }) => id === vm.component!.props.id) + if (idx === -1) return + + const { vm } = instances[idx] + if (!vm) return + userOnClose?.(vm) + + const removedHeight = vm.el!.offsetHeight + instances.splice(idx, 1) + + // adjust other instances vertical offset + const len = instances.length + if (len < 1) return + for (let i = idx; i < len; i++) { + const pos = + parseInt(instances[i].vm.el!.style['top'], 10) - removedHeight - 16 + + instances[i].vm.component!.props.offset = pos + } +} + +export function closeAll(): void { + for (let i = instances.length - 1; i >= 0; i--) { + const instance = instances[i].vm.component as any + instance.ctx.close() + } +} + +message.closeAll = closeAll + +export default message diff --git a/packages/components/message/src/message.ts b/packages/components/message/src/message.ts index ab92f77e05..68db13643e 100644 --- a/packages/components/message/src/message.ts +++ b/packages/components/message/src/message.ts @@ -1,137 +1,90 @@ -import { createVNode, render } from 'vue' -import { isVNode } from '@element-plus/utils/util' -import PopupManager from '@element-plus/utils/popup-manager' -import isServer from '@element-plus/utils/isServer' -import MessageConstructor from './index.vue' +import { buildProp } from '@element-plus/utils/props' -import type { ComponentPublicInstance } from 'vue' -import type { - IMessage, - MessageQueue, - IMessageOptions, - MessageVM, - IMessageHandle, - MessageParams, -} from './types' +import type { VNode, ExtractPropTypes } from 'vue' -const instances: MessageQueue = [] -let seed = 1 +export const messageTypes = ['success', 'info', 'warning', 'error'] as const -// TODO: Since Notify.ts is basically the same like this file. So we could do some encapsulation against them to -// reduce code duplication. -const Message: IMessage = function ( - opts: MessageParams = {} as MessageParams -): IMessageHandle { - if (isServer) return +export const messageProps = { + customClass: { + type: String, + default: '', + }, + center: { + type: Boolean, + default: false, + }, + dangerouslyUseHTMLString: { + type: Boolean, + default: false, + }, + duration: { + type: Number, + default: 3000, + }, + iconClass: { + type: String, + default: '', + }, + id: { + type: String, + default: '', + }, + message: buildProp({ + type: [String, Object], + default: '', + }), + onClose: buildProp<() => void>({ + type: Function, + required: false, + }), + showClose: { + type: Boolean, + default: false, + }, + type: buildProp({ + type: String, + values: messageTypes, + default: 'info', + } as const), + offset: { + type: Number, + default: 20, + }, + zIndex: { + type: Number, + default: 0, + }, +} as const +export type MessageProps = ExtractPropTypes - if (typeof opts === 'string') { - opts = { - message: opts, - } - } +export const messageEmits = { + destroy: () => true, +} +export type MessageEmits = typeof messageEmits - let options: IMessageOptions = opts +export type MessageOptions = Omit +export type MessageOptionsTyped = Omit - let verticalOffset = opts.offset || 20 - instances.forEach(({ vm }) => { - verticalOffset += (vm.el.offsetHeight || 0) + 16 - }) - verticalOffset += 16 - - const id = `message_${seed++}` - const userOnClose = options.onClose - - options = { - ...options, - onClose: () => { - close(id, userOnClose) - }, - offset: verticalOffset, - id, - zIndex: PopupManager.nextZIndex(), - } - - const container = document.createElement('div') - - container.className = `container_${id}` - - const message = options.message - const vm = createVNode( - MessageConstructor, - options, - isVNode(options.message) ? { default: () => message } : null - ) - - // clean message element preventing mem leak - vm.props.onDestroy = () => { - render(null, container) - // since the element is destroy, then the VNode should be collected by GC as well - // we do not want cause any mem leak because we have returned vm as a reference to users - // so that we manually set it to false. - } - - render(vm, container) - // instances will remove this item when close function gets called. So we do not need to worry about it. - instances.push({ vm }) - document.body.appendChild(container.firstElementChild) - - return { - // instead of calling the onClose function directly, setting this value so that we can have the full lifecycle - // for out component, so that all closing steps will not be skipped. - close: () => - (( - vm.component.proxy as ComponentPublicInstance<{ visible: boolean }> - ).visible = false), - } -} as any - -export function close(id: string, userOnClose?: (vm: MessageVM) => void): void { - const idx = instances.findIndex(({ vm }) => { - const { id: _id } = vm.component.props - return id === _id - }) - if (idx === -1) { - return - } - - const { vm } = instances[idx] - if (!vm) return - userOnClose?.(vm) - - const removedHeight = vm.el.offsetHeight - instances.splice(idx, 1) - - // adjust other instances vertical offset - const len = instances.length - if (len < 1) return - for (let i = idx; i < len; i++) { - const pos = - parseInt(instances[i].vm.el.style['top'], 10) - removedHeight - 16 - - instances[i].vm.component.props.offset = pos - } +export interface MessageHandle { + close: () => void } -export function closeAll(): void { - for (let i = instances.length - 1; i >= 0; i--) { - const instance = instances[i].vm.component as any - instance.ctx.close() - } +export type MessageParams = Partial | string | VNode +export type MessageParamsTyped = Partial | string | VNode + +export interface MessagePartial { + (options?: MessageParams): MessageHandle + closeAll(): void + + success?: (options?: MessageParamsTyped) => MessageHandle + warning?: (options?: MessageParamsTyped) => MessageHandle + info?: (options?: MessageParamsTyped) => MessageHandle + error?: (options?: MessageParamsTyped) => MessageHandle +} +export type Message = Required + +type MessageQueueItem = { + vm: VNode } -;(['success', 'warning', 'info', 'error'] as const).forEach((type) => { - Message[type] = (options) => { - if (typeof options === 'string') { - options = { - message: options, - type, - } - } else { - options.type = type - } - return Message(options) - } -}) - -Message.closeAll = closeAll -export default Message +export type MessageQueue = MessageQueueItem[] diff --git a/packages/components/message/src/index.vue b/packages/components/message/src/message.vue similarity index 51% rename from packages/components/message/src/index.vue rename to packages/components/message/src/message.vue index f74e2f5d08..f2cc2c100e 100644 --- a/packages/components/message/src/index.vue +++ b/packages/components/message/src/message.vue @@ -28,7 +28,6 @@ {{ message }}

-