UNPKG

element-plus

Version:

A Component Library for Vue 3

160 lines (141 loc) 4.99 kB
import { createVNode, render } from 'vue' import isServer from '@element-plus/utils/isServer' import PopupManager from '@element-plus/utils/popup-manager' import { isVNode } from '@element-plus/utils/util' import NotificationConstructor from './notification.vue' import { notificationTypes } from './notification' import type { ComponentPublicInstance, VNode } from 'vue' import type { NotificationOptions, Notify, NotifyFn, NotificationQueue, NotificationProps, } 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> = function (options = {}) { if (isServer) 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> = { // default options end zIndex: PopupManager.nextZIndex(), offset: verticalOffset, ...options, id, onClose: () => { close(id, position, userOnClose) }, } const container = document.createElement('div') const vm = createVNode( NotificationConstructor, props, isVNode(props.message) ? { default: () => props.message, } : null ) // 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 }) 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 }, } } 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 = 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!.proxy as ComponentPublicInstance<{ visible: boolean }> ).visible = false }) } } notify.closeAll = closeAll export default notify as Notify