refactor(component): improve code (#7959)

This commit is contained in:
三咲智子 2022-05-29 09:42:31 +08:00 committed by GitHub
parent ff5ea7e2ed
commit 3adb0f2077
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 164 additions and 147 deletions

View File

@ -1,5 +1,4 @@
[
"packages/components/autocomplete/",
"packages/components/cascader-panel/",
"packages/components/cascader/",
"packages/components/checkbox/",

View File

@ -5,12 +5,10 @@
:class="[ns.b(), ns.m(type), ns.is('center', center), ns.is(effect)]"
role="alert"
>
<el-icon
v-if="showIcon && iconComponent"
:class="[ns.e('icon'), isBigIcon]"
>
<el-icon v-if="showIcon && iconComponent" :class="iconClass">
<component :is="iconComponent" />
</el-icon>
<div :class="ns.e('content')">
<span
v-if="title || $slots.title"
@ -58,21 +56,21 @@ const slots = useSlots()
const ns = useNamespace('alert')
// state
const visible = ref(true)
// computed
const iconComponent = computed(
() => TypeComponentsMap[props.type] || TypeComponentsMap['info']
)
const isBigIcon = computed(
() => props.description || { [ns.is('big')]: slots.default }
)
const iconClass = computed(() => [
ns.e('icon'),
{ [ns.is('big')]: !!props.description || !!slots.default },
])
const isBoldTitle = computed(
() => props.description || { [ns.is('bold')]: slots.default }
)
// methods
const close = (evt: MouseEvent) => {
visible.value = false
emit('close', evt)

View File

@ -6,13 +6,18 @@ import {
isString,
} from '@element-plus/utils'
import { useTooltipContentProps } from '@element-plus/components/tooltip'
import { UPDATE_MODEL_EVENT } from '@element-plus/constants'
import {
CHANGE_EVENT,
INPUT_EVENT,
UPDATE_MODEL_EVENT,
} from '@element-plus/constants'
import type { ExtractPropTypes } from 'vue'
import type Autocomplete from './autocomplete.vue'
import type { Placement } from '@element-plus/components/popper'
import type { Awaitable } from '@element-plus/utils'
export type AutocompleteData = { value: string }[]
export type AutocompleteData = Record<string, any>[]
export type AutocompleteFetchSuggestionsCallback = (
data: AutocompleteData
) => void
@ -81,8 +86,8 @@ export type AutocompleteProps = ExtractPropTypes<typeof autocompleteProps>
export const autocompleteEmits = {
[UPDATE_MODEL_EVENT]: (value: string) => isString(value),
input: (value: string) => isString(value),
change: (value: string) => isString(value),
[INPUT_EVENT]: (value: string) => isString(value),
[CHANGE_EVENT]: (value: string) => isString(value),
focus: (evt: FocusEvent) => evt instanceof FocusEvent,
blur: (evt: FocusEvent) => evt instanceof FocusEvent,
clear: () => true,

View File

@ -95,19 +95,25 @@ import {
nextTick,
onMounted,
ref,
useAttrs as useCompAttrs,
useAttrs as useRawAttrs,
} from 'vue'
import { debounce } from 'lodash-unified'
import { onClickOutside } from '@vueuse/core'
import { Loading } from '@element-plus/icons-vue'
import { useAttrs, useNamespace } from '@element-plus/hooks'
import { generateId, isArray, throwError } from '@element-plus/utils'
import { UPDATE_MODEL_EVENT } from '@element-plus/constants'
import {
CHANGE_EVENT,
INPUT_EVENT,
UPDATE_MODEL_EVENT,
} from '@element-plus/constants'
import ElInput from '@element-plus/components/input'
import ElScrollbar from '@element-plus/components/scrollbar'
import ElTooltip from '@element-plus/components/tooltip'
import ElIcon from '@element-plus/components/icon'
import { Loading } from '@element-plus/icons-vue'
import { autocompleteEmits, autocompleteProps } from './autocomplete'
import type { AutocompleteData } from './autocomplete'
import type { StyleValue } from 'vue'
import type { TooltipInstance } from '@element-plus/components/tooltip'
import type { InputInstance } from '@element-plus/components/input'
@ -122,73 +128,71 @@ const COMPONENT_NAME = 'ElAutocomplete'
const props = defineProps(autocompleteProps)
const emit = defineEmits(autocompleteEmits)
const ns = useNamespace('autocomplete')
let isClear = false
const attrs = useAttrs()
const compAttrs = useCompAttrs()
const suggestions = ref<any[]>([])
const highlightedIndex = ref(-1)
const dropdownWidth = ref('')
const activated = ref(false)
const suggestionDisabled = ref(false)
const loading = ref(false)
const rawAttrs = useRawAttrs()
const ns = useNamespace('autocomplete')
const inputRef = ref<InputInstance>()
const regionRef = ref<HTMLElement>()
const popperRef = ref<TooltipInstance>()
const listboxRef = ref<HTMLElement>()
const listboxId = computed(() => {
return ns.b(String(generateId()))
})
const styles = computed(() => compAttrs.style as StyleValue)
let isClear = false
const suggestions = ref<AutocompleteData>([])
const highlightedIndex = ref(-1)
const dropdownWidth = ref('')
const activated = ref(false)
const suggestionDisabled = ref(false)
const loading = ref(false)
const listboxId = computed(() => ns.b(String(generateId())))
const styles = computed(() => rawAttrs.style as StyleValue)
const suggestionVisible = computed(() => {
const isValidData = isArray(suggestions.value) && suggestions.value.length > 0
const isValidData = suggestions.value.length > 0
return (isValidData || loading.value) && activated.value
})
const suggestionLoading = computed(() => {
return !props.hideLoading && loading.value
})
const onSuggestionShow = () => {
nextTick(() => {
if (suggestionVisible.value) {
dropdownWidth.value = `${inputRef.value!.$el.offsetWidth}px`
}
})
const suggestionLoading = computed(() => !props.hideLoading && loading.value)
const onSuggestionShow = async () => {
await nextTick()
if (suggestionVisible.value) {
dropdownWidth.value = `${inputRef.value!.$el.offsetWidth}px`
}
}
const getData = async (queryString: string) => {
if (suggestionDisabled.value) {
return
}
loading.value = true
const cb = (suggestionsArg: any[]) => {
if (suggestionDisabled.value) return
const cb = (suggestionList: AutocompleteData) => {
loading.value = false
if (suggestionDisabled.value) {
return
}
if (isArray(suggestionsArg)) {
suggestions.value = suggestionsArg
if (suggestionDisabled.value) return
if (isArray(suggestionList)) {
suggestions.value = suggestionList
highlightedIndex.value = props.highlightFirstItem ? 0 : -1
} else {
throwError(COMPONENT_NAME, 'autocomplete suggestions must be an array')
}
}
loading.value = true
if (isArray(props.fetchSuggestions)) {
cb(props.fetchSuggestions)
} else {
const result = await props.fetchSuggestions(queryString, cb)
if (isArray(result)) {
cb(result)
}
if (isArray(result)) cb(result)
}
}
const debouncedGetData = debounce(getData, props.debounce)
const handleInput = (value: string) => {
const valuePresented = Boolean(value)
emit('input', value)
const handleInput = (value: string) => {
const valuePresented = !!value
emit(INPUT_EVENT, value)
emit(UPDATE_MODEL_EVENT, value)
suggestionDisabled.value = false
activated.value ||= isClear && valuePresented
@ -202,9 +206,11 @@ const handleInput = (value: string) => {
}
debouncedGetData(value)
}
const handleChange = (value: string) => {
emit('change', value)
emit(CHANGE_EVENT, value)
}
const handleFocus = (evt: FocusEvent) => {
activated.value = true
emit('focus', evt)
@ -212,16 +218,19 @@ const handleFocus = (evt: FocusEvent) => {
debouncedGetData(String(props.modelValue))
}
}
const handleBlur = (evt: FocusEvent) => {
emit('blur', evt)
}
const handleClear = () => {
activated.value = false
isClear = true
emit(UPDATE_MODEL_EVENT, '')
emit('clear')
}
const handleKeyEnter = () => {
const handleKeyEnter = async () => {
if (
suggestionVisible.value &&
highlightedIndex.value >= 0 &&
@ -230,19 +239,20 @@ const handleKeyEnter = () => {
handleSelect(suggestions.value[highlightedIndex.value])
} else if (props.selectWhenUnmatched) {
emit('select', { value: props.modelValue })
nextTick(() => {
suggestions.value = []
highlightedIndex.value = -1
})
await nextTick()
suggestions.value = []
highlightedIndex.value = -1
}
}
const handleKeyEscape = (e) => {
const handleKeyEscape = (evt: Event) => {
if (suggestionVisible.value) {
e.preventDefault()
e.stopPropagation()
evt.preventDefault()
evt.stopPropagation()
close()
}
}
const close = () => {
activated.value = false
}
@ -251,23 +261,23 @@ const focus = () => {
inputRef.value?.focus()
}
const handleSelect = (item: any) => {
emit('input', item[props.valueKey])
const handleSelect = async (item: any) => {
emit(INPUT_EVENT, item[props.valueKey])
emit(UPDATE_MODEL_EVENT, item[props.valueKey])
emit('select', item)
nextTick(() => {
suggestions.value = []
highlightedIndex.value = -1
})
await nextTick()
suggestions.value = []
highlightedIndex.value = -1
}
const highlight = (index: number) => {
if (!suggestionVisible.value || loading.value) {
return
}
if (!suggestionVisible.value || loading.value) return
if (index < 0) {
highlightedIndex.value = -1
return
}
if (index >= suggestions.value.length) {
index = suggestions.value.length - 1
}

View File

@ -1,4 +1,9 @@
import { buildProps, definePropType, iconPropType } from '@element-plus/utils'
import {
buildProps,
definePropType,
iconPropType,
isNumber,
} from '@element-plus/utils'
import { componentSizes } from '@element-plus/constants'
import type { ExtractPropTypes } from 'vue'
import type { ObjectFitProperty } from 'csstype'
@ -9,7 +14,7 @@ export const avatarProps = buildProps({
type: [Number, String],
values: componentSizes,
default: '',
validator: (val: unknown): val is number => typeof val === 'number',
validator: (val: unknown): val is number => isNumber(val),
},
shape: {
type: String,

View File

@ -1,4 +1,5 @@
import type { ExtractPropTypes } from 'vue'
import type Backtop from './backtop.vue'
export const backtopProps = {
visibilityHeight: {
@ -24,3 +25,5 @@ export const backtopEmits = {
click: (evt: MouseEvent) => evt instanceof MouseEvent,
}
export type BacktopEmits = typeof backtopEmits
export type BacktopInstance = InstanceType<typeof Backtop>

View File

@ -20,7 +20,6 @@ import { ElIcon } from '@element-plus/components/icon'
import { easeInOutCubic, throwError } from '@element-plus/utils'
import { CaretTop } from '@element-plus/icons-vue'
import { useNamespace } from '@element-plus/hooks'
import { backtopEmits, backtopProps } from './backtop'
const COMPONENT_NAME = 'ElBacktop'
@ -43,6 +42,8 @@ const backTopStyle = computed(() => ({
}))
const scrollToTop = () => {
// TODO: use https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollTo, with behavior: 'smooth'
if (!el.value) return
const beginTime = Date.now()
const beginValue = el.value.scrollTop
@ -68,6 +69,7 @@ const handleClick = (event: MouseEvent) => {
const handleScrollThrottled = useThrottleFn(handleScroll, 300)
useEventListener(container, 'scroll', handleScrollThrottled)
onMounted(() => {
container.value = document
el.value = document.documentElement
@ -79,7 +81,5 @@ onMounted(() => {
}
container.value = el.value
}
useEventListener(container, 'scroll', handleScrollThrottled)
})
</script>

View File

@ -18,14 +18,13 @@
</template>
<script lang="ts" setup>
import { getCurrentInstance, inject, ref } from 'vue'
import { getCurrentInstance, inject, ref, toRefs } from 'vue'
import ElIcon from '@element-plus/components/icon'
import { breadcrumbKey } from '@element-plus/tokens'
import { useNamespace } from '@element-plus/hooks'
import { breadcrumbItemProps } from './breadcrumb-item'
import type { Router } from 'vue-router'
import type { BreadcrumbProps } from './breadcrumb'
defineOptions({
name: 'ElBreadcrumbItem',
@ -34,12 +33,11 @@ defineOptions({
const props = defineProps(breadcrumbItemProps)
const instance = getCurrentInstance()!
const router = instance.appContext.config.globalProperties.$router as Router
const breadcrumbInjection = inject(breadcrumbKey, {} as BreadcrumbProps)!
const breadcrumbContext = inject(breadcrumbKey, undefined)!
const ns = useNamespace('breadcrumb')
const { separator, separatorIcon } = breadcrumbInjection
const { separator, separatorIcon } = toRefs(breadcrumbContext)
const router = instance.appContext.config.globalProperties.$router as Router
const link = ref<HTMLSpanElement>()

View File

@ -1,5 +1,10 @@
import { buildProps, definePropType } from '@element-plus/utils'
import { UPDATE_MODEL_EVENT } from '@element-plus/constants'
import {
buildProps,
definePropType,
isArray,
isDate,
} from '@element-plus/utils'
import { INPUT_EVENT, UPDATE_MODEL_EVENT } from '@element-plus/constants'
import type { ExtractPropTypes } from 'vue'
import type Calendar from './calendar.vue'
@ -10,23 +15,23 @@ export type CalendarDateType =
| 'next-year'
| 'today'
const isValidRange = (range: unknown): range is [Date, Date] =>
isArray(range) && range.length === 2 && range.every((item) => isDate(item))
export const calendarProps = buildProps({
modelValue: {
type: Date,
},
range: {
type: definePropType<[Date, Date]>(Array),
validator: (range: unknown): range is [Date, Date] =>
Array.isArray(range) &&
range.length === 2 &&
range.every((item) => item instanceof Date),
validator: isValidRange,
},
} as const)
export type CalendarProps = ExtractPropTypes<typeof calendarProps>
export const calendarEmits = {
[UPDATE_MODEL_EVENT]: (value: Date) => value instanceof Date,
input: (value: Date) => value instanceof Date,
[UPDATE_MODEL_EVENT]: (value: Date) => isDate(value),
[INPUT_EVENT]: (value: Date) => isDate(value),
}
export type CalendarEmits = typeof calendarEmits

View File

@ -49,6 +49,7 @@ import dayjs from 'dayjs'
import { ElButton, ElButtonGroup } from '@element-plus/components/button'
import { useLocale, useNamespace } from '@element-plus/hooks'
import { debugWarn } from '@element-plus/utils'
import { INPUT_EVENT, UPDATE_MODEL_EVENT } from '@element-plus/constants'
import DateTable from './date-table.vue'
import { calendarEmits, calendarProps } from './calendar'
@ -66,32 +67,11 @@ const props = defineProps(calendarProps)
const emit = defineEmits(calendarEmits)
const ns = useNamespace('calendar')
const { t, lang } = useLocale()
const selectedDay = ref<Dayjs>()
const now = dayjs().locale(lang.value)
const prevMonthDayjs = computed(() => {
return date.value.subtract(1, 'month').date(1)
})
const nextMonthDayjs = computed(() => {
return date.value.add(1, 'month').date(1)
})
const prevYearDayjs = computed(() => {
return date.value.subtract(1, 'year').date(1)
})
const nextYearDayjs = computed(() => {
return date.value.add(1, 'year').date(1)
})
const i18nDate = computed(() => {
const pickedMonth = `el.datepicker.month${date.value.format('M')}`
return `${date.value.year()} ${t('el.datepicker.year')} ${t(pickedMonth)}`
})
const realSelectedDay = computed<Dayjs | undefined>({
get() {
if (!props.modelValue) return selectedDay.value
@ -102,11 +82,36 @@ const realSelectedDay = computed<Dayjs | undefined>({
selectedDay.value = val
const result = val.toDate()
emit('input', result)
emit('update:modelValue', result)
emit(INPUT_EVENT, result)
emit(UPDATE_MODEL_EVENT, result)
},
})
// if range is valid, we get a two-digit array
const validatedRange = computed(() => {
if (!props.range) return []
const rangeArrDayjs = props.range.map((_) => dayjs(_).locale(lang.value))
const [startDayjs, endDayjs] = rangeArrDayjs
if (startDayjs.isAfter(endDayjs)) {
debugWarn(COMPONENT_NAME, 'end time should be greater than start time')
return []
}
if (startDayjs.isSame(endDayjs, 'month')) {
// same month
return calculateValidatedDateRange(startDayjs, endDayjs)
} else {
// two months
if (startDayjs.add(1, 'month').month() !== endDayjs.month()) {
debugWarn(
COMPONENT_NAME,
'start time and end time interval must not exceed two months'
)
return []
}
return calculateValidatedDateRange(startDayjs, endDayjs)
}
})
const date: ComputedRef<Dayjs> = computed(() => {
if (!props.modelValue) {
if (realSelectedDay.value) {
@ -119,6 +124,15 @@ const date: ComputedRef<Dayjs> = computed(() => {
return dayjs(props.modelValue).locale(lang.value)
}
})
const prevMonthDayjs = computed(() => date.value.subtract(1, 'month').date(1))
const nextMonthDayjs = computed(() => date.value.add(1, 'month').date(1))
const prevYearDayjs = computed(() => date.value.subtract(1, 'year').date(1))
const nextYearDayjs = computed(() => date.value.add(1, 'year').date(1))
const i18nDate = computed(() => {
const pickedMonth = `el.datepicker.month${date.value.format('M')}`
return `${date.value.year()} ${t('el.datepicker.year')} ${t(pickedMonth)}`
})
// https://github.com/element-plus/element-plus/issues/3155
// Calculate the validate date range according to the start and end dates
@ -194,31 +208,6 @@ const calculateValidatedDateRange = (
}
}
// if range is valid, we get a two-digit array
const validatedRange = computed(() => {
if (!props.range) return []
const rangeArrDayjs = props.range.map((_) => dayjs(_).locale(lang.value))
const [startDayjs, endDayjs] = rangeArrDayjs
if (startDayjs.isAfter(endDayjs)) {
debugWarn(COMPONENT_NAME, 'end time should be greater than start time')
return []
}
if (startDayjs.isSame(endDayjs, 'month')) {
// same month
return calculateValidatedDateRange(startDayjs, endDayjs)
} else {
// two months
if (startDayjs.add(1, 'month').month() !== endDayjs.month()) {
debugWarn(
COMPONENT_NAME,
'start time and end time interval must not exceed two months'
)
return []
}
return calculateValidatedDateRange(startDayjs, endDayjs)
}
})
const pickDay = (day: Dayjs) => {
realSelectedDay.value = day
}

View File

@ -48,11 +48,10 @@ import {
getPrevMonthLastDays,
toNestedArr,
} from './date-table'
import type { CalendarDateCell, CalendarDateCellType } from './date-table'
import type { Dayjs } from 'dayjs'
dayjs.extend(localeData)
defineOptions({
name: 'DateTable',
})
@ -60,6 +59,8 @@ defineOptions({
const props = defineProps(dateTableProps)
const emit = defineEmits(dateTableEmits)
dayjs.extend(localeData)
const { t, lang } = useLocale()
const nsTable = useNamespace('calendar-table')
const nsDay = useNamespace('calendar-day')

View File

@ -13,6 +13,7 @@ export const cardProps = buildProps({
},
shadow: {
type: String,
values: ['always', 'hover', 'never'],
default: 'always',
},
} as const)

View File

@ -1,4 +1,6 @@
import { buildProps, isBoolean } from '@element-plus/utils'
import { CHANGE_EVENT } from '@element-plus/constants'
import type CheckTag from './check-tag.vue'
import type { ExtractPropTypes } from 'vue'
@ -12,7 +14,7 @@ export type CheckTagProps = ExtractPropTypes<typeof checkTagProps>
export const checkTagEmits = {
'update:checked': (value: boolean) => isBoolean(value),
change: (value: boolean) => isBoolean(value),
[CHANGE_EVENT]: (value: boolean) => isBoolean(value),
}
export type CheckTagEmits = typeof checkTagEmits

View File

@ -5,6 +5,7 @@
</template>
<script lang="ts" setup>
import { CHANGE_EVENT } from '@element-plus/constants'
import { useNamespace } from '@element-plus/hooks'
import { checkTagEmits, checkTagProps } from './check-tag'
@ -18,7 +19,7 @@ const ns = useNamespace('check-tag')
const handleChange = () => {
const checked = !props.checked
emit('change', checked)
emit(CHANGE_EVENT, checked)
emit('update:checked', checked)
}
</script>