Feat(use): add useMessage useNotification (#6527)
* feat(Message): add useMessage hook * feat(Notification): add useNotification hook * feat(Message): add Hook demo * feat(Notification): add Hook demo * test(Message): update demo snap * test(Notification): update demo snap * docs(Message): update docs with FAQ * docs(Notification): update docs with FAQpull/6538/head
parent
b61c88e5df
commit
6eb4d8f5c5
|
@ -78,5 +78,24 @@ export function renderHelper<T = Record<string, any>>(
|
||||||
}
|
}
|
||||||
return v ?? defaultV;
|
return v ?? defaultV;
|
||||||
}
|
}
|
||||||
|
export function wrapPromiseFn(openFn: (resolve: VoidFunction) => VoidFunction) {
|
||||||
|
let closeFn: VoidFunction;
|
||||||
|
|
||||||
|
const closePromise = new Promise<boolean>(resolve => {
|
||||||
|
closeFn = openFn(() => {
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const result: any = () => {
|
||||||
|
closeFn?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
result.then = (filled: VoidFunction, rejected: VoidFunction) =>
|
||||||
|
closePromise.then(filled, rejected);
|
||||||
|
result.promise = closePromise;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export { isOn, cacheStringFunction, camelize, hyphenate, capitalize, resolvePropValue };
|
export { isOn, cacheStringFunction, camelize, hyphenate, capitalize, resolvePropValue };
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
import Notice from '../vc-notification/Notice';
|
||||||
|
import type { NoticeProps } from '../vc-notification/Notice';
|
||||||
|
import useStyle from './style';
|
||||||
|
import type { NoticeType } from './interface';
|
||||||
|
import {
|
||||||
|
CheckCircleFilled,
|
||||||
|
CloseCircleFilled,
|
||||||
|
ExclamationCircleFilled,
|
||||||
|
InfoCircleFilled,
|
||||||
|
LoadingOutlined,
|
||||||
|
} from '@ant-design/icons-vue';
|
||||||
|
import type { VueNode } from '../_util/type';
|
||||||
|
import classNames from '../_util/classNames';
|
||||||
|
import { useConfigContextInject } from '../config-provider/context';
|
||||||
|
import { computed, defineComponent } from 'vue';
|
||||||
|
|
||||||
|
export const TypeIcon = {
|
||||||
|
info: <InfoCircleFilled />,
|
||||||
|
success: <CheckCircleFilled />,
|
||||||
|
error: <CloseCircleFilled />,
|
||||||
|
warning: <ExclamationCircleFilled />,
|
||||||
|
loading: <LoadingOutlined />,
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface PureContentProps {
|
||||||
|
prefixCls: string;
|
||||||
|
type?: NoticeType;
|
||||||
|
icon?: VueNode;
|
||||||
|
children: VueNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PureContent = defineComponent({
|
||||||
|
name: 'PureContent',
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: ['prefixCls', 'type', 'icon'] as any,
|
||||||
|
|
||||||
|
setup(props, { slots }) {
|
||||||
|
return () => (
|
||||||
|
<div
|
||||||
|
class={classNames(`${props.prefixCls}-custom-content`, `${props.prefixCls}-${props.type}`)}
|
||||||
|
>
|
||||||
|
{props.icon || TypeIcon[props.type!]}
|
||||||
|
<span>{slots.default?.()}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface PurePanelProps
|
||||||
|
extends Omit<NoticeProps, 'prefixCls' | 'eventKey'>,
|
||||||
|
Omit<PureContentProps, 'prefixCls' | 'children'> {
|
||||||
|
prefixCls?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @private Internal Component. Do not use in your production. */
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'PurePanel',
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: ['prefixCls', 'class', 'type', 'icon', 'content'] as any,
|
||||||
|
setup(props, { slots, attrs }) {
|
||||||
|
const { getPrefixCls } = useConfigContextInject();
|
||||||
|
const prefixCls = computed(() => props.staticPrefixCls || getPrefixCls('message'));
|
||||||
|
const [, hashId] = useStyle(prefixCls);
|
||||||
|
return (
|
||||||
|
<Notice
|
||||||
|
{...attrs}
|
||||||
|
prefixCls={prefixCls.value}
|
||||||
|
class={classNames(hashId, `${prefixCls.value}-notice-pure-panel`)}
|
||||||
|
noticeKey="pure"
|
||||||
|
duration={null}
|
||||||
|
>
|
||||||
|
<PureContent prefixCls={props.prefixCls} type={props.type} icon={props.icon}>
|
||||||
|
{slots.default?.()}
|
||||||
|
</PureContent>
|
||||||
|
</Notice>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -12,6 +12,14 @@ exports[`renders ./components/message/demo/duration.vue correctly 1`] = `
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`renders ./components/message/demo/hook.vue correctly 1`] = `
|
||||||
|
<!--teleport start-->
|
||||||
|
<!--teleport end-->
|
||||||
|
<button class="ant-btn ant-btn-primary" type="button">
|
||||||
|
<!----><span>Display normal message</span>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`renders ./components/message/demo/info.vue correctly 1`] = `
|
exports[`renders ./components/message/demo/info.vue correctly 1`] = `
|
||||||
<button class="ant-btn ant-btn-primary" type="button">
|
<button class="ant-btn ant-btn-primary" type="button">
|
||||||
<!----><span>Display normal message</span>
|
<!----><span>Display normal message</span>
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
<docs>
|
||||||
|
---
|
||||||
|
order: 10
|
||||||
|
title:
|
||||||
|
zh-CN: Hooks 调用(推荐)
|
||||||
|
en-US: Hooks Usage (Recommend)
|
||||||
|
---
|
||||||
|
|
||||||
|
## zh-CN
|
||||||
|
|
||||||
|
通过 `message.useMessage` 创建支持读取 context 的 `contextHolder`。请注意,我们推荐通过顶层注册的方式代替 `message` 静态方法,因为静态方法无法消费上下文,因而 ConfigProvider 的数据也不会生效。
|
||||||
|
|
||||||
|
## en-US
|
||||||
|
|
||||||
|
Use `message.useMessage` to get `contextHolder` with context accessible issue. Please note that, we recommend to use top level registration instead of `message` static method, because static method cannot consume context, and ConfigProvider data will not work.
|
||||||
|
|
||||||
|
</docs>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<contextHolder />
|
||||||
|
<a-button type="primary" @click="info">Display normal message</a-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
const [messageApi, contextHolder] = message.useMessage();
|
||||||
|
|
||||||
|
const info = () => {
|
||||||
|
messageApi.info('Hello, Ant Design!');
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<demo-sort>
|
<demo-sort>
|
||||||
|
<Hook />
|
||||||
<info />
|
<info />
|
||||||
<duration />
|
<duration />
|
||||||
<other />
|
<other />
|
||||||
|
@ -20,6 +21,7 @@ import customStyleVue from './custom-style.vue';
|
||||||
import CN from '../index.zh-CN.md';
|
import CN from '../index.zh-CN.md';
|
||||||
import US from '../index.en-US.md';
|
import US from '../index.en-US.md';
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
|
import Hook from './hook.vue';
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
CN,
|
CN,
|
||||||
US,
|
US,
|
||||||
|
@ -31,6 +33,7 @@ export default defineComponent({
|
||||||
Thenable,
|
Thenable,
|
||||||
Update,
|
Update,
|
||||||
customStyleVue,
|
customStyleVue,
|
||||||
|
Hook,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
return {};
|
return {};
|
||||||
|
|
|
@ -64,6 +64,7 @@ Methods for global configuration and destruction are also provided:
|
||||||
|
|
||||||
- `message.config(options)`
|
- `message.config(options)`
|
||||||
- `message.destroy()`
|
- `message.destroy()`
|
||||||
|
- `message.useMessage()`
|
||||||
|
|
||||||
#### message.config
|
#### message.config
|
||||||
|
|
||||||
|
@ -85,3 +86,33 @@ message.config({
|
||||||
| prefixCls | The prefix className of message node | string | `ant-message` | 3.0 |
|
| prefixCls | The prefix className of message node | string | `ant-message` | 3.0 |
|
||||||
| rtl | Whether to enable RTL mode | boolean | false | 3.0 |
|
| rtl | Whether to enable RTL mode | boolean | false | 3.0 |
|
||||||
| top | distance from top | string | `8px` | |
|
| top | distance from top | string | `8px` | |
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### Why I can not access context, Pinia, ConfigProvider `locale/prefixCls/theme` in message?
|
||||||
|
|
||||||
|
antdv will dynamic create Vue instance by `Vue.render` when call message methods. Whose context is different with origin code located context.
|
||||||
|
|
||||||
|
When you need context info (like ConfigProvider context), you can use `message.useMessage` to get `api` instance and `contextHolder` node. And put it in your children:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<template>
|
||||||
|
<contextHolder />
|
||||||
|
<!-- <component :is='contextHolder'/> -->
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
const [messageApi, contextHolder] = message.useMessage();
|
||||||
|
messageApi.open({
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** You must insert `contextHolder` into your children with hooks. You can use origin method if you do not need context connection.
|
||||||
|
|
||||||
|
> [App Package Component](/components/app) can be used to simplify the problem of `useMessage` and other methods that need to manually implant contextHolder.
|
||||||
|
|
||||||
|
### How to set static methods prefixCls ?
|
||||||
|
|
||||||
|
You can config with [`ConfigProvider.config`](/components/config-provider#configproviderconfig-4130)
|
||||||
|
|
|
@ -9,7 +9,7 @@ import type { Key, VueNode } from '../_util/type';
|
||||||
import type { NotificationInstance } from '../vc-notification/Notification';
|
import type { NotificationInstance } from '../vc-notification/Notification';
|
||||||
import classNames from '../_util/classNames';
|
import classNames from '../_util/classNames';
|
||||||
import useStyle from './style';
|
import useStyle from './style';
|
||||||
|
import useMessage from './useMessage';
|
||||||
let defaultDuration = 3;
|
let defaultDuration = 3;
|
||||||
let defaultTop: string;
|
let defaultTop: string;
|
||||||
let messageInstance: NotificationInstance;
|
let messageInstance: NotificationInstance;
|
||||||
|
@ -70,6 +70,7 @@ function getMessageInstance(args: MessageArgsProps, callback: (i: NotificationIn
|
||||||
callback(messageInstance);
|
callback(messageInstance);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Notification.newInstance(
|
Notification.newInstance(
|
||||||
{
|
{
|
||||||
appContext: args.appContext,
|
appContext: args.appContext,
|
||||||
|
@ -225,7 +226,7 @@ export function attachTypeApi(originalApi: MessageApi, type: NoticeType) {
|
||||||
typeList.forEach(type => attachTypeApi(api, type));
|
typeList.forEach(type => attachTypeApi(api, type));
|
||||||
|
|
||||||
api.warn = api.warning;
|
api.warn = api.warning;
|
||||||
|
api.useMessage = useMessage;
|
||||||
export interface MessageInstance {
|
export interface MessageInstance {
|
||||||
info(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
info(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
||||||
success(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
success(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
||||||
|
@ -233,6 +234,7 @@ export interface MessageInstance {
|
||||||
warning(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
warning(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
||||||
loading(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
loading(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
||||||
open(args: MessageArgsProps): MessageType;
|
open(args: MessageArgsProps): MessageType;
|
||||||
|
useMessage: typeof useMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageApi extends MessageInstance {
|
export interface MessageApi extends MessageInstance {
|
||||||
|
|
|
@ -67,6 +67,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*7qMTRoq3ZGkAAA
|
||||||
|
|
||||||
- `message.config(options)`
|
- `message.config(options)`
|
||||||
- `message.destroy()`
|
- `message.destroy()`
|
||||||
|
- `message.useMessage()`
|
||||||
|
|
||||||
#### message.config
|
#### message.config
|
||||||
|
|
||||||
|
@ -88,3 +89,33 @@ message.config({
|
||||||
| prefixCls | 消息节点的 className 前缀 | string | `ant-message` | 3.0 | |
|
| prefixCls | 消息节点的 className 前缀 | string | `ant-message` | 3.0 | |
|
||||||
| rtl | 是否开启 RTL 模式 | boolean | false | | |
|
| rtl | 是否开启 RTL 模式 | boolean | false | | |
|
||||||
| top | 消息距离顶部的位置 | string | `8px` | | |
|
| top | 消息距离顶部的位置 | string | `8px` | | |
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### 为什么 message 不能获取 context、Pinia 的内容和 ConfigProvider 的 `locale/prefixCls/theme` 等配置?
|
||||||
|
|
||||||
|
直接调用 message 方法,antdv 会通过 `Vue.render` 动态创建新的 Vue 实体。其 context 与当前代码所在 context 并不相同,因而无法获取 context 信息。
|
||||||
|
|
||||||
|
当你需要 context 信息(例如 ConfigProvider 配置的内容)时,可以通过 `message.useMessage` 方法会返回 `api` 实体以及 `contextHolder` 节点。将其插入到你需要获取 context 位置即可:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<template>
|
||||||
|
<contextHolder />
|
||||||
|
<!-- <component :is='contextHolder'/> -->
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
const [messageApi, contextHolder] = message.useMessage();
|
||||||
|
messageApi.open({
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
**异同**:通过 hooks 创建的 `contextHolder` 必须插入到子元素节点中才会生效,当你不需要上下文信息时请直接调用。
|
||||||
|
|
||||||
|
> 可通过 [App 包裹组件](/components/app-cn) 简化 `useMessage` 等方法需要手动植入 contextHolder 的问题。
|
||||||
|
|
||||||
|
### 静态方法如何设置 prefixCls ?
|
||||||
|
|
||||||
|
你可以通过 [`ConfigProvider.config`](/components/config-provider-cn#configproviderconfig-4130) 进行设置。
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
import type { CSSProperties } from 'vue';
|
||||||
|
import type { Key, VueNode } from '../_util/type';
|
||||||
|
|
||||||
|
export type NoticeType = 'info' | 'success' | 'error' | 'warning' | 'loading';
|
||||||
|
|
||||||
|
export interface ConfigOptions {
|
||||||
|
top?: number;
|
||||||
|
duration?: number;
|
||||||
|
prefixCls?: string;
|
||||||
|
getContainer?: () => HTMLElement;
|
||||||
|
transitionName?: string;
|
||||||
|
maxCount?: number;
|
||||||
|
rtl?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArgsProps {
|
||||||
|
content: VueNode;
|
||||||
|
duration?: number;
|
||||||
|
type?: NoticeType;
|
||||||
|
onClose?: () => void;
|
||||||
|
icon?: VueNode;
|
||||||
|
key?: string | number;
|
||||||
|
style?: CSSProperties;
|
||||||
|
className?: string;
|
||||||
|
onClick?: (e: Event) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type JointContent = VueNode | ArgsProps;
|
||||||
|
|
||||||
|
export interface MessageType extends PromiseLike<boolean> {
|
||||||
|
(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TypeOpen = (
|
||||||
|
content: JointContent,
|
||||||
|
duration?: number | VoidFunction, // Also can use onClose directly
|
||||||
|
onClose?: VoidFunction,
|
||||||
|
) => MessageType;
|
||||||
|
|
||||||
|
export interface MessageInstance {
|
||||||
|
info: TypeOpen;
|
||||||
|
success: TypeOpen;
|
||||||
|
error: TypeOpen;
|
||||||
|
warning: TypeOpen;
|
||||||
|
loading: TypeOpen;
|
||||||
|
open(args: ArgsProps): MessageType;
|
||||||
|
destroy(key?: Key): void;
|
||||||
|
}
|
|
@ -0,0 +1,228 @@
|
||||||
|
import { shallowRef, computed, defineComponent } from 'vue';
|
||||||
|
import { useNotification as useVcNotification } from '../vc-notification';
|
||||||
|
import type { NotificationAPI } from '../vc-notification';
|
||||||
|
import CloseOutlined from '@ant-design/icons-vue';
|
||||||
|
import useStyle from './style';
|
||||||
|
import type {
|
||||||
|
MessageInstance,
|
||||||
|
ArgsProps,
|
||||||
|
MessageType,
|
||||||
|
ConfigOptions,
|
||||||
|
NoticeType,
|
||||||
|
TypeOpen,
|
||||||
|
} from './interface';
|
||||||
|
|
||||||
|
import { PureContent } from './PurePanel';
|
||||||
|
import { getMotion } from '../vc-trigger/utils/motionUtil';
|
||||||
|
import type { Key } from '../_util/type';
|
||||||
|
import { wrapPromiseFn } from '../_util/util';
|
||||||
|
import type { VNode } from 'vue';
|
||||||
|
import useConfigInject from '../config-provider/hooks/useConfigInject';
|
||||||
|
import classNames from '../_util/classNames';
|
||||||
|
|
||||||
|
const DEFAULT_OFFSET = 8;
|
||||||
|
const DEFAULT_DURATION = 3;
|
||||||
|
|
||||||
|
// ==============================================================================
|
||||||
|
// == Holder ==
|
||||||
|
// ==============================================================================
|
||||||
|
type HolderProps = ConfigOptions & {
|
||||||
|
onAllRemoved?: VoidFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface HolderRef extends NotificationAPI {
|
||||||
|
prefixCls: string;
|
||||||
|
hashId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Holder = defineComponent({
|
||||||
|
name: 'Holder',
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: [
|
||||||
|
'top',
|
||||||
|
'prefixCls',
|
||||||
|
'getContainer',
|
||||||
|
'maxCount',
|
||||||
|
'duration',
|
||||||
|
'rtl',
|
||||||
|
'transitionName',
|
||||||
|
'onAllRemoved',
|
||||||
|
] as any,
|
||||||
|
setup(props, { expose }) {
|
||||||
|
const { getPrefixCls, getPopupContainer } = useConfigInject('message', props);
|
||||||
|
|
||||||
|
const prefixCls = computed(() => getPrefixCls('message', props.prefixCls));
|
||||||
|
|
||||||
|
const [, hashId] = useStyle(prefixCls);
|
||||||
|
|
||||||
|
// =============================== Style ===============================
|
||||||
|
const getStyles = () => ({
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
top: top ?? DEFAULT_OFFSET,
|
||||||
|
});
|
||||||
|
const getClassName = () => classNames(hashId.value, props.rtl ? `${prefixCls.value}-rtl` : '');
|
||||||
|
|
||||||
|
// ============================== Motion ===============================
|
||||||
|
const getNotificationMotion = () =>
|
||||||
|
getMotion({
|
||||||
|
prefixCls: prefixCls.value,
|
||||||
|
animation: props.animation ?? `move-up`,
|
||||||
|
transitionName: props.transitionName,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================ Close Icon =============================
|
||||||
|
const mergedCloseIcon = (
|
||||||
|
<span class={`${prefixCls.value}-close-x`}>
|
||||||
|
<CloseOutlined class={`${prefixCls.value}-close-icon`} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================== Origin ===============================
|
||||||
|
const [api, holder] = useVcNotification({
|
||||||
|
//@ts-ignore
|
||||||
|
getStyles,
|
||||||
|
prefixCls: prefixCls.value,
|
||||||
|
getClassName,
|
||||||
|
motion: getNotificationMotion,
|
||||||
|
closable: false,
|
||||||
|
closeIcon: mergedCloseIcon,
|
||||||
|
duration: props.duration ?? DEFAULT_DURATION,
|
||||||
|
getContainer: () =>
|
||||||
|
props.staticGetContainer?.() || getPopupContainer.value?.() || document.body,
|
||||||
|
maxCount: props.maxCount,
|
||||||
|
onAllRemoved: props.onAllRemoved,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ================================ Ref ================================
|
||||||
|
expose({
|
||||||
|
...api,
|
||||||
|
prefixCls,
|
||||||
|
hashId,
|
||||||
|
});
|
||||||
|
return holder;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==============================================================================
|
||||||
|
// == Hook ==
|
||||||
|
// ==============================================================================
|
||||||
|
let keyIndex = 0;
|
||||||
|
|
||||||
|
export function useInternalMessage(
|
||||||
|
messageConfig?: HolderProps,
|
||||||
|
): readonly [MessageInstance, () => VNode] {
|
||||||
|
const holderRef = shallowRef<HolderRef>(null);
|
||||||
|
// ================================ API ================================
|
||||||
|
const wrapAPI = computed(() => {
|
||||||
|
// Wrap with notification content
|
||||||
|
// >>> close
|
||||||
|
const close = (key: Key) => {
|
||||||
|
holderRef.value?.close(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
// >>> Open
|
||||||
|
const open = (config: ArgsProps): MessageType => {
|
||||||
|
if (!holderRef.value) {
|
||||||
|
const fakeResult: any = () => {};
|
||||||
|
fakeResult.then = () => {};
|
||||||
|
return fakeResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { open: originOpen, prefixCls, hashId } = holderRef.value;
|
||||||
|
const noticePrefixCls = `${prefixCls}-notice`;
|
||||||
|
const { content, icon, type, key, className, onClose, ...restConfig } = config;
|
||||||
|
|
||||||
|
let mergedKey: Key = key!;
|
||||||
|
if (mergedKey === undefined || mergedKey === null) {
|
||||||
|
keyIndex += 1;
|
||||||
|
mergedKey = `antd-message-${keyIndex}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrapPromiseFn(resolve => {
|
||||||
|
originOpen({
|
||||||
|
...restConfig,
|
||||||
|
key: mergedKey,
|
||||||
|
content: (
|
||||||
|
<PureContent prefixCls={prefixCls} type={type} icon={icon}>
|
||||||
|
{content}
|
||||||
|
</PureContent>
|
||||||
|
),
|
||||||
|
placement: 'top',
|
||||||
|
// @ts-ignore
|
||||||
|
class: classNames(type && `${noticePrefixCls}-${type}`, hashId, className),
|
||||||
|
onClose: () => {
|
||||||
|
onClose?.();
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return close function
|
||||||
|
return () => {
|
||||||
|
close(mergedKey);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// >>> destroy
|
||||||
|
const destroy = (key?: Key) => {
|
||||||
|
if (key !== undefined) {
|
||||||
|
close(key);
|
||||||
|
} else {
|
||||||
|
holderRef.value?.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clone = {
|
||||||
|
open,
|
||||||
|
destroy,
|
||||||
|
} as MessageInstance;
|
||||||
|
|
||||||
|
const keys: NoticeType[] = ['info', 'success', 'warning', 'error', 'loading'];
|
||||||
|
keys.forEach(type => {
|
||||||
|
const typeOpen: TypeOpen = (jointContent, duration, onClose) => {
|
||||||
|
let config: ArgsProps;
|
||||||
|
if (jointContent && typeof jointContent === 'object' && 'content' in jointContent) {
|
||||||
|
config = jointContent;
|
||||||
|
} else {
|
||||||
|
config = {
|
||||||
|
content: jointContent as VNode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Params
|
||||||
|
let mergedDuration: number | undefined;
|
||||||
|
let mergedOnClose: VoidFunction | undefined;
|
||||||
|
if (typeof duration === 'function') {
|
||||||
|
mergedOnClose = duration;
|
||||||
|
} else {
|
||||||
|
mergedDuration = duration;
|
||||||
|
mergedOnClose = onClose;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedConfig = {
|
||||||
|
onClose: mergedOnClose,
|
||||||
|
duration: mergedDuration,
|
||||||
|
...config,
|
||||||
|
type,
|
||||||
|
};
|
||||||
|
|
||||||
|
return open(mergedConfig);
|
||||||
|
};
|
||||||
|
|
||||||
|
clone[type] = typeOpen;
|
||||||
|
});
|
||||||
|
|
||||||
|
return clone;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================== Return ===============================
|
||||||
|
return [
|
||||||
|
wrapAPI.value,
|
||||||
|
() => <Holder key="message-holder" {...messageConfig} ref={holderRef} />,
|
||||||
|
] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useMessage(messageConfig?: ConfigOptions) {
|
||||||
|
return useInternalMessage(messageConfig);
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import useStyle from './style';
|
||||||
|
import useConfigInject from '../config-provider/hooks/useConfigInject';
|
||||||
|
import type { IconType } from './interface';
|
||||||
|
import Notice from '../vc-notification/Notice';
|
||||||
|
import classNames from '../_util/classNames';
|
||||||
|
import type { NoticeProps } from '../vc-notification/Notice';
|
||||||
|
import type { VueNode } from '../_util/type';
|
||||||
|
import {
|
||||||
|
CheckCircleOutlined,
|
||||||
|
CloseCircleOutlined,
|
||||||
|
CloseOutlined,
|
||||||
|
ExclamationCircleOutlined,
|
||||||
|
InfoCircleOutlined,
|
||||||
|
} from '@ant-design/icons-vue';
|
||||||
|
import { renderHelper } from '../_util/util';
|
||||||
|
|
||||||
|
export function getCloseIcon(prefixCls: string, closeIcon?: VueNode) {
|
||||||
|
return (
|
||||||
|
closeIcon || (
|
||||||
|
<span class={`${prefixCls}-close-x`}>
|
||||||
|
<CloseOutlined class={`${prefixCls}-close-icon`} />
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PureContentProps {
|
||||||
|
prefixCls: string;
|
||||||
|
icon?: VueNode;
|
||||||
|
message?: VueNode;
|
||||||
|
description?: VueNode;
|
||||||
|
btn?: VueNode;
|
||||||
|
type?: IconType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeToIcon = {
|
||||||
|
success: CheckCircleOutlined,
|
||||||
|
info: InfoCircleOutlined,
|
||||||
|
error: CloseCircleOutlined,
|
||||||
|
warning: ExclamationCircleOutlined,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PureContent({
|
||||||
|
prefixCls,
|
||||||
|
icon,
|
||||||
|
type,
|
||||||
|
message,
|
||||||
|
description,
|
||||||
|
btn,
|
||||||
|
}: PureContentProps) {
|
||||||
|
let iconNode = null;
|
||||||
|
if (icon) {
|
||||||
|
iconNode = <span class={`${prefixCls}-icon`}>{renderHelper(icon)}</span>;
|
||||||
|
} else if (type) {
|
||||||
|
const Icon = typeToIcon[type];
|
||||||
|
iconNode = <Icon class={`${prefixCls}-icon ${prefixCls}-icon-${type}`} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={classNames({
|
||||||
|
[`${prefixCls}-with-icon`]: iconNode,
|
||||||
|
})}
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
{iconNode}
|
||||||
|
<div class={`${prefixCls}-message`}>{message}</div>
|
||||||
|
<div class={`${prefixCls}-description`}>{description}</div>
|
||||||
|
{btn && <div class={`${prefixCls}-btn`}>{btn}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PurePanelProps
|
||||||
|
extends Omit<NoticeProps, 'prefixCls' | 'eventKey'>,
|
||||||
|
Omit<PureContentProps, 'prefixCls' | 'children'> {
|
||||||
|
prefixCls?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @private Internal Component. Do not use in your production. */
|
||||||
|
export default function PurePanel(props: PurePanelProps) {
|
||||||
|
const { getPrefixCls } = useConfigInject('notification', props);
|
||||||
|
const prefixCls = computed(() => props.prefixCls || getPrefixCls('notification'));
|
||||||
|
const noticePrefixCls = `${prefixCls.value}-notice`;
|
||||||
|
|
||||||
|
const [, hashId] = useStyle(prefixCls);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Notice
|
||||||
|
{...props}
|
||||||
|
prefixCls={prefixCls.value}
|
||||||
|
class={classNames(hashId.value, `${noticePrefixCls}-pure-panel`)}
|
||||||
|
noticeKey="pure"
|
||||||
|
duration={null}
|
||||||
|
closable={props.closable}
|
||||||
|
closeIcon={getCloseIcon(prefixCls.value, props.closeIcon)}
|
||||||
|
>
|
||||||
|
<PureContent
|
||||||
|
prefixCls={noticePrefixCls}
|
||||||
|
icon={props.icon}
|
||||||
|
type={props.type}
|
||||||
|
message={props.message}
|
||||||
|
description={props.description}
|
||||||
|
btn={props.btn}
|
||||||
|
/>
|
||||||
|
</Notice>
|
||||||
|
);
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,61 @@
|
||||||
|
<docs>
|
||||||
|
---
|
||||||
|
order: 10
|
||||||
|
title:
|
||||||
|
zh-CN: Hooks 调用(推荐)
|
||||||
|
en-US: Hooks Usage (Recommend)
|
||||||
|
---
|
||||||
|
|
||||||
|
## zh-CN
|
||||||
|
|
||||||
|
通过 `notification.useNotification` 创建支持读取 context 的 `contextHolder`。请注意,我们推荐通过顶层注册的方式代替 `message` 静态方法,因为静态方法无法消费上下文,因而 ConfigProvider 的数据也不会生效。
|
||||||
|
|
||||||
|
## en-US
|
||||||
|
|
||||||
|
Use `notification.useNotification` to get `contextHolder` with context accessible issue. Please note that, we recommend to use top level registration instead of `notification` static method, because static method cannot consume context, and ConfigProvider data will not work.
|
||||||
|
|
||||||
|
</docs>
|
||||||
|
<template>
|
||||||
|
<a-space>
|
||||||
|
<a-button type="primary" @click="() => open('topLeft')">
|
||||||
|
<RadiusUpleftOutlined />
|
||||||
|
topLeft
|
||||||
|
</a-button>
|
||||||
|
<a-button type="primary" @click="() => open('topRight')">
|
||||||
|
<RadiusUprightOutlined />
|
||||||
|
topRight
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
<a-divider />
|
||||||
|
<a-space>
|
||||||
|
<a-button type="primary" @click="() => open('bottomLeft')">
|
||||||
|
<RadiusBottomleftOutlined />
|
||||||
|
bottomLeft
|
||||||
|
</a-button>
|
||||||
|
<a-button type="primary" @click="() => open('bottomRight')">
|
||||||
|
<RadiusBottomrightOutlined />
|
||||||
|
bottomRight
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
<contextHolder />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {
|
||||||
|
RadiusBottomleftOutlined,
|
||||||
|
RadiusBottomrightOutlined,
|
||||||
|
RadiusUpleftOutlined,
|
||||||
|
RadiusUprightOutlined,
|
||||||
|
} from '@ant-design/icons-vue';
|
||||||
|
import { NotificationPlacement, notification } from 'ant-design-vue';
|
||||||
|
const [api, contextHolder] = notification.useNotification();
|
||||||
|
const open = (placement: NotificationPlacement) => openNotification(placement);
|
||||||
|
const openNotification = (placement: NotificationPlacement) => {
|
||||||
|
api.info({
|
||||||
|
message: `Notification ${placement}`,
|
||||||
|
description:
|
||||||
|
'This is the content of the notification. This is the content of the notification. This is the content of the notification.',
|
||||||
|
placement,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<demo-sort>
|
<demo-sort>
|
||||||
|
<hook />
|
||||||
<basic />
|
<basic />
|
||||||
<duratioin />
|
<duratioin />
|
||||||
<with-icon />
|
<with-icon />
|
||||||
|
@ -12,6 +13,7 @@
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Basic from './basic.vue';
|
import Basic from './basic.vue';
|
||||||
|
import Hook from './hook.vue';
|
||||||
import Duratioin from './duration.vue';
|
import Duratioin from './duration.vue';
|
||||||
import WithIcon from './with-icon.vue';
|
import WithIcon from './with-icon.vue';
|
||||||
import CustomIcon from './custom-icon.vue';
|
import CustomIcon from './custom-icon.vue';
|
||||||
|
@ -35,6 +37,7 @@ export default defineComponent({
|
||||||
CustomStyle,
|
CustomStyle,
|
||||||
Placement,
|
Placement,
|
||||||
Update,
|
Update,
|
||||||
|
Hook,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
return {};
|
return {};
|
||||||
|
|
|
@ -26,6 +26,7 @@ To display a notification message at any of the four corners of the viewport. Ty
|
||||||
- `notification.open(config)`
|
- `notification.open(config)`
|
||||||
- `notification.close(key: String)`
|
- `notification.close(key: String)`
|
||||||
- `notification.destroy()`
|
- `notification.destroy()`
|
||||||
|
- `notification.useNotification()`
|
||||||
|
|
||||||
The properties of config are as follows:
|
The properties of config are as follows:
|
||||||
|
|
||||||
|
@ -74,3 +75,33 @@ notification.config({
|
||||||
| placement | Position of Notification, can be one of `topLeft` `topRight` `bottomLeft` `bottomRight` | string | `topRight` | |
|
| placement | Position of Notification, can be one of `topLeft` `topRight` `bottomLeft` `bottomRight` | string | `topRight` | |
|
||||||
| rtl | Whether to enable RTL mode | boolean | false | |
|
| rtl | Whether to enable RTL mode | boolean | false | |
|
||||||
| top | Distance from the top of the viewport, when `placement` is `topRight` or `topLeft` (unit: pixels). | string | `24px` | |
|
| top | Distance from the top of the viewport, when `placement` is `topRight` or `topLeft` (unit: pixels). | string | `24px` | |
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### Why I can not access context, Pinia, ConfigProvider `locale/prefixCls/theme` in notification?
|
||||||
|
|
||||||
|
antdv will dynamic create Vue instance by `Vue.render` when call notification methods. Whose context is different with origin code located context.
|
||||||
|
|
||||||
|
When you need context info (like ConfigProvider context), you can use `notification.useNotification` to get `api` instance and `contextHolder` node. And put it in your children:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<template>
|
||||||
|
<contextHolder />
|
||||||
|
<!-- <component :is='contextHolder'/> -->
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { notification } from 'ant-design-vue';
|
||||||
|
const [notificationApi, contextHolder] = notification.useNotification();
|
||||||
|
notificationApi.open({
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** You must insert `contextHolder` into your children with hooks. You can use origin method if you do not need context connection.
|
||||||
|
|
||||||
|
> [App Package Component](/components/app) can be used to simplify the problem of `useNotification` and other methods that need to manually implant contextHolder.
|
||||||
|
|
||||||
|
### How to set static methods prefixCls ?
|
||||||
|
|
||||||
|
You can config with [`ConfigProvider.config`](/components/config-provider#configproviderconfig-4130)
|
||||||
|
|
|
@ -11,6 +11,8 @@ import { globalConfig } from '../config-provider';
|
||||||
import type { NotificationInstance as VCNotificationInstance } from '../vc-notification/Notification';
|
import type { NotificationInstance as VCNotificationInstance } from '../vc-notification/Notification';
|
||||||
import classNames from '../_util/classNames';
|
import classNames from '../_util/classNames';
|
||||||
import useStyle from './style';
|
import useStyle from './style';
|
||||||
|
import useNotification from './useNotification';
|
||||||
|
|
||||||
export type NotificationPlacement =
|
export type NotificationPlacement =
|
||||||
| 'top'
|
| 'top'
|
||||||
| 'topLeft'
|
| 'topLeft'
|
||||||
|
@ -284,6 +286,7 @@ iconTypes.forEach(type => {
|
||||||
});
|
});
|
||||||
|
|
||||||
api.warn = api.warning;
|
api.warn = api.warning;
|
||||||
|
api.useNotification = useNotification;
|
||||||
|
|
||||||
export interface NotificationInstance {
|
export interface NotificationInstance {
|
||||||
success(args: NotificationArgsProps): void;
|
success(args: NotificationArgsProps): void;
|
||||||
|
@ -298,6 +301,7 @@ export interface NotificationApi extends NotificationInstance {
|
||||||
close(key: string): void;
|
close(key: string): void;
|
||||||
config(options: ConfigProps): void;
|
config(options: ConfigProps): void;
|
||||||
destroy(): void;
|
destroy(): void;
|
||||||
|
useNotification: typeof useNotification;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @private test Only function. Not work on production */
|
/** @private test Only function. Not work on production */
|
||||||
|
|
|
@ -27,6 +27,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*9hTIToR-3YYAAA
|
||||||
- `notification.open(config)`
|
- `notification.open(config)`
|
||||||
- `notification.close(key: String)`
|
- `notification.close(key: String)`
|
||||||
- `notification.destroy()`
|
- `notification.destroy()`
|
||||||
|
- `notification.useNotification()`
|
||||||
|
|
||||||
config 参数如下:
|
config 参数如下:
|
||||||
|
|
||||||
|
@ -75,3 +76,33 @@ notification.config({
|
||||||
| placement | 弹出位置,可选 `topLeft` `topRight` `bottomLeft` `bottomRight` | string | topRight | |
|
| placement | 弹出位置,可选 `topLeft` `topRight` `bottomLeft` `bottomRight` | string | topRight | |
|
||||||
| rtl | 是否开启 RTL 模式 | boolean | false | 3.0 |
|
| rtl | 是否开启 RTL 模式 | boolean | false | 3.0 |
|
||||||
| top | 消息从顶部弹出时,距离顶部的位置,单位像素。 | string | `24px` | |
|
| top | 消息从顶部弹出时,距离顶部的位置,单位像素。 | string | `24px` | |
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### 为什么 notification 不能获取 context、Pinia 的内容和 ConfigProvider 的 `locale/prefixCls/theme` 等配置?
|
||||||
|
|
||||||
|
直接调用 notification 方法,antdv 会通过 `Vue.render` 动态创建新的 Vue 实体。其 context 与当前代码所在 context 并不相同,因而无法获取 context 信息。
|
||||||
|
|
||||||
|
当你需要 context 信息(例如 ConfigProvider 配置的内容)时,可以通过 `notification.useNotification` 方法会返回 `api` 实体以及 `contextHolder` 节点。将其插入到你需要获取 context 位置即可:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<template>
|
||||||
|
<contextHolder />
|
||||||
|
<!-- <component :is='contextHolder'/> -->
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { notification } from 'ant-design-vue';
|
||||||
|
const [notificationApi, contextHolder] = notification.useNotification();
|
||||||
|
notificationApi.open({
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
**异同**:通过 hooks 创建的 `contextHolder` 必须插入到子元素节点中才会生效,当你不需要上下文信息时请直接调用。
|
||||||
|
|
||||||
|
> 可通过 [App 包裹组件](/components/app-cn) 简化 `useNotification` 等方法需要手动植入 contextHolder 的问题。
|
||||||
|
|
||||||
|
### 静态方法如何设置 prefixCls ?
|
||||||
|
|
||||||
|
你可以通过 [`ConfigProvider.config`](/components/config-provider-cn#configproviderconfig-4130) 进行设置。
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
import type { CSSProperties } from 'vue';
|
||||||
|
import type { Key, VueNode } from '../_util/type';
|
||||||
|
|
||||||
|
export type NotificationPlacement =
|
||||||
|
| 'top'
|
||||||
|
| 'topLeft'
|
||||||
|
| 'topRight'
|
||||||
|
| 'bottom'
|
||||||
|
| 'bottomLeft'
|
||||||
|
| 'bottomRight';
|
||||||
|
|
||||||
|
export type IconType = 'success' | 'info' | 'error' | 'warning';
|
||||||
|
|
||||||
|
export interface ArgsProps {
|
||||||
|
message: VueNode;
|
||||||
|
description?: VueNode;
|
||||||
|
btn?: VueNode;
|
||||||
|
key?: Key;
|
||||||
|
onClose?: () => void;
|
||||||
|
duration?: number | null;
|
||||||
|
icon?: VueNode;
|
||||||
|
placement?: NotificationPlacement;
|
||||||
|
style?: CSSProperties;
|
||||||
|
className?: string;
|
||||||
|
readonly type?: IconType;
|
||||||
|
onClick?: () => void;
|
||||||
|
closeIcon?: VueNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
type StaticFn = (args: ArgsProps) => void;
|
||||||
|
|
||||||
|
export interface NotificationInstance {
|
||||||
|
success: StaticFn;
|
||||||
|
error: StaticFn;
|
||||||
|
info: StaticFn;
|
||||||
|
warning: StaticFn;
|
||||||
|
open: StaticFn;
|
||||||
|
destroy(key?: Key): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GlobalConfigProps {
|
||||||
|
top?: number;
|
||||||
|
bottom?: number;
|
||||||
|
duration?: number;
|
||||||
|
prefixCls?: string;
|
||||||
|
getContainer?: () => HTMLElement;
|
||||||
|
placement?: NotificationPlacement;
|
||||||
|
closeIcon?: VueNode;
|
||||||
|
rtl?: boolean;
|
||||||
|
maxCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotificationConfig {
|
||||||
|
top?: number;
|
||||||
|
bottom?: number;
|
||||||
|
prefixCls?: string;
|
||||||
|
getContainer?: () => HTMLElement;
|
||||||
|
placement?: NotificationPlacement;
|
||||||
|
maxCount?: number;
|
||||||
|
rtl?: boolean;
|
||||||
|
}
|
|
@ -0,0 +1,154 @@
|
||||||
|
import type { VNode } from 'vue';
|
||||||
|
import { shallowRef, computed, defineComponent } from 'vue';
|
||||||
|
import { useNotification as useVcNotification } from '../vc-notification';
|
||||||
|
import type { NotificationAPI } from '../vc-notification';
|
||||||
|
import type {
|
||||||
|
NotificationInstance,
|
||||||
|
ArgsProps,
|
||||||
|
NotificationPlacement,
|
||||||
|
NotificationConfig,
|
||||||
|
} from './interface';
|
||||||
|
|
||||||
|
import useStyle from './style';
|
||||||
|
import { getCloseIcon, PureContent } from './PurePanel';
|
||||||
|
import { getMotion, getPlacementStyle } from './util';
|
||||||
|
import useConfigInject from '../config-provider/hooks/useConfigInject';
|
||||||
|
import classNames from '../_util/classNames';
|
||||||
|
import type { Key } from '../_util/type';
|
||||||
|
|
||||||
|
const DEFAULT_OFFSET = 24;
|
||||||
|
const DEFAULT_DURATION = 4.5;
|
||||||
|
|
||||||
|
// ==============================================================================
|
||||||
|
// == Holder ==
|
||||||
|
// ==============================================================================
|
||||||
|
type HolderProps = NotificationConfig & {
|
||||||
|
onAllRemoved?: VoidFunction;
|
||||||
|
getPopupContainer?: () => HTMLElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface HolderRef extends NotificationAPI {
|
||||||
|
prefixCls: string;
|
||||||
|
hashId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Holder = defineComponent({
|
||||||
|
name: 'Holder',
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: ['prefixCls', 'class', 'type', 'icon', 'content', 'onAllRemoved'],
|
||||||
|
setup(props: HolderProps, { expose }) {
|
||||||
|
const { getPrefixCls, getPopupContainer } = useConfigInject('notification', props);
|
||||||
|
const prefixCls = computed(() => props.prefixCls || getPrefixCls('notification'));
|
||||||
|
// =============================== Style ===============================
|
||||||
|
const getStyles = (placement: NotificationPlacement) =>
|
||||||
|
getPlacementStyle(placement, props.top ?? DEFAULT_OFFSET, props.bottom ?? DEFAULT_OFFSET);
|
||||||
|
|
||||||
|
// Style
|
||||||
|
const [, hashId] = useStyle(prefixCls);
|
||||||
|
|
||||||
|
const getClassName = () => classNames(hashId.value, { [`${prefixCls.value}-rtl`]: props.rtl });
|
||||||
|
|
||||||
|
// ============================== Motion ===============================
|
||||||
|
const getNotificationMotion = () => getMotion(prefixCls.value);
|
||||||
|
|
||||||
|
// ============================== Origin ===============================
|
||||||
|
const [api, holder] = useVcNotification({
|
||||||
|
prefixCls: prefixCls.value,
|
||||||
|
getStyles,
|
||||||
|
getClassName,
|
||||||
|
motion: getNotificationMotion,
|
||||||
|
closable: true,
|
||||||
|
closeIcon: getCloseIcon(prefixCls.value),
|
||||||
|
duration: DEFAULT_DURATION,
|
||||||
|
getContainer: () =>
|
||||||
|
props.getPopupContainer?.() || getPopupContainer.value?.() || document.body,
|
||||||
|
maxCount: props.maxCount,
|
||||||
|
hashId: hashId.value,
|
||||||
|
onAllRemoved: props.onAllRemoved,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ================================ Ref ================================
|
||||||
|
expose({
|
||||||
|
...api,
|
||||||
|
prefixCls: prefixCls.value,
|
||||||
|
hashId,
|
||||||
|
});
|
||||||
|
return holder;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==============================================================================
|
||||||
|
// == Hook ==
|
||||||
|
// ==============================================================================
|
||||||
|
export function useInternalNotification(
|
||||||
|
notificationConfig?: HolderProps,
|
||||||
|
): readonly [NotificationInstance, () => VNode] {
|
||||||
|
const holderRef = shallowRef<HolderRef>(null);
|
||||||
|
|
||||||
|
// ================================ API ================================
|
||||||
|
const wrapAPI = computed(() => {
|
||||||
|
// Wrap with notification content
|
||||||
|
|
||||||
|
// >>> Open
|
||||||
|
const open = (config: ArgsProps) => {
|
||||||
|
if (!holderRef.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { open: originOpen, prefixCls, hashId } = holderRef.value;
|
||||||
|
const noticePrefixCls = `${prefixCls}-notice`;
|
||||||
|
|
||||||
|
const { message, description, icon, type, btn, className, ...restConfig } = config;
|
||||||
|
return originOpen({
|
||||||
|
placement: 'topRight',
|
||||||
|
...restConfig,
|
||||||
|
content: (
|
||||||
|
<PureContent
|
||||||
|
prefixCls={noticePrefixCls}
|
||||||
|
icon={icon}
|
||||||
|
type={type}
|
||||||
|
message={message}
|
||||||
|
description={description}
|
||||||
|
btn={btn}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
// @ts-ignore
|
||||||
|
class: classNames(type && `${noticePrefixCls}-${type}`, hashId, className),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// >>> destroy
|
||||||
|
const destroy = (key?: Key) => {
|
||||||
|
if (key !== undefined) {
|
||||||
|
holderRef.value?.close(key);
|
||||||
|
} else {
|
||||||
|
holderRef.value?.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clone = {
|
||||||
|
open,
|
||||||
|
destroy,
|
||||||
|
} as NotificationInstance;
|
||||||
|
|
||||||
|
const keys = ['success', 'info', 'warning', 'error'] as const;
|
||||||
|
keys.forEach(type => {
|
||||||
|
clone[type] = config =>
|
||||||
|
open({
|
||||||
|
...config,
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return clone;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================== Return ===============================
|
||||||
|
return [
|
||||||
|
wrapAPI.value,
|
||||||
|
() => <Holder key="notification-holder" {...notificationConfig} ref={holderRef} />,
|
||||||
|
] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useNotification(notificationConfig?: NotificationConfig) {
|
||||||
|
return useInternalNotification(notificationConfig);
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
import type { CSSProperties } from 'vue';
|
||||||
|
import type { NotificationPlacement } from './interface';
|
||||||
|
import type { CSSMotionProps } from '../_util/transition';
|
||||||
|
|
||||||
|
export function getPlacementStyle(
|
||||||
|
placement: NotificationPlacement,
|
||||||
|
top: number | string,
|
||||||
|
bottom: number | string,
|
||||||
|
) {
|
||||||
|
let style: CSSProperties;
|
||||||
|
top = typeof top === 'number' ? `${top}px` : top;
|
||||||
|
bottom = typeof bottom === 'number' ? `${bottom}px` : bottom;
|
||||||
|
switch (placement) {
|
||||||
|
case 'top':
|
||||||
|
style = {
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
right: 'auto',
|
||||||
|
top,
|
||||||
|
bottom: 'auto',
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'topLeft':
|
||||||
|
style = {
|
||||||
|
left: 0,
|
||||||
|
top,
|
||||||
|
bottom: 'auto',
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'topRight':
|
||||||
|
style = {
|
||||||
|
right: 0,
|
||||||
|
top,
|
||||||
|
bottom: 'auto',
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'bottom':
|
||||||
|
style = {
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
right: 'auto',
|
||||||
|
top: 'auto',
|
||||||
|
bottom,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'bottomLeft':
|
||||||
|
style = {
|
||||||
|
left: 0,
|
||||||
|
top: 'auto',
|
||||||
|
bottom,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
style = {
|
||||||
|
right: 0,
|
||||||
|
top: 'auto',
|
||||||
|
bottom,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMotion(prefixCls: string): CSSMotionProps {
|
||||||
|
return {
|
||||||
|
name: `${prefixCls}-fade`,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,230 @@
|
||||||
|
import type { CSSProperties } from 'vue';
|
||||||
|
import { watch, computed, defineComponent, ref, TransitionGroup } from 'vue';
|
||||||
|
import type { NoticeProps } from './Notice';
|
||||||
|
import Notice from './Notice';
|
||||||
|
import type { CSSMotionProps } from '../_util/transition';
|
||||||
|
import { getTransitionGroupProps } from '../_util/transition';
|
||||||
|
import type { Key, VueNode } from '../_util/type';
|
||||||
|
import classNames from '../_util/classNames';
|
||||||
|
import Portal from '../_util/Portal';
|
||||||
|
|
||||||
|
let seed = 0;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
export function getUuid() {
|
||||||
|
const id = seed;
|
||||||
|
seed += 1;
|
||||||
|
return `rcNotification_${now}_${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Placement = 'top' | 'topLeft' | 'topRight' | 'bottom' | 'bottomLeft' | 'bottomRight';
|
||||||
|
|
||||||
|
export interface OpenConfig extends NoticeProps {
|
||||||
|
key: Key;
|
||||||
|
placement?: Placement;
|
||||||
|
content?: VueNode;
|
||||||
|
duration?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Placements = Partial<Record<Placement, OpenConfig[]>>;
|
||||||
|
|
||||||
|
export interface NoticeContent extends Omit<NoticeProps, 'prefixCls' | 'noticeKey' | 'onClose'> {
|
||||||
|
prefixCls?: string;
|
||||||
|
key?: Key;
|
||||||
|
updateMark?: string;
|
||||||
|
content?: any;
|
||||||
|
onClose?: () => void;
|
||||||
|
style?: CSSProperties;
|
||||||
|
class?: String;
|
||||||
|
placement?: Placement;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
add: (noticeProps: NoticeContent) => void;
|
||||||
|
component: Notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HookNotificationProps {
|
||||||
|
prefixCls?: string;
|
||||||
|
transitionName?: string;
|
||||||
|
animation?: string | CSSMotionProps | ((placement?: Placement) => CSSMotionProps);
|
||||||
|
maxCount?: number;
|
||||||
|
closeIcon?: any;
|
||||||
|
hashId?: string;
|
||||||
|
// Hook Notification
|
||||||
|
remove: (key: Key) => void;
|
||||||
|
notices: NotificationState;
|
||||||
|
getStyles?: (placement?: Placement) => CSSProperties;
|
||||||
|
getClassName?: (placement?: Placement) => string;
|
||||||
|
onAllRemoved?: VoidFunction;
|
||||||
|
getContainer?: () => HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationState = {
|
||||||
|
notice: NoticeContent & {
|
||||||
|
userPassKey?: Key;
|
||||||
|
};
|
||||||
|
holderCallback?: HolderReadyCallback;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
const Notification = defineComponent<HookNotificationProps>({
|
||||||
|
name: 'HookNotification',
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: [
|
||||||
|
'prefixCls',
|
||||||
|
'transitionName',
|
||||||
|
'animation',
|
||||||
|
'maxCount',
|
||||||
|
'closeIcon',
|
||||||
|
'hashId',
|
||||||
|
'remove',
|
||||||
|
'notices',
|
||||||
|
'getStyles',
|
||||||
|
'getClassName',
|
||||||
|
'onAllRemoved',
|
||||||
|
'getContainer',
|
||||||
|
] as any,
|
||||||
|
setup(props, { attrs, slots }) {
|
||||||
|
const hookRefs = new Map<Key, HTMLDivElement>();
|
||||||
|
const notices = computed(() => props.notices);
|
||||||
|
const transitionProps = computed(() => {
|
||||||
|
let name = props.transitionName;
|
||||||
|
if (!name && props.animation) {
|
||||||
|
switch (typeof props.animation) {
|
||||||
|
case 'string':
|
||||||
|
name = props.animation;
|
||||||
|
break;
|
||||||
|
case 'function':
|
||||||
|
name = props.animation().name;
|
||||||
|
break;
|
||||||
|
case 'object':
|
||||||
|
name = props.animation.name;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
name = `${props.prefixCls}-fade`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getTransitionGroupProps(name);
|
||||||
|
});
|
||||||
|
|
||||||
|
const remove = (key: Key) => props.remove(key);
|
||||||
|
const placements = ref({} as Record<Placement, NotificationState>);
|
||||||
|
watch(notices, () => {
|
||||||
|
const nextPlacements = {} as any;
|
||||||
|
// init placements with animation
|
||||||
|
Object.keys(placements.value).forEach(placement => {
|
||||||
|
nextPlacements[placement] = [];
|
||||||
|
});
|
||||||
|
props.notices.forEach(config => {
|
||||||
|
const { placement = 'topRight' } = config.notice;
|
||||||
|
if (placement) {
|
||||||
|
nextPlacements[placement] = nextPlacements[placement] || [];
|
||||||
|
nextPlacements[placement].push(config);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
placements.value = nextPlacements;
|
||||||
|
});
|
||||||
|
|
||||||
|
const placementList = computed(() => Object.keys(placements.value) as Placement[]);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const { prefixCls, closeIcon = slots.closeIcon?.({ prefixCls }) } = props;
|
||||||
|
const noticeNodes = placementList.value.map(placement => {
|
||||||
|
const noticesForPlacement = placements.value[placement];
|
||||||
|
const classes = props.getClassName?.(placement);
|
||||||
|
const styles = props.getStyles?.(placement);
|
||||||
|
const noticeNodesForPlacement = noticesForPlacement.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} class={classNames(noticeProps.class, props.hashId)}>
|
||||||
|
{typeof content === 'function' ? content({ prefixCls }) : content}
|
||||||
|
</Notice>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const className = {
|
||||||
|
[prefixCls]: 1,
|
||||||
|
[`${prefixCls}-${placement}`]: 1,
|
||||||
|
[attrs.class as string]: !!attrs.class,
|
||||||
|
[props.hashId]: true,
|
||||||
|
[classes]: !!classes,
|
||||||
|
};
|
||||||
|
function onAfterLeave() {
|
||||||
|
if (noticesForPlacement.length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Reflect.deleteProperty(placements.value, placement);
|
||||||
|
props.onAllRemoved?.();
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={placement}
|
||||||
|
class={className}
|
||||||
|
style={
|
||||||
|
(attrs.style as CSSProperties) ||
|
||||||
|
styles || {
|
||||||
|
top: '65px',
|
||||||
|
left: '50%',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TransitionGroup tag="div" {...transitionProps.value} onAfterLeave={onAfterLeave}>
|
||||||
|
{noticeNodesForPlacement}
|
||||||
|
</TransitionGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return <Portal getContainer={props.getContainer}>{noticeNodes}</Portal>;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Notification;
|
|
@ -1,7 +1,8 @@
|
||||||
import { getTransitionGroupProps } from '../_util/transition';
|
import { getTransitionGroupProps } from '../_util/transition';
|
||||||
import type { Key } from '../_util/type';
|
import type { Key, VueNode } from '../_util/type';
|
||||||
import type { CSSProperties } from 'vue';
|
import type { CSSProperties } from 'vue';
|
||||||
import {
|
import {
|
||||||
|
shallowRef,
|
||||||
createVNode,
|
createVNode,
|
||||||
computed,
|
computed,
|
||||||
defineComponent,
|
defineComponent,
|
||||||
|
@ -33,6 +34,14 @@ export interface NoticeContent extends Omit<NoticeProps, 'prefixCls' | 'noticeKe
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
class?: String;
|
class?: String;
|
||||||
}
|
}
|
||||||
|
export type Placement = 'top' | 'topLeft' | 'topRight' | 'bottom' | 'bottomLeft' | 'bottomRight';
|
||||||
|
|
||||||
|
export interface OpenConfig extends NoticeProps {
|
||||||
|
key: Key;
|
||||||
|
placement?: Placement;
|
||||||
|
content?: VueNode;
|
||||||
|
duration?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
export type NoticeFunc = (noticeProps: NoticeContent) => void;
|
export type NoticeFunc = (noticeProps: NoticeContent) => void;
|
||||||
export type HolderReadyCallback = (
|
export type HolderReadyCallback = (
|
||||||
|
@ -220,7 +229,7 @@ Notification.newInstance = function newNotificationInstance(properties, callback
|
||||||
compatConfig: { MODE: 3 },
|
compatConfig: { MODE: 3 },
|
||||||
name: 'NotificationWrapper',
|
name: 'NotificationWrapper',
|
||||||
setup(_props, { attrs }) {
|
setup(_props, { attrs }) {
|
||||||
const notiRef = ref();
|
const notiRef = shallowRef();
|
||||||
const prefixCls = computed(() => globalConfigForApi.getPrefixCls(name, customizePrefixCls));
|
const prefixCls = computed(() => globalConfigForApi.getPrefixCls(name, customizePrefixCls));
|
||||||
const [, hashId] = useStyle(prefixCls);
|
const [, hashId] = useStyle(prefixCls);
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
// based on rc-notification 4.5.7
|
|
||||||
import Notification from './Notification';
|
import Notification from './Notification';
|
||||||
|
import useNotification from './useNotification';
|
||||||
|
import type { NotificationAPI } from './useNotification';
|
||||||
|
export { useNotification, NotificationAPI };
|
||||||
export default Notification;
|
export default Notification;
|
||||||
|
|
|
@ -0,0 +1,184 @@
|
||||||
|
import type { CSSProperties } from 'vue';
|
||||||
|
import { shallowRef, watch, ref, computed } from 'vue';
|
||||||
|
import HookNotification, { getUuid } from './HookNotification';
|
||||||
|
import type { NotificationInstance, OpenConfig, Placement } from './Notification';
|
||||||
|
import type { CSSMotionProps } from '../_util/transition';
|
||||||
|
import type { Key, VueNode } from '../_util/type';
|
||||||
|
import type { HolderReadyCallback, NoticeContent } from './HookNotification';
|
||||||
|
|
||||||
|
const defaultGetContainer = () => document.body;
|
||||||
|
|
||||||
|
type OptionalConfig = Partial<OpenConfig>;
|
||||||
|
|
||||||
|
export interface NotificationConfig {
|
||||||
|
prefixCls?: string;
|
||||||
|
/** Customize container. It will repeat call which means you should return same container element. */
|
||||||
|
getContainer?: () => HTMLElement;
|
||||||
|
motion?: CSSMotionProps | ((placement?: Placement) => CSSMotionProps);
|
||||||
|
closeIcon?: VueNode;
|
||||||
|
closable?: boolean;
|
||||||
|
maxCount?: number;
|
||||||
|
duration?: number;
|
||||||
|
/** @private. Config for notification holder style. Safe to remove if refactor */
|
||||||
|
getClassName?: (placement?: Placement) => string;
|
||||||
|
/** @private. Config for notification holder style. Safe to remove if refactor */
|
||||||
|
getStyles?: (placement?: Placement) => CSSProperties;
|
||||||
|
/** @private Trigger when all the notification closed. */
|
||||||
|
onAllRemoved?: VoidFunction;
|
||||||
|
hashId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotificationAPI {
|
||||||
|
open: (config: OptionalConfig) => void;
|
||||||
|
close: (key: Key) => void;
|
||||||
|
destroy: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OpenTask {
|
||||||
|
type: 'open';
|
||||||
|
config: OpenConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CloseTask {
|
||||||
|
type: 'close';
|
||||||
|
key: Key;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DestroyTask {
|
||||||
|
type: 'destroy';
|
||||||
|
}
|
||||||
|
|
||||||
|
type Task = OpenTask | CloseTask | DestroyTask;
|
||||||
|
|
||||||
|
let uniqueKey = 0;
|
||||||
|
|
||||||
|
function mergeConfig<T>(...objList: Partial<T>[]): T {
|
||||||
|
const clone: T = {} as T;
|
||||||
|
|
||||||
|
objList.forEach(obj => {
|
||||||
|
if (obj) {
|
||||||
|
Object.keys(obj).forEach(key => {
|
||||||
|
const val = obj[key];
|
||||||
|
|
||||||
|
if (val !== undefined) {
|
||||||
|
clone[key] = val;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useNotification(rootConfig: NotificationConfig = {}) {
|
||||||
|
const {
|
||||||
|
getContainer = defaultGetContainer,
|
||||||
|
motion,
|
||||||
|
prefixCls,
|
||||||
|
maxCount,
|
||||||
|
getClassName,
|
||||||
|
getStyles,
|
||||||
|
onAllRemoved,
|
||||||
|
...shareConfig
|
||||||
|
} = rootConfig;
|
||||||
|
|
||||||
|
const notices = ref([]);
|
||||||
|
const notificationsRef = shallowRef<NotificationInstance>();
|
||||||
|
const add = (originNotice: NoticeContent, holderCallback?: HolderReadyCallback) => {
|
||||||
|
const key = originNotice.key || getUuid();
|
||||||
|
const notice: NoticeContent & { key: Key; userPassKey?: Key } = {
|
||||||
|
...originNotice,
|
||||||
|
key,
|
||||||
|
};
|
||||||
|
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) {
|
||||||
|
notice.key = updatedNotices[0].notice.key as Key;
|
||||||
|
notice.updateMark = getUuid();
|
||||||
|
notice.userPassKey = key;
|
||||||
|
updatedNotices.shift();
|
||||||
|
}
|
||||||
|
updatedNotices.push({ notice, holderCallback } as any);
|
||||||
|
}
|
||||||
|
notices.value = updatedNotices;
|
||||||
|
};
|
||||||
|
const removeNotice = (removeKey: Key) => {
|
||||||
|
notices.value = notices.value.filter(({ notice: { key, userPassKey } }) => {
|
||||||
|
const mergedKey = userPassKey || key;
|
||||||
|
return mergedKey !== removeKey;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const destroy = () => {
|
||||||
|
notices.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const contextHolder = computed(() => (
|
||||||
|
<HookNotification
|
||||||
|
ref={notificationsRef}
|
||||||
|
prefixCls={prefixCls}
|
||||||
|
maxCount={maxCount}
|
||||||
|
notices={notices.value}
|
||||||
|
remove={removeNotice}
|
||||||
|
getClassName={getClassName}
|
||||||
|
getStyles={getStyles}
|
||||||
|
animation={motion}
|
||||||
|
hashId={rootConfig.hashId}
|
||||||
|
onAllRemoved={onAllRemoved}
|
||||||
|
getContainer={getContainer}
|
||||||
|
></HookNotification>
|
||||||
|
));
|
||||||
|
|
||||||
|
const taskQueue = ref([] as Task[]);
|
||||||
|
// ========================= Refs =========================
|
||||||
|
const api = computed(() => {
|
||||||
|
return {
|
||||||
|
open: (config: OpenConfig) => {
|
||||||
|
const mergedConfig = mergeConfig(shareConfig, config);
|
||||||
|
//@ts-ignore
|
||||||
|
if (mergedConfig.key === null || mergedConfig.key === undefined) {
|
||||||
|
//@ts-ignore
|
||||||
|
mergedConfig.key = `vc-notification-${uniqueKey}`;
|
||||||
|
uniqueKey += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
taskQueue.value = [...taskQueue.value, { type: 'open', config: mergedConfig as any }];
|
||||||
|
},
|
||||||
|
close: key => {
|
||||||
|
taskQueue.value = [...taskQueue.value, { type: 'close', key }];
|
||||||
|
},
|
||||||
|
destroy: () => {
|
||||||
|
taskQueue.value = [...taskQueue.value, { type: 'destroy' }];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// ======================== Effect ========================
|
||||||
|
watch(taskQueue, () => {
|
||||||
|
// Flush task when node ready
|
||||||
|
if (taskQueue.value.length) {
|
||||||
|
taskQueue.value.forEach(task => {
|
||||||
|
switch (task.type) {
|
||||||
|
case 'open':
|
||||||
|
// @ts-ignore
|
||||||
|
add(task.config);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'close':
|
||||||
|
removeNotice(task.key);
|
||||||
|
break;
|
||||||
|
case 'destroy':
|
||||||
|
destroy();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
taskQueue.value = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ======================== Return ========================
|
||||||
|
return [api.value, () => contextHolder.value] as const;
|
||||||
|
}
|
Loading…
Reference in New Issue