mirror of
https://github.com/element-plus/element-plus.git
synced 2026-03-13 07:51:17 +08:00
refactor(components): refactor message (#3524)
* refactor(components): refactor message * chore: remove unused
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<typeof Message>
|
||||
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'
|
||||
|
||||
120
packages/components/message/src/message-method.ts
Normal file
120
packages/components/message/src/message-method.ts
Normal file
@@ -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<MessageProps> = {
|
||||
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
|
||||
@@ -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<string | VNode>({
|
||||
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<typeof messageProps>
|
||||
|
||||
if (typeof opts === 'string') {
|
||||
opts = {
|
||||
message: opts,
|
||||
}
|
||||
}
|
||||
export const messageEmits = {
|
||||
destroy: () => true,
|
||||
}
|
||||
export type MessageEmits = typeof messageEmits
|
||||
|
||||
let options: IMessageOptions = <IMessageOptions>opts
|
||||
export type MessageOptions = Omit<MessageProps, 'id'>
|
||||
export type MessageOptionsTyped = Omit<MessageOptions, 'type'>
|
||||
|
||||
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<MessageOptions> | string | VNode
|
||||
export type MessageParamsTyped = Partial<MessageOptionsTyped> | 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<MessagePartial>
|
||||
|
||||
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[]
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
{{ message }}
|
||||
</p>
|
||||
<!-- Caution here, message could've been compromised, never use user's input as message -->
|
||||
<!-- eslint-disable-next-line -->
|
||||
<p v-else class="el-message__content" v-html="message"></p>
|
||||
</slot>
|
||||
<div
|
||||
@@ -40,71 +39,49 @@
|
||||
</transition>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { defineComponent, computed, ref, onMounted } from 'vue'
|
||||
import { useEventListener, useTimeoutFn } from '@vueuse/core'
|
||||
import { EVENT_CODE } from '@element-plus/utils/aria'
|
||||
import { on, off } from '@element-plus/utils/dom'
|
||||
import { messageEmits, messageProps } from './message'
|
||||
|
||||
// MessageVM is an alias of vue.VNode
|
||||
import type { PropType } from 'vue'
|
||||
import type { Indexable } from '@element-plus/utils/types'
|
||||
import type { MessageVM } from './types'
|
||||
const TypeMap: Indexable<string> = {
|
||||
success: 'success',
|
||||
info: 'info',
|
||||
warning: 'warning',
|
||||
error: 'error',
|
||||
import type { CSSProperties } from 'vue'
|
||||
import type { MessageProps } from './message'
|
||||
|
||||
const typeMap: Record<MessageProps['type'], string> = {
|
||||
success: 'el-icon-success',
|
||||
info: 'el-icon-info',
|
||||
warning: 'el-icon-warning',
|
||||
error: 'el-icon-error',
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElMessage',
|
||||
props: {
|
||||
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: {
|
||||
type: [String, Object] as PropType<string | MessageVM>,
|
||||
default: '',
|
||||
},
|
||||
onClose: {
|
||||
type: Function as PropType<() => void>,
|
||||
required: true,
|
||||
},
|
||||
showClose: { type: Boolean, default: false },
|
||||
type: { type: String, default: 'info' },
|
||||
offset: { type: Number, default: 20 },
|
||||
zIndex: { type: Number, default: 0 },
|
||||
},
|
||||
emits: ['destroy'],
|
||||
setup(props) {
|
||||
const typeClass = computed(() => {
|
||||
const type = !props.iconClass && props.type
|
||||
return type && TypeMap[type] ? `el-icon-${TypeMap[type]}` : ''
|
||||
})
|
||||
const customStyle = computed(() => {
|
||||
return {
|
||||
top: `${props.offset}px`,
|
||||
zIndex: props.zIndex,
|
||||
}
|
||||
})
|
||||
|
||||
props: messageProps,
|
||||
emits: messageEmits,
|
||||
|
||||
setup(props) {
|
||||
const visible = ref(false)
|
||||
let timer = null
|
||||
let timer: (() => void) | undefined = undefined
|
||||
|
||||
const typeClass = computed(() =>
|
||||
props.iconClass ? props.iconClass : typeMap[props.type] ?? ''
|
||||
)
|
||||
const customStyle = computed<CSSProperties>(() => ({
|
||||
top: `${props.offset}px`,
|
||||
zIndex: props.zIndex,
|
||||
}))
|
||||
|
||||
function startTimer() {
|
||||
if (props.duration > 0) {
|
||||
timer = setTimeout(() => {
|
||||
if (visible.value) {
|
||||
close()
|
||||
}
|
||||
}, props.duration)
|
||||
;({ stop: timer } = useTimeoutFn(() => {
|
||||
if (visible.value) close()
|
||||
}, props.duration))
|
||||
}
|
||||
}
|
||||
|
||||
function clearTimer() {
|
||||
clearTimeout(timer)
|
||||
timer = null
|
||||
timer?.()
|
||||
}
|
||||
|
||||
function close() {
|
||||
@@ -125,12 +102,9 @@ export default defineComponent({
|
||||
onMounted(() => {
|
||||
startTimer()
|
||||
visible.value = true
|
||||
on(document, 'keydown', keydown)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
off(document, 'keydown', keydown)
|
||||
})
|
||||
useEventListener(document, 'keydown', keydown)
|
||||
|
||||
return {
|
||||
typeClass,
|
||||
@@ -1,47 +0,0 @@
|
||||
import type { VNode } from 'vue'
|
||||
|
||||
export interface IMessageHandle {
|
||||
close: () => void
|
||||
}
|
||||
|
||||
export type IMessageOptions = {
|
||||
customClass?: string
|
||||
center?: boolean
|
||||
dangerouslyUseHTMLString?: boolean // default false
|
||||
duration?: number // default 3000
|
||||
iconClass?: string
|
||||
id?: string
|
||||
message?: string | VNode
|
||||
offset?: number // defaults 20
|
||||
onClose?: () => void
|
||||
showClose?: boolean // default false
|
||||
type?: 'success' | 'warning' | 'info' | 'error' | ''
|
||||
zIndex?: number
|
||||
}
|
||||
|
||||
export type MessageType = 'success' | 'warning' | 'info' | 'error' | ''
|
||||
|
||||
export type IMessageDispatcher = (
|
||||
options?: IMessageOptions | string
|
||||
) => IMessageHandle
|
||||
export type MessageParams = IMessageOptions | string
|
||||
export type TypedMessageParams<T extends MessageType> =
|
||||
| ({ type: T } & Omit<IMessageOptions, 'type'>)
|
||||
| string
|
||||
|
||||
export interface IMessage {
|
||||
(options?: MessageParams): IMessageHandle
|
||||
success: (options?: TypedMessageParams<'success'>) => IMessageHandle
|
||||
warning: (options?: TypedMessageParams<'warning'>) => IMessageHandle
|
||||
info: (options?: TypedMessageParams<'info'>) => IMessageHandle
|
||||
error: (options?: TypedMessageParams<'error'>) => IMessageHandle
|
||||
closeAll(): void
|
||||
}
|
||||
|
||||
export type MessageVM = VNode
|
||||
|
||||
type MessageQueueItem = {
|
||||
vm: MessageVM
|
||||
}
|
||||
|
||||
export type MessageQueue = Array<MessageQueueItem>
|
||||
Reference in New Issue
Block a user