refactor: badge

pull/4134/head
tanjinzhou 2021-05-25 17:30:22 +08:00
parent 372ac5c729
commit e442b0d1ec
10 changed files with 587 additions and 440 deletions

View File

@ -4,26 +4,16 @@ import classNames from '../_util/classNames';
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,
ExtractPropTypes,
CSSProperties,
VNode,
App,
Plugin,
reactive,
computed,
} from 'vue';
import { defineComponent, ExtractPropTypes, CSSProperties, computed, ref, watch } from 'vue';
import { tuple } from '../_util/type';
import Ribbon from './Ribbon';
import { isPresetColor } from './utils';
import useConfigInject from '../_util/hooks/useConfigInject';
import isNumeric from '../_util/isNumeric';
export const badgeProps = {
/** Number to show in badge */
count: PropTypes.VNodeChild,
count: PropTypes.any,
showZero: PropTypes.looseBool,
/** Max count to show */
overflowCount: PropTypes.number.def(99),
@ -43,205 +33,189 @@ export const badgeProps = {
export type BadgeProps = Partial<ExtractPropTypes<typeof badgeProps>>;
const Badge = defineComponent({
export default defineComponent({
name: 'ABadge',
Ribbon,
props: badgeProps,
setup(props, { slots }) {
const configProvider = inject('configProvider', defaultConfigProvider);
const state = reactive({
badgeCount: undefined,
slots: ['text', 'count'],
setup(props, { slots, attrs }) {
const { prefixCls, direction } = useConfigInject('badge', props);
// ================================ Misc ================================
const numberedDisplayCount = computed(() => {
return ((props.count as number) > (props.overflowCount as number)
? `${props.overflowCount}+`
: props.count) as string | number | null;
});
const getNumberedDispayCount = () => {
const { overflowCount } = props;
const count = state.badgeCount;
const displayCount = count > overflowCount ? `${overflowCount}+` : count;
return displayCount;
};
const hasStatus = computed(
() =>
(props.status !== null && props.status !== undefined) ||
(props.color !== null && props.color !== undefined),
);
const getDispayCount = computed(() => {
// dot mode don't need count
if (isDot.value) {
return '';
}
return getNumberedDispayCount();
});
const isZero = computed(
() => numberedDisplayCount.value === '0' || numberedDisplayCount.value === 0,
);
const getScrollNumberTitle = () => {
const { title } = props;
const count = state.badgeCount;
if (title) {
return title;
}
return typeof count === 'string' || typeof count === 'number' ? count : undefined;
};
const showAsDot = computed(() => (props.dot && !isZero.value) || hasStatus.value);
const getStyleWithOffset = () => {
const { offset, numberStyle } = props;
return offset
? {
right: `${-parseInt(offset[0] as string, 10)}px`,
marginTop: isNumeric(offset[1]) ? `${offset[1]}px` : offset[1],
...numberStyle,
}
: { ...numberStyle };
};
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 mergedCount = computed(() => (showAsDot.value ? '' : numberedDisplayCount.value));
const isHidden = computed(() => {
const { showZero } = props;
const isEmpty =
getDispayCount.value === null ||
getDispayCount.value === undefined ||
getDispayCount.value === '';
return (isEmpty || (isZero.value && !showZero)) && !isDot.value;
mergedCount.value === null || mergedCount.value === undefined || mergedCount.value === '';
return (isEmpty || (isZero.value && !props.showZero)) && !showAsDot.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>;
};
// Count should be cache in case hidden change it
const livingCount = ref(props.count);
const getBadgeClassName = (prefixCls: string, children: VNode[]) => {
const status = hasStatus.value;
return classNames(prefixCls, {
[`${prefixCls}-status`]: status,
[`${prefixCls}-dot-status`]: status && props.dot && !isZero.value,
[`${prefixCls}-not-a-wrapper`]: !children.length,
});
};
// We need cache count since remove motion should not change count display
const displayCount = ref(mergedCount.value);
const renderDispayComponent = () => {
const count = state.badgeCount;
const customNode = count;
if (!customNode || typeof customNode !== 'object') {
return undefined;
// We will cache the dot status to avoid shaking on leaved motion
const isDotRef = ref(showAsDot.value);
watch(
[() => props.count, mergedCount, showAsDot],
() => {
if (!isHidden.value) {
livingCount.value = props.count;
displayCount.value = mergedCount.value;
isDotRef.value = showAsDot.value;
}
},
{ immediate: true },
);
// Shared styles
const statusCls = computed(() => ({
[`${prefixCls.value}-status-dot`]: hasStatus.value,
[`${prefixCls.value}-status-${props.status}`]: !!props.status,
[`${prefixCls.value}-status-${props.color}`]: isPresetColor(props.color),
}));
const statusStyle = computed(() => {
if (props.color && !isPresetColor(props.color)) {
return { background: props.color };
} else {
return {};
}
return cloneElement(
customNode,
});
const scrollNumberCls = computed(() => ({
[`${prefixCls.value}-dot`]: isDotRef.value,
[`${prefixCls.value}-count`]: !isDotRef.value,
[`${prefixCls.value}-count-sm`]: props.size === 'small',
[`${prefixCls.value}-multiple-words`]:
!isDotRef.value && displayCount.value && displayCount.value.toString().length > 1,
[`${prefixCls.value}-status-${status}`]: !!status,
[`${prefixCls.value}-status-${props.color}`]: isPresetColor(props.color),
}));
return () => {
const { offset, title, color } = props;
const style = attrs.style as CSSProperties;
const text = getPropsSlot(slots, props, 'text');
const pre = prefixCls.value;
const count = livingCount.value;
let children = flattenChildren(slots.default?.());
children = children.length ? children : null;
const visible = !!(!isHidden.value || slots.count);
// =============================== Styles ===============================
const mergedStyle = (() => {
if (!offset) {
return { ...style };
}
const offsetStyle: CSSProperties = {
marginTop: isNumeric(offset[1]) ? `${offset[1]}px` : offset[1],
};
if (direction.value === 'rtl') {
offsetStyle.left = `${parseInt(offset[0] as string, 10)}px`;
} else {
offsetStyle.right = `${-parseInt(offset[0] as string, 10)}px`;
}
return {
...offsetStyle,
...style,
};
})();
// =============================== Render ===============================
// >>> Title
const titleNode =
title ?? (typeof count === 'string' || typeof count === 'number' ? count : undefined);
// >>> Status Text
const statusTextNode =
visible || !text ? null : <span class={`${pre}-status-text`}>{text}</span>;
// >>> Display Component
const displayNode = cloneElement(
slots.count?.(),
{
style: getStyleWithOffset(),
style: mergedStyle,
},
false,
);
};
const renderBadgeNumber = (prefixCls: string, scrollNumberPrefixCls: string) => {
const { status, color, size } = props;
const count = state.badgeCount;
const displayCount = getDispayCount.value;
const scrollNumberCls = {
[`${prefixCls}-dot`]: isDot.value,
[`${prefixCls}-count`]: !isDot.value,
[`${prefixCls}-count-sm`]: size === 'small',
[`${prefixCls}-multiple-words`]:
!isDot.value && count && count.toString && count.toString().length > 1,
[`${prefixCls}-status-${status}`]: !!status,
[`${prefixCls}-status-${color}`]: isPresetColor(color),
};
let statusStyle = getStyleWithOffset();
if (color && !isPresetColor(color)) {
statusStyle = statusStyle || {};
statusStyle.background = color;
}
return isHidden.value ? null : (
<ScrollNumber
prefixCls={scrollNumberPrefixCls}
data-show={!isHidden.value}
v-show={!isHidden.value}
class={scrollNumberCls}
count={displayCount}
displayComponent={renderDispayComponent()}
title={getScrollNumberTitle()}
style={statusStyle}
key="scrollNumber"
/>
const badgeClassName = classNames(
pre,
{
[`${pre}-status`]: hasStatus.value,
[`${pre}-not-a-wrapper`]: !children,
[`${pre}-rtl`]: direction.value === 'rtl',
},
attrs.class,
);
};
return () => {
const {
prefixCls: customizePrefixCls,
scrollNumberPrefixCls: customizeScrollNumberPrefixCls,
status,
color,
} = props;
const text = getPropsSlot(slots, props, 'text');
const getPrefixCls = configProvider.getPrefixCls;
const prefixCls = getPrefixCls('badge', customizePrefixCls);
const scrollNumberPrefixCls = getPrefixCls('scroll-number', customizeScrollNumberPrefixCls);
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;
if (!children && hasStatus.value) {
const statusTextColor = mergedStyle.color;
return (
<span class={getBadgeClassName(prefixCls, children)} style={styleWithOffset}>
<span class={statusCls} style={statusStyle} />
<span style={{ color: statusTextColor }} class={`${prefixCls}-status-text`}>
<span {...attrs} class={badgeClassName} style={mergedStyle}>
<span class={statusCls.value} style={statusStyle.value} />
<span style={{ color: statusTextColor }} class={`${pre}-status-text`}>
{text}
</span>
</span>
);
}
const transitionProps = getTransitionProps(children.length ? `${prefixCls}-zoom` : '');
const transitionProps = getTransitionProps(children ? `${pre}-zoom` : '', {
appear: false,
});
let scrollNumberStyle: CSSProperties = { ...mergedStyle, ...props.numberStyle };
if (color && !isPresetColor(color)) {
scrollNumberStyle = scrollNumberStyle || {};
scrollNumberStyle.background = color;
}
return (
<span class={getBadgeClassName(prefixCls, children)}>
<span {...attrs} class={badgeClassName}>
{children}
<Transition {...transitionProps}>{scrollNumber}</Transition>
{statusText}
<Transition {...transitionProps}>
<ScrollNumber
v-show={visible}
prefixCls={props.scrollNumberPrefixCls}
show={visible}
class={scrollNumberCls.value}
count={displayCount.value}
title={titleNode}
style={scrollNumberStyle}
key="scrollNumber"
>
{displayNode}
</ScrollNumber>
</Transition>
{statusTextNode}
</span>
);
};
},
});
Badge.install = function(app: App) {
app.component(Badge.name, Badge);
app.component(Badge.Ribbon.displayName, Badge.Ribbon);
return app;
};
export default Badge as typeof Badge &
Plugin & {
readonly Ribbon: typeof Ribbon;
};

View File

@ -1,60 +1,55 @@
import { LiteralUnion, tuple } from '../_util/type';
import { PresetColorType } from '../_util/colors';
import { isPresetColor } from './utils';
import { defaultConfigProvider } from '../config-provider';
import { HTMLAttributes, FunctionalComponent, VNodeTypes, inject, CSSProperties } from 'vue';
import { CSSProperties, defineComponent, PropType, ExtractPropTypes, computed } from 'vue';
import PropTypes from '../_util/vue-types';
import useConfigInject from '../_util/hooks/useConfigInject';
type RibbonPlacement = 'start' | 'end';
export interface RibbonProps extends HTMLAttributes {
prefixCls?: string;
text?: VNodeTypes;
color?: LiteralUnion<PresetColorType, string>;
placement?: RibbonPlacement;
}
const Ribbon: FunctionalComponent<RibbonProps> = (props, { attrs, slots }) => {
const { prefixCls: customizePrefixCls, color, text = slots.text?.(), placement = 'end' } = props;
const { class: className, style } = attrs;
const children = slots.default?.();
const { getPrefixCls, direction } = inject('configProvider', defaultConfigProvider);
const prefixCls = getPrefixCls('ribbon', customizePrefixCls);
const colorInPreset = isPresetColor(color);
const ribbonCls = [
prefixCls,
`${prefixCls}-placement-${placement}`,
{
[`${prefixCls}-rtl`]: direction === 'rtl',
[`${prefixCls}-color-${color}`]: colorInPreset,
},
className,
];
const colorStyle: CSSProperties = {};
const cornerColorStyle: CSSProperties = {};
if (color && !colorInPreset) {
colorStyle.background = color;
cornerColorStyle.color = color;
}
return (
<div class={`${prefixCls}-wrapper`}>
{children}
<div class={ribbonCls} style={{ ...colorStyle, ...(style as CSSProperties) }}>
<span class={`${prefixCls}-text`}>{text}</span>
<div class={`${prefixCls}-corner`} style={cornerColorStyle} />
</div>
</div>
);
};
Ribbon.displayName = 'ABadgeRibbon';
Ribbon.inheritAttrs = false;
Ribbon.props = {
const ribbonProps = {
prefix: PropTypes.string,
color: PropTypes.string,
color: { type: String as PropType<LiteralUnion<PresetColorType, string>> },
text: PropTypes.any,
placement: PropTypes.oneOf(tuple('start', 'end')),
placement: PropTypes.oneOf(tuple('start', 'end')).def('end'),
};
export default Ribbon;
export type RibbonProps = Partial<ExtractPropTypes<typeof ribbonProps>>;
export default defineComponent({
name: 'ABadgeRibbon',
inheritAttrs: false,
props: ribbonProps,
slots: ['text'],
setup(props, { attrs, slots }) {
const { prefixCls, direction } = useConfigInject('ribbon', props);
const colorInPreset = computed(() => isPresetColor(props.color));
const ribbonCls = computed(() => [
prefixCls.value,
`${prefixCls.value}-placement-${props.placement}`,
{
[`${prefixCls.value}-rtl`]: direction.value === 'rtl',
[`${prefixCls.value}-color-${props.color}`]: colorInPreset.value,
},
]);
return () => {
const { class: className, style, ...restAttrs } = attrs;
const colorStyle: CSSProperties = {};
const cornerColorStyle: CSSProperties = {};
if (props.color && !colorInPreset.value) {
colorStyle.background = props.color;
cornerColorStyle.color = props.color;
}
return (
<div class={`${prefixCls.value}-wrapper`} {...restAttrs}>
{slots.default?.()}
<div
class={[ribbonCls.value, className]}
style={{ ...colorStyle, ...(style as CSSProperties) }}
>
<span class={`${prefixCls.value}-text`}>{props.text || slots.text?.()}</span>
<div class={`${prefixCls.value}-corner`} style={cornerColorStyle} />
</div>
</div>
);
};
},
});

View File

@ -1,217 +1,90 @@
import classNames from '../_util/classNames';
import PropTypes from '../_util/vue-types';
import { omit } from 'lodash-es';
import { cloneElement } from '../_util/vnode';
import { defaultConfigProvider } from '../config-provider';
import {
defineComponent,
inject,
nextTick,
onBeforeUnmount,
onUpdated,
reactive,
watch,
ExtractPropTypes,
CSSProperties,
DefineComponent,
HTMLAttributes,
} from 'vue';
function getNumberArray(num: string | number | undefined | null) {
return num
? num
.toString()
.split('')
.reverse()
.map(i => {
const current = Number(i);
return isNaN(current) ? i : current;
})
: [];
}
import useConfigInject from '../_util/hooks/useConfigInject';
import SingleNumber from './SingleNumber';
import { filterEmpty } from '../_util/props-util';
export const scrollNumberProps = {
prefixCls: PropTypes.string,
count: PropTypes.any,
component: PropTypes.string,
title: PropTypes.oneOfType([PropTypes.number, PropTypes.string, null]),
displayComponent: PropTypes.any,
onAnimated: PropTypes.func,
show: Boolean,
};
export type ScrollNumberProps = ExtractPropTypes<typeof scrollNumberProps>;
export type ScrollNumberProps = Partial<ExtractPropTypes<typeof scrollNumberProps>>;
export default defineComponent({
name: 'ScrollNumber',
inheritAttrs: false,
props: scrollNumberProps,
emits: ['animated'],
setup(props, { emit, attrs }) {
const configProvider = inject('configProvider', defaultConfigProvider);
const state = reactive({
animateStarted: true,
lastCount: undefined,
sCount: props.count,
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;
}
//
if (currentCount > lastCount) {
if (currentDigit >= lastDigit) {
return 10 + num;
}
return 20 + num;
}
if (currentDigit <= lastDigit) {
return 10 + num;
}
return num;
};
const handleAnimated = () => {
emit('animated');
};
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(
<p
key={i.toString()}
class={classNames(className, {
current: position === i,
})}
>
{i % 10}
</p>,
);
}
return childrenToReturn;
};
const renderCurrentNumber = (prefixCls: string, num: number | string, i: number) => {
if (typeof num === 'number') {
const position = getPositionByNum(num, i);
const removeTransition =
state.animateStarted || getNumberArray(state.lastCount)[i] === undefined;
const style = {
transition: removeTransition ? 'none' : undefined,
msTransform: `translateY(${-position * 100}%)`,
WebkitTransform: `translateY(${-position * 100}%)`,
transform: `translateY(${-position * 100}%)`,
};
return (
<span class={`${prefixCls}-only`} style={style} key={i}>
{renderNumberList(position, `${prefixCls}-only-unit`)}
</span>
);
}
return (
<span key="symbol" class={`${prefixCls}-symbol`}>
{num}
</span>
);
};
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 state.sCount;
};
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,
setup(props, { attrs, slots }) {
const { prefixCls } = useConfigInject('scroll-number', props);
return () => {
const {
prefixCls: customizePrefixCls,
count,
title,
show,
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 };
class: className,
style,
...restProps
} = { ...props, ...attrs } as ScrollNumberProps & HTMLAttributes & { style: CSSProperties };
// ============================ Render ============================
const newProps = {
...restProps,
title,
style: tempStyle,
class: classNames(prefixCls, className),
style,
'data-show': props.show,
class: classNames(prefixCls.value, className),
title: title as string,
};
// Only integer need motion
let numberNodes: any = count;
if (count && Number(count) % 1 === 0) {
const numberList = String(count).split('');
numberNodes = numberList.map((num, i) => (
<SingleNumber
prefixCls={prefixCls.value}
count={Number(count)}
value={num}
key={numberList.length - i}
/>
));
}
// 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`;
newProps.style = {
...(style as CSSProperties),
boxShadow: `0 0 0 1px ${style.borderColor} inset`,
};
}
const children = filterEmpty(slots.default?.());
if (children && children.length) {
return cloneElement(
children,
{
class: classNames(`${prefixCls.value}-custom-component`),
},
false,
);
}
return <Tag {...newProps}>{renderNumberElement(prefixCls)}</Tag>;
return <Tag {...newProps}>{numberNodes}</Tag>;
};
},
});

View File

@ -0,0 +1,131 @@
import { computed, CSSProperties, defineComponent, onUnmounted, reactive, ref, watch } from 'vue';
import classNames from '../_util/classNames';
export interface UnitNumberProps {
prefixCls: string;
value: string | number;
offset?: number;
current?: boolean;
}
function UnitNumber({ prefixCls, value, current, offset = 0 }: UnitNumberProps) {
let style: CSSProperties | undefined;
if (offset) {
style = {
position: 'absolute',
top: `${offset}00%`,
left: 0,
};
}
return (
<p
style={style}
class={classNames(`${prefixCls}-only-unit`, {
current,
})}
>
{value}
</p>
);
}
function getOffset(start: number, end: number, unit: -1 | 1) {
let index = start;
let offset = 0;
while ((index + 10) % 10 !== end) {
index += unit;
offset += unit;
}
return offset;
}
export default defineComponent({
name: 'SingleNumber',
props: {
prefixCls: String,
value: String,
count: Number,
},
setup(props) {
const originValue = computed(() => Number(props.value));
const originCount = computed(() => Math.abs(props.count));
const state = reactive({
prevValue: originValue.value,
prevCount: originCount.value,
});
// ============================= Events =============================
const onTransitionEnd = () => {
state.prevValue = originValue.value;
state.prevCount = originCount.value;
};
const timeout = ref();
// Fallback if transition event not support
watch(
originValue,
() => {
clearTimeout(timeout.value);
timeout.value = setTimeout(() => {
onTransitionEnd();
}, 1000);
},
{ flush: 'post' },
);
onUnmounted(() => {
clearTimeout(timeout.value);
});
return () => {
let unitNodes: any[];
let offsetStyle: CSSProperties = {};
const value = originValue.value;
if (state.prevValue === value || Number.isNaN(value) || Number.isNaN(state.prevValue)) {
// Nothing to change
unitNodes = [UnitNumber({ ...props, current: true } as UnitNumberProps)];
offsetStyle = {
transition: 'none',
};
} else {
unitNodes = [];
// Fill basic number units
const end = value + 10;
const unitNumberList: number[] = [];
for (let index = value; index <= end; index += 1) {
unitNumberList.push(index);
}
// Fill with number unit nodes
const prevIndex = unitNumberList.findIndex(n => n % 10 === state.prevValue);
unitNodes = unitNumberList.map((n, index) => {
const singleUnit = n % 10;
return UnitNumber({
...props,
value: singleUnit,
offset: index - prevIndex,
current: index === prevIndex,
} as UnitNumberProps);
});
// Calculate container offset value
const unit = state.prevCount < originCount.value ? 1 : -1;
offsetStyle = {
transform: `translateY(${-getOffset(state.prevValue, value, unit)}00%)`,
};
}
return (
<span
class={`${props.prefixCls}-only`}
style={offsetStyle}
onTransitionend={() => onTransitionEnd()}
>
{unitNodes}
</span>
);
};
},
});

View File

@ -1,3 +1,14 @@
import { App, Plugin } from 'vue';
import Badge from './Badge';
import Ribbon from './Ribbon';
export default Badge;
Badge.install = function(app: App) {
app.component(Badge.name, Badge);
app.component(Ribbon.name, Ribbon);
return app;
};
export default Badge as typeof Badge &
Plugin & {
readonly Ribbon: typeof Ribbon;
};

View File

@ -9,10 +9,10 @@
position: relative;
display: inline-block;
color: unset;
line-height: 1;
&-count {
z-index: @zindex-badge;
min-width: @badge-height;
height: @badge-height;
padding: 0 6px;
@ -22,7 +22,7 @@
line-height: @badge-height;
white-space: nowrap;
text-align: center;
background: @highlight-color;
background: @badge-color;
border-radius: (@badge-height / 2);
box-shadow: 0 0 0 1px @shadow-color-inverse;
a,
@ -45,7 +45,9 @@
}
&-dot {
z-index: @zindex-badge;
width: @badge-dot-size;
min-width: @badge-dot-size;
height: @badge-dot-size;
background: @highlight-color;
border-radius: 100%;
@ -58,9 +60,12 @@
position: absolute;
top: 0;
right: 0;
z-index: @zindex-badge;
transform: translate(50%, -50%);
transform-origin: 100% 0%;
&.@{iconfont-css-prefix}-spin {
animation: antBadgeLoadingCircle 1s infinite linear;
}
}
&-status {
@ -124,24 +129,39 @@
&-zoom-appear,
&-zoom-enter {
animation: antZoomBadgeIn 0.3s @ease-out-back;
animation: antZoomBadgeIn @animation-duration-slow @ease-out-back;
animation-fill-mode: both;
}
&-zoom-leave {
animation: antZoomBadgeOut 0.3s @ease-in-back;
animation: antZoomBadgeOut @animation-duration-slow @ease-in-back;
animation-fill-mode: both;
}
&-not-a-wrapper {
.@{badge-prefix-cls}-zoom-appear,
.@{badge-prefix-cls}-zoom-enter {
animation: antNoWrapperZoomBadgeIn @animation-duration-slow @ease-out-back;
}
.@{badge-prefix-cls}-zoom-leave {
animation: antNoWrapperZoomBadgeOut @animation-duration-slow @ease-in-back;
}
&:not(.@{badge-prefix-cls}-status) {
vertical-align: middle;
}
.@{number-prefix-cls}-custom-component {
transform: none;
}
.@{number-prefix-cls}-custom-component,
.@{ant-prefix}-scroll-number {
position: relative;
top: auto;
display: block;
transform-origin: 50% 50%;
}
.@{badge-prefix-cls}-count {
@ -161,15 +181,25 @@
}
}
// Safari will blink with transform when inner element has absolute style.
.safari-fix-motion() {
-webkit-transform-style: preserve-3d;
-webkit-backface-visibility: hidden;
}
.@{number-prefix-cls} {
overflow: hidden;
&-only {
position: relative;
display: inline-block;
height: @badge-height;
transition: all 0.3s @ease-in-out;
transition: all @animation-duration-slow @ease-in-out;
.safari-fix-motion;
> p.@{number-prefix-cls}-only-unit {
height: @badge-height;
margin: 0;
.safari-fix-motion;
}
}
@ -198,4 +228,36 @@
}
}
@keyframes antNoWrapperZoomBadgeIn {
0% {
transform: scale(0);
opacity: 0;
}
100% {
transform: scale(1);
}
}
@keyframes antNoWrapperZoomBadgeOut {
0% {
transform: scale(1);
}
100% {
transform: scale(0);
opacity: 0;
}
}
@keyframes antBadgeLoadingCircle {
0% {
transform-origin: 50%;
}
100% {
transform: translate(50%, -50%) rotate(360deg);
transform-origin: 50%;
}
}
@import './ribbon';
@import './rtl';

View File

@ -0,0 +1,104 @@
.@{badge-prefix-cls} {
&-rtl {
direction: rtl;
}
&-count,
&-dot,
.@{number-prefix-cls}-custom-component {
.@{badge-prefix-cls}-rtl & {
right: auto;
left: 0;
direction: ltr;
transform: translate(-50%, -50%);
transform-origin: 0% 0%;
}
}
.@{badge-prefix-cls}-rtl& .@{number-prefix-cls}-custom-component {
right: auto;
left: 0;
transform: translate(-50%, -50%);
transform-origin: 0% 0%;
}
&-status {
&-text {
.@{badge-prefix-cls}-rtl & {
margin-right: 8px;
margin-left: 0;
}
}
}
&-zoom-appear,
&-zoom-enter {
.@{badge-prefix-cls}-rtl & {
animation-name: antZoomBadgeInRtl;
}
}
&-zoom-leave {
.@{badge-prefix-cls}-rtl & {
animation-name: antZoomBadgeOutRtl;
}
}
&-not-a-wrapper {
.@{badge-prefix-cls}-count {
transform: none;
}
}
}
.@{ribbon-prefix-cls}-rtl {
direction: rtl;
&.@{ribbon-prefix-cls}-placement-end {
right: unset;
left: -8px;
border-bottom-right-radius: @border-radius-sm;
border-bottom-left-radius: 0;
.@{ribbon-prefix-cls}-corner {
right: unset;
left: 0;
border-color: currentColor currentColor transparent transparent;
&::after {
border-color: currentColor currentColor transparent transparent;
}
}
}
&.@{ribbon-prefix-cls}-placement-start {
right: -8px;
left: unset;
border-bottom-right-radius: 0;
border-bottom-left-radius: @border-radius-sm;
.@{ribbon-prefix-cls}-corner {
right: 0;
left: unset;
border-color: currentColor transparent transparent currentColor;
&::after {
border-color: currentColor transparent transparent currentColor;
}
}
}
}
@keyframes antZoomBadgeInRtl {
0% {
transform: scale(0) translate(-50%, -50%);
opacity: 0;
}
100% {
transform: scale(1) translate(-50%, -50%);
}
}
@keyframes antZoomBadgeOutRtl {
0% {
transform: scale(1) translate(-50%, -50%);
}
100% {
transform: scale(0) translate(-50%, -50%);
opacity: 0;
}
}

View File

@ -1,5 +1,5 @@
import { PresetColorTypes } from '../_util/colors';
export function isPresetColor(color?: string): boolean {
return (PresetColorTypes as string[]).indexOf(color) !== -1;
return (PresetColorTypes as any[]).indexOf(color) !== -1;
}

View File

@ -1,30 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<meta
name="description"
content="An enterprise-class UI components based on Ant Design and Vue"
/>
<title>Ant Design Vue</title>
<meta
name="keywords"
content="ant design vue,ant-design-vue,ant-design-vue admin,ant design pro,vue ant design,vue ant design pro,vue ant design admin,ant design vue官网,ant design vue中文文档,ant design vue文档"
/>
<link rel="shortcut icon" type="image/x-icon" href="https://qn.antdv.com/favicon.ico" />
<style id="nprogress-style">
#nprogress {
display: none;
}
</style>
</head>
<body>
<div id="app"></div>
</body>
</html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<meta name="description" content="An enterprise-class UI components based on Ant Design and Vue" />
<title>Ant Design Vue</title>
<meta name="keywords"
content="ant design vue,ant-design-vue,ant-design-vue admin,ant design pro,vue ant design,vue ant design pro,vue ant design admin,ant design vue官网,ant design vue中文文档,ant design vue文档" />
<link rel="shortcut icon" type="image/x-icon" href="https://qn.antdv.com/favicon.ico" />
<style id="nprogress-style">
#nprogress {
display: none;
}
</style>
</head>
<body>
<div id="app" style="padding: 50px"></div>
</body>
</html>

0
v3-changelog.md Normal file
View File