refactor: notification
parent
f6725f86c2
commit
f6760bff27
|
@ -8,11 +8,11 @@ title:
|
|||
|
||||
## zh-CN
|
||||
|
||||
使用 style 和 className 来定义样式。
|
||||
使用 `style` 和 `class` 来定义样式。
|
||||
|
||||
## en-US
|
||||
|
||||
The style and className are available to customize Notification.
|
||||
The `style` and `class` are available to customize Notification.
|
||||
|
||||
</docs>
|
||||
|
||||
|
@ -33,6 +33,7 @@ export default defineComponent({
|
|||
width: '600px',
|
||||
marginLeft: `${335 - 600}px`,
|
||||
},
|
||||
class: 'notification-custom-class',
|
||||
});
|
||||
};
|
||||
return {
|
||||
|
@ -41,3 +42,8 @@ export default defineComponent({
|
|||
},
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
.notification-custom-class {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,26 +1,25 @@
|
|||
@import '../../style/themes/index';
|
||||
@import '../../style/mixins/index';
|
||||
|
||||
.popover-customize-bg(@notification-prefix-cls, @popover-background);
|
||||
|
||||
@notification-prefix-cls: ~'@{ant-prefix}-notification';
|
||||
@notification-width: 384px;
|
||||
@notification-padding-vertical: 16px;
|
||||
@notification-padding-horizontal: 24px;
|
||||
@notification-padding: @notification-padding-vertical @notification-padding-horizontal;
|
||||
@notification-margin-bottom: 16px;
|
||||
@notification-margin-edge: 24px;
|
||||
|
||||
.@{notification-prefix-cls} {
|
||||
.reset-component();
|
||||
|
||||
position: fixed;
|
||||
z-index: @zindex-notification;
|
||||
width: @notification-width;
|
||||
max-width: ~'calc(100vw - 32px)';
|
||||
margin-right: 24px;
|
||||
margin-right: @notification-margin-edge;
|
||||
|
||||
&-topLeft,
|
||||
&-bottomLeft {
|
||||
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-appear.@{notification-prefix-cls}-fade-appear-active {
|
||||
|
@ -33,18 +32,31 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
&-hook-holder {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&-notice {
|
||||
position: relative;
|
||||
width: @notification-width;
|
||||
max-width: ~'calc(100vw - @{notification-margin-edge} * 2)';
|
||||
margin-bottom: @notification-margin-bottom;
|
||||
margin-left: auto;
|
||||
padding: @notification-padding;
|
||||
overflow: hidden;
|
||||
line-height: 1.5;
|
||||
line-height: @line-height-base;
|
||||
word-wrap: break-word;
|
||||
background: @notification-bg;
|
||||
border-radius: @border-radius-base;
|
||||
box-shadow: @shadow-2;
|
||||
|
||||
.@{notification-prefix-cls}-topLeft &,
|
||||
.@{notification-prefix-cls}-bottomLeft & {
|
||||
margin-right: auto;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&-message {
|
||||
display: inline-block;
|
||||
margin-bottom: 8px;
|
||||
color: @heading-color;
|
||||
font-size: @font-size-lg;
|
||||
|
@ -57,6 +69,7 @@
|
|||
max-width: 4px;
|
||||
background-color: transparent;
|
||||
pointer-events: none;
|
||||
|
||||
&::before {
|
||||
display: block;
|
||||
content: '';
|
||||
|
@ -97,12 +110,15 @@
|
|||
&-success {
|
||||
color: @success-color;
|
||||
}
|
||||
|
||||
&-info {
|
||||
color: @info-color;
|
||||
}
|
||||
|
||||
&-warning {
|
||||
color: @warning-color;
|
||||
}
|
||||
|
||||
&-error {
|
||||
color: @error-color;
|
||||
}
|
||||
|
@ -116,7 +132,12 @@
|
|||
outline: none;
|
||||
|
||||
&:hover {
|
||||
color: shade(@text-color-secondary, 40%);
|
||||
& when (@theme = dark) {
|
||||
color: fade(@white, 85%);
|
||||
}
|
||||
& when not (@theme = dark) {
|
||||
color: shade(@text-color-secondary, 40%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -134,9 +155,9 @@
|
|||
|
||||
&-fade-enter,
|
||||
&-fade-appear {
|
||||
opacity: 0;
|
||||
.notification-fade-effect();
|
||||
|
||||
opacity: 0;
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
|
@ -164,6 +185,7 @@
|
|||
left: @notification-width;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
left: 0;
|
||||
opacity: 1;
|
||||
|
@ -175,6 +197,7 @@
|
|||
right: @notification-width;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
right: 0;
|
||||
opacity: 1;
|
||||
|
@ -185,10 +208,9 @@
|
|||
0% {
|
||||
max-height: 150px;
|
||||
margin-bottom: @notification-margin-bottom;
|
||||
padding-top: @notification-padding;
|
||||
padding-bottom: @notification-padding;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
max-height: 0;
|
||||
margin-bottom: 0;
|
||||
|
@ -197,3 +219,5 @@
|
|||
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,251 @@
|
|||
import { getTransitionGroupProps } from 'ant-design-vue/es/_util/transition';
|
||||
import type { Key } from 'ant-design-vue/es/_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;
|
||||
|
||||
useNotification: () => [NoticeFunc, any];
|
||||
}
|
||||
|
||||
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',
|
||||
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,
|
||||
});
|
||||
return () => {
|
||||
const { prefixCls, closeIcon = slots.closeIcon?.() } = 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,
|
||||
...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);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
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;
|
|
@ -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';
|
||||
|
||||
export default Notification;
|
Loading…
Reference in New Issue