@@ -4,26 +4,16 @@ import classNames from '../_util/classNames';
4
4
import { getPropsSlot , flattenChildren } from '../_util/props-util' ;
5
5
import { cloneElement } from '../_util/vnode' ;
6
6
import { getTransitionProps , Transition } from '../_util/transition' ;
7
- import isNumeric from '../_util/isNumeric' ;
8
- import { defaultConfigProvider } from '../config-provider' ;
9
- import {
10
- inject ,
11
- defineComponent ,
12
- ExtractPropTypes ,
13
- CSSProperties ,
14
- VNode ,
15
- App ,
16
- Plugin ,
17
- reactive ,
18
- computed ,
19
- } from 'vue' ;
7
+ import { defineComponent , ExtractPropTypes , CSSProperties , computed , ref , watch } from 'vue' ;
20
8
import { tuple } from '../_util/type' ;
21
9
import Ribbon from './Ribbon' ;
22
10
import { isPresetColor } from './utils' ;
11
+ import useConfigInject from '../_util/hooks/useConfigInject' ;
12
+ import isNumeric from '../_util/isNumeric' ;
23
13
24
14
export const badgeProps = {
25
15
/** Number to show in badge */
26
- count : PropTypes . VNodeChild ,
16
+ count : PropTypes . any ,
27
17
showZero : PropTypes . looseBool ,
28
18
/** Max count to show */
29
19
overflowCount : PropTypes . number . def ( 99 ) ,
@@ -43,205 +33,189 @@ export const badgeProps = {
43
33
44
34
export type BadgeProps = Partial < ExtractPropTypes < typeof badgeProps > > ;
45
35
46
- const Badge = defineComponent ( {
36
+ export default defineComponent ( {
47
37
name : 'ABadge' ,
48
38
Ribbon,
49
39
props : badgeProps ,
50
- setup ( props , { slots } ) {
51
- const configProvider = inject ( 'configProvider' , defaultConfigProvider ) ;
52
- const state = reactive ( {
53
- badgeCount : undefined ,
40
+ slots : [ 'text' , 'count' ] ,
41
+ setup ( props , { slots, attrs } ) {
42
+ const { prefixCls, direction } = useConfigInject ( 'badge' , props ) ;
43
+
44
+ // ================================ Misc ================================
45
+ const numberedDisplayCount = computed ( ( ) => {
46
+ return ( ( props . count as number ) > ( props . overflowCount as number )
47
+ ? `${ props . overflowCount } +`
48
+ : props . count ) as string | number | null ;
54
49
} ) ;
55
50
56
- const getNumberedDispayCount = ( ) => {
57
- const { overflowCount } = props ;
58
- const count = state . badgeCount ;
59
- const displayCount = count > overflowCount ? `${ overflowCount } +` : count ;
60
- return displayCount ;
61
- } ;
62
-
63
- const getDispayCount = computed ( ( ) => {
64
- // dot mode don't need count
65
- if ( isDot . value ) {
66
- return '' ;
67
- }
68
- return getNumberedDispayCount ( ) ;
69
- } ) ;
70
-
71
- const getScrollNumberTitle = ( ) => {
72
- const { title } = props ;
73
- const count = state . badgeCount ;
74
- if ( title ) {
75
- return title ;
76
- }
77
- return typeof count === 'string' || typeof count === 'number' ? count : undefined ;
78
- } ;
79
-
80
- const getStyleWithOffset = ( ) => {
81
- const { offset, numberStyle } = props ;
82
- return offset
83
- ? {
84
- right : `${ - parseInt ( offset [ 0 ] as string , 10 ) } px` ,
85
- marginTop : isNumeric ( offset [ 1 ] ) ? `${ offset [ 1 ] } px` : offset [ 1 ] ,
86
- ...numberStyle ,
87
- }
88
- : { ...numberStyle } ;
89
- } ;
51
+ const hasStatus = computed (
52
+ ( ) =>
53
+ ( props . status !== null && props . status !== undefined ) ||
54
+ ( props . color !== null && props . color !== undefined ) ,
55
+ ) ;
90
56
91
- const hasStatus = computed ( ( ) => {
92
- const { status, color } = props ;
93
- return ! ! status || ! ! color ;
94
- } ) ;
57
+ const isZero = computed (
58
+ ( ) => numberedDisplayCount . value === '0' || numberedDisplayCount . value === 0 ,
59
+ ) ;
95
60
96
- const isZero = computed ( ( ) => {
97
- const numberedDispayCount = getNumberedDispayCount ( ) ;
98
- return numberedDispayCount === '0' || numberedDispayCount === 0 ;
99
- } ) ;
61
+ const showAsDot = computed ( ( ) => ( props . dot && ! isZero . value ) || hasStatus . value ) ;
100
62
101
- const isDot = computed ( ( ) => {
102
- const { dot } = props ;
103
- return ( dot && ! isZero . value ) || hasStatus . value ;
104
- } ) ;
63
+ const mergedCount = computed ( ( ) => ( showAsDot . value ? '' : numberedDisplayCount . value ) ) ;
105
64
106
65
const isHidden = computed ( ( ) => {
107
- const { showZero } = props ;
108
66
const isEmpty =
109
- getDispayCount . value === null ||
110
- getDispayCount . value === undefined ||
111
- getDispayCount . value === '' ;
112
- return ( isEmpty || ( isZero . value && ! showZero ) ) && ! isDot . value ;
67
+ mergedCount . value === null || mergedCount . value === undefined || mergedCount . value === '' ;
68
+ return ( isEmpty || ( isZero . value && ! props . showZero ) ) && ! showAsDot . value ;
113
69
} ) ;
114
70
115
- const renderStatusText = ( prefixCls : string ) => {
116
- const text = getPropsSlot ( slots , props , 'text' ) ;
117
- const hidden = isHidden . value ;
118
- return hidden || ! text ? null : < span class = { `${ prefixCls } -status-text` } > { text } </ span > ;
119
- } ;
71
+ // Count should be cache in case hidden change it
72
+ const livingCount = ref ( props . count ) ;
73
+
74
+ // We need cache count since remove motion should not change count display
75
+ const displayCount = ref ( mergedCount . value ) ;
76
+
77
+ // We will cache the dot status to avoid shaking on leaved motion
78
+ const isDotRef = ref ( showAsDot . value ) ;
79
+
80
+ watch (
81
+ [ ( ) => props . count , mergedCount , showAsDot ] ,
82
+ ( ) => {
83
+ if ( ! isHidden . value ) {
84
+ livingCount . value = props . count ;
85
+ displayCount . value = mergedCount . value ;
86
+ isDotRef . value = showAsDot . value ;
87
+ }
88
+ } ,
89
+ { immediate : true } ,
90
+ ) ;
91
+
92
+ // Shared styles
93
+ const statusCls = computed ( ( ) => ( {
94
+ [ `${ prefixCls . value } -status-dot` ] : hasStatus . value ,
95
+ [ `${ prefixCls . value } -status-${ props . status } ` ] : ! ! props . status ,
96
+ [ `${ prefixCls . value } -status-${ props . color } ` ] : isPresetColor ( props . color ) ,
97
+ } ) ) ;
98
+
99
+ const statusStyle = computed ( ( ) => {
100
+ if ( props . color && ! isPresetColor ( props . color ) ) {
101
+ return { background : props . color } ;
102
+ } else {
103
+ return { } ;
104
+ }
105
+ } ) ;
120
106
121
- const getBadgeClassName = ( prefixCls : string , children : VNode [ ] ) => {
122
- const status = hasStatus . value ;
123
- return classNames ( prefixCls , {
124
- [ `${ prefixCls } -status` ] : status ,
125
- [ `${ prefixCls } -dot-status` ] : status && props . dot && ! isZero . value ,
126
- [ `${ prefixCls } -not-a-wrapper` ] : ! children . length ,
127
- } ) ;
128
- } ;
107
+ const scrollNumberCls = computed ( ( ) => ( {
108
+ [ `${ prefixCls . value } -dot` ] : isDotRef . value ,
109
+ [ `${ prefixCls . value } -count` ] : ! isDotRef . value ,
110
+ [ `${ prefixCls . value } -count-sm` ] : props . size === 'small' ,
111
+ [ `${ prefixCls . value } -multiple-words` ] :
112
+ ! isDotRef . value && displayCount . value && displayCount . value . toString ( ) . length > 1 ,
113
+ [ `${ prefixCls . value } -status-${ status } ` ] : ! ! status ,
114
+ [ `${ prefixCls . value } -status-${ props . color } ` ] : isPresetColor ( props . color ) ,
115
+ } ) ) ;
129
116
130
- const renderDispayComponent = ( ) => {
131
- const count = state . badgeCount ;
132
- const customNode = count ;
133
- if ( ! customNode || typeof customNode !== 'object' ) {
134
- return undefined ;
135
- }
136
- return cloneElement (
137
- customNode ,
117
+ return ( ) => {
118
+ const { offset, title, color } = props ;
119
+ const style = attrs . style as CSSProperties ;
120
+ const text = getPropsSlot ( slots , props , 'text' ) ;
121
+ const pre = prefixCls . value ;
122
+ const count = livingCount . value ;
123
+ let children = flattenChildren ( slots . default ?.( ) ) ;
124
+ children = children . length ? children : null ;
125
+
126
+ const visible = ! ! ( ! isHidden . value || slots . count ) ;
127
+
128
+ // =============================== Styles ===============================
129
+ const mergedStyle = ( ( ) => {
130
+ if ( ! offset ) {
131
+ return { ...style } ;
132
+ }
133
+
134
+ const offsetStyle : CSSProperties = {
135
+ marginTop : isNumeric ( offset [ 1 ] ) ? `${ offset [ 1 ] } px` : offset [ 1 ] ,
136
+ } ;
137
+ if ( direction . value === 'rtl' ) {
138
+ offsetStyle . left = `${ parseInt ( offset [ 0 ] as string , 10 ) } px` ;
139
+ } else {
140
+ offsetStyle . right = `${ - parseInt ( offset [ 0 ] as string , 10 ) } px` ;
141
+ }
142
+
143
+ return {
144
+ ...offsetStyle ,
145
+ ...style ,
146
+ } ;
147
+ } ) ( ) ;
148
+
149
+ // =============================== Render ===============================
150
+ // >>> Title
151
+ const titleNode =
152
+ title ?? ( typeof count === 'string' || typeof count === 'number' ? count : undefined ) ;
153
+
154
+ // >>> Status Text
155
+ const statusTextNode =
156
+ visible || ! text ? null : < span class = { `${ pre } -status-text` } > { text } </ span > ;
157
+
158
+ // >>> Display Component
159
+ const displayNode = cloneElement (
160
+ slots . count ?.( ) ,
138
161
{
139
- style : getStyleWithOffset ( ) ,
162
+ style : mergedStyle ,
140
163
} ,
141
164
false ,
142
165
) ;
143
- } ;
144
-
145
- const renderBadgeNumber = ( prefixCls : string , scrollNumberPrefixCls : string ) => {
146
- const { status, color, size } = props ;
147
- const count = state . badgeCount ;
148
- const displayCount = getDispayCount . value ;
149
-
150
- const scrollNumberCls = {
151
- [ `${ prefixCls } -dot` ] : isDot . value ,
152
- [ `${ prefixCls } -count` ] : ! isDot . value ,
153
- [ `${ prefixCls } -count-sm` ] : size === 'small' ,
154
- [ `${ prefixCls } -multiple-words` ] :
155
- ! isDot . value && count && count . toString && count . toString ( ) . length > 1 ,
156
- [ `${ prefixCls } -status-${ status } ` ] : ! ! status ,
157
- [ `${ prefixCls } -status-${ color } ` ] : isPresetColor ( color ) ,
158
- } ;
159
-
160
- let statusStyle = getStyleWithOffset ( ) ;
161
- if ( color && ! isPresetColor ( color ) ) {
162
- statusStyle = statusStyle || { } ;
163
- statusStyle . background = color ;
164
- }
165
166
166
- return isHidden . value ? null : (
167
- < ScrollNumber
168
- prefixCls = { scrollNumberPrefixCls }
169
- data-show = { ! isHidden . value }
170
- v-show = { ! isHidden . value }
171
- class = { scrollNumberCls }
172
- count = { displayCount }
173
- displayComponent = { renderDispayComponent ( ) }
174
- title = { getScrollNumberTitle ( ) }
175
- style = { statusStyle }
176
- key = "scrollNumber"
177
- />
167
+ const badgeClassName = classNames (
168
+ pre ,
169
+ {
170
+ [ `${ pre } -status` ] : hasStatus . value ,
171
+ [ `${ pre } -not-a-wrapper` ] : ! children ,
172
+ [ `${ pre } -rtl` ] : direction . value === 'rtl' ,
173
+ } ,
174
+ attrs . class ,
178
175
) ;
179
- } ;
180
176
181
- return ( ) => {
182
- const {
183
- prefixCls : customizePrefixCls ,
184
- scrollNumberPrefixCls : customizeScrollNumberPrefixCls ,
185
- status,
186
- color,
187
- } = props ;
188
-
189
- const text = getPropsSlot ( slots , props , 'text' ) ;
190
- const getPrefixCls = configProvider . getPrefixCls ;
191
- const prefixCls = getPrefixCls ( 'badge' , customizePrefixCls ) ;
192
- const scrollNumberPrefixCls = getPrefixCls ( 'scroll-number' , customizeScrollNumberPrefixCls ) ;
193
-
194
- const children = flattenChildren ( slots . default ?.( ) ) ;
195
- let count = getPropsSlot ( slots , props , 'count' ) ;
196
- if ( Array . isArray ( count ) ) {
197
- count = count [ 0 ] ;
198
- }
199
- state . badgeCount = count ;
200
- const scrollNumber = renderBadgeNumber ( prefixCls , scrollNumberPrefixCls ) ;
201
- const statusText = renderStatusText ( prefixCls ) ;
202
- const statusCls = classNames ( {
203
- [ `${ prefixCls } -status-dot` ] : hasStatus . value ,
204
- [ `${ prefixCls } -status-${ status } ` ] : ! ! status ,
205
- [ `${ prefixCls } -status-${ color } ` ] : isPresetColor ( color ) ,
206
- } ) ;
207
- const statusStyle : CSSProperties = { } ;
208
- if ( color && ! isPresetColor ( color ) ) {
209
- statusStyle . background = color ;
210
- }
211
177
// <Badge status="success" />
212
- if ( ! children . length && hasStatus . value ) {
213
- const styleWithOffset = getStyleWithOffset ( ) ;
214
- const statusTextColor = styleWithOffset && styleWithOffset . color ;
178
+ if ( ! children && hasStatus . value ) {
179
+ const statusTextColor = mergedStyle . color ;
215
180
return (
216
- < span class = { getBadgeClassName ( prefixCls , children ) } style = { styleWithOffset } >
217
- < span class = { statusCls } style = { statusStyle } />
218
- < span style = { { color : statusTextColor } } class = { `${ prefixCls } -status-text` } >
181
+ < span { ... attrs } class = { badgeClassName } style = { mergedStyle } >
182
+ < span class = { statusCls . value } style = { statusStyle . value } />
183
+ < span style = { { color : statusTextColor } } class = { `${ pre } -status-text` } >
219
184
{ text }
220
185
</ span >
221
186
</ span >
222
187
) ;
223
188
}
224
189
225
- const transitionProps = getTransitionProps ( children . length ? `${ prefixCls } -zoom` : '' ) ;
190
+ const transitionProps = getTransitionProps ( children ? `${ pre } -zoom` : '' , {
191
+ appear : false ,
192
+ } ) ;
193
+ let scrollNumberStyle : CSSProperties = { ...mergedStyle , ...props . numberStyle } ;
194
+ if ( color && ! isPresetColor ( color ) ) {
195
+ scrollNumberStyle = scrollNumberStyle || { } ;
196
+ scrollNumberStyle . background = color ;
197
+ }
226
198
227
199
return (
228
- < span class = { getBadgeClassName ( prefixCls , children ) } >
200
+ < span { ... attrs } class = { badgeClassName } >
229
201
{ children }
230
- < Transition { ...transitionProps } > { scrollNumber } </ Transition >
231
- { statusText }
202
+ < Transition { ...transitionProps } >
203
+ < ScrollNumber
204
+ v-show = { visible }
205
+ prefixCls = { props . scrollNumberPrefixCls }
206
+ show = { visible }
207
+ class = { scrollNumberCls . value }
208
+ count = { displayCount . value }
209
+ title = { titleNode }
210
+ style = { scrollNumberStyle }
211
+ key = "scrollNumber"
212
+ >
213
+ { displayNode }
214
+ </ ScrollNumber >
215
+ </ Transition >
216
+ { statusTextNode }
232
217
</ span >
233
218
) ;
234
219
} ;
235
220
} ,
236
221
} ) ;
237
-
238
- Badge . install = function ( app : App ) {
239
- app . component ( Badge . name , Badge ) ;
240
- app . component ( Badge . Ribbon . displayName , Badge . Ribbon ) ;
241
- return app ;
242
- } ;
243
-
244
- export default Badge as typeof Badge &
245
- Plugin & {
246
- readonly Ribbon : typeof Ribbon ;
247
- } ;
0 commit comments