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 ScrollNumber from './ScrollNumber';
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 { getTransitionProps, Transition } from '../_util/transition';
import isNumeric from '../_util/isNumeric';
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 Ribbon from './Ribbon';
import { isPresetColor } from './utils';
const BadgeProps = {
export const badgeProps = {
/** Number to show in badge */
count: PropTypes.VNodeChild,
showZero: PropTypes.looseBool,
/** Max count to show */
overflowCount: PropTypes.number,
overflowCount: PropTypes.number.def(99),
/** whether to show red dot without number */
dot: PropTypes.looseBool,
prefixCls: PropTypes.string,
scrollNumberPrefixCls: PropTypes.string,
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,
text: PropTypes.VNodeChild,
offset: PropTypes.arrayOf(PropTypes.oneOfType([String, Number])),
@ -29,48 +41,44 @@ const BadgeProps = {
title: PropTypes.string,
};
export type BadgeProps = Partial<ExtractPropTypes<typeof badgeProps>>;
const Badge = defineComponent({
name: 'ABadge',
Ribbon,
props: initDefaultProps(BadgeProps, {
showZero: false,
dot: false,
overflowCount: 99,
}) as typeof BadgeProps,
setup() {
return {
configProvider: inject('configProvider', defaultConfigProvider),
props: badgeProps,
setup(props, { slots }) {
const configProvider = inject('configProvider', defaultConfigProvider);
const state = reactive({
badgeCount: undefined,
};
},
methods: {
getNumberedDispayCount() {
const { overflowCount } = this.$props;
const count = this.badgeCount;
});
const getNumberedDispayCount = () => {
const { overflowCount } = props;
const count = state.badgeCount;
const displayCount = count > overflowCount ? `${overflowCount}+` : count;
return displayCount;
},
};
getDispayCount() {
const isDot = this.isDot();
const getDispayCount = computed(() => {
// dot mode don't need count
if (isDot) {
if (isDot.value) {
return '';
}
return this.getNumberedDispayCount();
},
return getNumberedDispayCount();
});
getScrollNumberTitle() {
const { title } = this.$props;
const count = this.badgeCount;
const getScrollNumberTitle = () => {
const { title } = props;
const count = state.badgeCount;
if (title) {
return title;
}
return typeof count === 'string' || typeof count === 'number' ? count : undefined;
},
};
getStyleWithOffset() {
const { offset, numberStyle } = this.$props;
const getStyleWithOffset = () => {
const { offset, numberStyle } = props;
return offset
? {
right: `${-parseInt(offset[0] as string, 10)}px`,
@ -78,47 +86,49 @@ const Badge = defineComponent({
...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, {
[`${prefixCls}-status`]: hasStatus,
[`${prefixCls}-dot-status`]: hasStatus && this.dot && !this.isZero(),
[`${prefixCls}-status`]: status,
[`${prefixCls}-dot-status`]: status && props.dot && !isZero.value,
[`${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 { dot } = this.$props;
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 renderDispayComponent = () => {
const count = state.badgeCount;
const customNode = count;
if (!customNode || typeof customNode !== 'object') {
return undefined;
@ -126,103 +136,102 @@ const Badge = defineComponent({
return cloneElement(
customNode,
{
style: this.getStyleWithOffset(),
style: getStyleWithOffset(),
},
false,
);
},
};
renderBadgeNumber(prefixCls: string, scrollNumberPrefixCls: string) {
const { status, color } = this.$props;
const count = this.badgeCount;
const displayCount = this.getDispayCount();
const isDot = this.isDot();
const hidden = this.isHidden();
const renderBadgeNumber = (prefixCls: string, scrollNumberPrefixCls: string) => {
const { status, color, size } = props;
const count = state.badgeCount;
const displayCount = getDispayCount.value;
const scrollNumberCls = {
[`${prefixCls}-dot`]: isDot,
[`${prefixCls}-count`]: !isDot,
[`${prefixCls}-dot`]: isDot.value,
[`${prefixCls}-count`]: !isDot.value,
[`${prefixCls}-count-sm`]: size === 'small',
[`${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-${color}`]: isPresetColor(color),
};
let statusStyle = this.getStyleWithOffset();
let statusStyle = getStyleWithOffset();
if (color && !isPresetColor(color)) {
statusStyle = statusStyle || {};
statusStyle.background = color;
}
return hidden ? null : (
return isHidden.value ? null : (
<ScrollNumber
prefixCls={scrollNumberPrefixCls}
data-show={!hidden}
v-show={!hidden}
data-show={!isHidden.value}
v-show={!isHidden.value}
class={scrollNumberCls}
count={displayCount}
displayComponent={this.renderDispayComponent()}
title={this.getScrollNumberTitle()}
displayComponent={renderDispayComponent()}
title={getScrollNumberTitle()}
style={statusStyle}
key="scrollNumber"
/>
);
},
},
};
render() {
const {
prefixCls: customizePrefixCls,
scrollNumberPrefixCls: customizeScrollNumberPrefixCls,
status,
color,
} = this;
return () => {
const {
prefixCls: customizePrefixCls,
scrollNumberPrefixCls: customizeScrollNumberPrefixCls,
status,
color,
} = props;
const text = getComponent(this, 'text');
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('badge', customizePrefixCls);
const scrollNumberPrefixCls = getPrefixCls('scroll-number', customizeScrollNumberPrefixCls);
const text = getPropsSlot(slots, props, 'text');
const getPrefixCls = configProvider.getPrefixCls;
const prefixCls = getPrefixCls('badge', customizePrefixCls);
const scrollNumberPrefixCls = getPrefixCls('scroll-number', customizeScrollNumberPrefixCls);
const children = getSlot(this);
let count = getComponent(this, 'count');
if (Array.isArray(count)) {
count = count[0];
}
this.badgeCount = count;
const scrollNumber = this.renderBadgeNumber(prefixCls, scrollNumberPrefixCls);
const statusText = this.renderStatusText(prefixCls);
const statusCls = classNames({
[`${prefixCls}-status-dot`]: this.hasStatus(),
[`${prefixCls}-status-${status}`]: !!status,
[`${prefixCls}-status-${color}`]: isPresetColor(color),
});
const statusStyle: CSSProperties = {};
if (color && !isPresetColor(color)) {
statusStyle.background = color;
}
// <Badge status="success" />
if (!children.length && this.hasStatus()) {
const styleWithOffset = this.getStyleWithOffset();
const statusTextColor = styleWithOffset && styleWithOffset.color;
return (
<span class={this.getBadgeClassName(prefixCls, children)} style={styleWithOffset}>
<span class={statusCls} style={statusStyle} />
<span style={{ color: statusTextColor }} class={`${prefixCls}-status-text`}>
{text}
const children = flattenChildren(slots.default?.());
let count = getPropsSlot(slots, props, 'count');
if (Array.isArray(count)) {
count = count[0];
}
state.badgeCount = count;
const scrollNumber = renderBadgeNumber(prefixCls, scrollNumberPrefixCls);
const statusText = renderStatusText(prefixCls);
const statusCls = classNames({
[`${prefixCls}-status-dot`]: hasStatus.value,
[`${prefixCls}-status-${status}`]: !!status,
[`${prefixCls}-status-${color}`]: isPresetColor(color),
});
const statusStyle: CSSProperties = {};
if (color && !isPresetColor(color)) {
statusStyle.background = color;
}
// <Badge status="success" />
if (!children.length && hasStatus.value) {
const styleWithOffset = getStyleWithOffset();
const statusTextColor = styleWithOffset && styleWithOffset.color;
return (
<span class={getBadgeClassName(prefixCls, children)} style={styleWithOffset}>
<span class={statusCls} style={statusStyle} />
<span style={{ color: statusTextColor }} class={`${prefixCls}-status-text`}>
{text}
</span>
</span>
);
}
const transitionProps = getTransitionProps(children.length ? `${prefixCls}-zoom` : '');
return (
<span class={getBadgeClassName(prefixCls, children)}>
{children}
<Transition {...transitionProps}>{scrollNumber}</Transition>
{statusText}
</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 PropTypes from '../_util/vue-types';
import BaseMixin from '../_util/BaseMixin';
import omit from 'omit.js';
import { omit } from 'lodash-es';
import { cloneElement } from '../_util/vnode';
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) {
return num
@ -19,7 +29,7 @@ function getNumberArray(num: string | number | undefined | null) {
: [];
}
const ScrollNumberProps = {
export const scrollNumberProps = {
prefixCls: PropTypes.string,
count: PropTypes.any,
component: PropTypes.string,
@ -28,68 +38,30 @@ const ScrollNumberProps = {
onAnimated: PropTypes.func,
};
export type ScrollNumberProps = ExtractPropTypes<typeof scrollNumberProps>;
export default defineComponent({
name: 'ScrollNumber',
mixins: [BaseMixin],
inheritAttrs: false,
props: ScrollNumberProps,
props: scrollNumberProps,
emits: ['animated'],
setup() {
return {
configProvider: inject('configProvider', defaultConfigProvider),
lastCount: undefined,
timeout: undefined,
};
},
data() {
return {
setup(props, { emit, attrs }) {
const configProvider = inject('configProvider', defaultConfigProvider);
const state = reactive({
animateStarted: true,
sCount: this.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);
lastCount: undefined,
sCount: props.count,
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;
}
//
@ -103,12 +75,19 @@ export default defineComponent({
return 10 + num;
}
return num;
},
handleAnimated() {
this.$emit('animated');
},
};
const handleAnimated = () => {
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 = [];
for (let i = 0; i < 30; i++) {
childrenToReturn.push(
@ -122,14 +101,14 @@ export default defineComponent({
</p>,
);
}
return childrenToReturn;
},
renderCurrentNumber(prefixCls: string, num: number | string, i: number) {
};
const renderCurrentNumber = (prefixCls: string, num: number | string, i: number) => {
if (typeof num === 'number') {
const position = this.getPositionByNum(num, i);
const position = getPositionByNum(num, i);
const removeTransition =
this.animateStarted || getNumberArray(this.lastCount)[i] === undefined;
state.animateStarted || getNumberArray(state.lastCount)[i] === undefined;
const style = {
transition: removeTransition ? 'none' : undefined,
msTransform: `translateY(${-position * 100}%)`,
@ -138,7 +117,7 @@ export default defineComponent({
};
return (
<span class={`${prefixCls}-only`} style={style} key={i}>
{this.renderNumberList(position, `${prefixCls}-only-unit`)}
{renderNumberList(position, `${prefixCls}-only-unit`)}
</span>
);
}
@ -147,57 +126,92 @@ export default defineComponent({
{num}
</span>
);
},
};
renderNumberElement(prefixCls: string) {
const { sCount } = this;
if (sCount && Number(sCount) % 1 === 0) {
return getNumberArray(sCount)
.map((num, i) => this.renderCurrentNumber(prefixCls, num, i))
const renderNumberElement = (prefixCls: string) => {
if (state.sCount && Number(state.sCount) % 1 === 0) {
return getNumberArray(state.sCount)
.map((num, i) => renderCurrentNumber(prefixCls, num, i))
.reverse();
}
return 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;
return state.sCount;
};
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 {
padding: 0 8px;
}

View File

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