|
|
|
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<NoticeProps, 'prefixCls' | 'noticeKey' | 'onClose'> {
|
|
|
|
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<NotificationProps>({
|
|
|
|
name: 'Notification',
|
|
|
|
inheritAttrs: false,
|
|
|
|
props: ['prefixCls', 'transitionName', 'animation', 'maxCount', 'closeIcon'] as any,
|
|
|
|
setup(props, { attrs, expose, slots }) {
|
|
|
|
const hookRefs = new Map<Key, HTMLDivElement>();
|
|
|
|
const notices = ref<NotificationState>([]);
|
|
|
|
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 (
|
|
|
|
<div
|
|
|
|
key={key}
|
|
|
|
class={`${prefixCls}-hook-holder`}
|
|
|
|
ref={(div: HTMLDivElement) => {
|
|
|
|
if (typeof key === 'undefined') {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (div) {
|
|
|
|
hookRefs.set(key, div);
|
|
|
|
holderCallback(div, noticeProps);
|
|
|
|
} else {
|
|
|
|
hookRefs.delete(key);
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return (
|
|
|
|
<Notice {...noticeProps}>
|
|
|
|
{typeof content === 'function' ? content({ prefixCls }) : content}
|
|
|
|
</Notice>
|
|
|
|
);
|
|
|
|
});
|
|
|
|
const className = {
|
|
|
|
[prefixCls]: 1,
|
|
|
|
[attrs.class as string]: !!attrs.class,
|
|
|
|
};
|
|
|
|
return (
|
|
|
|
<div
|
|
|
|
class={className}
|
|
|
|
style={
|
|
|
|
(attrs.style as CSSProperties) || {
|
|
|
|
top: '65px',
|
|
|
|
left: '50%',
|
|
|
|
}
|
|
|
|
}
|
|
|
|
>
|
|
|
|
<TransitionGroup tag="div" {...transitionProps.value}>
|
|
|
|
{noticeNodes}
|
|
|
|
</TransitionGroup>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
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 (
|
|
|
|
<ConfigProvider {...global} notUpdateGlobalConfig={true} prefixCls={rootPrefixCls}>
|
|
|
|
<Notification
|
|
|
|
ref={notiRef}
|
|
|
|
{...attrs}
|
|
|
|
prefixCls={prefixCls}
|
|
|
|
transitionName={transitionName}
|
|
|
|
/>
|
|
|
|
</ConfigProvider>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
const vm = createVNode(Wrapper, props);
|
|
|
|
vm.appContext = appContext || vm.appContext;
|
|
|
|
vueRender(vm, div);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default Notification;
|