ant-design-vue/components/vc-notification/Notification.tsx
2022-05-21 10:56:28 +08:00

263 lines
7.8 KiB
Vue

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;