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;
|
||||
}
|
||||
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 };
|
||||
|
|
|
@ -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>
|
||||
`;
|
||||
|
||||
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`] = `
|
||||
<button class="ant-btn ant-btn-primary" type="button">
|
||||
<!----><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>
|
||||
<demo-sort>
|
||||
<Hook />
|
||||
<info />
|
||||
<duration />
|
||||
<other />
|
||||
|
@ -20,6 +21,7 @@ import customStyleVue from './custom-style.vue';
|
|||
import CN from '../index.zh-CN.md';
|
||||
import US from '../index.en-US.md';
|
||||
import { defineComponent } from 'vue';
|
||||
import Hook from './hook.vue';
|
||||
export default defineComponent({
|
||||
CN,
|
||||
US,
|
||||
|
@ -31,6 +33,7 @@ export default defineComponent({
|
|||
Thenable,
|
||||
Update,
|
||||
customStyleVue,
|
||||
Hook,
|
||||
},
|
||||
setup() {
|
||||
return {};
|
||||
|
|
|
@ -64,6 +64,7 @@ Methods for global configuration and destruction are also provided:
|
|||
|
||||
- `message.config(options)`
|
||||
- `message.destroy()`
|
||||
- `message.useMessage()`
|
||||
|
||||
#### message.config
|
||||
|
||||
|
@ -85,3 +86,33 @@ message.config({
|
|||
| prefixCls | The prefix className of message node | string | `ant-message` | 3.0 |
|
||||
| rtl | Whether to enable RTL mode | boolean | false | 3.0 |
|
||||
| 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 classNames from '../_util/classNames';
|
||||
import useStyle from './style';
|
||||
|
||||
import useMessage from './useMessage';
|
||||
let defaultDuration = 3;
|
||||
let defaultTop: string;
|
||||
let messageInstance: NotificationInstance;
|
||||
|
@ -70,6 +70,7 @@ function getMessageInstance(args: MessageArgsProps, callback: (i: NotificationIn
|
|||
callback(messageInstance);
|
||||
return;
|
||||
}
|
||||
|
||||
Notification.newInstance(
|
||||
{
|
||||
appContext: args.appContext,
|
||||
|
@ -225,7 +226,7 @@ export function attachTypeApi(originalApi: MessageApi, type: NoticeType) {
|
|||
typeList.forEach(type => attachTypeApi(api, type));
|
||||
|
||||
api.warn = api.warning;
|
||||
|
||||
api.useMessage = useMessage;
|
||||
export interface MessageInstance {
|
||||
info(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;
|
||||
loading(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
||||
open(args: MessageArgsProps): MessageType;
|
||||
useMessage: typeof useMessage;
|
||||
}
|
||||
|
||||
export interface MessageApi extends MessageInstance {
|
||||
|
|
|
@ -67,6 +67,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*7qMTRoq3ZGkAAA
|
|||
|
||||
- `message.config(options)`
|
||||
- `message.destroy()`
|
||||
- `message.useMessage()`
|
||||
|
||||
#### message.config
|
||||
|
||||
|
@ -88,3 +89,33 @@ message.config({
|
|||
| prefixCls | 消息节点的 className 前缀 | string | `ant-message` | 3.0 | |
|
||||
| rtl | 是否开启 RTL 模式 | boolean | false | | |
|
||||
| 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>
|
||||
<demo-sort>
|
||||
<hook />
|
||||
<basic />
|
||||
<duratioin />
|
||||
<with-icon />
|
||||
|
@ -12,6 +13,7 @@
|
|||
</template>
|
||||
<script lang="ts">
|
||||
import Basic from './basic.vue';
|
||||
import Hook from './hook.vue';
|
||||
import Duratioin from './duration.vue';
|
||||
import WithIcon from './with-icon.vue';
|
||||
import CustomIcon from './custom-icon.vue';
|
||||
|
@ -35,6 +37,7 @@ export default defineComponent({
|
|||
CustomStyle,
|
||||
Placement,
|
||||
Update,
|
||||
Hook,
|
||||
},
|
||||
setup() {
|
||||
return {};
|
||||
|
|
|
@ -26,6 +26,7 @@ To display a notification message at any of the four corners of the viewport. Ty
|
|||
- `notification.open(config)`
|
||||
- `notification.close(key: String)`
|
||||
- `notification.destroy()`
|
||||
- `notification.useNotification()`
|
||||
|
||||
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` | |
|
||||
| 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` | |
|
||||
|
||||
## 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 classNames from '../_util/classNames';
|
||||
import useStyle from './style';
|
||||
import useNotification from './useNotification';
|
||||
|
||||
export type NotificationPlacement =
|
||||
| 'top'
|
||||
| 'topLeft'
|
||||
|
@ -284,6 +286,7 @@ iconTypes.forEach(type => {
|
|||
});
|
||||
|
||||
api.warn = api.warning;
|
||||
api.useNotification = useNotification;
|
||||
|
||||
export interface NotificationInstance {
|
||||
success(args: NotificationArgsProps): void;
|
||||
|
@ -298,6 +301,7 @@ export interface NotificationApi extends NotificationInstance {
|
|||
close(key: string): void;
|
||||
config(options: ConfigProps): void;
|
||||
destroy(): void;
|
||||
useNotification: typeof useNotification;
|
||||
}
|
||||
|
||||
/** @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.close(key: String)`
|
||||
- `notification.destroy()`
|
||||
- `notification.useNotification()`
|
||||
|
||||
config 参数如下:
|
||||
|
||||
|
@ -75,3 +76,33 @@ notification.config({
|
|||
| placement | 弹出位置,可选 `topLeft` `topRight` `bottomLeft` `bottomRight` | string | topRight | |
|
||||
| rtl | 是否开启 RTL 模式 | boolean | false | 3.0 |
|
||||
| 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 type { Key } from '../_util/type';
|
||||
import type { Key, VueNode } from '../_util/type';
|
||||
import type { CSSProperties } from 'vue';
|
||||
import {
|
||||
shallowRef,
|
||||
createVNode,
|
||||
computed,
|
||||
defineComponent,
|
||||
|
@ -33,6 +34,14 @@ export interface NoticeContent extends Omit<NoticeProps, 'prefixCls' | 'noticeKe
|
|||
style?: CSSProperties;
|
||||
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 HolderReadyCallback = (
|
||||
|
@ -220,7 +229,7 @@ Notification.newInstance = function newNotificationInstance(properties, callback
|
|||
compatConfig: { MODE: 3 },
|
||||
name: 'NotificationWrapper',
|
||||
setup(_props, { attrs }) {
|
||||
const notiRef = ref();
|
||||
const notiRef = shallowRef();
|
||||
const prefixCls = computed(() => globalConfigForApi.getPrefixCls(name, customizePrefixCls));
|
||||
const [, hashId] = useStyle(prefixCls);
|
||||
onMounted(() => {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
// based on rc-notification 4.5.7
|
||||
import Notification from './Notification';
|
||||
|
||||
import useNotification from './useNotification';
|
||||
import type { NotificationAPI } from './useNotification';
|
||||
export { useNotification, NotificationAPI };
|
||||
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