mirror of
https://github.com/element-plus/element-plus.git
synced 2026-03-13 07:51:17 +08:00
* feat(components): [message] add `placement` option & method * fix: resolve test hanging issue caused by reactive circular dependency * test: add test * feat: add placement `top-left/top-right/bottom-left/bottom-right` * refactor: split large normalizeOptions function * docs: adjust description text * chore: remove unused height expose * refactor: simpify code * style: opt-in center style & simplified animations * style: replace :not(.center) with :is(.left,.right) for more explicit * feat: add `placement` to `config-provider` * fix: fix test warning when placement is undefined * refactor: remove useless style & simpify types * fix: avoid circular dependency * chore: types related * Update docs/examples/config-provider/message.vue Co-authored-by: Noblet Ouways <91417411+Dsaquel@users.noreply.github.com> * Update packages/components/config-provider/__tests__/config-provider.test.tsx Co-authored-by: Noblet Ouways <91417411+Dsaquel@users.noreply.github.com> * chore: make typecheck happy & format * style: add top/bottom transition * chore: format --------- Co-authored-by: zhixiaotong <947803089@qq.com> Co-authored-by: Noblet Ouways <91417411+Dsaquel@users.noreply.github.com> Co-authored-by: Dsaquel <291874700n@gmail.com>
261 lines
7.5 KiB
TypeScript
261 lines
7.5 KiB
TypeScript
import { createVNode, isVNode, render } from 'vue'
|
|
import {
|
|
debugWarn,
|
|
hasOwn,
|
|
isBoolean,
|
|
isClient,
|
|
isElement,
|
|
isFunction,
|
|
isNumber,
|
|
isString,
|
|
} from '@element-plus/utils'
|
|
import { messageConfig } from '@element-plus/components/config-provider'
|
|
import MessageConstructor from './message.vue'
|
|
import {
|
|
MESSAGE_DEFAULT_PLACEMENT,
|
|
messageDefaults,
|
|
messagePlacement,
|
|
messageTypes,
|
|
} from './message'
|
|
import { getOrCreatePlacementInstances, placementInstances } from './instance'
|
|
|
|
import type { MessageContext } from './instance'
|
|
import type { AppContext } from 'vue'
|
|
import type {
|
|
Message,
|
|
MessageFn,
|
|
MessageHandler,
|
|
MessageOptions,
|
|
MessageParams,
|
|
MessageParamsNormalized,
|
|
MessagePlacement,
|
|
MessageType,
|
|
} from './message'
|
|
|
|
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 normalizeAppendTo = (normalized: MessageOptions) => {
|
|
const appendTo = normalized.appendTo
|
|
if (!appendTo) {
|
|
normalized.appendTo = document.body
|
|
} else if (isString(normalized.appendTo)) {
|
|
let appendTo = document.querySelector<HTMLElement>(normalized.appendTo)
|
|
|
|
// should fallback to default value with a warning
|
|
if (!isElement(appendTo)) {
|
|
debugWarn(
|
|
'ElMessage',
|
|
'the appendTo option is not an HTMLElement. Falling back to document.body.'
|
|
)
|
|
appendTo = document.body
|
|
}
|
|
normalized.appendTo = appendTo
|
|
}
|
|
}
|
|
|
|
const normalizePlacement = (normalized: MessageOptions) => {
|
|
// if placement is not passed and global has config, use global config
|
|
if (
|
|
!normalized.placement &&
|
|
isString(messageConfig.placement) &&
|
|
messageConfig.placement
|
|
) {
|
|
normalized.placement = messageConfig.placement as
|
|
| MessagePlacement
|
|
| undefined
|
|
}
|
|
// if placement is not passed and global has no config, use default config
|
|
if (!normalized.placement) {
|
|
normalized.placement = MESSAGE_DEFAULT_PLACEMENT
|
|
}
|
|
// if placement is not valid, use default config
|
|
if (!messagePlacement.includes(normalized.placement!)) {
|
|
debugWarn(
|
|
'ElMessage',
|
|
`Invalid placement: ${normalized.placement}. Falling back to '${MESSAGE_DEFAULT_PLACEMENT}'.`
|
|
)
|
|
normalized.placement = MESSAGE_DEFAULT_PLACEMENT
|
|
}
|
|
}
|
|
|
|
const normalizeOptions = (params?: MessageParams) => {
|
|
const options: MessageOptions =
|
|
!params || isString(params) || isVNode(params) || isFunction(params)
|
|
? { message: params }
|
|
: params
|
|
|
|
const normalized: MessageOptions = {
|
|
...messageDefaults,
|
|
...options,
|
|
}
|
|
|
|
normalizeAppendTo(normalized)
|
|
normalizePlacement(normalized)
|
|
|
|
// When grouping is configured globally,
|
|
// if grouping is manually set when calling message individually and it is not equal to the default value,
|
|
// the global configuration cannot override the current setting. default => false
|
|
if (isBoolean(messageConfig.grouping) && !normalized.grouping) {
|
|
normalized.grouping = messageConfig.grouping
|
|
}
|
|
if (isNumber(messageConfig.duration) && normalized.duration === 3000) {
|
|
normalized.duration = messageConfig.duration
|
|
}
|
|
if (isNumber(messageConfig.offset) && normalized.offset === 16) {
|
|
normalized.offset = messageConfig.offset
|
|
}
|
|
if (isBoolean(messageConfig.showClose) && !normalized.showClose) {
|
|
normalized.showClose = messageConfig.showClose
|
|
}
|
|
if (isBoolean(messageConfig.plain) && !normalized.plain) {
|
|
normalized.plain = messageConfig.plain
|
|
}
|
|
|
|
return normalized as MessageParamsNormalized
|
|
}
|
|
|
|
const closeMessage = (instance: MessageContext) => {
|
|
const placement = instance.props.placement || MESSAGE_DEFAULT_PLACEMENT
|
|
const instances = placementInstances[placement]
|
|
|
|
const idx = instances.indexOf(instance)
|
|
if (idx === -1) return
|
|
instances.splice(idx, 1)
|
|
const { handler } = instance
|
|
handler.close()
|
|
}
|
|
|
|
const createMessage = (
|
|
{ appendTo, ...options }: MessageParamsNormalized,
|
|
context?: AppContext | null
|
|
): MessageContext => {
|
|
const id = `message_${seed++}`
|
|
const userOnClose = options.onClose
|
|
|
|
const container = document.createElement('div')
|
|
|
|
const props = {
|
|
...options,
|
|
// now the zIndex will be used inside the message.vue component instead of here.
|
|
// zIndex: nextIndex() + options.zIndex
|
|
id,
|
|
onClose: () => {
|
|
userOnClose?.()
|
|
closeMessage(instance)
|
|
},
|
|
|
|
// clean message element preventing mem leak
|
|
onDestroy: () => {
|
|
// 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(null, container)
|
|
},
|
|
}
|
|
const vnode = createVNode(
|
|
MessageConstructor,
|
|
props,
|
|
isFunction(props.message) || isVNode(props.message)
|
|
? {
|
|
default: isFunction(props.message)
|
|
? props.message
|
|
: () => props.message,
|
|
}
|
|
: null
|
|
)
|
|
vnode.appContext = context || message._context
|
|
|
|
render(vnode, container)
|
|
// instances will remove this item when close function gets called. So we do not need to worry about it.
|
|
appendTo.appendChild(container.firstElementChild!)
|
|
|
|
const vm = vnode.component!
|
|
|
|
const handler: MessageHandler = {
|
|
// 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.exposed!.close()
|
|
},
|
|
}
|
|
|
|
const instance: MessageContext = {
|
|
id,
|
|
vnode,
|
|
vm,
|
|
handler,
|
|
props: (vnode.component as any).props,
|
|
}
|
|
|
|
return instance
|
|
}
|
|
|
|
const message: MessageFn &
|
|
Partial<Message> & { _context: AppContext | null } = (
|
|
options = {},
|
|
context
|
|
) => {
|
|
if (!isClient) return { close: () => undefined }
|
|
|
|
const normalized = normalizeOptions(options)
|
|
const instances = getOrCreatePlacementInstances(
|
|
normalized.placement || MESSAGE_DEFAULT_PLACEMENT
|
|
)
|
|
|
|
if (normalized.grouping && instances.length) {
|
|
const instance = instances.find(
|
|
({ vnode: vm }) => vm.props?.message === normalized.message
|
|
)
|
|
if (instance) {
|
|
instance.props.repeatNum += 1
|
|
instance.props.type = normalized.type
|
|
return instance.handler
|
|
}
|
|
}
|
|
|
|
if (isNumber(messageConfig.max) && instances.length >= messageConfig.max) {
|
|
return { close: () => undefined }
|
|
}
|
|
|
|
const instance = createMessage(normalized, context)
|
|
|
|
instances.push(instance)
|
|
return instance.handler
|
|
}
|
|
|
|
messageTypes.forEach((type) => {
|
|
message[type] = (options = {}, appContext) => {
|
|
const normalized = normalizeOptions(options)
|
|
return message({ ...normalized, type }, appContext)
|
|
}
|
|
})
|
|
|
|
export function closeAll(type?: MessageType): void {
|
|
for (const placement in placementInstances) {
|
|
if (hasOwn(placementInstances, placement)) {
|
|
// Create a copy of instances to avoid modification during iteration
|
|
const instances: MessageContext[] = [...placementInstances[placement]]
|
|
for (const instance of instances) {
|
|
if (!type || type === instance.props.type) {
|
|
instance.handler.close()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export function closeAllByPlacement(placement: MessagePlacement) {
|
|
if (!placementInstances[placement]) return
|
|
// Create a copy of instances to avoid modification during iteration
|
|
const instances = [...placementInstances[placement]]
|
|
instances.forEach((instance) => instance.handler.close())
|
|
}
|
|
|
|
message.closeAll = closeAll
|
|
message.closeAllByPlacement = closeAllByPlacement
|
|
message._context = null
|
|
|
|
export default message as Message
|