mirror of
https://github.com/element-plus/element-plus.git
synced 2026-03-13 07:51:17 +08:00
* refactor: extract isClient and isNumber isBoolean * test: update test * refactor: extract isClient to browser
180 lines
5.4 KiB
TypeScript
180 lines
5.4 KiB
TypeScript
import { createVNode, render } from 'vue'
|
|
import {
|
|
debugWarn,
|
|
isClient,
|
|
isElement,
|
|
isString,
|
|
isVNode,
|
|
} from '@element-plus/utils'
|
|
import NotificationConstructor from './notification.vue'
|
|
import { notificationTypes } from './notification'
|
|
|
|
import type { AppContext, Ref, VNode } from 'vue'
|
|
import type {
|
|
NotificationOptions,
|
|
NotificationProps,
|
|
NotificationQueue,
|
|
Notify,
|
|
NotifyFn,
|
|
} from './notification'
|
|
|
|
// This should be a queue but considering there were `non-autoclosable` notifications.
|
|
const notifications: Record<
|
|
NotificationOptions['position'],
|
|
NotificationQueue
|
|
> = {
|
|
'top-left': [],
|
|
'top-right': [],
|
|
'bottom-left': [],
|
|
'bottom-right': [],
|
|
}
|
|
|
|
// the gap size between each notification
|
|
const GAP_SIZE = 16
|
|
let seed = 1
|
|
|
|
const notify: NotifyFn & Partial<Notify> & { _context: AppContext | null } =
|
|
function (options = {}, context: AppContext | null = null) {
|
|
if (!isClient) return { close: () => undefined }
|
|
|
|
if (typeof options === 'string' || isVNode(options)) {
|
|
options = { message: options }
|
|
}
|
|
|
|
const position = options.position || 'top-right'
|
|
|
|
let verticalOffset = options.offset || 0
|
|
notifications[position].forEach(({ vm }) => {
|
|
verticalOffset += (vm.el?.offsetHeight || 0) + GAP_SIZE
|
|
})
|
|
verticalOffset += GAP_SIZE
|
|
|
|
const id = `notification_${seed++}`
|
|
const userOnClose = options.onClose
|
|
const props: Partial<NotificationProps> = {
|
|
...options,
|
|
offset: verticalOffset,
|
|
id,
|
|
onClose: () => {
|
|
close(id, position, userOnClose)
|
|
},
|
|
}
|
|
|
|
let appendTo: HTMLElement | null = document.body
|
|
if (isElement(options.appendTo)) {
|
|
appendTo = options.appendTo
|
|
} else if (isString(options.appendTo)) {
|
|
appendTo = document.querySelector(options.appendTo)
|
|
}
|
|
|
|
// should fallback to default value with a warning
|
|
if (!isElement(appendTo)) {
|
|
debugWarn(
|
|
'ElNotification',
|
|
'the appendTo option is not an HTMLElement. Falling back to document.body.'
|
|
)
|
|
appendTo = document.body
|
|
}
|
|
|
|
const container = document.createElement('div')
|
|
|
|
const vm = createVNode(
|
|
NotificationConstructor,
|
|
props,
|
|
isVNode(props.message)
|
|
? {
|
|
default: () => props.message,
|
|
}
|
|
: null
|
|
)
|
|
vm.appContext = context ?? notify._context
|
|
|
|
// clean notification element preventing mem leak
|
|
vm.props!.onDestroy = () => {
|
|
render(null, container)
|
|
}
|
|
|
|
// instances will remove this item when close function gets called. So we do not need to worry about it.
|
|
render(vm, container)
|
|
notifications[position].push({ vm })
|
|
appendTo.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!.exposed as { visible: Ref<boolean> }).visible.value =
|
|
false
|
|
},
|
|
}
|
|
}
|
|
notificationTypes.forEach((type) => {
|
|
notify[type] = (options = {}) => {
|
|
if (typeof options === 'string' || isVNode(options)) {
|
|
options = {
|
|
message: options,
|
|
}
|
|
}
|
|
return notify({
|
|
...options,
|
|
type,
|
|
})
|
|
}
|
|
})
|
|
|
|
/**
|
|
* This function gets called when user click `x` button or press `esc` or the time reached its limitation.
|
|
* Emitted by transition@before-leave event so that we can fetch the current notification.offsetHeight, if this was called
|
|
* by @after-leave the DOM element will be removed from the page thus we can no longer fetch the offsetHeight.
|
|
* @param {String} id notification id to be closed
|
|
* @param {Position} position the positioning strategy
|
|
* @param {Function} userOnClose the callback called when close passed by user
|
|
*/
|
|
export function close(
|
|
id: string,
|
|
position: NotificationOptions['position'],
|
|
userOnClose?: (vm: VNode) => void
|
|
): void {
|
|
// maybe we can store the index when inserting the vm to notification list.
|
|
const orientedNotifications = notifications[position]
|
|
const idx = orientedNotifications.findIndex(
|
|
({ vm }) => vm.component?.props.id === id
|
|
)
|
|
if (idx === -1) return
|
|
const { vm } = orientedNotifications[idx]
|
|
if (!vm) return
|
|
// calling user's on close function before notification gets removed from DOM.
|
|
userOnClose?.(vm)
|
|
|
|
// note that this is called @before-leave, that's why we were able to fetch this property.
|
|
const removedHeight = vm.el!.offsetHeight
|
|
const verticalPos = position.split('-')[0]
|
|
orientedNotifications.splice(idx, 1)
|
|
const len = orientedNotifications.length
|
|
if (len < 1) return
|
|
// starting from the removing item.
|
|
for (let i = idx; i < len; i++) {
|
|
// new position equals the current offsetTop minus removed height plus 16px(the gap size between each item)
|
|
const { el, component } = orientedNotifications[i].vm
|
|
const pos =
|
|
Number.parseInt(el!.style[verticalPos], 10) - removedHeight - GAP_SIZE
|
|
component!.props.offset = pos
|
|
}
|
|
}
|
|
|
|
export function closeAll(): void {
|
|
// loop through all directions, close them at once.
|
|
for (const orientedNotifications of Object.values(notifications)) {
|
|
orientedNotifications.forEach(({ vm }) => {
|
|
// same as the previous close method, we'd like to make sure lifecycle gets handle properly.
|
|
;(vm.component!.exposed as { visible: Ref<boolean> }).visible.value =
|
|
false
|
|
})
|
|
}
|
|
}
|
|
|
|
notify.closeAll = closeAll
|
|
notify._context = null
|
|
|
|
export default notify as Notify
|