refactor: message & notification (#5113)
* refactor: notification * refactor: message * refactor: notification * test: update message & notification testrefactor-modal
parent
8a3724ff89
commit
ad76bb678d
|
@ -114,6 +114,7 @@ export { default as Modal } from './modal';
|
||||||
export type { StatisticProps } from './statistic';
|
export type { StatisticProps } from './statistic';
|
||||||
export { default as Statistic, StatisticCountdown } from './statistic';
|
export { default as Statistic, StatisticCountdown } from './statistic';
|
||||||
|
|
||||||
|
export type { NotificationPlacement } from './notification';
|
||||||
export { default as notification } from './notification';
|
export { default as notification } from './notification';
|
||||||
|
|
||||||
export type { PageHeaderProps } from './page-header';
|
export type { PageHeaderProps } from './page-header';
|
||||||
|
|
|
@ -9,6 +9,8 @@ import type { TransformCellTextProps } from '../table/interface';
|
||||||
import LocaleReceiver from '../locale-provider/LocaleReceiver';
|
import LocaleReceiver from '../locale-provider/LocaleReceiver';
|
||||||
import type { RequiredMark } from '../form/Form';
|
import type { RequiredMark } from '../form/Form';
|
||||||
import type { MaybeRef } from '../_util/type';
|
import type { MaybeRef } from '../_util/type';
|
||||||
|
import message from '../message';
|
||||||
|
import notification from '../notification';
|
||||||
|
|
||||||
export type SizeType = 'small' | 'middle' | 'large' | undefined;
|
export type SizeType = 'small' | 'middle' | 'large' | undefined;
|
||||||
|
|
||||||
|
@ -248,6 +250,17 @@ const ConfigProvider = defineComponent({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (props.direction) {
|
||||||
|
message.config({
|
||||||
|
rtl: props.direction === 'rtl',
|
||||||
|
});
|
||||||
|
notification.config({
|
||||||
|
rtl: props.direction === 'rtl',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return () => (
|
return () => (
|
||||||
<LocaleReceiver children={(_, __, legacyLocale) => renderProvider(legacyLocale as Locale)} />
|
<LocaleReceiver children={(_, __, legacyLocale) => renderProvider(legacyLocale as Locale)} />
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`renders ./components/message/demo/custom-style.vue correctly 1`] = `
|
||||||
|
<button class="ant-btn" type="button">
|
||||||
|
<!----><span>Customized style</span>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`renders ./components/message/demo/duration.vue correctly 1`] = `
|
exports[`renders ./components/message/demo/duration.vue correctly 1`] = `
|
||||||
<button class="ant-btn" type="button">
|
<button class="ant-btn" type="button">
|
||||||
<!----><span>Customized display duration</span>
|
<!----><span>Customized display duration</span>
|
||||||
|
@ -36,6 +42,11 @@ exports[`renders ./components/message/demo/thenable.vue correctly 1`] = `
|
||||||
|
|
||||||
exports[`renders ./components/message/demo/update.vue correctly 1`] = `
|
exports[`renders ./components/message/demo/update.vue correctly 1`] = `
|
||||||
<button class="ant-btn ant-btn-primary" type="button">
|
<button class="ant-btn ant-btn-primary" type="button">
|
||||||
<!----><span>Open the message box</span>
|
<!----><span>Open the message box (update by key)</span>
|
||||||
|
</button>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<button class="ant-btn ant-btn-primary" type="button">
|
||||||
|
<!----><span>Open the message box (update by reactive)</span>
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { asyncExpect } from '../../../tests/utils';
|
import { asyncExpect } from '../../../tests/utils';
|
||||||
import message from '..';
|
import message, { getInstance } from '..';
|
||||||
import SmileOutlined from '@ant-design/icons-vue/SmileOutlined';
|
import SmileOutlined from '@ant-design/icons-vue/SmileOutlined';
|
||||||
|
|
||||||
describe('message', () => {
|
describe('message', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
document.body.outerHTML = '';
|
document.body.outerHTML = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -11,6 +12,11 @@ describe('message', () => {
|
||||||
message.destroy();
|
message.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
message.destroy();
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
it('should be able to config top', async () => {
|
it('should be able to config top', async () => {
|
||||||
message.config({
|
message.config({
|
||||||
top: '100px',
|
top: '100px',
|
||||||
|
@ -41,53 +47,42 @@ describe('message', () => {
|
||||||
message.info('test');
|
message.info('test');
|
||||||
}
|
}
|
||||||
message.info('last');
|
message.info('last');
|
||||||
await asyncExpect(() => {
|
await Promise.resolve();
|
||||||
|
jest.runAllTimers();
|
||||||
expect(document.querySelectorAll('.ant-message-notice').length).toBe(5);
|
expect(document.querySelectorAll('.ant-message-notice').length).toBe(5);
|
||||||
expect(document.querySelectorAll('.ant-message-notice')[4].textContent).toBe('last');
|
expect(document.querySelectorAll('.ant-message-notice')[4].textContent).toBe('last');
|
||||||
}, 0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to hide manually', async () => {
|
it('should be able to hide manually', async () => {
|
||||||
const hide1 = message.info('whatever', 0);
|
const hide1 = message.info('whatever', 0);
|
||||||
const hide2 = message.info('whatever', 0);
|
const hide2 = message.info('whatever', 0);
|
||||||
await asyncExpect(() => {
|
await Promise.resolve();
|
||||||
expect(document.querySelectorAll('.ant-message-notice').length).toBe(2);
|
expect(document.querySelectorAll('.ant-message-notice').length).toBe(2);
|
||||||
hide1();
|
hide1();
|
||||||
}, 0);
|
jest.runAllTimers();
|
||||||
await asyncExpect(() => {
|
expect(getInstance().component.value.notices).toHaveLength(1);
|
||||||
expect(document.querySelectorAll('.ant-message-notice').length).toBe(1);
|
|
||||||
hide2();
|
hide2();
|
||||||
}, 0);
|
jest.runAllTimers();
|
||||||
await asyncExpect(() => {
|
expect(getInstance().component.value.notices).toHaveLength(0);
|
||||||
expect(document.querySelectorAll('.ant-message-notice').length).toBe(0);
|
|
||||||
}, 0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to destroy globally', async () => {
|
it('should be able to destroy globally', async () => {
|
||||||
await asyncExpect(() => {
|
|
||||||
message.info('whatever', 0);
|
message.info('whatever', 0);
|
||||||
});
|
|
||||||
await asyncExpect(() => {
|
|
||||||
message.info('whatever', 0);
|
message.info('whatever', 0);
|
||||||
});
|
await Promise.resolve();
|
||||||
await asyncExpect(() => {
|
|
||||||
expect(document.querySelectorAll('.ant-message').length).toBe(1);
|
expect(document.querySelectorAll('.ant-message').length).toBe(1);
|
||||||
expect(document.querySelectorAll('.ant-message-notice').length).toBe(2);
|
expect(document.querySelectorAll('.ant-message-notice').length).toBe(2);
|
||||||
});
|
|
||||||
await asyncExpect(() => {
|
|
||||||
message.destroy();
|
message.destroy();
|
||||||
});
|
|
||||||
await asyncExpect(() => {
|
|
||||||
expect(document.querySelectorAll('.ant-message').length).toBe(0);
|
expect(document.querySelectorAll('.ant-message').length).toBe(0);
|
||||||
expect(document.querySelectorAll('.ant-message-notice').length).toBe(0);
|
expect(document.querySelectorAll('.ant-message-notice').length).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('should not need to use duration argument when using the onClose arguments', () => {
|
it('should not need to use duration argument when using the onClose arguments', () => {
|
||||||
message.info('whatever', () => {});
|
message.info('whatever', () => {});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have the default duration when using the onClose arguments', done => {
|
it('should have the default duration when using the onClose arguments', done => {
|
||||||
|
jest.useRealTimers();
|
||||||
const defaultDuration = 3;
|
const defaultDuration = 3;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
message.info('whatever', () => {
|
message.info('whatever', () => {
|
||||||
|
@ -99,6 +94,7 @@ describe('message', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be called like promise', done => {
|
it('should be called like promise', done => {
|
||||||
|
jest.useRealTimers();
|
||||||
const defaultDuration = 3;
|
const defaultDuration = 3;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
message.info('whatever').then(() => {
|
message.info('whatever').then(() => {
|
||||||
|
@ -112,38 +108,32 @@ describe('message', () => {
|
||||||
// https:// github.com/ant-design/ant-design/issues/8201
|
// https:// github.com/ant-design/ant-design/issues/8201
|
||||||
it('should hide message correctly', async () => {
|
it('should hide message correctly', async () => {
|
||||||
let hide = message.loading('Action in progress..', 0);
|
let hide = message.loading('Action in progress..', 0);
|
||||||
await asyncExpect(() => {
|
await Promise.resolve();
|
||||||
expect(document.querySelectorAll('.ant-message-notice').length).toBe(1);
|
expect(document.querySelectorAll('.ant-message-notice').length).toBe(1);
|
||||||
hide();
|
hide();
|
||||||
}, 0);
|
await Promise.resolve();
|
||||||
await asyncExpect(() => {
|
jest.runAllTimers();
|
||||||
expect(document.querySelectorAll('.ant-message-notice').length).toBe(0);
|
expect(document.querySelectorAll('.ant-message-notice').length).toBe(0);
|
||||||
}, 0);
|
|
||||||
});
|
});
|
||||||
it('should allow custom icon', async () => {
|
it('should allow custom icon', async () => {
|
||||||
message.open({ content: 'Message', icon: <SmileOutlined /> });
|
message.open({ content: 'Message', icon: <SmileOutlined /> });
|
||||||
await asyncExpect(() => {
|
await Promise.resolve();
|
||||||
expect(document.querySelectorAll('.anticon-smile').length).toBe(1);
|
expect(document.querySelectorAll('.anticon-smile').length).toBe(1);
|
||||||
}, 0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have no icon', async () => {
|
it('should have no icon', async () => {
|
||||||
message.open({ content: 'Message' });
|
message.open({ content: 'Message' });
|
||||||
await asyncExpect(() => {
|
await Promise.resolve();
|
||||||
expect(document.querySelectorAll('.ant-message-notice .anticon').length).toBe(0);
|
expect(document.querySelectorAll('.ant-message-notice .anticon').length).toBe(0);
|
||||||
}, 0);
|
|
||||||
});
|
});
|
||||||
// https://github.com/ant-design/ant-design/issues/8201
|
// https://github.com/ant-design/ant-design/issues/8201
|
||||||
it('should destroy messages correctly', async () => {
|
it('should destroy messages correctly', async () => {
|
||||||
message.loading('Action in progress1..', 0);
|
message.loading('Action in progress1..', 0);
|
||||||
message.loading('Action in progress2..', 0);
|
message.loading('Action in progress2..', 0);
|
||||||
setTimeout(() => message.destroy(), 1000);
|
setTimeout(() => message.destroy(), 1000);
|
||||||
|
await Promise.resolve();
|
||||||
await asyncExpect(() => {
|
|
||||||
expect(document.querySelectorAll('.ant-message-notice').length).toBe(2);
|
expect(document.querySelectorAll('.ant-message-notice').length).toBe(2);
|
||||||
}, 0);
|
jest.runAllTimers();
|
||||||
await asyncExpect(() => {
|
|
||||||
expect(document.querySelectorAll('.ant-message-notice').length).toBe(0);
|
expect(document.querySelectorAll('.ant-message-notice').length).toBe(0);
|
||||||
}, 1500);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
<docs>
|
||||||
|
---
|
||||||
|
order: 6
|
||||||
|
title:
|
||||||
|
zh-CN: 自定义样式
|
||||||
|
en-US: Customized style
|
||||||
|
---
|
||||||
|
|
||||||
|
## zh-CN
|
||||||
|
|
||||||
|
使用 `style` 和 `class` 来定义样式。
|
||||||
|
|
||||||
|
## en-US
|
||||||
|
|
||||||
|
The `style` and `class` are available to customize Message.
|
||||||
|
|
||||||
|
</docs>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<a-button @click="success">Customized style</a-button>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
export default defineComponent({
|
||||||
|
setup() {
|
||||||
|
const success = () => {
|
||||||
|
message.success({
|
||||||
|
content: () => 'This is a prompt message with custom className and style',
|
||||||
|
class: 'custom-class',
|
||||||
|
style: {
|
||||||
|
marginTop: '20vh',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
success,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.custom-class {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -6,6 +6,7 @@
|
||||||
<loading />
|
<loading />
|
||||||
<thenable />
|
<thenable />
|
||||||
<update />
|
<update />
|
||||||
|
<customStyleVue />
|
||||||
</demo-sort>
|
</demo-sort>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -15,6 +16,7 @@ import Duration from './duration.vue';
|
||||||
import Loading from './loading.vue';
|
import Loading from './loading.vue';
|
||||||
import Thenable from './thenable.vue';
|
import Thenable from './thenable.vue';
|
||||||
import Update from './update.vue';
|
import Update from './update.vue';
|
||||||
|
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';
|
||||||
|
@ -28,6 +30,7 @@ export default defineComponent({
|
||||||
Loading,
|
Loading,
|
||||||
Thenable,
|
Thenable,
|
||||||
Update,
|
Update,
|
||||||
|
customStyleVue,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
return {};
|
return {};
|
||||||
|
|
|
@ -8,20 +8,25 @@ title:
|
||||||
|
|
||||||
## zh-CN
|
## zh-CN
|
||||||
|
|
||||||
可以通过唯一的 `key` 来更新内容。
|
可以通过唯一的 `key` 来更新内容、或者响应式数据。
|
||||||
|
|
||||||
## en-US
|
## en-US
|
||||||
|
|
||||||
Update message content with unique `key`.
|
Update message content with unique `key`,or use reactive data.
|
||||||
|
|
||||||
</docs>
|
</docs>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<a-button type="primary" @click="openMessage">Open the message box</a-button>
|
<a-button type="primary" @click="openMessage">Open the message box (update by key)</a-button>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<a-button type="primary" @click="openMessage2">
|
||||||
|
Open the message box (update by reactive)
|
||||||
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent, ref } from 'vue';
|
||||||
const key = 'updatable';
|
const key = 'updatable';
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
|
@ -31,8 +36,17 @@ export default defineComponent({
|
||||||
message.success({ content: 'Loaded!', key, duration: 2 });
|
message.success({ content: 'Loaded!', key, duration: 2 });
|
||||||
}, 1000);
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
const content = ref('Loading...');
|
||||||
|
const openMessage2 = () => {
|
||||||
|
// content must use function
|
||||||
|
message.loading({ content: () => content.value });
|
||||||
|
setTimeout(() => {
|
||||||
|
content.value = 'Loaded!';
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
return {
|
return {
|
||||||
openMessage,
|
openMessage,
|
||||||
|
openMessage2,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -25,7 +25,7 @@ This components provides some static methods, with usage and arguments as follow
|
||||||
|
|
||||||
| Argument | Description | Type | Default |
|
| Argument | Description | Type | Default |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| content | content of the message | string\| VNode | - |
|
| content | content of the message | string\| VNode \| () => VNode | - |
|
||||||
| duration | time(seconds) before auto-dismiss, don't dismiss if set to 0 | number | 1.5 |
|
| duration | time(seconds) before auto-dismiss, don't dismiss if set to 0 | number | 1.5 |
|
||||||
| onClose | Specify a function that will be called when the message is closed | Function | - |
|
| onClose | Specify a function that will be called when the message is closed | Function | - |
|
||||||
|
|
||||||
|
@ -48,11 +48,15 @@ The properties of config are as follows:
|
||||||
|
|
||||||
| Property | Description | Type | Default | Version |
|
| Property | Description | Type | Default | Version |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| content | content of the message | string\| VNode | - | |
|
| class | Customized CSS class | string | - |
|
||||||
|
| content | content of the message | string\| VNode \| () => VNode | - | |
|
||||||
| duration | time(seconds) before auto-dismiss, don't dismiss if set to 0 | number | 3 | |
|
| duration | time(seconds) before auto-dismiss, don't dismiss if set to 0 | number | 3 | |
|
||||||
| onClose | Specify a function that will be called when the message is closed | function | - | |
|
| onClose | Specify a function that will be called when the message is closed | function | - | |
|
||||||
| icon | Customized Icon | VNode | - | |
|
| icon | Customized Icon | VNode \| ()=> VNode | - | |
|
||||||
| key | The unique identifier of the Message | string\|number | - | |
|
| key | The unique identifier of the Message | string\|number | - | |
|
||||||
|
| style | Customized inline style | CSSProperties | - | |
|
||||||
|
| onClick | Specify a function that will be called when the message is clicked | function | - | |
|
||||||
|
| onClose | Specify a function that will be called when the message is closed | function | - | |
|
||||||
|
|
||||||
### Global static methods
|
### Global static methods
|
||||||
|
|
||||||
|
@ -68,12 +72,16 @@ message.config({
|
||||||
top: '100px',
|
top: '100px',
|
||||||
duration: 2,
|
duration: 2,
|
||||||
maxCount: 3,
|
maxCount: 3,
|
||||||
|
rtl: true,
|
||||||
|
prefixCls: 'my-message',
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
| Argument | Description | Type | Default |
|
| Argument | Description | Type | Default | Version |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| duration | time before auto-dismiss, in seconds | number | 1.5 |
|
| duration | time before auto-dismiss, in seconds | number | 1.5 | |
|
||||||
| getContainer | Return the mount node for Message | () => HTMLElement | () => document.body |
|
| getContainer | Return the mount node for Message | () => HTMLElement | () => document.body | |
|
||||||
| maxCount | max message show, drop oldest if exceed limit | number | - |
|
| maxCount | max message show, drop oldest if exceed limit | number | - | |
|
||||||
| top | distance from top | string | `24px` |
|
| 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` |
|
||||||
|
|
|
@ -5,18 +5,65 @@ import ExclamationCircleFilled from '@ant-design/icons-vue/ExclamationCircleFill
|
||||||
import CloseCircleFilled from '@ant-design/icons-vue/CloseCircleFilled';
|
import CloseCircleFilled from '@ant-design/icons-vue/CloseCircleFilled';
|
||||||
import CheckCircleFilled from '@ant-design/icons-vue/CheckCircleFilled';
|
import CheckCircleFilled from '@ant-design/icons-vue/CheckCircleFilled';
|
||||||
import InfoCircleFilled from '@ant-design/icons-vue/InfoCircleFilled';
|
import InfoCircleFilled from '@ant-design/icons-vue/InfoCircleFilled';
|
||||||
import type { VueNode } from '../_util/type';
|
import type { Key, VueNode } from '../_util/type';
|
||||||
|
import type { NotificationInstance } from '../vc-notification/Notification';
|
||||||
|
import classNames from '../_util/classNames';
|
||||||
|
|
||||||
let defaultDuration = 3;
|
let defaultDuration = 3;
|
||||||
let defaultTop: string;
|
let defaultTop: string;
|
||||||
let messageInstance: any;
|
let messageInstance: NotificationInstance;
|
||||||
let key = 1;
|
let key = 1;
|
||||||
let localPrefixCls = '';
|
let localPrefixCls = '';
|
||||||
let transitionName = 'move-up';
|
let transitionName = 'move-up';
|
||||||
|
let hasTransitionName = false;
|
||||||
let getContainer = () => document.body;
|
let getContainer = () => document.body;
|
||||||
let maxCount: number;
|
let maxCount: number;
|
||||||
|
let rtl = false;
|
||||||
|
|
||||||
function getMessageInstance(args: MessageArgsProps, callback: (i: any) => void) {
|
export function getKeyThenIncreaseKey() {
|
||||||
|
return key++;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigOptions {
|
||||||
|
top?: string;
|
||||||
|
duration?: number;
|
||||||
|
prefixCls?: string;
|
||||||
|
getContainer?: () => HTMLElement;
|
||||||
|
transitionName?: string;
|
||||||
|
maxCount?: number;
|
||||||
|
rtl?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMessageConfig(options: ConfigOptions) {
|
||||||
|
if (options.top !== undefined) {
|
||||||
|
defaultTop = options.top;
|
||||||
|
messageInstance = null; // delete messageInstance for new defaultTop
|
||||||
|
}
|
||||||
|
if (options.duration !== undefined) {
|
||||||
|
defaultDuration = options.duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.prefixCls !== undefined) {
|
||||||
|
localPrefixCls = options.prefixCls;
|
||||||
|
}
|
||||||
|
if (options.getContainer !== undefined) {
|
||||||
|
getContainer = options.getContainer;
|
||||||
|
}
|
||||||
|
if (options.transitionName !== undefined) {
|
||||||
|
transitionName = options.transitionName;
|
||||||
|
messageInstance = null; // delete messageInstance for new transitionName
|
||||||
|
hasTransitionName = true;
|
||||||
|
}
|
||||||
|
if (options.maxCount !== undefined) {
|
||||||
|
maxCount = options.maxCount;
|
||||||
|
messageInstance = null;
|
||||||
|
}
|
||||||
|
if (options.rtl !== undefined) {
|
||||||
|
rtl = options.rtl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMessageInstance(args: MessageArgsProps, callback: (i: NotificationInstance) => void) {
|
||||||
if (messageInstance) {
|
if (messageInstance) {
|
||||||
callback(messageInstance);
|
callback(messageInstance);
|
||||||
return;
|
return;
|
||||||
|
@ -27,6 +74,7 @@ function getMessageInstance(args: MessageArgsProps, callback: (i: any) => void)
|
||||||
prefixCls: args.prefixCls || localPrefixCls,
|
prefixCls: args.prefixCls || localPrefixCls,
|
||||||
rootPrefixCls: args.rootPrefixCls,
|
rootPrefixCls: args.rootPrefixCls,
|
||||||
transitionName,
|
transitionName,
|
||||||
|
hasTransitionName,
|
||||||
style: { top: defaultTop }, // 覆盖原来的样式
|
style: { top: defaultTop }, // 覆盖原来的样式
|
||||||
getContainer,
|
getContainer,
|
||||||
maxCount,
|
maxCount,
|
||||||
|
@ -49,7 +97,7 @@ export interface ThenableArgument {
|
||||||
(val: any): void;
|
(val: any): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconMap = {
|
const typeToIcon = {
|
||||||
info: InfoCircleFilled,
|
info: InfoCircleFilled,
|
||||||
success: CheckCircleFilled,
|
success: CheckCircleFilled,
|
||||||
error: CloseCircleFilled,
|
error: CloseCircleFilled,
|
||||||
|
@ -57,16 +105,14 @@ const iconMap = {
|
||||||
loading: LoadingOutlined,
|
loading: LoadingOutlined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface MessageType {
|
export interface MessageType extends PromiseLike<any> {
|
||||||
(): void;
|
(): void;
|
||||||
then: (fill: ThenableArgument, reject: ThenableArgument) => Promise<void>;
|
|
||||||
promise: Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageArgsProps {
|
export interface MessageArgsProps {
|
||||||
content: string | (() => VueNode) | VueNode;
|
content: string | (() => VueNode) | VueNode;
|
||||||
duration: number | null;
|
duration?: number;
|
||||||
type: NoticeType;
|
type?: NoticeType;
|
||||||
prefixCls?: string;
|
prefixCls?: string;
|
||||||
rootPrefixCls?: string;
|
rootPrefixCls?: string;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
|
@ -75,12 +121,13 @@ export interface MessageArgsProps {
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
class?: string;
|
class?: string;
|
||||||
appContext?: any;
|
appContext?: any;
|
||||||
|
onClick?: (e: MouseEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function notice(args: MessageArgsProps): MessageType {
|
function notice(args: MessageArgsProps): MessageType {
|
||||||
const duration = args.duration !== undefined ? args.duration : defaultDuration;
|
const duration = args.duration !== undefined ? args.duration : defaultDuration;
|
||||||
|
|
||||||
const target = args.key || key++;
|
const target = args.key || getKeyThenIncreaseKey();
|
||||||
const closePromise = new Promise(resolve => {
|
const closePromise = new Promise(resolve => {
|
||||||
const callback = () => {
|
const callback = () => {
|
||||||
if (typeof args.onClose === 'function') {
|
if (typeof args.onClose === 'function') {
|
||||||
|
@ -95,18 +142,21 @@ function notice(args: MessageArgsProps): MessageType {
|
||||||
style: args.style || {},
|
style: args.style || {},
|
||||||
class: args.class,
|
class: args.class,
|
||||||
content: ({ prefixCls }) => {
|
content: ({ prefixCls }) => {
|
||||||
const Icon = iconMap[args.type];
|
const Icon = typeToIcon[args.type];
|
||||||
const iconNode = Icon ? <Icon /> : '';
|
const iconNode = Icon ? <Icon /> : '';
|
||||||
|
const messageClass = classNames(`${prefixCls}-custom-content`, {
|
||||||
|
[`${prefixCls}-${args.type}`]: args.type,
|
||||||
|
[`${prefixCls}-rtl`]: rtl === true,
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<div
|
<div class={messageClass}>
|
||||||
class={`${prefixCls}-custom-content${args.type ? ` ${prefixCls}-${args.type}` : ''}`}
|
{typeof args.icon === 'function' ? args.icon() : args.icon || iconNode}
|
||||||
>
|
|
||||||
{typeof args.icon === 'function' ? args.icon : args.icon || iconNode}
|
|
||||||
<span>{typeof args.content === 'function' ? args.content() : args.content}</span>
|
<span>{typeof args.content === 'function' ? args.content() : args.content}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onClose: callback,
|
onClose: callback,
|
||||||
|
onClick: args.onClick,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -121,7 +171,7 @@ function notice(args: MessageArgsProps): MessageType {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigDuration = number | (() => void);
|
type ConfigDuration = number;
|
||||||
type JointContent = VueNode | MessageArgsProps;
|
type JointContent = VueNode | MessageArgsProps;
|
||||||
export type ConfigOnClose = () => void;
|
export type ConfigOnClose = () => void;
|
||||||
|
|
||||||
|
@ -132,73 +182,64 @@ function isArgsProps(content: JointContent): content is MessageArgsProps {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfigOptions {
|
|
||||||
top?: string;
|
|
||||||
duration?: number;
|
|
||||||
prefixCls?: string;
|
|
||||||
getContainer?: () => HTMLElement;
|
|
||||||
transitionName?: string;
|
|
||||||
maxCount?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const api: any = {
|
const api: any = {
|
||||||
open: notice,
|
open: notice,
|
||||||
config(options: ConfigOptions) {
|
config: setMessageConfig,
|
||||||
if (options.top !== undefined) {
|
destroy(messageKey?: Key) {
|
||||||
defaultTop = options.top;
|
|
||||||
messageInstance = null; // delete messageInstance for new defaultTop
|
|
||||||
}
|
|
||||||
if (options.duration !== undefined) {
|
|
||||||
defaultDuration = options.duration;
|
|
||||||
}
|
|
||||||
if (options.prefixCls !== undefined) {
|
|
||||||
localPrefixCls = options.prefixCls;
|
|
||||||
}
|
|
||||||
if (options.getContainer !== undefined) {
|
|
||||||
getContainer = options.getContainer;
|
|
||||||
}
|
|
||||||
if (options.transitionName !== undefined) {
|
|
||||||
transitionName = options.transitionName;
|
|
||||||
messageInstance = null; // delete messageInstance for new transitionName
|
|
||||||
}
|
|
||||||
if (options.maxCount !== undefined) {
|
|
||||||
maxCount = options.maxCount;
|
|
||||||
messageInstance = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
destroy() {
|
|
||||||
if (messageInstance) {
|
if (messageInstance) {
|
||||||
messageInstance.destroy();
|
if (messageKey) {
|
||||||
|
const { removeNotice } = messageInstance;
|
||||||
|
removeNotice(messageKey);
|
||||||
|
} else {
|
||||||
|
const { destroy } = messageInstance;
|
||||||
|
destroy();
|
||||||
messageInstance = null;
|
messageInstance = null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
['success', 'info', 'warning', 'error', 'loading'].forEach(type => {
|
export function attachTypeApi(originalApi: MessageApi, type: NoticeType) {
|
||||||
api[type] = (content: JointContent, duration: ConfigDuration, onClose?: ConfigOnClose) => {
|
originalApi[type] = (
|
||||||
|
content: JointContent,
|
||||||
|
duration?: ConfigDuration,
|
||||||
|
onClose?: ConfigOnClose,
|
||||||
|
) => {
|
||||||
if (isArgsProps(content)) {
|
if (isArgsProps(content)) {
|
||||||
return api.open({ ...content, type });
|
return originalApi.open({ ...content, type });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof duration === 'function') {
|
if (typeof duration === 'function') {
|
||||||
onClose = duration;
|
onClose = duration;
|
||||||
duration = undefined;
|
duration = undefined;
|
||||||
}
|
}
|
||||||
return api.open({ content, duration, type, onClose });
|
|
||||||
|
return originalApi.open({ content, duration, type, onClose });
|
||||||
};
|
};
|
||||||
});
|
}
|
||||||
|
|
||||||
|
(['success', 'info', 'warning', 'error', 'loading'] as NoticeType[]).forEach(type =>
|
||||||
|
attachTypeApi(api, type),
|
||||||
|
);
|
||||||
|
|
||||||
api.warn = api.warning;
|
api.warn = api.warning;
|
||||||
|
|
||||||
export interface MessageApi {
|
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;
|
||||||
error(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
error(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
||||||
warn(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
|
||||||
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;
|
||||||
config(options: ConfigOptions): void;
|
|
||||||
destroy(): void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MessageApi extends MessageInstance {
|
||||||
|
warn(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
|
||||||
|
config(options: ConfigOptions): void;
|
||||||
|
destroy(messageKey?: Key): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @private test Only function. Not work on production */
|
||||||
|
export const getInstance = () => (process.env.NODE_ENV === 'test' ? messageInstance : null);
|
||||||
|
|
||||||
export default api as MessageApi;
|
export default api as MessageApi;
|
||||||
|
|
|
@ -25,8 +25,8 @@ cover: https://gw.alipayobjects.com/zos/alicdn/hAkKTIW0K/Message.svg
|
||||||
- `message.loading(content, [duration], onClose)`
|
- `message.loading(content, [duration], onClose)`
|
||||||
|
|
||||||
| 参数 | 说明 | 类型 | 默认值 |
|
| 参数 | 说明 | 类型 | 默认值 |
|
||||||
| -------- | --------------------------------------------- | -------------- | ------ |
|
| --- | --- | --- | --- |
|
||||||
| content | 提示内容 | string\| VNode | - |
|
| content | 提示内容 | string\| VNode \| () => VNode | - |
|
||||||
| duration | 自动关闭的延时,单位秒。设为 0 时不自动关闭。 | number | 3 |
|
| duration | 自动关闭的延时,单位秒。设为 0 时不自动关闭。 | number | 3 |
|
||||||
| onClose | 关闭时触发的回调函数 | Function | - |
|
| onClose | 关闭时触发的回调函数 | Function | - |
|
||||||
|
|
||||||
|
@ -47,13 +47,19 @@ cover: https://gw.alipayobjects.com/zos/alicdn/hAkKTIW0K/Message.svg
|
||||||
- `message.warn(config)` // alias of warning
|
- `message.warn(config)` // alias of warning
|
||||||
- `message.loading(config)`
|
- `message.loading(config)`
|
||||||
|
|
||||||
|
`config` 对象属性如下:
|
||||||
|
|
||||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||||
| -------- | --------------------------------------------- | -------------- | ------ | ---- |
|
| --- | --- | --- | --- | --- |
|
||||||
| content | 提示内容 | string\| VNode | - | |
|
| class | 自定义 CSS class | string | - | |
|
||||||
|
| content | 提示内容 | string\| VNode \| ()=> VNode | - | |
|
||||||
| duration | 自动关闭的延时,单位秒。设为 0 时不自动关闭。 | number | 3 | |
|
| duration | 自动关闭的延时,单位秒。设为 0 时不自动关闭。 | number | 3 | |
|
||||||
| onClose | 关闭时触发的回调函数 | Function | - | |
|
| onClose | 关闭时触发的回调函数 | Function | - | |
|
||||||
| icon | 自定义图标 | VNode | - | |
|
| icon | 自定义图标 | VNode \| () => VNode | - | |
|
||||||
| key | 当前提示的唯一标志 | string \| number | - | |
|
| key | 当前提示的唯一标志 | string \| number | - | |
|
||||||
|
| style | 自定义内联样式 | CSSProperties | - | |
|
||||||
|
| onClick | 点击 message 时触发的回调函数 | function | - | |
|
||||||
|
| onClose | 关闭时触发的回调函数 | function | - | |
|
||||||
|
|
||||||
### 全局方法
|
### 全局方法
|
||||||
|
|
||||||
|
@ -69,12 +75,16 @@ message.config({
|
||||||
top: `100px`,
|
top: `100px`,
|
||||||
duration: 2,
|
duration: 2,
|
||||||
maxCount: 3,
|
maxCount: 3,
|
||||||
|
rtl: true,
|
||||||
|
prefixCls: 'my-message',
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
| 参数 | 说明 | 类型 | 默认值 |
|
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| duration | 默认自动关闭延时,单位秒 | number | 3 |
|
| duration | 默认自动关闭延时,单位秒 | number | 3 | |
|
||||||
| getContainer | 配置渲染节点的输出位置 | () => HTMLElement | () => document.body |
|
| getContainer | 配置渲染节点的输出位置 | () => HTMLElement | () => document.body | |
|
||||||
| maxCount | 最大显示数, 超过限制时,最早的消息会被自动关闭 | number | - |
|
| maxCount | 最大显示数, 超过限制时,最早的消息会被自动关闭 | number | - | |
|
||||||
| top | 消息距离顶部的位置 | string | `24px` |
|
| prefixCls | 消息节点的 className 前缀 | string | `ant-message` | 3.0 |
|
||||||
|
| rtl | 是否开启 RTL 模式 | boolean | false | | |
|
||||||
|
| top | 消息距离顶部的位置 | string | `8px` | |
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
.reset-component();
|
.reset-component();
|
||||||
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 16px;
|
top: 8px;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: @zindex-message;
|
z-index: @zindex-message;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -16,9 +16,6 @@
|
||||||
&-notice {
|
&-notice {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
&:first-child {
|
|
||||||
margin-top: -8px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&-notice-content {
|
&-notice-content {
|
||||||
|
@ -54,8 +51,7 @@
|
||||||
font-size: @font-size-lg;
|
font-size: @font-size-lg;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-notice.move-up-leave.move-up-leave-active {
|
&-notice.@{ant-prefix}-move-up-leave.@{ant-prefix}-move-up-leave-active {
|
||||||
overflow: hidden;
|
|
||||||
animation-name: MessageMoveOut;
|
animation-name: MessageMoveOut;
|
||||||
animation-duration: 0.3s;
|
animation-duration: 0.3s;
|
||||||
}
|
}
|
||||||
|
@ -67,9 +63,12 @@
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
max-height: 0;
|
max-height: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@import './rtl';
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
@import '../../style/themes/index';
|
||||||
|
@import '../../style/mixins/index';
|
||||||
|
|
||||||
|
@message-prefix-cls: ~'@{ant-prefix}-message';
|
||||||
|
|
||||||
|
.@{message-prefix-cls}-rtl {
|
||||||
|
direction: rtl;
|
||||||
|
|
||||||
|
span {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.@{iconfont-css-prefix} {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -42,7 +42,12 @@ exports[`renders ./components/notification/demo/placement.vue correctly 1`] = `
|
||||||
|
|
||||||
exports[`renders ./components/notification/demo/update.vue correctly 1`] = `
|
exports[`renders ./components/notification/demo/update.vue correctly 1`] = `
|
||||||
<button class="ant-btn ant-btn-primary" type="button">
|
<button class="ant-btn ant-btn-primary" type="button">
|
||||||
<!----><span>Open the notification box</span>
|
<!----><span>Open the notification box (update by key)</span>
|
||||||
|
</button>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<button class="ant-btn ant-btn-primary" type="button">
|
||||||
|
<!----><span>Open the notification box (update by reactive)</span>
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import { asyncExpect } from '../../../tests/utils';
|
import { asyncExpect } from '../../../tests/utils';
|
||||||
import notification from '..';
|
import notification, { getInstance } from '..';
|
||||||
import { StepBackwardOutlined } from '@ant-design/icons-vue';
|
import { StepBackwardOutlined } from '@ant-design/icons-vue';
|
||||||
|
|
||||||
describe('notification', () => {
|
describe('notification', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
document.body.outerHTML = '';
|
document.body.outerHTML = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
jest.useRealTimers();
|
||||||
notification.destroy();
|
notification.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -24,17 +26,18 @@ describe('notification', () => {
|
||||||
key: '2',
|
key: '2',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await asyncExpect(() => {
|
await Promise.resolve();
|
||||||
expect(document.querySelectorAll('.ant-notification-notice').length).toBe(2);
|
expect(document.querySelectorAll('.ant-notification-notice').length).toBe(2);
|
||||||
notification.close('1');
|
notification.close('1');
|
||||||
}, 0);
|
jest.runAllTimers();
|
||||||
await asyncExpect(() => {
|
expect(
|
||||||
expect(document.querySelectorAll('.ant-notification-notice').length).toBe(1);
|
(await getInstance('ant-notification-topRight-false')).component.value.notices,
|
||||||
|
).toHaveLength(1);
|
||||||
notification.close('2');
|
notification.close('2');
|
||||||
}, 0);
|
jest.runAllTimers();
|
||||||
await asyncExpect(() => {
|
expect(
|
||||||
expect(document.querySelectorAll('.ant-notification-notice').length).toBe(0);
|
(await getInstance('ant-notification-topRight-false')).component.value.notices,
|
||||||
}, 0);
|
).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to destroy globally', async () => {
|
it('should be able to destroy globally', async () => {
|
||||||
|
|
|
@ -31,7 +31,7 @@ export default defineComponent({
|
||||||
message: 'Notification Title',
|
message: 'Notification Title',
|
||||||
description:
|
description:
|
||||||
'This is the content of the notification. This is the content of the notification. This is the content of the notification.',
|
'This is the content of the notification. This is the content of the notification. This is the content of the notification.',
|
||||||
icon: h(SmileOutlined, { style: 'color: #108ee9' }),
|
icon: () => h(SmileOutlined, { style: 'color: #108ee9' }),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -8,11 +8,11 @@ title:
|
||||||
|
|
||||||
## zh-CN
|
## zh-CN
|
||||||
|
|
||||||
使用 style 和 className 来定义样式。
|
使用 `style` 和 `class` 来定义样式。
|
||||||
|
|
||||||
## en-US
|
## en-US
|
||||||
|
|
||||||
The style and className are available to customize Notification.
|
The `style` and `class` are available to customize Notification.
|
||||||
|
|
||||||
</docs>
|
</docs>
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ export default defineComponent({
|
||||||
width: '600px',
|
width: '600px',
|
||||||
marginLeft: `${335 - 600}px`,
|
marginLeft: `${335 - 600}px`,
|
||||||
},
|
},
|
||||||
|
class: 'notification-custom-class',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
|
@ -41,3 +42,8 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<style>
|
||||||
|
.notification-custom-class {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -46,6 +46,7 @@ import {
|
||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
import { notification } from 'ant-design-vue';
|
import { notification } from 'ant-design-vue';
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
|
import type { NotificationPlacement } from 'ant-design-vue';
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
RadiusUpleftOutlined,
|
RadiusUpleftOutlined,
|
||||||
|
@ -54,7 +55,7 @@ export default defineComponent({
|
||||||
RadiusBottomrightOutlined,
|
RadiusBottomrightOutlined,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const openNotification = (placement: string) => {
|
const openNotification = (placement: NotificationPlacement) => {
|
||||||
notification.open({
|
notification.open({
|
||||||
message: `Notification ${placement}`,
|
message: `Notification ${placement}`,
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -8,20 +8,27 @@ title:
|
||||||
|
|
||||||
## zh-CN
|
## zh-CN
|
||||||
|
|
||||||
可以通过唯一的 key 来更新内容。
|
可以通过唯一的 key 来更新内容, 或者通过响应式数据更新。
|
||||||
|
|
||||||
## en-US
|
## en-US
|
||||||
|
|
||||||
Update content with unique key.
|
Update content with unique key, or use reactive data.
|
||||||
|
|
||||||
</docs>
|
</docs>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<a-button type="primary" @click="openNotification">Open the notification box</a-button>
|
<a-button type="primary" @click="openNotification">
|
||||||
|
Open the notification box (update by key)
|
||||||
|
</a-button>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<a-button type="primary" @click="openNotification2">
|
||||||
|
Open the notification box (update by reactive)
|
||||||
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { notification } from 'ant-design-vue';
|
import { notification } from 'ant-design-vue';
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent, ref } from 'vue';
|
||||||
const key = 'updatable';
|
const key = 'updatable';
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
|
@ -39,8 +46,22 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
}, 1000);
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
const message = ref('Notification Title');
|
||||||
|
const description = ref('description');
|
||||||
|
const openNotification2 = () => {
|
||||||
|
// content must use function
|
||||||
|
notification.open({
|
||||||
|
message: () => message.value,
|
||||||
|
description: () => description.value,
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
message.value = 'New Title';
|
||||||
|
description.value = 'New description.';
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
return {
|
return {
|
||||||
openNotification,
|
openNotification,
|
||||||
|
openNotification2,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -31,30 +31,35 @@ The properties of config are as follows:
|
||||||
| Property | Description | Type | Default | Version |
|
| Property | Description | Type | Default | Version |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| bottom | Distance from the bottom of the viewport, when `placement` is `bottomRight` or `bottomLeft` (unit: pixels). | string | `24px` | |
|
| bottom | Distance from the bottom of the viewport, when `placement` is `bottomRight` or `bottomLeft` (unit: pixels). | string | `24px` | |
|
||||||
| btn | Customized close button | VNode | - | |
|
| btn | Customized close button | VNode \| () => VNode | - | |
|
||||||
| class | Customized CSS class | string | - | |
|
| class | Customized CSS class | string | - | |
|
||||||
| description | The content of notification box (required) | string\| VNode | - | |
|
| description | The content of notification box (required) | string\| VNode \| () => VNode | - | |
|
||||||
| duration | Time in seconds before Notification is closed. When set to 0 or null, it will never be closed automatically | number | 4.5 | |
|
| duration | Time in seconds before Notification is closed. When set to 0 or null, it will never be closed automatically | number | 4.5 | |
|
||||||
| getContainer | Return the mount node for Notification | () => HTMLNode | () => document.body | |
|
| getContainer | Return the mount node for Notification | () => HTMLNode | () => document.body | |
|
||||||
| icon | Customized icon | VNode | - | |
|
| icon | Customized icon | VNode \| () => VNode | - | |
|
||||||
| key | The unique identifier of the Notification | string | - | |
|
| key | The unique identifier of the Notification | string | - | |
|
||||||
| message | The title of notification box (required) | string\|VNode | - | |
|
| message | The title of notification box (required) | string\| VNode \| () => VNode | - | |
|
||||||
| 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` | |
|
||||||
| style | Customized inline style | Object \| string | - | |
|
| style | Customized inline style | Object \| string | - | |
|
||||||
| onClose | Specify a function that will be called when the close button is clicked | Function | - | |
|
| onClose | Specify a function that will be called when the close button is clicked | Function | - | |
|
||||||
| onClick | Specify a function that will be called when the notification is clicked | Function | - | |
|
| onClick | Specify a function that will be called when the notification is clicked | Function | - | |
|
||||||
| 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` | |
|
||||||
| closeIcon | custom close icon | VNode | - | |
|
| closeIcon | custom close icon | VNode \| () => VNode | - | |
|
||||||
|
|
||||||
`notification` also provides a global `config()` method that can be used for specifying the default options. Once this method is used, all the notification boxes will take into account these globally defined options when displaying.
|
`notification` also provides a global `config()` method that can be used for specifying the default options. Once this method is used, all the notification boxes will take into account these globally defined options when displaying.
|
||||||
|
|
||||||
- `notification.config(options)`
|
- `notification.config(options)`
|
||||||
|
|
||||||
|
> When you use `ConfigProvider` for global configuration, the system will automatically start RTL mode by default.(4.3.0+)
|
||||||
|
>
|
||||||
|
> When you want to use it alone, you can start the RTL mode through the following settings.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
notification.config({
|
notification.config({
|
||||||
placement: 'bottomRight',
|
placement: 'bottomRight',
|
||||||
bottom: '50px',
|
bottom: '50px',
|
||||||
duration: 3,
|
duration: 3,
|
||||||
|
rtl: true,
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -64,5 +69,7 @@ notification.config({
|
||||||
| duration | Time in seconds before Notification is closed. When set to 0 or null, it will never be closed automatically | number | 4.5 | |
|
| duration | Time in seconds before Notification is closed. When set to 0 or null, it will never be closed automatically | number | 4.5 | |
|
||||||
| getContainer | Return the mount node for Notification | () => HTMLNode | () => document.body | |
|
| getContainer | Return the mount node for Notification | () => HTMLNode | () => document.body | |
|
||||||
| 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 | |
|
||||||
| 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` | |
|
||||||
| closeIcon | custom close icon | VNode | - | |
|
| closeIcon | custom close icon | VNode \| () => VNode | - | |
|
||||||
|
| maxCount | Max Notification show, drop oldest if exceed limit | number | - | 3.0 |
|
||||||
|
|
|
@ -8,6 +8,8 @@ import CloseOutlined from '@ant-design/icons-vue/CloseOutlined';
|
||||||
import type { VueNode } from '../_util/type';
|
import type { VueNode } from '../_util/type';
|
||||||
import { renderHelper } from '../_util/util';
|
import { renderHelper } from '../_util/util';
|
||||||
import { globalConfig } from '../config-provider';
|
import { globalConfig } from '../config-provider';
|
||||||
|
import type { NotificationInstance as VCNotificationInstance } from '../vc-notification/Notification';
|
||||||
|
import classNames from '../_util/classNames';
|
||||||
|
|
||||||
export type NotificationPlacement = 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
|
export type NotificationPlacement = 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
|
||||||
|
|
||||||
|
@ -21,9 +23,11 @@ export interface ConfigProps {
|
||||||
placement?: NotificationPlacement;
|
placement?: NotificationPlacement;
|
||||||
getContainer?: () => HTMLElement;
|
getContainer?: () => HTMLElement;
|
||||||
closeIcon?: VueNode | (() => VueNode);
|
closeIcon?: VueNode | (() => VueNode);
|
||||||
|
rtl?: boolean;
|
||||||
|
maxCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const notificationInstance: { [key: string]: any } = {};
|
const notificationInstance: { [key: string]: VCNotificationInstance } = {};
|
||||||
let defaultDuration = 4.5;
|
let defaultDuration = 4.5;
|
||||||
let defaultTop = '24px';
|
let defaultTop = '24px';
|
||||||
let defaultBottom = '24px';
|
let defaultBottom = '24px';
|
||||||
|
@ -31,6 +35,8 @@ let defaultPrefixCls = '';
|
||||||
let defaultPlacement: NotificationPlacement = 'topRight';
|
let defaultPlacement: NotificationPlacement = 'topRight';
|
||||||
let defaultGetContainer = () => document.body;
|
let defaultGetContainer = () => document.body;
|
||||||
let defaultCloseIcon = null;
|
let defaultCloseIcon = null;
|
||||||
|
let rtl = false;
|
||||||
|
let maxCount: number;
|
||||||
|
|
||||||
function setNotificationConfig(options: ConfigProps) {
|
function setNotificationConfig(options: ConfigProps) {
|
||||||
const { duration, placement, bottom, top, getContainer, closeIcon, prefixCls } = options;
|
const { duration, placement, bottom, top, getContainer, closeIcon, prefixCls } = options;
|
||||||
|
@ -55,6 +61,12 @@ function setNotificationConfig(options: ConfigProps) {
|
||||||
if (closeIcon !== undefined) {
|
if (closeIcon !== undefined) {
|
||||||
defaultCloseIcon = closeIcon;
|
defaultCloseIcon = closeIcon;
|
||||||
}
|
}
|
||||||
|
if (options.rtl !== undefined) {
|
||||||
|
rtl = options.rtl;
|
||||||
|
}
|
||||||
|
if (options.maxCount !== undefined) {
|
||||||
|
maxCount = options.maxCount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPlacementStyle(
|
function getPlacementStyle(
|
||||||
|
@ -62,7 +74,7 @@ function getPlacementStyle(
|
||||||
top: string = defaultTop,
|
top: string = defaultTop,
|
||||||
bottom: string = defaultBottom,
|
bottom: string = defaultBottom,
|
||||||
) {
|
) {
|
||||||
let style;
|
let style: CSSProperties;
|
||||||
switch (placement) {
|
switch (placement) {
|
||||||
case 'topLeft':
|
case 'topLeft':
|
||||||
style = {
|
style = {
|
||||||
|
@ -106,20 +118,28 @@ function getNotificationInstance(
|
||||||
closeIcon = defaultCloseIcon,
|
closeIcon = defaultCloseIcon,
|
||||||
appContext,
|
appContext,
|
||||||
}: NotificationArgsProps,
|
}: NotificationArgsProps,
|
||||||
callback: (n: any) => void,
|
callback: (n: VCNotificationInstance) => void,
|
||||||
) {
|
) {
|
||||||
const { getPrefixCls } = globalConfig();
|
const { getPrefixCls } = globalConfig();
|
||||||
const prefixCls = getPrefixCls('notification', customizePrefixCls || defaultPrefixCls);
|
const prefixCls = getPrefixCls('notification', customizePrefixCls || defaultPrefixCls);
|
||||||
const cacheKey = `${prefixCls}-${placement}`;
|
const cacheKey = `${prefixCls}-${placement}-${rtl}`;
|
||||||
if (notificationInstance[cacheKey]) {
|
const cacheInstance = notificationInstance[cacheKey];
|
||||||
callback(notificationInstance[cacheKey]);
|
if (cacheInstance) {
|
||||||
|
Promise.resolve(cacheInstance).then(instance => {
|
||||||
|
callback(instance);
|
||||||
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const notificationClass = classNames(`${prefixCls}-${placement}`, {
|
||||||
|
[`${prefixCls}-rtl`]: rtl === true,
|
||||||
|
});
|
||||||
Notification.newInstance(
|
Notification.newInstance(
|
||||||
{
|
{
|
||||||
name: 'notification',
|
name: 'notification',
|
||||||
prefixCls: customizePrefixCls || defaultPrefixCls,
|
prefixCls: customizePrefixCls || defaultPrefixCls,
|
||||||
class: `${prefixCls}-${placement}`,
|
class: notificationClass,
|
||||||
style: getPlacementStyle(placement, top, bottom),
|
style: getPlacementStyle(placement, top, bottom),
|
||||||
appContext,
|
appContext,
|
||||||
getContainer,
|
getContainer,
|
||||||
|
@ -131,6 +151,8 @@ function getNotificationInstance(
|
||||||
);
|
);
|
||||||
return closeIconToRender;
|
return closeIconToRender;
|
||||||
},
|
},
|
||||||
|
maxCount,
|
||||||
|
hasTransitionName: true,
|
||||||
},
|
},
|
||||||
(notification: any) => {
|
(notification: any) => {
|
||||||
notificationInstance[cacheKey] = notification;
|
notificationInstance[cacheKey] = notification;
|
||||||
|
@ -206,25 +228,26 @@ function notice(args: NotificationArgsProps) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiBase = {
|
const api: any = {
|
||||||
open: notice,
|
open: notice,
|
||||||
close(key: string) {
|
close(key: string) {
|
||||||
Object.keys(notificationInstance).forEach(cacheKey =>
|
Object.keys(notificationInstance).forEach(cacheKey =>
|
||||||
notificationInstance[cacheKey].removeNotice(key),
|
Promise.resolve(notificationInstance[cacheKey]).then(instance => {
|
||||||
|
instance.removeNotice(key);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
config: setNotificationConfig,
|
config: setNotificationConfig,
|
||||||
destroy() {
|
destroy() {
|
||||||
Object.keys(notificationInstance).forEach(cacheKey => {
|
Object.keys(notificationInstance).forEach(cacheKey => {
|
||||||
notificationInstance[cacheKey].destroy();
|
Promise.resolve(notificationInstance[cacheKey]).then(instance => {
|
||||||
delete notificationInstance[cacheKey];
|
instance.destroy();
|
||||||
|
});
|
||||||
|
delete notificationInstance[cacheKey]; // lgtm[js/missing-await]
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type NotificationApi = typeof apiBase &
|
|
||||||
Record<IconType | 'warn', (args: Omit<NotificationArgsProps, 'type'>) => void>;
|
|
||||||
const api = apiBase as any as NotificationApi;
|
|
||||||
const iconTypes: IconType[] = ['success', 'info', 'warning', 'error'];
|
const iconTypes: IconType[] = ['success', 'info', 'warning', 'error'];
|
||||||
iconTypes.forEach(type => {
|
iconTypes.forEach(type => {
|
||||||
api[type] = args =>
|
api[type] = args =>
|
||||||
|
@ -235,4 +258,24 @@ iconTypes.forEach(type => {
|
||||||
});
|
});
|
||||||
|
|
||||||
api.warn = api.warning;
|
api.warn = api.warning;
|
||||||
export default api;
|
|
||||||
|
export interface NotificationInstance {
|
||||||
|
success(args: NotificationArgsProps): void;
|
||||||
|
error(args: NotificationArgsProps): void;
|
||||||
|
info(args: NotificationArgsProps): void;
|
||||||
|
warning(args: NotificationArgsProps): void;
|
||||||
|
open(args: NotificationArgsProps): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotificationApi extends NotificationInstance {
|
||||||
|
warn(args: NotificationArgsProps): void;
|
||||||
|
close(key: string): void;
|
||||||
|
config(options: ConfigProps): void;
|
||||||
|
destroy(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @private test Only function. Not work on production */
|
||||||
|
export const getInstance = async (cacheKey: string) =>
|
||||||
|
process.env.NODE_ENV === 'test' ? notificationInstance[cacheKey] : null;
|
||||||
|
|
||||||
|
export default api as NotificationApi;
|
||||||
|
|
|
@ -31,31 +31,36 @@ config 参数如下:
|
||||||
|
|
||||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| btn | 自定义关闭按钮 | VNode | - | |
|
| btn | 自定义关闭按钮 | VNode \| () => VNode | - | |
|
||||||
| bottom | 消息从底部弹出时,距离底部的位置,单位像素。 | string | `24px` | |
|
| bottom | 消息从底部弹出时,距离底部的位置,单位像素。 | string | `24px` | |
|
||||||
| class | 自定义 CSS class | string | - | |
|
| class | 自定义 CSS class | string | - | |
|
||||||
| description | 通知提醒内容,必选 | string \|VNode | - | |
|
| description | 通知提醒内容,必选 | string \| VNode \| () => VNode | - | |
|
||||||
| duration | 默认 4.5 秒后自动关闭,配置为 null 则不自动关闭 | number | 4.5 | |
|
| duration | 默认 4.5 秒后自动关闭,配置为 null 则不自动关闭 | number | 4.5 | |
|
||||||
| getContainer | 配置渲染节点的输出位置 | () => HTMLNode | () => document.body | |
|
| getContainer | 配置渲染节点的输出位置 | () => HTMLNode | () => document.body | |
|
||||||
| icon | 自定义图标 | VNode | - | |
|
| icon | 自定义图标 | VNode \| () => VNode | - | |
|
||||||
| key | 当前通知唯一标志 | string | - | |
|
| key | 当前通知唯一标志 | string | - | |
|
||||||
| message | 通知提醒标题,必选 | string \|VNode | - | |
|
| message | 通知提醒标题,必选 | string \| VNode \| () => VNode | - | |
|
||||||
| placement | 弹出位置,可选 `topLeft` `topRight` `bottomLeft` `bottomRight` | string | topRight | |
|
| placement | 弹出位置,可选 `topLeft` `topRight` `bottomLeft` `bottomRight` | string | topRight | |
|
||||||
| style | 自定义内联样式 | Object \| string | - | |
|
| style | 自定义内联样式 | Object \| string | - | |
|
||||||
| onClose | 点击默认关闭按钮时触发的回调函数 | Function | - | |
|
| onClose | 点击默认关闭按钮时触发的回调函数 | Function | - | |
|
||||||
| onClick | 点击通知时触发的回调函数 | Function | - | |
|
| onClick | 点击通知时触发的回调函数 | Function | - | |
|
||||||
| top | 消息从顶部弹出时,距离顶部的位置,单位像素。 | string | `24px` | |
|
| top | 消息从顶部弹出时,距离顶部的位置,单位像素。 | string | `24px` | |
|
||||||
| closeIcon | 自定义关闭图标 | VNode | - | |
|
| closeIcon | 自定义关闭图标 | VNode \| () => VNode | - | |
|
||||||
|
|
||||||
还提供了一个全局配置方法,在调用前提前配置,全局一次生效。
|
还提供了一个全局配置方法,在调用前提前配置,全局一次生效。
|
||||||
|
|
||||||
- `notification.config(options)`
|
- `notification.config(options)`
|
||||||
|
|
||||||
|
> 当你使用 `ConfigProvider` 进行全局化配置时,系统会默认自动开启 RTL 模式。(3.0+)
|
||||||
|
>
|
||||||
|
> 当你想单独使用,可通过如下设置开启 RTL 模式。
|
||||||
|
|
||||||
```js
|
```js
|
||||||
notification.config({
|
notification.config({
|
||||||
placement: 'bottomRight',
|
placement: 'bottomRight',
|
||||||
bottom: '50px',
|
bottom: '50px',
|
||||||
duration: 3,
|
duration: 3,
|
||||||
|
rtl: true,
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -66,4 +71,6 @@ notification.config({
|
||||||
| getContainer | 配置渲染节点的输出位置 | () => HTMLNode | () => document.body | |
|
| getContainer | 配置渲染节点的输出位置 | () => HTMLNode | () => document.body | |
|
||||||
| placement | 弹出位置,可选 `topLeft` `topRight` `bottomLeft` `bottomRight` | string | topRight | |
|
| placement | 弹出位置,可选 `topLeft` `topRight` `bottomLeft` `bottomRight` | string | topRight | |
|
||||||
| top | 消息从顶部弹出时,距离顶部的位置,单位像素。 | string | `24px` | |
|
| top | 消息从顶部弹出时,距离顶部的位置,单位像素。 | string | `24px` | |
|
||||||
| closeIcon | 自定义关闭图标 | VNode | - | |
|
| closeIcon | 自定义关闭图标 | VNode \| () => VNode | - | |
|
||||||
|
| rtl | 是否开启 RTL 模式 | boolean | false | 3.0 |
|
||||||
|
| maxCount | 最大显示数, 超过限制时,最早的消息会被自动关闭 | number | - | 3.0 |
|
||||||
|
|
|
@ -1,26 +1,25 @@
|
||||||
@import '../../style/themes/index';
|
@import '../../style/themes/index';
|
||||||
@import '../../style/mixins/index';
|
@import '../../style/mixins/index';
|
||||||
|
|
||||||
|
.popover-customize-bg(@notification-prefix-cls, @popover-background);
|
||||||
|
|
||||||
@notification-prefix-cls: ~'@{ant-prefix}-notification';
|
@notification-prefix-cls: ~'@{ant-prefix}-notification';
|
||||||
@notification-width: 384px;
|
@notification-width: 384px;
|
||||||
@notification-padding-vertical: 16px;
|
|
||||||
@notification-padding-horizontal: 24px;
|
|
||||||
@notification-padding: @notification-padding-vertical @notification-padding-horizontal;
|
@notification-padding: @notification-padding-vertical @notification-padding-horizontal;
|
||||||
@notification-margin-bottom: 16px;
|
@notification-margin-bottom: 16px;
|
||||||
|
@notification-margin-edge: 24px;
|
||||||
|
|
||||||
.@{notification-prefix-cls} {
|
.@{notification-prefix-cls} {
|
||||||
.reset-component();
|
.reset-component();
|
||||||
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: @zindex-notification;
|
z-index: @zindex-notification;
|
||||||
width: @notification-width;
|
margin-right: @notification-margin-edge;
|
||||||
max-width: ~'calc(100vw - 32px)';
|
|
||||||
margin-right: 24px;
|
|
||||||
|
|
||||||
&-topLeft,
|
&-topLeft,
|
||||||
&-bottomLeft {
|
&-bottomLeft {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
margin-left: 24px;
|
margin-left: @notification-margin-edge;
|
||||||
|
|
||||||
.@{notification-prefix-cls}-fade-enter.@{notification-prefix-cls}-fade-enter-active,
|
.@{notification-prefix-cls}-fade-enter.@{notification-prefix-cls}-fade-enter-active,
|
||||||
.@{notification-prefix-cls}-fade-appear.@{notification-prefix-cls}-fade-appear-active {
|
.@{notification-prefix-cls}-fade-appear.@{notification-prefix-cls}-fade-appear-active {
|
||||||
|
@ -33,18 +32,31 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-hook-holder {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
&-notice {
|
&-notice {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
width: @notification-width;
|
||||||
|
max-width: ~'calc(100vw - @{notification-margin-edge} * 2)';
|
||||||
margin-bottom: @notification-margin-bottom;
|
margin-bottom: @notification-margin-bottom;
|
||||||
|
margin-left: auto;
|
||||||
padding: @notification-padding;
|
padding: @notification-padding;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
line-height: 1.5;
|
line-height: @line-height-base;
|
||||||
|
word-wrap: break-word;
|
||||||
background: @notification-bg;
|
background: @notification-bg;
|
||||||
border-radius: @border-radius-base;
|
border-radius: @border-radius-base;
|
||||||
box-shadow: @shadow-2;
|
box-shadow: @shadow-2;
|
||||||
|
|
||||||
|
.@{notification-prefix-cls}-topLeft &,
|
||||||
|
.@{notification-prefix-cls}-bottomLeft & {
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&-message {
|
&-message {
|
||||||
display: inline-block;
|
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
color: @heading-color;
|
color: @heading-color;
|
||||||
font-size: @font-size-lg;
|
font-size: @font-size-lg;
|
||||||
|
@ -57,6 +69,7 @@
|
||||||
max-width: 4px;
|
max-width: 4px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
display: block;
|
display: block;
|
||||||
content: '';
|
content: '';
|
||||||
|
@ -97,12 +110,15 @@
|
||||||
&-success {
|
&-success {
|
||||||
color: @success-color;
|
color: @success-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-info {
|
&-info {
|
||||||
color: @info-color;
|
color: @info-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-warning {
|
&-warning {
|
||||||
color: @warning-color;
|
color: @warning-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-error {
|
&-error {
|
||||||
color: @error-color;
|
color: @error-color;
|
||||||
}
|
}
|
||||||
|
@ -116,9 +132,14 @@
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
& when (@theme = dark) {
|
||||||
|
color: fade(@white, 85%);
|
||||||
|
}
|
||||||
|
& when not (@theme = dark) {
|
||||||
color: shade(@text-color-secondary, 40%);
|
color: shade(@text-color-secondary, 40%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&-btn {
|
&-btn {
|
||||||
float: right;
|
float: right;
|
||||||
|
@ -134,9 +155,9 @@
|
||||||
|
|
||||||
&-fade-enter,
|
&-fade-enter,
|
||||||
&-fade-appear {
|
&-fade-appear {
|
||||||
opacity: 0;
|
|
||||||
.notification-fade-effect();
|
.notification-fade-effect();
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
animation-play-state: paused;
|
animation-play-state: paused;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,6 +185,7 @@
|
||||||
left: @notification-width;
|
left: @notification-width;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
left: 0;
|
left: 0;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
@ -175,6 +197,7 @@
|
||||||
right: @notification-width;
|
right: @notification-width;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
right: 0;
|
right: 0;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
@ -185,10 +208,9 @@
|
||||||
0% {
|
0% {
|
||||||
max-height: 150px;
|
max-height: 150px;
|
||||||
margin-bottom: @notification-margin-bottom;
|
margin-bottom: @notification-margin-bottom;
|
||||||
padding-top: @notification-padding;
|
|
||||||
padding-bottom: @notification-padding;
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
max-height: 0;
|
max-height: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
@ -197,3 +219,5 @@
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@import './rtl';
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
@import '../../style/themes/index';
|
||||||
|
@import '../../style/mixins/index';
|
||||||
|
|
||||||
|
@notification-prefix-cls: ~'@{ant-prefix}-notification';
|
||||||
|
|
||||||
|
.@{notification-prefix-cls} {
|
||||||
|
&-rtl {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-notice {
|
||||||
|
&-closable &-message {
|
||||||
|
.@{notification-prefix-cls}-rtl & {
|
||||||
|
padding-right: 0;
|
||||||
|
padding-left: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-with-icon &-message {
|
||||||
|
.@{notification-prefix-cls}-rtl & {
|
||||||
|
margin-right: 48px;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-with-icon &-description {
|
||||||
|
.@{notification-prefix-cls}-rtl & {
|
||||||
|
margin-right: 48px;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-icon {
|
||||||
|
.@{notification-prefix-cls}-rtl & {
|
||||||
|
margin-right: 4px;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-close {
|
||||||
|
.@{notification-prefix-cls}-rtl & {
|
||||||
|
right: auto;
|
||||||
|
left: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-btn {
|
||||||
|
.@{notification-prefix-cls}-rtl & {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,88 +0,0 @@
|
||||||
import PropTypes from '../_util/vue-types';
|
|
||||||
import { getComponent, getSlot } from '../_util/props-util';
|
|
||||||
import BaseMixin from '../_util/BaseMixin';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
mixins: [BaseMixin],
|
|
||||||
props: {
|
|
||||||
duration: PropTypes.number.def(1.5),
|
|
||||||
closable: PropTypes.looseBool,
|
|
||||||
prefixCls: PropTypes.string,
|
|
||||||
update: PropTypes.looseBool,
|
|
||||||
closeIcon: PropTypes.any,
|
|
||||||
onClose: PropTypes.func,
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
duration() {
|
|
||||||
this.restartCloseTimer();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
this.startCloseTimer();
|
|
||||||
},
|
|
||||||
updated() {
|
|
||||||
if (this.update) {
|
|
||||||
this.restartCloseTimer();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
beforeUnmount() {
|
|
||||||
this.clearCloseTimer();
|
|
||||||
this.willDestroy = true; // beforeUnmount调用后依然会触发onMouseleave事件
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
close(e) {
|
|
||||||
if (e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
this.clearCloseTimer();
|
|
||||||
this.__emit('close');
|
|
||||||
},
|
|
||||||
|
|
||||||
startCloseTimer() {
|
|
||||||
this.clearCloseTimer();
|
|
||||||
if (!this.willDestroy && this.duration) {
|
|
||||||
this.closeTimer = setTimeout(() => {
|
|
||||||
this.close();
|
|
||||||
}, this.duration * 1000);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
clearCloseTimer() {
|
|
||||||
if (this.closeTimer) {
|
|
||||||
clearTimeout(this.closeTimer);
|
|
||||||
this.closeTimer = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
restartCloseTimer() {
|
|
||||||
this.clearCloseTimer();
|
|
||||||
this.startCloseTimer();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { prefixCls, closable, clearCloseTimer, startCloseTimer, close, $attrs } = this;
|
|
||||||
const componentClass = `${prefixCls}-notice`;
|
|
||||||
const className = {
|
|
||||||
[`${componentClass}`]: 1,
|
|
||||||
[`${componentClass}-closable`]: closable,
|
|
||||||
};
|
|
||||||
const closeIcon = getComponent(this, 'closeIcon');
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class={className}
|
|
||||||
style={$attrs.style || { right: '50%' }}
|
|
||||||
onMouseenter={clearCloseTimer}
|
|
||||||
onMouseleave={startCloseTimer}
|
|
||||||
>
|
|
||||||
<div class={`${componentClass}-content`}>{getSlot(this)}</div>
|
|
||||||
{closable ? (
|
|
||||||
<a tabindex="0" onClick={close} class={`${componentClass}-close`}>
|
|
||||||
{closeIcon || <span class={`${componentClass}-close-x`} />}
|
|
||||||
</a>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
import type { Key } from '../_util/type';
|
||||||
|
import { Teleport, computed, defineComponent, onMounted, watch, onUnmounted } from 'vue';
|
||||||
|
import type { HTMLAttributes } from 'vue';
|
||||||
|
import type { MouseEventHandler } from '../_util/EventInterface';
|
||||||
|
import classNames from '../_util/classNames';
|
||||||
|
|
||||||
|
interface DivProps extends HTMLAttributes {
|
||||||
|
// Ideally we would allow all data-* props but this would depend on https://github.com/microsoft/TypeScript/issues/28960
|
||||||
|
'data-testid'?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NoticeProps {
|
||||||
|
prefixCls: string;
|
||||||
|
duration?: number | null;
|
||||||
|
updateMark?: string;
|
||||||
|
/** Mark as final key since set maxCount may keep the key but user pass key is different */
|
||||||
|
noticeKey: Key;
|
||||||
|
closeIcon?: any;
|
||||||
|
closable?: boolean;
|
||||||
|
props?: DivProps;
|
||||||
|
onClick?: MouseEventHandler;
|
||||||
|
onClose?: (key: Key) => void;
|
||||||
|
|
||||||
|
/** @private Only for internal usage. We don't promise that we will refactor this */
|
||||||
|
holder?: HTMLDivElement;
|
||||||
|
|
||||||
|
/** @private Provided by CSSMotionList */
|
||||||
|
visible?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent<NoticeProps>({
|
||||||
|
name: 'Notice',
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: [
|
||||||
|
'prefixCls',
|
||||||
|
'duration',
|
||||||
|
'updateMark',
|
||||||
|
'noticeKey',
|
||||||
|
'closeIcon',
|
||||||
|
'closable',
|
||||||
|
'props',
|
||||||
|
'onClick',
|
||||||
|
'onClose',
|
||||||
|
'holder',
|
||||||
|
'visible',
|
||||||
|
] as any,
|
||||||
|
setup(props, { attrs, slots }) {
|
||||||
|
let closeTimer: any;
|
||||||
|
const duration = computed(() => (props.duration === undefined ? 1.5 : props.duration));
|
||||||
|
const startCloseTimer = () => {
|
||||||
|
if (duration.value) {
|
||||||
|
closeTimer = setTimeout(() => {
|
||||||
|
close();
|
||||||
|
}, duration.value * 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearCloseTimer = () => {
|
||||||
|
if (closeTimer) {
|
||||||
|
clearTimeout(closeTimer);
|
||||||
|
closeTimer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const close = (e?: MouseEvent) => {
|
||||||
|
if (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
clearCloseTimer();
|
||||||
|
const { onClose, noticeKey } = props;
|
||||||
|
if (onClose) {
|
||||||
|
onClose(noticeKey);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const restartCloseTimer = () => {
|
||||||
|
clearCloseTimer();
|
||||||
|
startCloseTimer();
|
||||||
|
};
|
||||||
|
onMounted(() => {
|
||||||
|
startCloseTimer();
|
||||||
|
});
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearCloseTimer();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[duration, () => props.updateMark, () => props.visible],
|
||||||
|
([preDuration, preUpdateMark, preVisible], [newDuration, newUpdateMark, newVisible]) => {
|
||||||
|
if (
|
||||||
|
preDuration !== newDuration ||
|
||||||
|
preUpdateMark !== newUpdateMark ||
|
||||||
|
(preVisible !== newVisible && newVisible)
|
||||||
|
) {
|
||||||
|
restartCloseTimer();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ flush: 'post' },
|
||||||
|
);
|
||||||
|
return () => {
|
||||||
|
const { prefixCls, closable, closeIcon = slots.closeIcon?.(), onClick, holder } = props;
|
||||||
|
const { class: className, style } = attrs;
|
||||||
|
const componentClass = `${prefixCls}-notice`;
|
||||||
|
const dataOrAriaAttributeProps = Object.keys(attrs).reduce(
|
||||||
|
(acc: Record<string, string>, key: string) => {
|
||||||
|
if (key.substr(0, 5) === 'data-' || key.substr(0, 5) === 'aria-' || key === 'role') {
|
||||||
|
acc[key] = (attrs as any)[key];
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
const node = (
|
||||||
|
<div
|
||||||
|
class={classNames(componentClass, className, {
|
||||||
|
[`${componentClass}-closable`]: closable,
|
||||||
|
})}
|
||||||
|
style={style}
|
||||||
|
onMouseenter={clearCloseTimer}
|
||||||
|
onMouseleave={startCloseTimer}
|
||||||
|
onClick={onClick}
|
||||||
|
{...dataOrAriaAttributeProps}
|
||||||
|
>
|
||||||
|
<div class={`${componentClass}-content`}>{slots.default?.()}</div>
|
||||||
|
{closable ? (
|
||||||
|
<a tabindex={0} onClick={close} class={`${componentClass}-close`}>
|
||||||
|
{closeIcon || <span class={`${componentClass}-close-x`} />}
|
||||||
|
</a>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (holder) {
|
||||||
|
return <Teleport to={holder} v-slots={{ default: () => node }}></Teleport>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
|
@ -1,177 +0,0 @@
|
||||||
import { defineComponent, createVNode, render as vueRender, onMounted, ref } from 'vue';
|
|
||||||
import PropTypes from '../_util/vue-types';
|
|
||||||
import { getComponent } from '../_util/props-util';
|
|
||||||
import BaseMixin from '../_util/BaseMixin';
|
|
||||||
import createChainedFunction from '../_util/createChainedFunction';
|
|
||||||
import Notice from './Notice';
|
|
||||||
import { getTransitionGroupProps, TransitionGroup } from '../_util/transition';
|
|
||||||
import ConfigProvider, { globalConfigForApi } from '../config-provider';
|
|
||||||
|
|
||||||
function noop() {}
|
|
||||||
|
|
||||||
let seed = 0;
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
function getUuid() {
|
|
||||||
return `rcNotification_${now}_${seed++}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Notification = defineComponent({
|
|
||||||
mixins: [BaseMixin],
|
|
||||||
props: {
|
|
||||||
prefixCls: PropTypes.string.def('rc-notification'),
|
|
||||||
transitionName: PropTypes.string,
|
|
||||||
animation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).def('fade'),
|
|
||||||
maxCount: PropTypes.number,
|
|
||||||
closeIcon: PropTypes.any,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
notices: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getTransitionName() {
|
|
||||||
const props = this.$props;
|
|
||||||
let transitionName = props.transitionName;
|
|
||||||
if (!transitionName && props.animation) {
|
|
||||||
transitionName = `${props.prefixCls}-${props.animation}`;
|
|
||||||
}
|
|
||||||
return transitionName;
|
|
||||||
},
|
|
||||||
|
|
||||||
add(notice) {
|
|
||||||
const key = (notice.key = notice.key || getUuid());
|
|
||||||
const { maxCount } = this.$props;
|
|
||||||
this.setState(previousState => {
|
|
||||||
const notices = previousState.notices;
|
|
||||||
const noticeIndex = notices.map(v => v.key).indexOf(key);
|
|
||||||
const updatedNotices = notices.concat();
|
|
||||||
if (noticeIndex !== -1) {
|
|
||||||
updatedNotices.splice(noticeIndex, 1, notice);
|
|
||||||
} else {
|
|
||||||
if (maxCount && notices.length >= maxCount) {
|
|
||||||
// XXX, use key of first item to update new added (let React to move exsiting
|
|
||||||
// instead of remove and mount). Same key was used before for both a) external
|
|
||||||
// manual control and b) internal react 'key' prop , which is not that good.
|
|
||||||
notice.updateKey = updatedNotices[0].updateKey || updatedNotices[0].key;
|
|
||||||
updatedNotices.shift();
|
|
||||||
}
|
|
||||||
updatedNotices.push(notice);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
notices: updatedNotices,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
remove(key) {
|
|
||||||
this.setState(previousState => {
|
|
||||||
return {
|
|
||||||
notices: previousState.notices.filter(notice => notice.key !== key),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { prefixCls, notices, remove, getTransitionName, $attrs } = this;
|
|
||||||
const transitionProps = getTransitionGroupProps(getTransitionName());
|
|
||||||
const noticeNodes = notices.map((notice, index) => {
|
|
||||||
const update = Boolean(index === notices.length - 1 && notice.updateKey);
|
|
||||||
const key = notice.updateKey ? notice.updateKey : notice.key;
|
|
||||||
|
|
||||||
const { content, duration, closable, onClose, style, class: className } = notice;
|
|
||||||
const close = createChainedFunction(remove.bind(this, notice.key), onClose);
|
|
||||||
const noticeProps = {
|
|
||||||
prefixCls,
|
|
||||||
duration,
|
|
||||||
closable,
|
|
||||||
update,
|
|
||||||
closeIcon: getComponent(this, 'closeIcon', { prefixCls }),
|
|
||||||
onClose: close,
|
|
||||||
onClick: notice.onClick || noop,
|
|
||||||
style,
|
|
||||||
class: className,
|
|
||||||
key,
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Notice {...noticeProps}>
|
|
||||||
{typeof content === 'function' ? content({ prefixCls }) : content}
|
|
||||||
</Notice>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
const className = {
|
|
||||||
[prefixCls]: 1,
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class={className}
|
|
||||||
style={
|
|
||||||
$attrs.style || {
|
|
||||||
top: '65px',
|
|
||||||
left: '50%',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<TransitionGroup tag="span" {...transitionProps}>
|
|
||||||
{noticeNodes}
|
|
||||||
</TransitionGroup>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
Notification.newInstance = function newNotificationInstance(properties, callback) {
|
|
||||||
const {
|
|
||||||
name = 'notification',
|
|
||||||
getContainer,
|
|
||||||
appContext,
|
|
||||||
prefixCls: customizePrefixCls,
|
|
||||||
rootPrefixCls: customRootPrefixCls,
|
|
||||||
...props
|
|
||||||
} = properties || {};
|
|
||||||
const div = document.createElement('div');
|
|
||||||
if (getContainer) {
|
|
||||||
const root = getContainer();
|
|
||||||
root.appendChild(div);
|
|
||||||
} else {
|
|
||||||
document.body.appendChild(div);
|
|
||||||
}
|
|
||||||
const Wrapper = defineComponent({
|
|
||||||
setup(_props, { attrs }) {
|
|
||||||
const notiRef = ref();
|
|
||||||
onMounted(() => {
|
|
||||||
callback({
|
|
||||||
notice(noticeProps) {
|
|
||||||
notiRef.value?.add(noticeProps);
|
|
||||||
},
|
|
||||||
removeNotice(key) {
|
|
||||||
notiRef.value?.remove(key);
|
|
||||||
},
|
|
||||||
destroy() {
|
|
||||||
vueRender(null, div);
|
|
||||||
if (div.parentNode) {
|
|
||||||
div.parentNode.removeChild(div);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
const global = globalConfigForApi;
|
|
||||||
const prefixCls = global.getPrefixCls(name, customizePrefixCls);
|
|
||||||
const rootPrefixCls = global.getRootPrefixCls(customRootPrefixCls, prefixCls);
|
|
||||||
return (
|
|
||||||
<ConfigProvider {...global} notUpdateGlobalConfig={true} prefixCls={rootPrefixCls}>
|
|
||||||
<Notification ref={notiRef} {...attrs} prefixCls={prefixCls} />
|
|
||||||
</ConfigProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const vm = createVNode(Wrapper, props);
|
|
||||||
vm.appContext = appContext || vm.appContext;
|
|
||||||
vueRender(vm, div);
|
|
||||||
};
|
|
||||||
export default Notification;
|
|
|
@ -0,0 +1,262 @@
|
||||||
|
import { getTransitionGroupProps } from '../_util/transition';
|
||||||
|
import type { Key } from '../_util/type';
|
||||||
|
import type { CSSProperties } from 'vue';
|
||||||
|
import {
|
||||||
|
createVNode,
|
||||||
|
computed,
|
||||||
|
defineComponent,
|
||||||
|
ref,
|
||||||
|
TransitionGroup,
|
||||||
|
onMounted,
|
||||||
|
render as vueRender,
|
||||||
|
} from 'vue';
|
||||||
|
import type { NoticeProps } from './Notice';
|
||||||
|
import Notice from './Notice';
|
||||||
|
import ConfigProvider, { globalConfigForApi } from '../config-provider';
|
||||||
|
|
||||||
|
let seed = 0;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
function getUuid() {
|
||||||
|
const id = seed;
|
||||||
|
seed += 1;
|
||||||
|
return `rcNotification_${now}_${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NoticeContent extends Omit<NoticeProps, 'prefixCls' | 'noticeKey' | 'onClose'> {
|
||||||
|
prefixCls?: string;
|
||||||
|
key?: Key;
|
||||||
|
updateMark?: string;
|
||||||
|
content?: any;
|
||||||
|
onClose?: () => void;
|
||||||
|
style?: CSSProperties;
|
||||||
|
class?: String;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NoticeFunc = (noticeProps: NoticeContent) => void;
|
||||||
|
export type HolderReadyCallback = (
|
||||||
|
div: HTMLDivElement,
|
||||||
|
noticeProps: NoticeProps & { key: Key },
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export interface NotificationInstance {
|
||||||
|
notice: NoticeFunc;
|
||||||
|
removeNotice: (key: Key) => void;
|
||||||
|
destroy: () => void;
|
||||||
|
component: Notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotificationProps {
|
||||||
|
prefixCls?: string;
|
||||||
|
transitionName?: string;
|
||||||
|
animation?: string | object;
|
||||||
|
maxCount?: number;
|
||||||
|
closeIcon?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationState = {
|
||||||
|
notice: NoticeContent & {
|
||||||
|
userPassKey?: Key;
|
||||||
|
};
|
||||||
|
holderCallback?: HolderReadyCallback;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
const Notification = defineComponent<NotificationProps>({
|
||||||
|
name: 'Notification',
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: ['prefixCls', 'transitionName', 'animation', 'maxCount', 'closeIcon'] as any,
|
||||||
|
setup(props, { attrs, expose, slots }) {
|
||||||
|
const hookRefs = new Map<Key, HTMLDivElement>();
|
||||||
|
const notices = ref<NotificationState>([]);
|
||||||
|
const transitionProps = computed(() => {
|
||||||
|
const { prefixCls, animation = 'fade' } = props;
|
||||||
|
let name = props.transitionName;
|
||||||
|
if (!name && animation) {
|
||||||
|
name = `${prefixCls}-${animation}`;
|
||||||
|
}
|
||||||
|
return getTransitionGroupProps(name);
|
||||||
|
});
|
||||||
|
|
||||||
|
const add = (originNotice: NoticeContent, holderCallback?: HolderReadyCallback) => {
|
||||||
|
const key = originNotice.key || getUuid();
|
||||||
|
const notice: NoticeContent & { key: Key; userPassKey?: Key } = {
|
||||||
|
...originNotice,
|
||||||
|
key,
|
||||||
|
};
|
||||||
|
const { maxCount } = props;
|
||||||
|
const noticeIndex = notices.value.map(v => v.notice.key).indexOf(key);
|
||||||
|
const updatedNotices = notices.value.concat();
|
||||||
|
if (noticeIndex !== -1) {
|
||||||
|
updatedNotices.splice(noticeIndex, 1, { notice, holderCallback } as any);
|
||||||
|
} else {
|
||||||
|
if (maxCount && notices.value.length >= maxCount) {
|
||||||
|
// XXX, use key of first item to update new added (let React to move exsiting
|
||||||
|
// instead of remove and mount). Same key was used before for both a) external
|
||||||
|
// manual control and b) internal react 'key' prop , which is not that good.
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
|
||||||
|
// zombieJ: Not know why use `updateKey`. This makes Notice infinite loop in jest.
|
||||||
|
// Change to `updateMark` for compare instead.
|
||||||
|
// https://github.com/react-component/notification/commit/32299e6be396f94040bfa82517eea940db947ece
|
||||||
|
notice.key = updatedNotices[0].notice.key as Key;
|
||||||
|
notice.updateMark = getUuid();
|
||||||
|
|
||||||
|
// zombieJ: That's why. User may close by key directly.
|
||||||
|
// We need record this but not re-render to avoid upper issue
|
||||||
|
// https://github.com/react-component/notification/issues/129
|
||||||
|
notice.userPassKey = key;
|
||||||
|
|
||||||
|
updatedNotices.shift();
|
||||||
|
}
|
||||||
|
updatedNotices.push({ notice, holderCallback } as any);
|
||||||
|
}
|
||||||
|
notices.value = updatedNotices;
|
||||||
|
};
|
||||||
|
|
||||||
|
const remove = (removeKey: Key) => {
|
||||||
|
notices.value = notices.value.filter(({ notice: { key, userPassKey } }) => {
|
||||||
|
const mergedKey = userPassKey || key;
|
||||||
|
return mergedKey !== removeKey;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
expose({
|
||||||
|
add,
|
||||||
|
remove,
|
||||||
|
notices,
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
const { prefixCls, closeIcon = slots.closeIcon?.({ prefixCls }) } = props;
|
||||||
|
const noticeNodes = notices.value.map(({ notice, holderCallback }, index) => {
|
||||||
|
const updateMark = index === notices.value.length - 1 ? notice.updateMark : undefined;
|
||||||
|
const { key, userPassKey } = notice;
|
||||||
|
|
||||||
|
const { content } = notice;
|
||||||
|
const noticeProps = {
|
||||||
|
prefixCls,
|
||||||
|
closeIcon: typeof closeIcon === 'function' ? closeIcon({ prefixCls }) : closeIcon,
|
||||||
|
...(notice as any),
|
||||||
|
...notice.props,
|
||||||
|
key,
|
||||||
|
noticeKey: userPassKey || key,
|
||||||
|
updateMark,
|
||||||
|
onClose: (noticeKey: Key) => {
|
||||||
|
remove(noticeKey);
|
||||||
|
notice.onClose?.();
|
||||||
|
},
|
||||||
|
onClick: notice.onClick,
|
||||||
|
};
|
||||||
|
if (holderCallback) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
class={`${prefixCls}-hook-holder`}
|
||||||
|
ref={(div: HTMLDivElement) => {
|
||||||
|
if (typeof key === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (div) {
|
||||||
|
hookRefs.set(key, div);
|
||||||
|
holderCallback(div, noticeProps);
|
||||||
|
} else {
|
||||||
|
hookRefs.delete(key);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Notice {...noticeProps}>
|
||||||
|
{typeof content === 'function' ? content({ prefixCls }) : content}
|
||||||
|
</Notice>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const className = {
|
||||||
|
[prefixCls]: 1,
|
||||||
|
[attrs.class as string]: !!attrs.class,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={className}
|
||||||
|
style={
|
||||||
|
attrs.style || {
|
||||||
|
top: '65px',
|
||||||
|
left: '50%',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TransitionGroup tag="div" {...transitionProps.value}>
|
||||||
|
{noticeNodes}
|
||||||
|
</TransitionGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Notification.newInstance = function newNotificationInstance(properties, callback) {
|
||||||
|
const {
|
||||||
|
name = 'notification',
|
||||||
|
getContainer,
|
||||||
|
appContext,
|
||||||
|
prefixCls: customizePrefixCls,
|
||||||
|
rootPrefixCls: customRootPrefixCls,
|
||||||
|
transitionName: customTransitionName,
|
||||||
|
hasTransitionName,
|
||||||
|
...props
|
||||||
|
} = properties || {};
|
||||||
|
const div = document.createElement('div');
|
||||||
|
if (getContainer) {
|
||||||
|
const root = getContainer();
|
||||||
|
root.appendChild(div);
|
||||||
|
} else {
|
||||||
|
document.body.appendChild(div);
|
||||||
|
}
|
||||||
|
const Wrapper = defineComponent({
|
||||||
|
name: 'NotificationWrapper',
|
||||||
|
setup(_props, { attrs }) {
|
||||||
|
const notiRef = ref();
|
||||||
|
onMounted(() => {
|
||||||
|
callback({
|
||||||
|
notice(noticeProps: NoticeContent) {
|
||||||
|
notiRef.value?.add(noticeProps);
|
||||||
|
},
|
||||||
|
removeNotice(key: Key) {
|
||||||
|
notiRef.value?.remove(key);
|
||||||
|
},
|
||||||
|
destroy() {
|
||||||
|
vueRender(null, div);
|
||||||
|
if (div.parentNode) {
|
||||||
|
div.parentNode.removeChild(div);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
component: notiRef,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
const global = globalConfigForApi;
|
||||||
|
const prefixCls = global.getPrefixCls(name, customizePrefixCls);
|
||||||
|
const rootPrefixCls = global.getRootPrefixCls(customRootPrefixCls, prefixCls);
|
||||||
|
const transitionName = hasTransitionName
|
||||||
|
? customTransitionName
|
||||||
|
: `${rootPrefixCls}-${customTransitionName}`;
|
||||||
|
return (
|
||||||
|
<ConfigProvider {...global} notUpdateGlobalConfig={true} prefixCls={rootPrefixCls}>
|
||||||
|
<Notification
|
||||||
|
ref={notiRef}
|
||||||
|
{...attrs}
|
||||||
|
prefixCls={prefixCls}
|
||||||
|
transitionName={transitionName}
|
||||||
|
/>
|
||||||
|
</ConfigProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const vm = createVNode(Wrapper, props);
|
||||||
|
vm.appContext = appContext || vm.appContext;
|
||||||
|
vueRender(vm, div);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Notification;
|
|
@ -1,95 +0,0 @@
|
||||||
@notificationPrefixCls: rc-notification;
|
|
||||||
|
|
||||||
.@{notificationPrefixCls} {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 1000;
|
|
||||||
|
|
||||||
&-notice {
|
|
||||||
padding: 7px 20px 7px 10px;
|
|
||||||
border-radius: 3px 3px;
|
|
||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
|
||||||
border: 0px solid rgba(0, 0, 0, 0);
|
|
||||||
background: #fff;
|
|
||||||
display: block;
|
|
||||||
width: auto;
|
|
||||||
line-height: 1.5;
|
|
||||||
vertical-align: middle;
|
|
||||||
position: relative;
|
|
||||||
margin: 10px 0;
|
|
||||||
|
|
||||||
&-closable {
|
|
||||||
padding-right: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-close {
|
|
||||||
position: absolute;
|
|
||||||
right: 5px;
|
|
||||||
top: 3px;
|
|
||||||
color: #000;
|
|
||||||
cursor: pointer;
|
|
||||||
outline: none;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1;
|
|
||||||
text-shadow: 0 1px 0 #fff;
|
|
||||||
filter: alpha(opacity=20);
|
|
||||||
opacity: 0.2;
|
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
&-x:after {
|
|
||||||
content: '×';
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
filter: alpha(opacity=100);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-effect() {
|
|
||||||
animation-duration: 0.3s;
|
|
||||||
animation-fill-mode: both;
|
|
||||||
animation-timing-function: cubic-bezier(0.55, 0, 0.55, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
&-fade-enter {
|
|
||||||
opacity: 0;
|
|
||||||
.fade-effect();
|
|
||||||
animation-play-state: paused;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-fade-leave {
|
|
||||||
.fade-effect();
|
|
||||||
animation-play-state: paused;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-fade-enter&-fade-enter-active {
|
|
||||||
animation-name: rcNotificationFadeIn;
|
|
||||||
animation-play-state: running;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-fade-leave&-fade-leave-active {
|
|
||||||
animation-name: rcDialogFadeOut;
|
|
||||||
animation-play-state: running;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes rcNotificationFadeIn {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes rcDialogFadeOut {
|
|
||||||
0% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +1,4 @@
|
||||||
// based on rc-notification 3.3.1
|
// based on rc-notification 4.5.7
|
||||||
import Notification from './Notification';
|
import Notification from './Notification';
|
||||||
|
|
||||||
export default Notification;
|
export default Notification;
|
Loading…
Reference in New Issue