refactor: badge
parent
372ac5c729
commit
e442b0d1ec
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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>;
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue