chore: refactor tag
							parent
							
								
									be8faa5190
								
							
						
					
					
						commit
						09eb71d5fb
					
				|  | @ -1,4 +1,6 @@ | |||
| import { tuple } from './type'; | ||||
| import { ElementOf, tuple } from './type'; | ||||
| 
 | ||||
| export const PresetStatusColorTypes = tuple('success', 'processing', 'error', 'default', 'warning'); | ||||
| 
 | ||||
| export const PresetColorTypes = tuple( | ||||
|   'pink', | ||||
|  | @ -15,3 +17,6 @@ export const PresetColorTypes = tuple( | |||
|   'gold', | ||||
|   'lime', | ||||
| ); | ||||
| 
 | ||||
| export type PresetColorType = ElementOf<typeof PresetColorTypes>; | ||||
| export type PresetStatusColorType = ElementOf<typeof PresetStatusColorTypes>; | ||||
|  |  | |||
|  | @ -1,4 +1,22 @@ | |||
| export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; | ||||
| // https://stackoverflow.com/questions/46176165/ways-to-get-string-literal-type-of-array-values-without-enum-overhead
 | ||||
| export const tuple = <T extends string[]>(...args: T) => args; | ||||
| 
 | ||||
| export const tupleNum = (...args) => args; | ||||
| export const tupleNum = <T extends number[]>(...args: T) => args; | ||||
| 
 | ||||
| /** | ||||
|  * https://stackoverflow.com/a/59187769
 | ||||
|  * Extract the type of an element of an array/tuple without performing indexing | ||||
|  */ | ||||
| export type ElementOf<T> = T extends (infer E)[] ? E : T extends readonly (infer E)[] ? E : never; | ||||
| 
 | ||||
| /** | ||||
|  * https://github.com/Microsoft/TypeScript/issues/29729
 | ||||
|  */ | ||||
| export type LiteralUnion<T extends U, U> = T | (U & {}); | ||||
| 
 | ||||
| export type StringKeyOf<T> = Extract<keyof T, string>; | ||||
| 
 | ||||
| export type EventHandlers<E> = { | ||||
|   [K in StringKeyOf<E>]?: E[K] extends Function ? E[K] : (payload: E[K]) => void; | ||||
| }; | ||||
|  |  | |||
|  | @ -1,18 +1,18 @@ | |||
| import { nextTick, inject } from 'vue'; | ||||
| import { nextTick, inject, defineComponent } from 'vue'; | ||||
| import TransitionEvents from './css-animation/Event'; | ||||
| import raf from './raf'; | ||||
| import { ConfigConsumerProps } from '../config-provider'; | ||||
| import { findDOMNode } from './props-util'; | ||||
| let styleForPesudo; | ||||
| let styleForPesudo: HTMLStyleElement | null; | ||||
| 
 | ||||
| // Where el is the DOM element you'd like to test for visibility
 | ||||
| function isHidden(element) { | ||||
| function isHidden(element: HTMLElement) { | ||||
|   if (process.env.NODE_ENV === 'test') { | ||||
|     return false; | ||||
|   } | ||||
|   return !element || element.offsetParent === null; | ||||
| } | ||||
| function isNotGrey(color) { | ||||
| function isNotGrey(color: string) { | ||||
|   // eslint-disable-next-line no-useless-escape
 | ||||
|   const match = (color || '').match(/rgba?\((\d*), (\d*), (\d*)(, [\.\d]*)?\)/); | ||||
|   if (match && match[1] && match[2] && match[3]) { | ||||
|  | @ -20,7 +20,7 @@ function isNotGrey(color) { | |||
|   } | ||||
|   return true; | ||||
| } | ||||
| export default { | ||||
| export default defineComponent({ | ||||
|   name: 'Wave', | ||||
|   props: ['insertExtraNode'], | ||||
|   mounted() { | ||||
|  | @ -47,7 +47,7 @@ export default { | |||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     onClick(node, waveColor) { | ||||
|     onClick(node: HTMLElement, waveColor: string) { | ||||
|       if (!node || isHidden(node) || node.className.indexOf('-leave') >= 0) { | ||||
|         return; | ||||
|       } | ||||
|  | @ -87,7 +87,7 @@ export default { | |||
|       TransitionEvents.addStartEventListener(node, this.onTransitionStart); | ||||
|       TransitionEvents.addEndEventListener(node, this.onTransitionEnd); | ||||
|     }, | ||||
|     onTransitionStart(e) { | ||||
|     onTransitionStart(e: AnimationEvent) { | ||||
|       if (this._.isUnmounted) return; | ||||
| 
 | ||||
|       const node = findDOMNode(this); | ||||
|  | @ -99,7 +99,7 @@ export default { | |||
|         this.resetEffect(node); | ||||
|       } | ||||
|     }, | ||||
|     onTransitionEnd(e) { | ||||
|     onTransitionEnd(e: AnimationEvent) { | ||||
|       if (!e || e.animationName !== 'fadeEffect') { | ||||
|         return; | ||||
|       } | ||||
|  | @ -109,7 +109,7 @@ export default { | |||
|       const { insertExtraNode } = this.$props; | ||||
|       return insertExtraNode ? 'ant-click-animating' : 'ant-click-animating-without-extra-node'; | ||||
|     }, | ||||
|     bindAnimationEvent(node) { | ||||
|     bindAnimationEvent(node: HTMLElement) { | ||||
|       if ( | ||||
|         !node || | ||||
|         !node.getAttribute || | ||||
|  | @ -118,9 +118,9 @@ export default { | |||
|       ) { | ||||
|         return; | ||||
|       } | ||||
|       const onClick = e => { | ||||
|       const onClick = (e: MouseEvent) => { | ||||
|         // Fix radio button click twice
 | ||||
|         if (e.target.tagName === 'INPUT' || isHidden(e.target)) { | ||||
|         if ((e.target as HTMLElement).tagName === 'INPUT' || isHidden(e.target as HTMLElement)) { | ||||
|           return; | ||||
|         } | ||||
|         this.resetEffect(node); | ||||
|  | @ -146,7 +146,7 @@ export default { | |||
|       }; | ||||
|     }, | ||||
| 
 | ||||
|     resetEffect(node) { | ||||
|     resetEffect(node: HTMLElement) { | ||||
|       if (!node || node === this.extraNode || !(node instanceof Element)) { | ||||
|         return; | ||||
|       } | ||||
|  | @ -171,4 +171,4 @@ export default { | |||
|     } | ||||
|     return this.$slots.default && this.$slots.default()[0]; | ||||
|   }, | ||||
| }; | ||||
| }); | ||||
|  |  | |||
|  | @ -1,45 +0,0 @@ | |||
| import { inject } from 'vue'; | ||||
| import PropTypes from '../_util/vue-types'; | ||||
| import { ConfigConsumerProps } from '../config-provider'; | ||||
| 
 | ||||
| export default { | ||||
|   name: 'ACheckableTag', | ||||
|   props: { | ||||
|     prefixCls: PropTypes.string, | ||||
|     checked: PropTypes.bool, | ||||
|     onChange: PropTypes.func, | ||||
|     'onUpdate:checked': PropTypes.func, | ||||
|   }, | ||||
|   setup() { | ||||
|     return { | ||||
|       configProvider: inject('configProvider', ConfigConsumerProps), | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
|     classes() { | ||||
|       const { checked, prefixCls: customizePrefixCls } = this; | ||||
|       const getPrefixCls = this.configProvider.getPrefixCls; | ||||
|       const prefixCls = getPrefixCls('tag', customizePrefixCls); | ||||
|       return { | ||||
|         [`${prefixCls}`]: true, | ||||
|         [`${prefixCls}-checkable`]: true, | ||||
|         [`${prefixCls}-checkable-checked`]: checked, | ||||
|       }; | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     handleClick() { | ||||
|       const { checked } = this; | ||||
|       this.$emit('update:checked', !checked); | ||||
|       this.$emit('change', !checked); | ||||
|     }, | ||||
|   }, | ||||
|   render() { | ||||
|     const { classes, handleClick, $slots } = this; | ||||
|     return ( | ||||
|       <div class={classes} onClick={handleClick}> | ||||
|         {$slots.default && $slots.default()} | ||||
|       </div> | ||||
|     ); | ||||
|   }, | ||||
| }; | ||||
|  | @ -0,0 +1,44 @@ | |||
| import { inject, CSSProperties, SetupContext } from 'vue'; | ||||
| import classNames from 'classnames'; | ||||
| import { ConfigConsumerProps } from '../config-provider'; | ||||
| 
 | ||||
| export interface CheckableTagProps { | ||||
|   prefixCls?: string; | ||||
|   class?: string; | ||||
|   style?: CSSProperties; | ||||
|   checked: boolean; | ||||
|   onChange?: (checked: boolean) => void; | ||||
|   onClick?: (e: Event) => void; | ||||
| } | ||||
| 
 | ||||
| const CheckableTag = (props: CheckableTagProps, { slots }: SetupContext) => { | ||||
|   const { getPrefixCls } = inject('configProvider', ConfigConsumerProps); | ||||
|   const handleClick = (e: Event) => { | ||||
|     const { checked, onChange, onClick } = props; | ||||
|     if (onChange) { | ||||
|       onChange(!checked); | ||||
|     } | ||||
|     if (onClick) { | ||||
|       onClick(e); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const { prefixCls: customizePrefixCls, class: className, checked } = props; | ||||
|   const prefixCls = getPrefixCls('tag', customizePrefixCls); | ||||
|   const cls = classNames( | ||||
|     prefixCls, | ||||
|     { | ||||
|       [`${prefixCls}-checkable`]: true, | ||||
|       [`${prefixCls}-checkable-checked`]: checked, | ||||
|     }, | ||||
|     className, | ||||
|   ); | ||||
| 
 | ||||
|   return ( | ||||
|     <span class={cls} onClick={handleClick}> | ||||
|       {slots.default?.()} | ||||
|     </span> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export default CheckableTag; | ||||
|  | @ -1,121 +0,0 @@ | |||
| import { inject } from 'vue'; | ||||
| import CloseOutlined from '@ant-design/icons-vue/CloseOutlined'; | ||||
| import PropTypes from '../_util/vue-types'; | ||||
| import Wave from '../_util/wave'; | ||||
| import { hasProp, getOptionProps } from '../_util/props-util'; | ||||
| import BaseMixin from '../_util/BaseMixin'; | ||||
| import { ConfigConsumerProps } from '../config-provider'; | ||||
| 
 | ||||
| const PresetColorTypes = [ | ||||
|   'pink', | ||||
|   'red', | ||||
|   'yellow', | ||||
|   'orange', | ||||
|   'cyan', | ||||
|   'green', | ||||
|   'blue', | ||||
|   'purple', | ||||
|   'geekblue', | ||||
|   'magenta', | ||||
|   'volcano', | ||||
|   'gold', | ||||
|   'lime', | ||||
| ]; | ||||
| const PresetColorRegex = new RegExp(`^(${PresetColorTypes.join('|')})(-inverse)?$`); | ||||
| 
 | ||||
| export default { | ||||
|   name: 'ATag', | ||||
|   mixins: [BaseMixin], | ||||
|   props: { | ||||
|     prefixCls: PropTypes.string, | ||||
|     color: PropTypes.string, | ||||
|     closable: PropTypes.bool.def(false), | ||||
|     visible: PropTypes.bool, | ||||
|     onClose: PropTypes.func, | ||||
|     'onUpdate:visible': PropTypes.func, | ||||
|   }, | ||||
|   setup() { | ||||
|     return { | ||||
|       configProvider: inject('configProvider', ConfigConsumerProps), | ||||
|     }; | ||||
|   }, | ||||
|   data() { | ||||
|     let _visible = true; | ||||
|     const props = getOptionProps(this); | ||||
|     if ('visible' in props) { | ||||
|       _visible = this.visible; | ||||
|     } | ||||
|     return { | ||||
|       _visible, | ||||
|     }; | ||||
|   }, | ||||
|   watch: { | ||||
|     visible(val) { | ||||
|       this.setState({ | ||||
|         _visible: val, | ||||
|       }); | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     setVisible(visible, e) { | ||||
|       this.$emit('update:visible', false); | ||||
|       this.$emit('close', e); | ||||
|       if (e.defaultPrevented) { | ||||
|         return; | ||||
|       } | ||||
|       if (!hasProp(this, 'visible')) { | ||||
|         this.setState({ _visible: visible }); | ||||
|       } | ||||
|     }, | ||||
| 
 | ||||
|     handleIconClick(e) { | ||||
|       e.stopPropagation(); | ||||
|       this.setVisible(false, e); | ||||
|     }, | ||||
| 
 | ||||
|     isPresetColor() { | ||||
|       const { color } = this.$props; | ||||
|       if (!color) { | ||||
|         return false; | ||||
|       } | ||||
|       return PresetColorRegex.test(color); | ||||
|     }, | ||||
|     getTagStyle() { | ||||
|       const { color } = this.$props; | ||||
|       const isPresetColor = this.isPresetColor(); | ||||
|       return { | ||||
|         backgroundColor: color && !isPresetColor ? color : undefined, | ||||
|       }; | ||||
|     }, | ||||
| 
 | ||||
|     getTagClassName(prefixCls) { | ||||
|       const { color } = this.$props; | ||||
|       const isPresetColor = this.isPresetColor(); | ||||
|       return { | ||||
|         [prefixCls]: true, | ||||
|         [`${prefixCls}-${color}`]: isPresetColor, | ||||
|         [`${prefixCls}-has-color`]: color && !isPresetColor, | ||||
|       }; | ||||
|     }, | ||||
| 
 | ||||
|     renderCloseIcon() { | ||||
|       const { closable } = this.$props; | ||||
|       return closable ? <CloseOutlined onClick={this.handleIconClick} /> : null; | ||||
|     }, | ||||
|   }, | ||||
| 
 | ||||
|   render() { | ||||
|     const { prefixCls: customizePrefixCls } = this.$props; | ||||
|     const isNeedWave = 'onClick' in this.$attrs; | ||||
|     const getPrefixCls = this.configProvider.getPrefixCls; | ||||
|     const prefixCls = getPrefixCls('tag', customizePrefixCls); | ||||
|     const { _visible: visible } = this.$data; | ||||
|     const tag = ( | ||||
|       <span v-show={visible} class={this.getTagClassName(prefixCls)} style={this.getTagStyle()}> | ||||
|         {this.$slots.default && this.$slots.default()} | ||||
|         {this.renderCloseIcon()} | ||||
|       </span> | ||||
|     ); | ||||
|     return isNeedWave ? <Wave>{tag}</Wave> : tag; | ||||
|   }, | ||||
| }; | ||||
|  | @ -1,12 +0,0 @@ | |||
| import Tag from './Tag'; | ||||
| import CheckableTag from './CheckableTag'; | ||||
| 
 | ||||
| Tag.CheckableTag = CheckableTag; | ||||
| 
 | ||||
| /* istanbul ignore next */ | ||||
| Tag.install = function(app) { | ||||
|   app.component(Tag.name, Tag); | ||||
|   app.component(Tag.CheckableTag.name, Tag.CheckableTag); | ||||
| }; | ||||
| 
 | ||||
| export default Tag; | ||||
|  | @ -0,0 +1,143 @@ | |||
| import { | ||||
|   inject, | ||||
|   ref, | ||||
|   HTMLAttributes, | ||||
|   VNodeChild, | ||||
|   Events, | ||||
|   defineComponent, | ||||
|   SetupContext, | ||||
|   App, | ||||
|   watchEffect, | ||||
| } from 'vue'; | ||||
| import classNames from 'classnames'; | ||||
| import omit from 'omit.js'; | ||||
| import CloseOutlined from '@ant-design/icons-vue/CloseOutlined'; | ||||
| import Wave from '../_util/wave'; | ||||
| import { | ||||
|   PresetColorTypes, | ||||
|   PresetStatusColorTypes, | ||||
|   PresetColorType, | ||||
|   PresetStatusColorType, | ||||
| } from '../_util/colors'; | ||||
| import { LiteralUnion, EventHandlers } from '../_util/type'; | ||||
| import { ConfigConsumerProps } from '../config-provider'; | ||||
| import CheckableTag from './CheckableTag'; | ||||
| 
 | ||||
| const PresetColorRegex = new RegExp(`^(${PresetColorTypes.join('|')})(-inverse)?$`); | ||||
| const PresetStatusColorRegex = new RegExp(`^(${PresetStatusColorTypes.join('|')})$`); | ||||
| 
 | ||||
| export interface TagProps extends HTMLAttributes, Partial<EventHandlers<Events>> { | ||||
|   prefixCls?: string; | ||||
|   color?: LiteralUnion<PresetColorType | PresetStatusColorType, string>; | ||||
|   closable?: boolean; | ||||
|   closeIcon?: VNodeChild | JSX.Element; | ||||
|   visible?: boolean; | ||||
|   onClose?: Function; | ||||
|   icon?: VNodeChild | JSX.Element; | ||||
| } | ||||
| 
 | ||||
| const Tag = defineComponent({ | ||||
|   setup(_: TagProps, { slots, attrs }: SetupContext) { | ||||
|     const { getPrefixCls } = inject('configProvider', ConfigConsumerProps); | ||||
| 
 | ||||
|     const visible = ref(true); | ||||
| 
 | ||||
|     return () => { | ||||
|       const { | ||||
|         prefixCls: customizePrefixCls, | ||||
|         style, | ||||
|         icon, | ||||
|         color, | ||||
|         onClose, | ||||
|         closeIcon, | ||||
|         closable = false, | ||||
|         ...props | ||||
|       } = attrs as TagProps; | ||||
| 
 | ||||
|       watchEffect(() => { | ||||
|         if ('visible' in props) { | ||||
|           visible.value = props.visible!; | ||||
|         } | ||||
|       }); | ||||
| 
 | ||||
|       const isPresetColor = (): boolean => { | ||||
|         if (!color) { | ||||
|           return false; | ||||
|         } | ||||
|         return PresetColorRegex.test(color) || PresetStatusColorRegex.test(color); | ||||
|       }; | ||||
| 
 | ||||
|       const presetColor = isPresetColor(); | ||||
|       const prefixCls = getPrefixCls('tag', customizePrefixCls); | ||||
| 
 | ||||
|       const handleCloseClick = (e: MouseEvent) => { | ||||
|         e.stopPropagation(); | ||||
|         if (onClose) { | ||||
|           onClose(e); | ||||
|         } | ||||
| 
 | ||||
|         if (e.defaultPrevented) { | ||||
|           return; | ||||
|         } | ||||
|         if (!('visible' in props)) { | ||||
|           visible.value = false; | ||||
|         } | ||||
|       }; | ||||
| 
 | ||||
|       const renderCloseIcon = () => { | ||||
|         if (closable) { | ||||
|           return closeIcon ? ( | ||||
|             <div class={`${prefixCls}-close-icon`} onClick={handleCloseClick}> | ||||
|               {closeIcon} | ||||
|             </div> | ||||
|           ) : ( | ||||
|             <CloseOutlined class={`${prefixCls}-close-icon`} onClick={handleCloseClick} /> | ||||
|           ); | ||||
|         } | ||||
|         return null; | ||||
|       }; | ||||
| 
 | ||||
|       const tagStyle = { | ||||
|         backgroundColor: color && !isPresetColor() ? color : undefined, | ||||
|       }; | ||||
| 
 | ||||
|       const tagClassName = classNames(prefixCls, { | ||||
|         [`${prefixCls}-${color}`]: presetColor, | ||||
|         [`${prefixCls}-has-color`]: color && !presetColor, | ||||
|         [`${prefixCls}-hidden`]: !visible.value, | ||||
|       }); | ||||
| 
 | ||||
|       const tagProps = omit(props, ['visible']); | ||||
|       const iconNode = icon || null; | ||||
|       const children = slots.default?.(); | ||||
|       const kids = iconNode ? ( | ||||
|         <> | ||||
|           {iconNode} | ||||
|           <span>{children}</span> | ||||
|         </> | ||||
|       ) : ( | ||||
|         children | ||||
|       ); | ||||
| 
 | ||||
|       const isNeedWave = 'onClick' in props; | ||||
| 
 | ||||
|       const tagNode = ( | ||||
|         <span {...tagProps} class={tagClassName} style={tagStyle}> | ||||
|           {kids} | ||||
|           {renderCloseIcon()} | ||||
|         </span> | ||||
|       ); | ||||
| 
 | ||||
|       return isNeedWave ? <Wave>{tagNode}</Wave> : tagNode; | ||||
|     }; | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| Tag.CheckableTag = CheckableTag; | ||||
| 
 | ||||
| Tag.install = (app: App) => { | ||||
|   app.component(Tag.name, Tag); | ||||
|   app.component(CheckableTag.name, CheckableTag); | ||||
| }; | ||||
| 
 | ||||
| export default Tag; | ||||
		Loading…
	
		Reference in New Issue
	
	 Amour1688
						Amour1688