import { getTransitionGroupProps } from '../_util/transition'; import type { Key } from '../_util/type'; import type { CSSProperties } from 'vue'; import { createVNode, computed, defineComponent, ref, TransitionGroup, onMounted, render as vueRender, } from 'vue'; import type { NoticeProps } from './Notice'; import Notice from './Notice'; import ConfigProvider, { globalConfigForApi } from '../config-provider'; let seed = 0; const now = Date.now(); function getUuid() { const id = seed; seed += 1; return `rcNotification_${now}_${id}`; } export interface NoticeContent extends Omit { prefixCls?: string; key?: Key; updateMark?: string; content?: any; onClose?: () => void; style?: CSSProperties; class?: String; } export type NoticeFunc = (noticeProps: NoticeContent) => void; export type HolderReadyCallback = ( div: HTMLDivElement, noticeProps: NoticeProps & { key: Key }, ) => void; export interface NotificationInstance { notice: NoticeFunc; removeNotice: (key: Key) => void; destroy: () => void; component: Notification; } export interface NotificationProps { prefixCls?: string; transitionName?: string; animation?: string | object; maxCount?: number; closeIcon?: any; } type NotificationState = { notice: NoticeContent & { userPassKey?: Key; }; holderCallback?: HolderReadyCallback; }[]; const Notification = defineComponent({ name: 'Notification', inheritAttrs: false, props: ['prefixCls', 'transitionName', 'animation', 'maxCount', 'closeIcon'] as any, setup(props, { attrs, expose, slots }) { const hookRefs = new Map(); const notices = ref([]); const transitionProps = computed(() => { const { prefixCls, animation = 'fade' } = props; let name = props.transitionName; if (!name && animation) { name = `${prefixCls}-${animation}`; } return getTransitionGroupProps(name); }); const add = (originNotice: NoticeContent, holderCallback?: HolderReadyCallback) => { const key = originNotice.key || getUuid(); const notice: NoticeContent & { key: Key; userPassKey?: Key } = { ...originNotice, key, }; const { maxCount } = props; const noticeIndex = notices.value.map(v => v.notice.key).indexOf(key); const updatedNotices = notices.value.concat(); if (noticeIndex !== -1) { updatedNotices.splice(noticeIndex, 1, { notice, holderCallback } as any); } else { if (maxCount && notices.value.length >= maxCount) { // XXX, use key of first item to update new added (let React to move exsiting // instead of remove and mount). Same key was used before for both a) external // manual control and b) internal react 'key' prop , which is not that good. // eslint-disable-next-line no-param-reassign // zombieJ: Not know why use `updateKey`. This makes Notice infinite loop in jest. // Change to `updateMark` for compare instead. // https://github.com/react-component/notification/commit/32299e6be396f94040bfa82517eea940db947ece notice.key = updatedNotices[0].notice.key as Key; notice.updateMark = getUuid(); // zombieJ: That's why. User may close by key directly. // We need record this but not re-render to avoid upper issue // https://github.com/react-component/notification/issues/129 notice.userPassKey = key; updatedNotices.shift(); } updatedNotices.push({ notice, holderCallback } as any); } notices.value = updatedNotices; }; const remove = (removeKey: Key) => { notices.value = notices.value.filter(({ notice: { key, userPassKey } }) => { const mergedKey = userPassKey || key; return mergedKey !== removeKey; }); }; expose({ add, remove, notices, }); return () => { const { prefixCls, closeIcon = slots.closeIcon?.({ prefixCls }) } = props; const noticeNodes = notices.value.map(({ notice, holderCallback }, index) => { const updateMark = index === notices.value.length - 1 ? notice.updateMark : undefined; const { key, userPassKey } = notice; const { content } = notice; const noticeProps = { prefixCls, closeIcon: typeof closeIcon === 'function' ? closeIcon({ prefixCls }) : closeIcon, ...(notice as any), ...notice.props, key, noticeKey: userPassKey || key, updateMark, onClose: (noticeKey: Key) => { remove(noticeKey); notice.onClose?.(); }, onClick: notice.onClick, }; if (holderCallback) { return (
{ if (typeof key === 'undefined') { return; } if (div) { hookRefs.set(key, div); holderCallback(div, noticeProps); } else { hookRefs.delete(key); } }} /> ); } return ( {typeof content === 'function' ? content({ prefixCls }) : content} ); }); const className = { [prefixCls]: 1, [attrs.class as string]: !!attrs.class, }; return (
{noticeNodes}
); }; }, }); Notification.newInstance = function newNotificationInstance(properties, callback) { const { name = 'notification', getContainer, appContext, prefixCls: customizePrefixCls, rootPrefixCls: customRootPrefixCls, transitionName: customTransitionName, hasTransitionName, ...props } = properties || {}; const div = document.createElement('div'); if (getContainer) { const root = getContainer(); root.appendChild(div); } else { document.body.appendChild(div); } const Wrapper = defineComponent({ name: 'NotificationWrapper', setup(_props, { attrs }) { const notiRef = ref(); onMounted(() => { callback({ notice(noticeProps: NoticeContent) { notiRef.value?.add(noticeProps); }, removeNotice(key: Key) { notiRef.value?.remove(key); }, destroy() { vueRender(null, div); if (div.parentNode) { div.parentNode.removeChild(div); } }, component: notiRef, }); }); return () => { const global = globalConfigForApi; const prefixCls = global.getPrefixCls(name, customizePrefixCls); const rootPrefixCls = global.getRootPrefixCls(customRootPrefixCls, prefixCls); const transitionName = hasTransitionName ? customTransitionName : `${rootPrefixCls}-${customTransitionName}`; return ( ); }; }, }); const vm = createVNode(Wrapper, props); vm.appContext = appContext || vm.appContext; vueRender(vm, div); }; export default Notification;