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 (#21747)
* 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>
This commit is contained in:
@@ -141,14 +141,15 @@ In this section, you can learn how to use Config Provider to provide experimenta
|
||||
|
||||
### Message Attribute
|
||||
|
||||
| Attribute | Description | Type | Default |
|
||||
| ------------------ | ------------------------------------------------------------------------------ | ---------- | ------- |
|
||||
| max | the maximum number of messages that can be displayed at the same time | ^[number] | — |
|
||||
| grouping ^(2.8.2) | merge messages with the same content, type of VNode message is not supported | ^[boolean] | — |
|
||||
| duration ^(2.8.2) | display duration, millisecond. If set to 0, it will not turn off automatically | ^[number] | — |
|
||||
| showClose ^(2.8.2) | whether to show a close button | ^[boolean] | — |
|
||||
| offset ^(2.8.2) | set the distance to the top of viewport | ^[number] | — |
|
||||
| plain ^(2.9.11) | whether message is plain | ^[boolean] | — |
|
||||
| Attribute | Description | Type | Default |
|
||||
| ------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | ------- |
|
||||
| max | the maximum number of messages that can be displayed at the same time | ^[number] | — |
|
||||
| grouping ^(2.8.2) | merge messages with the same content, type of VNode message is not supported | ^[boolean] | — |
|
||||
| duration ^(2.8.2) | display duration, millisecond. If set to 0, it will not turn off automatically | ^[number] | — |
|
||||
| showClose ^(2.8.2) | whether to show a close button | ^[boolean] | — |
|
||||
| offset ^(2.8.2) | set the distance to the top of viewport | ^[number] | — |
|
||||
| plain ^(2.9.11) | whether message is plain | ^[boolean] | — |
|
||||
| placement ^(2.11.0) | message placement position | ^[enum]`'top' \| 'top-left' \| 'top-right' \| 'bottom' \| 'bottom-left' \| 'bottom-right'` | — |
|
||||
|
||||
### Config Provider Slots
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ Used to show feedback after an activity. The difference with Notification is tha
|
||||
|
||||
## Basic usage
|
||||
|
||||
Displays at the top, and disappears after 3 seconds.
|
||||
Displays at the top by default, and disappears after 3 seconds. You can control the position using the `placement` property.
|
||||
|
||||
:::demo The setup of Message is very similar to notification, so parts of the options won't be explained in detail here. You can check the options table below combined with notification doc to understand it. Element Plus has registered a `$message` method for invoking. Message can take a string or a VNode as parameter, and it will be shown as the main body.
|
||||
|
||||
@@ -73,6 +73,16 @@ message/grouping
|
||||
|
||||
:::
|
||||
|
||||
## Placement ^(2.11.0)
|
||||
|
||||
Control the position where messages appear. Messages can be displayed at the top (default) or other placements of the viewport.
|
||||
|
||||
:::demo
|
||||
|
||||
message/placement
|
||||
|
||||
:::
|
||||
|
||||
## Global method
|
||||
|
||||
Element Plus has added a global method `$message` for `app.config.globalProperties`. So in a vue instance you can call `Message` like what we did in this page.
|
||||
@@ -110,21 +120,22 @@ ElMessage({}, appContext)
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Description | Type | Default |
|
||||
| ------------------------ | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ------- |
|
||||
| message | message text | ^[string] / ^[VNode] / ^[Function]`() => VNode` | '' |
|
||||
| type | message type | ^[enum]`'primary' (2.9.11) \| 'success' \| 'warning' \| 'info' \| 'error'` | info |
|
||||
| plain ^(2.6.3) | whether message is plain | ^[boolean] | false |
|
||||
| icon | custom icon component, overrides `type` | ^[string] / ^[Component] | — |
|
||||
| dangerouslyUseHTMLString | whether `message` is treated as HTML string | ^[boolean] | false |
|
||||
| customClass | custom class name for Message | ^[string] | '' |
|
||||
| duration | display duration, millisecond. If set to 0, it will not turn off automatically | ^[number] | 3000 |
|
||||
| showClose | whether to show a close button | ^[boolean] | false |
|
||||
| onClose | callback function when closed with the message instance as the parameter | ^[Function]`() => void` | — |
|
||||
| offset | set the distance to the top of viewport | ^[number] | 16 |
|
||||
| appendTo | set the root element for the message, default to `document.body` | ^[CSSSelector] / ^[HTMLElement] | — |
|
||||
| grouping | merge messages with the same content, type of VNode message is not supported | ^[boolean] | false |
|
||||
| repeatNum | The number of repetitions, similar to badge, is used as the initial number when used with `grouping` | ^[number] | 1 |
|
||||
| Name | Description | Type | Default |
|
||||
| ------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | ------- |
|
||||
| message | message text | ^[string] / ^[VNode] / ^[Function]`() => VNode` | '' |
|
||||
| type | message type | ^[enum]`'primary' (2.9.11) \| 'success' \| 'warning' \| 'info' \| 'error'` | info |
|
||||
| plain ^(2.6.3) | whether message is plain | ^[boolean] | false |
|
||||
| icon | custom icon component, overrides `type` | ^[string] / ^[Component] | — |
|
||||
| dangerouslyUseHTMLString | whether `message` is treated as HTML string | ^[boolean] | false |
|
||||
| customClass | custom class name for Message | ^[string] | '' |
|
||||
| duration | display duration, millisecond. If set to 0, it will not turn off automatically | ^[number] | 3000 |
|
||||
| showClose | whether to show a close button | ^[boolean] | false |
|
||||
| onClose | callback function when closed with the message instance as the parameter | ^[Function]`() => void` | — |
|
||||
| offset | set the distance to the viewport edge (top when placement is 'top', bottom when placement is 'bottom') | ^[number] | 16 |
|
||||
| placement ^(2.11.0) | message placement position | ^[enum]`'top' \| 'top-left' \| 'top-right' \| 'bottom' \| 'bottom-left' \| 'bottom-right'` | top |
|
||||
| appendTo | set the root element for the message, default to `document.body` | ^[CSSSelector] / ^[HTMLElement] | — |
|
||||
| grouping | merge messages with the same content, type of VNode message is not supported | ^[boolean] | false |
|
||||
| repeatNum | The number of repetitions, similar to badge, is used as the initial number when used with `grouping` | ^[number] | 1 |
|
||||
|
||||
### Methods
|
||||
|
||||
|
||||
@@ -13,8 +13,10 @@ import { ElMessage } from 'element-plus'
|
||||
const config = reactive({
|
||||
max: 3,
|
||||
plain: true,
|
||||
placement: 'bottom',
|
||||
})
|
||||
|
||||
const open = () => {
|
||||
ElMessage('This is a message.')
|
||||
ElMessage('This is a message from bottom.')
|
||||
}
|
||||
</script>
|
||||
|
||||
71
docs/examples/message/placement.vue
Normal file
71
docs/examples/message/placement.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<el-button class="!ml-0" :plain="true" @click="openMsg()"> Top </el-button>
|
||||
<el-button class="!ml-0" :plain="true" @click="openMsg('top-left')">
|
||||
Top Left
|
||||
</el-button>
|
||||
<el-button class="!ml-0" :plain="true" @click="openMsg('top-right')">
|
||||
Top Right
|
||||
</el-button>
|
||||
<el-button class="!ml-0" :plain="true" @click="openMsg('bottom')">
|
||||
Bottom
|
||||
</el-button>
|
||||
<el-button class="!ml-0" :plain="true" @click="openMsg('bottom-left')">
|
||||
Bottom Left
|
||||
</el-button>
|
||||
<el-button class="!ml-0" :plain="true" @click="openMsg('bottom-right')">
|
||||
Bottom Right
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
import type { MessagePlacement, MessageType } from 'element-plus'
|
||||
|
||||
let topCount = 0
|
||||
let bottomCount = 0
|
||||
let topLeftCount = 0
|
||||
let topRightCount = 0
|
||||
let bottomLeftCount = 0
|
||||
let bottomRightCount = 0
|
||||
|
||||
const openMsg = (placement: MessagePlacement = 'top') => {
|
||||
let count = 0
|
||||
let type: MessageType = 'success'
|
||||
|
||||
switch (placement) {
|
||||
case 'top':
|
||||
count = ++topCount
|
||||
type = 'success'
|
||||
break
|
||||
case 'bottom':
|
||||
count = ++bottomCount
|
||||
type = 'warning'
|
||||
break
|
||||
case 'top-left':
|
||||
count = ++topLeftCount
|
||||
type = 'info'
|
||||
break
|
||||
case 'top-right':
|
||||
count = ++topRightCount
|
||||
type = 'primary'
|
||||
break
|
||||
case 'bottom-left':
|
||||
count = ++bottomLeftCount
|
||||
type = 'warning'
|
||||
break
|
||||
case 'bottom-right':
|
||||
count = ++bottomRightCount
|
||||
type = 'error'
|
||||
break
|
||||
}
|
||||
|
||||
ElMessage({
|
||||
message: `This is a message from the ${placement} ${count}`,
|
||||
type,
|
||||
placement,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -402,6 +402,7 @@ describe('config-provider', () => {
|
||||
})
|
||||
const overrideConfig = reactive({
|
||||
max: 1,
|
||||
placement: 'bottom-left',
|
||||
})
|
||||
const open = () => {
|
||||
ElMessage('this is a message.')
|
||||
@@ -420,7 +421,12 @@ describe('config-provider', () => {
|
||||
await wrapper.find('.el-button').trigger('click')
|
||||
await wrapper.find('.el-button').trigger('click')
|
||||
await nextTick()
|
||||
const messages = document.querySelectorAll('.el-message')
|
||||
expect(document.querySelectorAll('.el-message').length).toBe(1)
|
||||
|
||||
const classList = messages[0].classList
|
||||
expect(classList.contains('is-left')).toBe(true)
|
||||
expect(classList.contains('is-bottom')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ import { configProviderProps } from './config-provider-props'
|
||||
|
||||
import type { MessageConfigContext } from '@element-plus/components/message'
|
||||
|
||||
export const messageConfig: MessageConfigContext = {}
|
||||
export const messageConfig: MessageConfigContext = {
|
||||
placement: 'top',
|
||||
}
|
||||
|
||||
const ConfigProvider = defineComponent({
|
||||
name: 'ElConfigProvider',
|
||||
|
||||
@@ -51,6 +51,38 @@ describe('Message on command', () => {
|
||||
expect(document.querySelectorAll(selector).length).toBe(0)
|
||||
})
|
||||
|
||||
test('it should close all messages with mixed placement', async () => {
|
||||
const onClose = vi.fn()
|
||||
const instances = []
|
||||
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const instance = Message({
|
||||
duration: 0,
|
||||
placement: 'top',
|
||||
onClose,
|
||||
})
|
||||
instances.push(instance)
|
||||
}
|
||||
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const instance = Message({
|
||||
duration: 0,
|
||||
placement: 'bottom',
|
||||
onClose,
|
||||
})
|
||||
instances.push(instance)
|
||||
}
|
||||
|
||||
await rAF()
|
||||
const elements = document.querySelectorAll(selector)
|
||||
expect(elements.length).toBe(4)
|
||||
|
||||
Message.closeAll()
|
||||
await rAF()
|
||||
expect(onClose).toHaveBeenCalledTimes(4)
|
||||
expect(document.querySelectorAll(selector).length).toBe(0)
|
||||
})
|
||||
|
||||
test('it should close all messages of the specified type', async () => {
|
||||
const onClose = vi.fn()
|
||||
const instances = []
|
||||
@@ -84,6 +116,43 @@ describe('Message on command', () => {
|
||||
Message.closeAll()
|
||||
})
|
||||
|
||||
test('it should close all messages by specified placement', async () => {
|
||||
const onClose = vi.fn()
|
||||
const instances = []
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const instance = Message({
|
||||
duration: 0,
|
||||
placement: 'top',
|
||||
onClose,
|
||||
})
|
||||
instances.push(instance)
|
||||
}
|
||||
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const instance = Message({
|
||||
duration: 0,
|
||||
placement: 'bottom',
|
||||
onClose,
|
||||
})
|
||||
instances.push(instance)
|
||||
}
|
||||
|
||||
await rAF()
|
||||
const elements = document.querySelectorAll(selector)
|
||||
expect(elements.length).toBe(5)
|
||||
|
||||
Message.closeAllByPlacement('top')
|
||||
await rAF()
|
||||
expect(onClose).toHaveBeenCalledTimes(3)
|
||||
expect(document.querySelectorAll(selector).length).toBe(2)
|
||||
|
||||
Message.closeAllByPlacement('bottom')
|
||||
await rAF()
|
||||
expect(onClose).toHaveBeenCalledTimes(5)
|
||||
expect(document.querySelectorAll(selector).length).toBe(0)
|
||||
})
|
||||
|
||||
test('it should stack messages', async () => {
|
||||
const messages = [Message(), Message(), Message()]
|
||||
await rAF()
|
||||
|
||||
@@ -15,6 +15,8 @@ type MessageInstance = ComponentPublicInstance<{
|
||||
visible: boolean
|
||||
iconComponent: string | Component
|
||||
customStyle: CSSProperties
|
||||
placement?: 'top' | 'bottom'
|
||||
offset: number
|
||||
}>
|
||||
|
||||
const onClose = vi.fn()
|
||||
@@ -183,4 +185,77 @@ describe('Message.vue', () => {
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('placement', () => {
|
||||
test('should render with top placement by default', () => {
|
||||
const wrapper = _mount({
|
||||
slots: { default: AXIOM },
|
||||
})
|
||||
const vm = wrapper.vm as MessageInstance
|
||||
expect(vm.customStyle).toHaveProperty('top', '16px')
|
||||
expect(wrapper.classes()).not.toContain('is-bottom')
|
||||
})
|
||||
|
||||
test('should render with top-left placement', () => {
|
||||
const wrapper = _mount({
|
||||
slots: { default: AXIOM },
|
||||
props: {
|
||||
placement: 'top-left',
|
||||
},
|
||||
})
|
||||
const vm = wrapper.vm as MessageInstance
|
||||
expect(vm.customStyle).toHaveProperty('top', '16px')
|
||||
expect(vm.placement).toBe('top-left')
|
||||
})
|
||||
|
||||
test('should render with top-right placement', () => {
|
||||
const wrapper = _mount({
|
||||
slots: { default: AXIOM },
|
||||
props: {
|
||||
placement: 'top-right',
|
||||
},
|
||||
})
|
||||
const vm = wrapper.vm as MessageInstance
|
||||
expect(vm.customStyle).toHaveProperty('top', '16px')
|
||||
expect(vm.placement).toBe('top-right')
|
||||
})
|
||||
|
||||
test('should render with bottom placement', () => {
|
||||
const wrapper = _mount({
|
||||
slots: { default: AXIOM },
|
||||
props: {
|
||||
placement: 'bottom',
|
||||
},
|
||||
})
|
||||
const vm = wrapper.vm as MessageInstance
|
||||
expect(vm.customStyle).toHaveProperty('bottom', '16px')
|
||||
expect(wrapper.classes()).toContain('is-bottom')
|
||||
})
|
||||
|
||||
test('should render with bottom-left placement', () => {
|
||||
const wrapper = _mount({
|
||||
slots: { default: AXIOM },
|
||||
props: {
|
||||
placement: 'bottom-left',
|
||||
},
|
||||
})
|
||||
const vm = wrapper.vm as MessageInstance
|
||||
expect(vm.customStyle).toHaveProperty('bottom', '16px')
|
||||
expect(vm.placement).toBe('bottom-left')
|
||||
expect(wrapper.classes()).toContain('is-bottom')
|
||||
})
|
||||
|
||||
test('should render with bottom-right placement', () => {
|
||||
const wrapper = _mount({
|
||||
slots: { default: AXIOM },
|
||||
props: {
|
||||
placement: 'bottom-right',
|
||||
},
|
||||
})
|
||||
const vm = wrapper.vm as MessageInstance
|
||||
expect(vm.customStyle).toHaveProperty('bottom', '16px')
|
||||
expect(vm.placement).toBe('bottom-right')
|
||||
expect(wrapper.classes()).toContain('is-bottom')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import { shallowReactive } from 'vue'
|
||||
|
||||
import type { ComponentInternalInstance, VNode } from 'vue'
|
||||
import type { Mutable } from '@element-plus/utils'
|
||||
import type { MessageHandler, MessageProps } from './message'
|
||||
import type { MessageHandler, MessagePlacement, MessageProps } from './message'
|
||||
|
||||
export type MessageContext = {
|
||||
id: string
|
||||
@@ -12,9 +12,19 @@ export type MessageContext = {
|
||||
props: Mutable<MessageProps>
|
||||
}
|
||||
|
||||
export const instances: MessageContext[] = shallowReactive([])
|
||||
export const placementInstances = shallowReactive(
|
||||
{} as Record<MessagePlacement, MessageContext[]>
|
||||
)
|
||||
|
||||
export const getInstance = (id: string) => {
|
||||
export const getOrCreatePlacementInstances = (placement: MessagePlacement) => {
|
||||
if (!placementInstances[placement]) {
|
||||
placementInstances[placement] = shallowReactive([])
|
||||
}
|
||||
return placementInstances[placement]
|
||||
}
|
||||
|
||||
export const getInstance = (id: string, placement: MessagePlacement) => {
|
||||
const instances = placementInstances[placement] || []
|
||||
const idx = instances.findIndex((instance) => instance.id === id)
|
||||
const current = instances[idx]
|
||||
let prev: MessageContext | undefined
|
||||
@@ -24,13 +34,21 @@ export const getInstance = (id: string) => {
|
||||
return { current, prev }
|
||||
}
|
||||
|
||||
export const getLastOffset = (id: string): number => {
|
||||
const { prev } = getInstance(id)
|
||||
export const getLastOffset = (
|
||||
id: string,
|
||||
placement: MessagePlacement
|
||||
): number => {
|
||||
const { prev } = getInstance(id, placement)
|
||||
if (!prev) return 0
|
||||
return prev.vm.exposed!.bottom.value
|
||||
}
|
||||
|
||||
export const getOffsetOrSpace = (id: string, offset: number) => {
|
||||
export const getOffsetOrSpace = (
|
||||
id: string,
|
||||
offset: number,
|
||||
placement: MessagePlacement
|
||||
) => {
|
||||
const instances = placementInstances[placement] || []
|
||||
const idx = instances.findIndex((instance) => instance.id === id)
|
||||
return idx > 0 ? 16 : offset
|
||||
}
|
||||
|
||||
@@ -23,7 +23,19 @@ export const messageTypes = [
|
||||
'error',
|
||||
] as const
|
||||
|
||||
export const messagePlacement = [
|
||||
'top',
|
||||
'top-left',
|
||||
'top-right',
|
||||
'bottom',
|
||||
'bottom-left',
|
||||
'bottom-right',
|
||||
] as const
|
||||
|
||||
export const MESSAGE_DEFAULT_PLACEMENT = 'top'
|
||||
|
||||
export type MessageType = typeof messageTypes[number]
|
||||
export type MessagePlacement = typeof messagePlacement[number]
|
||||
/** @deprecated please use `MessageType` instead */
|
||||
export type messageType = MessageType // will be removed in 3.0.0.
|
||||
|
||||
@@ -34,6 +46,7 @@ export interface MessageConfigContext {
|
||||
offset?: number
|
||||
showClose?: boolean
|
||||
plain?: boolean
|
||||
placement?: string
|
||||
}
|
||||
|
||||
export const messageDefaults = mutable({
|
||||
@@ -48,6 +61,7 @@ export const messageDefaults = mutable({
|
||||
type: 'info',
|
||||
plain: false,
|
||||
offset: 16,
|
||||
placement: undefined,
|
||||
zIndex: 0,
|
||||
grouping: false,
|
||||
repeatNum: 1,
|
||||
@@ -137,6 +151,14 @@ export const messageProps = buildProps({
|
||||
type: Number,
|
||||
default: messageDefaults.offset,
|
||||
},
|
||||
/**
|
||||
* @description message placement position
|
||||
*/
|
||||
placement: {
|
||||
type: String,
|
||||
values: messagePlacement,
|
||||
default: messageDefaults.placement,
|
||||
},
|
||||
/**
|
||||
* @description input box size
|
||||
*/
|
||||
@@ -198,6 +220,7 @@ export interface MessageHandler {
|
||||
export type MessageFn = {
|
||||
(options?: MessageParams, appContext?: null | AppContext): MessageHandler
|
||||
closeAll(type?: MessageType): void
|
||||
closeAllByPlacement(position: MessagePlacement): void
|
||||
}
|
||||
export type MessageTypedFn = (
|
||||
options?: MessageParamsWithType,
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
{ [ns.m(type)]: type },
|
||||
ns.is('closable', showClose),
|
||||
ns.is('plain', plain),
|
||||
ns.is('bottom', verticalProperty === 'bottom'),
|
||||
horizontalClass,
|
||||
customClass,
|
||||
]"
|
||||
:style="customStyle"
|
||||
@@ -52,7 +54,11 @@ import { EVENT_CODE } from '@element-plus/constants'
|
||||
import ElBadge from '@element-plus/components/badge'
|
||||
import { useGlobalComponentSettings } from '@element-plus/components/config-provider'
|
||||
import { ElIcon } from '@element-plus/components/icon'
|
||||
import { messageEmits, messageProps } from './message'
|
||||
import {
|
||||
MESSAGE_DEFAULT_PLACEMENT,
|
||||
messageEmits,
|
||||
messageProps,
|
||||
} from './message'
|
||||
import { getLastOffset, getOffsetOrSpace } from './instance'
|
||||
|
||||
import type { BadgeProps } from '@element-plus/components/badge'
|
||||
@@ -89,13 +95,27 @@ const iconComponent = computed(
|
||||
() => props.icon || TypeComponentsMap[props.type] || ''
|
||||
)
|
||||
|
||||
const lastOffset = computed(() => getLastOffset(props.id))
|
||||
const offset = computed(
|
||||
() => getOffsetOrSpace(props.id, props.offset) + lastOffset.value
|
||||
)
|
||||
const placement = computed(() => props.placement || MESSAGE_DEFAULT_PLACEMENT)
|
||||
|
||||
const lastOffset = computed(() => getLastOffset(props.id, placement.value))
|
||||
const offset = computed(() => {
|
||||
return (
|
||||
getOffsetOrSpace(props.id, props.offset, placement.value) + lastOffset.value
|
||||
)
|
||||
})
|
||||
const bottom = computed(() => height.value + offset.value)
|
||||
const horizontalClass = computed(() => {
|
||||
if (placement.value.includes('left')) return ns.is('left')
|
||||
if (placement.value.includes('right')) return ns.is('right')
|
||||
return ns.is('center')
|
||||
})
|
||||
|
||||
const verticalProperty = computed(() =>
|
||||
placement.value.startsWith('top') ? 'top' : 'bottom'
|
||||
)
|
||||
|
||||
const customStyle = computed<CSSProperties>(() => ({
|
||||
top: `${offset.value}px`,
|
||||
[verticalProperty.value]: `${offset.value}px`,
|
||||
zIndex: currentZIndex.value,
|
||||
}))
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createVNode, isVNode, render } from 'vue'
|
||||
import {
|
||||
debugWarn,
|
||||
hasOwn,
|
||||
isBoolean,
|
||||
isClient,
|
||||
isElement,
|
||||
@@ -10,8 +11,13 @@ import {
|
||||
} from '@element-plus/utils'
|
||||
import { messageConfig } from '@element-plus/components/config-provider'
|
||||
import MessageConstructor from './message.vue'
|
||||
import { messageDefaults, messageTypes } from './message'
|
||||
import { instances } from './instance'
|
||||
import {
|
||||
MESSAGE_DEFAULT_PLACEMENT,
|
||||
messageDefaults,
|
||||
messagePlacement,
|
||||
messageTypes,
|
||||
} from './message'
|
||||
import { getOrCreatePlacementInstances, placementInstances } from './instance'
|
||||
|
||||
import type { MessageContext } from './instance'
|
||||
import type { AppContext } from 'vue'
|
||||
@@ -22,6 +28,7 @@ import type {
|
||||
MessageOptions,
|
||||
MessageParams,
|
||||
MessageParamsNormalized,
|
||||
MessagePlacement,
|
||||
MessageType,
|
||||
} from './message'
|
||||
|
||||
@@ -29,18 +36,9 @@ 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 normalizeOptions = (params?: MessageParams) => {
|
||||
const options: MessageOptions =
|
||||
!params || isString(params) || isVNode(params) || isFunction(params)
|
||||
? { message: params }
|
||||
: params
|
||||
|
||||
const normalized = {
|
||||
...messageDefaults,
|
||||
...options,
|
||||
}
|
||||
|
||||
if (!normalized.appendTo) {
|
||||
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)
|
||||
@@ -53,9 +51,48 @@ const normalizeOptions = (params?: MessageParams) => {
|
||||
)
|
||||
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,
|
||||
@@ -80,9 +117,11 @@ const normalizeOptions = (params?: MessageParams) => {
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -161,6 +200,9 @@ const message: MessageFn &
|
||||
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(
|
||||
@@ -191,17 +233,28 @@ messageTypes.forEach((type) => {
|
||||
})
|
||||
|
||||
export function closeAll(type?: MessageType): void {
|
||||
// Create a copy of instances to avoid modification during iteration
|
||||
const instancesToClose = [...instances]
|
||||
|
||||
for (const instance of instancesToClose) {
|
||||
if (!type || type === instance.props.type) {
|
||||
instance.handler.close()
|
||||
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
|
||||
|
||||
@@ -15,16 +15,27 @@
|
||||
border-style: getCssVar('border-style');
|
||||
border-color: getCssVar('message', 'border-color');
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 20px;
|
||||
transform: translateX(-50%);
|
||||
background-color: getCssVar('message', 'bg-color');
|
||||
transition: opacity getCssVar('transition-duration'), transform 0.4s, top 0.4s;
|
||||
transition: opacity getCssVar('transition-duration'), transform 0.4s, top 0.4s,
|
||||
bottom 0.4s;
|
||||
padding: getCssVar('message', 'padding');
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
&.is-left {
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
&.is-right {
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
&.is-center {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
@include when(plain) {
|
||||
background-color: getCssVar('bg-color', 'overlay');
|
||||
border-color: getCssVar('bg-color', 'overlay');
|
||||
@@ -90,5 +101,20 @@
|
||||
.#{$namespace}-message-fade-enter-from,
|
||||
.#{$namespace}-message-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -100%);
|
||||
|
||||
&:is(.is-left, .is-right) {
|
||||
transform: translateY(-100%);
|
||||
|
||||
&.is-bottom {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-center {
|
||||
transform: translate(-50%, -100%);
|
||||
|
||||
&.is-bottom {
|
||||
transform: translate(-50%, 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user