refactor(v3/badge): use composition api (#4076)

pull/4134/head
言肆 2021-05-25 13:44:11 +08:00 committed by GitHub
parent 69b9f80a01
commit 372ac5c729
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 287 additions and 252 deletions

View File

@ -1,27 +1,39 @@
import PropTypes from '../_util/vue-types'; import PropTypes from '../_util/vue-types';
import ScrollNumber from './ScrollNumber'; import ScrollNumber from './ScrollNumber';
import classNames from '../_util/classNames'; import classNames from '../_util/classNames';
import { initDefaultProps, getComponent, getSlot } from '../_util/props-util'; import { getPropsSlot, flattenChildren } from '../_util/props-util';
import { cloneElement } from '../_util/vnode'; import { cloneElement } from '../_util/vnode';
import { getTransitionProps, Transition } from '../_util/transition'; import { getTransitionProps, Transition } from '../_util/transition';
import isNumeric from '../_util/isNumeric'; import isNumeric from '../_util/isNumeric';
import { defaultConfigProvider } from '../config-provider'; import { defaultConfigProvider } from '../config-provider';
import { inject, defineComponent, CSSProperties, VNode, App, Plugin } from 'vue'; import {
inject,
defineComponent,
ExtractPropTypes,
CSSProperties,
VNode,
App,
Plugin,
reactive,
computed,
} from 'vue';
import { tuple } from '../_util/type'; import { tuple } from '../_util/type';
import Ribbon from './Ribbon'; import Ribbon from './Ribbon';
import { isPresetColor } from './utils'; import { isPresetColor } from './utils';
const BadgeProps = { export const badgeProps = {
/** Number to show in badge */ /** Number to show in badge */
count: PropTypes.VNodeChild, count: PropTypes.VNodeChild,
showZero: PropTypes.looseBool, showZero: PropTypes.looseBool,
/** Max count to show */ /** Max count to show */
overflowCount: PropTypes.number, overflowCount: PropTypes.number.def(99),
/** whether to show red dot without number */ /** whether to show red dot without number */
dot: PropTypes.looseBool, dot: PropTypes.looseBool,
prefixCls: PropTypes.string, prefixCls: PropTypes.string,
scrollNumberPrefixCls: PropTypes.string, scrollNumberPrefixCls: PropTypes.string,
status: PropTypes.oneOf(tuple('success', 'processing', 'default', 'error', 'warning')), status: PropTypes.oneOf(tuple('success', 'processing', 'default', 'error', 'warning')),
// sync antd@4.6.0
size: PropTypes.oneOf(tuple('default', 'small')).def('default'),
color: PropTypes.string, color: PropTypes.string,
text: PropTypes.VNodeChild, text: PropTypes.VNodeChild,
offset: PropTypes.arrayOf(PropTypes.oneOfType([String, Number])), offset: PropTypes.arrayOf(PropTypes.oneOfType([String, Number])),
@ -29,48 +41,44 @@ const BadgeProps = {
title: PropTypes.string, title: PropTypes.string,
}; };
export type BadgeProps = Partial<ExtractPropTypes<typeof badgeProps>>;
const Badge = defineComponent({ const Badge = defineComponent({
name: 'ABadge', name: 'ABadge',
Ribbon, Ribbon,
props: initDefaultProps(BadgeProps, { props: badgeProps,
showZero: false, setup(props, { slots }) {
dot: false, const configProvider = inject('configProvider', defaultConfigProvider);
overflowCount: 99, const state = reactive({
}) as typeof BadgeProps,
setup() {
return {
configProvider: inject('configProvider', defaultConfigProvider),
badgeCount: undefined, badgeCount: undefined,
}; });
},
methods: { const getNumberedDispayCount = () => {
getNumberedDispayCount() { const { overflowCount } = props;
const { overflowCount } = this.$props; const count = state.badgeCount;
const count = this.badgeCount;
const displayCount = count > overflowCount ? `${overflowCount}+` : count; const displayCount = count > overflowCount ? `${overflowCount}+` : count;
return displayCount; return displayCount;
}, };
getDispayCount() { const getDispayCount = computed(() => {
const isDot = this.isDot();
// dot mode don't need count // dot mode don't need count
if (isDot) { if (isDot.value) {
return ''; return '';
} }
return this.getNumberedDispayCount(); return getNumberedDispayCount();
}, });
getScrollNumberTitle() { const getScrollNumberTitle = () => {
const { title } = this.$props; const { title } = props;
const count = this.badgeCount; const count = state.badgeCount;
if (title) { if (title) {
return title; return title;
} }
return typeof count === 'string' || typeof count === 'number' ? count : undefined; return typeof count === 'string' || typeof count === 'number' ? count : undefined;
}, };
getStyleWithOffset() { const getStyleWithOffset = () => {
const { offset, numberStyle } = this.$props; const { offset, numberStyle } = props;
return offset return offset
? { ? {
right: `${-parseInt(offset[0] as string, 10)}px`, right: `${-parseInt(offset[0] as string, 10)}px`,
@ -78,47 +86,49 @@ const Badge = defineComponent({
...numberStyle, ...numberStyle,
} }
: { ...numberStyle }; : { ...numberStyle };
}, };
getBadgeClassName(prefixCls: string, children: VNode[]) {
const hasStatus = this.hasStatus(); const hasStatus = computed(() => {
const { status, color } = props;
return !!status || !!color;
});
const isZero = computed(() => {
const numberedDispayCount = getNumberedDispayCount();
return numberedDispayCount === '0' || numberedDispayCount === 0;
});
const isDot = computed(() => {
const { dot } = props;
return (dot && !isZero.value) || hasStatus.value;
});
const isHidden = computed(() => {
const { showZero } = props;
const isEmpty =
getDispayCount.value === null ||
getDispayCount.value === undefined ||
getDispayCount.value === '';
return (isEmpty || (isZero.value && !showZero)) && !isDot.value;
});
const renderStatusText = (prefixCls: string) => {
const text = getPropsSlot(slots, props, 'text');
const hidden = isHidden.value;
return hidden || !text ? null : <span class={`${prefixCls}-status-text`}>{text}</span>;
};
const getBadgeClassName = (prefixCls: string, children: VNode[]) => {
const status = hasStatus.value;
return classNames(prefixCls, { return classNames(prefixCls, {
[`${prefixCls}-status`]: hasStatus, [`${prefixCls}-status`]: status,
[`${prefixCls}-dot-status`]: hasStatus && this.dot && !this.isZero(), [`${prefixCls}-dot-status`]: status && props.dot && !isZero.value,
[`${prefixCls}-not-a-wrapper`]: !children.length, [`${prefixCls}-not-a-wrapper`]: !children.length,
}); });
}, };
hasStatus() {
const { status, color } = this.$props;
return !!status || !!color;
},
isZero() {
const numberedDispayCount = this.getNumberedDispayCount();
return numberedDispayCount === '0' || numberedDispayCount === 0;
},
isDot() { const renderDispayComponent = () => {
const { dot } = this.$props; const count = state.badgeCount;
const isZero = this.isZero();
return (dot && !isZero) || this.hasStatus();
},
isHidden() {
const { showZero } = this.$props;
const displayCount = this.getDispayCount();
const isZero = this.isZero();
const isDot = this.isDot();
const isEmpty = displayCount === null || displayCount === undefined || displayCount === '';
return (isEmpty || (isZero && !showZero)) && !isDot;
},
renderStatusText(prefixCls: string) {
const text = getComponent(this, 'text');
const hidden = this.isHidden();
return hidden || !text ? null : <span class={`${prefixCls}-status-text`}>{text}</span>;
},
renderDispayComponent() {
const count = this.badgeCount;
const customNode = count; const customNode = count;
if (!customNode || typeof customNode !== 'object') { if (!customNode || typeof customNode !== 'object') {
return undefined; return undefined;
@ -126,103 +136,102 @@ const Badge = defineComponent({
return cloneElement( return cloneElement(
customNode, customNode,
{ {
style: this.getStyleWithOffset(), style: getStyleWithOffset(),
}, },
false, false,
); );
}, };
renderBadgeNumber(prefixCls: string, scrollNumberPrefixCls: string) { const renderBadgeNumber = (prefixCls: string, scrollNumberPrefixCls: string) => {
const { status, color } = this.$props; const { status, color, size } = props;
const count = this.badgeCount; const count = state.badgeCount;
const displayCount = this.getDispayCount(); const displayCount = getDispayCount.value;
const isDot = this.isDot();
const hidden = this.isHidden();
const scrollNumberCls = { const scrollNumberCls = {
[`${prefixCls}-dot`]: isDot, [`${prefixCls}-dot`]: isDot.value,
[`${prefixCls}-count`]: !isDot, [`${prefixCls}-count`]: !isDot.value,
[`${prefixCls}-count-sm`]: size === 'small',
[`${prefixCls}-multiple-words`]: [`${prefixCls}-multiple-words`]:
!isDot && count && count.toString && count.toString().length > 1, !isDot.value && count && count.toString && count.toString().length > 1,
[`${prefixCls}-status-${status}`]: !!status, [`${prefixCls}-status-${status}`]: !!status,
[`${prefixCls}-status-${color}`]: isPresetColor(color), [`${prefixCls}-status-${color}`]: isPresetColor(color),
}; };
let statusStyle = this.getStyleWithOffset(); let statusStyle = getStyleWithOffset();
if (color && !isPresetColor(color)) { if (color && !isPresetColor(color)) {
statusStyle = statusStyle || {}; statusStyle = statusStyle || {};
statusStyle.background = color; statusStyle.background = color;
} }
return hidden ? null : ( return isHidden.value ? null : (
<ScrollNumber <ScrollNumber
prefixCls={scrollNumberPrefixCls} prefixCls={scrollNumberPrefixCls}
data-show={!hidden} data-show={!isHidden.value}
v-show={!hidden} v-show={!isHidden.value}
class={scrollNumberCls} class={scrollNumberCls}
count={displayCount} count={displayCount}
displayComponent={this.renderDispayComponent()} displayComponent={renderDispayComponent()}
title={this.getScrollNumberTitle()} title={getScrollNumberTitle()}
style={statusStyle} style={statusStyle}
key="scrollNumber" key="scrollNumber"
/> />
); );
}, };
},
render() { return () => {
const { const {
prefixCls: customizePrefixCls, prefixCls: customizePrefixCls,
scrollNumberPrefixCls: customizeScrollNumberPrefixCls, scrollNumberPrefixCls: customizeScrollNumberPrefixCls,
status, status,
color, color,
} = this; } = props;
const text = getComponent(this, 'text'); const text = getPropsSlot(slots, props, 'text');
const getPrefixCls = this.configProvider.getPrefixCls; const getPrefixCls = configProvider.getPrefixCls;
const prefixCls = getPrefixCls('badge', customizePrefixCls); const prefixCls = getPrefixCls('badge', customizePrefixCls);
const scrollNumberPrefixCls = getPrefixCls('scroll-number', customizeScrollNumberPrefixCls); const scrollNumberPrefixCls = getPrefixCls('scroll-number', customizeScrollNumberPrefixCls);
const children = getSlot(this); const children = flattenChildren(slots.default?.());
let count = getComponent(this, 'count'); let count = getPropsSlot(slots, props, 'count');
if (Array.isArray(count)) { if (Array.isArray(count)) {
count = count[0]; count = count[0];
} }
this.badgeCount = count; state.badgeCount = count;
const scrollNumber = this.renderBadgeNumber(prefixCls, scrollNumberPrefixCls); const scrollNumber = renderBadgeNumber(prefixCls, scrollNumberPrefixCls);
const statusText = this.renderStatusText(prefixCls); const statusText = renderStatusText(prefixCls);
const statusCls = classNames({ const statusCls = classNames({
[`${prefixCls}-status-dot`]: this.hasStatus(), [`${prefixCls}-status-dot`]: hasStatus.value,
[`${prefixCls}-status-${status}`]: !!status, [`${prefixCls}-status-${status}`]: !!status,
[`${prefixCls}-status-${color}`]: isPresetColor(color), [`${prefixCls}-status-${color}`]: isPresetColor(color),
}); });
const statusStyle: CSSProperties = {}; const statusStyle: CSSProperties = {};
if (color && !isPresetColor(color)) { if (color && !isPresetColor(color)) {
statusStyle.background = color; statusStyle.background = color;
} }
// <Badge status="success" /> // <Badge status="success" />
if (!children.length && this.hasStatus()) { if (!children.length && hasStatus.value) {
const styleWithOffset = this.getStyleWithOffset(); const styleWithOffset = getStyleWithOffset();
const statusTextColor = styleWithOffset && styleWithOffset.color; const statusTextColor = styleWithOffset && styleWithOffset.color;
return ( return (
<span class={this.getBadgeClassName(prefixCls, children)} style={styleWithOffset}> <span class={getBadgeClassName(prefixCls, children)} style={styleWithOffset}>
<span class={statusCls} style={statusStyle} /> <span class={statusCls} style={statusStyle} />
<span style={{ color: statusTextColor }} class={`${prefixCls}-status-text`}> <span style={{ color: statusTextColor }} class={`${prefixCls}-status-text`}>
{text} {text}
</span>
</span> </span>
);
}
const transitionProps = getTransitionProps(children.length ? `${prefixCls}-zoom` : '');
return (
<span class={getBadgeClassName(prefixCls, children)}>
{children}
<Transition {...transitionProps}>{scrollNumber}</Transition>
{statusText}
</span> </span>
); );
} };
const transitionProps = getTransitionProps(children.length ? `${prefixCls}-zoom` : '');
return (
<span class={this.getBadgeClassName(prefixCls, children)}>
{children}
<Transition {...transitionProps}>{scrollNumber}</Transition>
{statusText}
</span>
);
}, },
}); });

View File

@ -1,10 +1,20 @@
import classNames from '../_util/classNames'; import classNames from '../_util/classNames';
import PropTypes from '../_util/vue-types'; import PropTypes from '../_util/vue-types';
import BaseMixin from '../_util/BaseMixin'; import { omit } from 'lodash-es';
import omit from 'omit.js';
import { cloneElement } from '../_util/vnode'; import { cloneElement } from '../_util/vnode';
import { defaultConfigProvider } from '../config-provider'; import { defaultConfigProvider } from '../config-provider';
import { CSSProperties, defineComponent, inject } from 'vue'; import {
defineComponent,
inject,
nextTick,
onBeforeUnmount,
onUpdated,
reactive,
watch,
ExtractPropTypes,
CSSProperties,
DefineComponent,
} from 'vue';
function getNumberArray(num: string | number | undefined | null) { function getNumberArray(num: string | number | undefined | null) {
return num return num
@ -19,7 +29,7 @@ function getNumberArray(num: string | number | undefined | null) {
: []; : [];
} }
const ScrollNumberProps = { export const scrollNumberProps = {
prefixCls: PropTypes.string, prefixCls: PropTypes.string,
count: PropTypes.any, count: PropTypes.any,
component: PropTypes.string, component: PropTypes.string,
@ -28,68 +38,30 @@ const ScrollNumberProps = {
onAnimated: PropTypes.func, onAnimated: PropTypes.func,
}; };
export type ScrollNumberProps = ExtractPropTypes<typeof scrollNumberProps>;
export default defineComponent({ export default defineComponent({
name: 'ScrollNumber', name: 'ScrollNumber',
mixins: [BaseMixin],
inheritAttrs: false, inheritAttrs: false,
props: ScrollNumberProps, props: scrollNumberProps,
emits: ['animated'], emits: ['animated'],
setup() { setup(props, { emit, attrs }) {
return { const configProvider = inject('configProvider', defaultConfigProvider);
configProvider: inject('configProvider', defaultConfigProvider), const state = reactive({
lastCount: undefined,
timeout: undefined,
};
},
data() {
return {
animateStarted: true, animateStarted: true,
sCount: this.count, lastCount: undefined,
}; sCount: props.count,
},
watch: {
count() {
this.lastCount = this.sCount;
this.setState({
animateStarted: true,
});
},
},
updated() {
const { animateStarted, count } = this;
if (animateStarted) {
this.clearTimeout();
// Let browser has time to reset the scroller before actually
// performing the transition.
this.timeout = setTimeout(() => {
this.setState(
{
animateStarted: false,
sCount: count,
},
this.handleAnimated,
);
});
}
},
beforeUnmount() {
this.clearTimeout();
},
methods: {
clearTimeout() {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = undefined;
}
},
getPositionByNum(num: number, i: number) {
const { sCount } = this;
const currentCount = Math.abs(Number(sCount));
const lastCount = Math.abs(Number(this.lastCount));
const currentDigit = Math.abs(getNumberArray(sCount)[i] as number);
const lastDigit = Math.abs(getNumberArray(this.lastCount)[i] as number);
if (this.animateStarted) { timeout: undefined,
});
const getPositionByNum = (num: number, i: number) => {
const currentCount = Math.abs(Number(state.sCount));
const lastCount = Math.abs(Number(state.lastCount));
const currentDigit = Math.abs(getNumberArray(state.sCount)[i] as number);
const lastDigit = Math.abs(getNumberArray(state.lastCount)[i] as number);
if (state.animateStarted) {
return 10 + num; return 10 + num;
} }
// //
@ -103,12 +75,19 @@ export default defineComponent({
return 10 + num; return 10 + num;
} }
return num; return num;
}, };
handleAnimated() { const handleAnimated = () => {
this.$emit('animated'); emit('animated');
}, };
renderNumberList(position: number, className: string) { const _clearTimeout = () => {
if (state.timeout) {
clearTimeout(state.timeout);
state.timeout = undefined;
}
};
const renderNumberList = (position: number, className: string) => {
const childrenToReturn = []; const childrenToReturn = [];
for (let i = 0; i < 30; i++) { for (let i = 0; i < 30; i++) {
childrenToReturn.push( childrenToReturn.push(
@ -122,14 +101,14 @@ export default defineComponent({
</p>, </p>,
); );
} }
return childrenToReturn; return childrenToReturn;
}, };
renderCurrentNumber(prefixCls: string, num: number | string, i: number) {
const renderCurrentNumber = (prefixCls: string, num: number | string, i: number) => {
if (typeof num === 'number') { if (typeof num === 'number') {
const position = this.getPositionByNum(num, i); const position = getPositionByNum(num, i);
const removeTransition = const removeTransition =
this.animateStarted || getNumberArray(this.lastCount)[i] === undefined; state.animateStarted || getNumberArray(state.lastCount)[i] === undefined;
const style = { const style = {
transition: removeTransition ? 'none' : undefined, transition: removeTransition ? 'none' : undefined,
msTransform: `translateY(${-position * 100}%)`, msTransform: `translateY(${-position * 100}%)`,
@ -138,7 +117,7 @@ export default defineComponent({
}; };
return ( return (
<span class={`${prefixCls}-only`} style={style} key={i}> <span class={`${prefixCls}-only`} style={style} key={i}>
{this.renderNumberList(position, `${prefixCls}-only-unit`)} {renderNumberList(position, `${prefixCls}-only-unit`)}
</span> </span>
); );
} }
@ -147,57 +126,92 @@ export default defineComponent({
{num} {num}
</span> </span>
); );
}, };
renderNumberElement(prefixCls: string) { const renderNumberElement = (prefixCls: string) => {
const { sCount } = this; if (state.sCount && Number(state.sCount) % 1 === 0) {
if (sCount && Number(sCount) % 1 === 0) { return getNumberArray(state.sCount)
return getNumberArray(sCount) .map((num, i) => renderCurrentNumber(prefixCls, num, i))
.map((num, i) => this.renderCurrentNumber(prefixCls, num, i))
.reverse(); .reverse();
} }
return sCount; return state.sCount;
},
},
render() {
const { prefixCls: customizePrefixCls, title, component: Tag = 'sup', displayComponent } = this;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('scroll-number', customizePrefixCls);
const { class: className, style = {} } = this.$attrs as {
class?: string;
style?: CSSProperties;
}; };
if (displayComponent) {
return cloneElement(displayComponent, {
class: classNames(
`${prefixCls}-custom-component`,
displayComponent.props && displayComponent.props.class,
),
});
}
// fix https://fb.me/react-unknown-prop
const restProps = omit({ ...this.$props, ...this.$attrs }, [
'count',
'onAnimated',
'component',
'prefixCls',
'displayComponent',
]);
const tempStyle = { ...style };
const newProps = {
...restProps,
title,
style: tempStyle,
class: classNames(prefixCls, className),
};
// allow specify the border
// mock border-color by box-shadow for compatible with old usage:
// <Badge count={4} style={{ backgroundColor: '#fff', color: '#999', borderColor: '#d9d9d9' }} />
if (style && style.borderColor) {
newProps.style.boxShadow = `0 0 0 1px ${style.borderColor} inset`;
}
return <Tag {...newProps}>{this.renderNumberElement(prefixCls)}</Tag>; watch(
() => props.count,
() => {
state.lastCount = state.sCount;
state.animateStarted = true;
},
);
onUpdated(() => {
if (state.animateStarted) {
_clearTimeout();
// Let browser has time to reset the scroller before actually
// performing the transition.
state.timeout = setTimeout(() => {
state.animateStarted = false;
state.sCount = props.count;
nextTick(() => {
handleAnimated();
});
});
}
});
onBeforeUnmount(() => {
_clearTimeout();
});
// configProvider: inject('configProvider', defaultConfigProvider),
// lastCount: undefined,
// timeout: undefined,
return () => {
const {
prefixCls: customizePrefixCls,
title,
component: Tag = ('sup' as unknown) as DefineComponent,
displayComponent,
} = props;
const getPrefixCls = configProvider.getPrefixCls;
const prefixCls = getPrefixCls('scroll-number', customizePrefixCls);
const { class: className, style = {} } = attrs as {
class?: string;
style?: CSSProperties;
};
if (displayComponent) {
return cloneElement(displayComponent, {
class: classNames(
`${prefixCls}-custom-component`,
displayComponent.props && displayComponent.props.class,
),
});
}
// fix https://fb.me/react-unknown-prop
const restProps = omit({ ...props, ...attrs }, [
'count',
'onAnimated',
'component',
'prefixCls',
'displayComponent',
]);
const tempStyle = { ...style };
const newProps = {
...restProps,
title,
style: tempStyle,
class: classNames(prefixCls, className),
};
// allow specify the border
// mock border-color by box-shadow for compatible with old usage:
// <Badge count={4} style={{ backgroundColor: '#fff', color: '#999', borderColor: '#d9d9d9' }} />
if (style && style.borderColor) {
newProps.style.boxShadow = `0 0 0 1px ${style.borderColor} inset`;
}
return <Tag {...newProps}>{renderNumberElement(prefixCls)}</Tag>;
};
}, },
}); });

View File

@ -31,6 +31,15 @@
} }
} }
&-count-sm {
min-width: @badge-height-sm;
height: @badge-height-sm;
padding: 0;
font-size: @badge-font-size-sm;
line-height: @badge-height-sm;
border-radius: (@badge-height-sm / 2);
}
&-multiple-words { &-multiple-words {
padding: 0 8px; padding: 0 8px;
} }

View File

@ -581,11 +581,14 @@
// Badge // Badge
// --- // ---
@badge-height: 20px; @badge-height: 20px;
@badge-height-sm: 14px;
@badge-dot-size: 6px; @badge-dot-size: 6px;
@badge-font-size: @font-size-sm; @badge-font-size: @font-size-sm;
@badge-font-size-sm: @font-size-sm;
@badge-font-weight: normal; @badge-font-weight: normal;
@badge-status-size: 6px; @badge-status-size: 6px;
@badge-text-color: @component-background; @badge-text-color: @component-background;
@badge-color: @highlight-color;
// Rate // Rate
// --- // ---