refactor(components): [time-picker] script setup (#8128)

* refactor(components): [time-picker] panel-time-range

* Refactor `panel-time-range` to script setup.

* refactor(components): [time-picker] panel-time-range

* Fix typing issues in `panel-time-range`.
* Extract common code to `use-time-panel`.

* refactor(components): [time-picker] panel-time-picker

* Replace duplicated logic with `use-time-panel`.

Co-authored-by: JeremyWuuuuu <15975785+JeremyWuuuuu@users.noreply.github.com>
This commit is contained in:
Jeremy 2022-06-06 17:35:10 +08:00 committed by GitHub
parent f8547411e5
commit beacaf6ce4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 325 additions and 339 deletions

View File

@ -0,0 +1,86 @@
import type { Dayjs } from 'dayjs'
type UseTimePanelProps = {
getAvailableHours: (role: string, compare?: Dayjs) => number[]
getAvailableMinutes: (hour: number, role: string, compare?: Dayjs) => number[]
getAvailableSeconds: (
hour: number,
minute: number,
role: string,
compare?: Dayjs
) => number[]
}
export const useTimePanel = ({
getAvailableHours,
getAvailableMinutes,
getAvailableSeconds,
}: UseTimePanelProps) => {
const getAvailableTime = (
date: Dayjs,
role: string,
first: boolean,
compareDate?: Dayjs
) => {
const availableTimeGetters = {
hour: getAvailableHours,
minute: getAvailableMinutes,
second: getAvailableSeconds,
} as const
let result = date
;(['hour', 'minute', 'second'] as const).forEach((type) => {
if (availableTimeGetters[type]) {
let availableTimeSlots
const method = availableTimeGetters[type]
switch (type) {
case 'minute': {
availableTimeSlots = (method as typeof getAvailableMinutes)(
result.hour(),
role,
compareDate
)
break
}
case 'second': {
availableTimeSlots = (method as typeof getAvailableSeconds)(
result.hour(),
result.minute(),
role,
compareDate
)
break
}
default: {
availableTimeSlots = (method as typeof getAvailableHours)(
role,
compareDate
)
break
}
}
if (
availableTimeSlots?.length &&
!availableTimeSlots.includes(result[type]())
) {
const pos = first ? 0 : availableTimeSlots.length - 1
result = result[type](availableTimeSlots[pos]) as unknown as Dayjs
}
}
})
return result
}
const timePickerOptions: Record<string, (...args: any[]) => void> = {}
const onSetOption = ([key, val]: [string, (...args: any[]) => void]) => {
timePickerOptions[key] = val
}
return {
timePickerOptions,
getAvailableTime,
onSetOption,
}
}

View File

@ -19,17 +19,17 @@ export const basicTimeSpinnerProps = buildProps({
arrowControl: Boolean,
amPmMode: {
// 'a': am/pm; 'A': AM/PM
type: definePropType<'a' | 'A'>(String),
type: definePropType<'a' | 'A' | ''>(String),
default: '',
},
disabledHours: {
type: definePropType<(role: string, comparingDate?: Dayjs) => boolean[]>(
type: definePropType<(role: string, comparingDate?: Dayjs) => number[]>(
Function
),
},
disabledMinutes: {
type: definePropType<
(hour: number, role: string, comparingDate?: Dayjs) => boolean[]
(hour: number, role: string, comparingDate?: Dayjs) => number[]
>(Function),
},
disabledSeconds: {
@ -39,7 +39,7 @@ export const basicTimeSpinnerProps = buildProps({
minute: number,
role: string,
comparingDate?: Dayjs
) => boolean[]
) => number[]
>(Function),
},
} as const)

View File

@ -11,7 +11,7 @@ export const panelTimePickerProps = buildProps({
},
datetimeRole: String,
parsedValue: {
type: definePropType<string | Dayjs>([Object, String]),
type: definePropType<Dayjs>(Object),
},
format: {
type: String,

View File

@ -7,7 +7,7 @@ export const panelTimeRangeProps = buildProps({
visible: Boolean,
actualVisible: Boolean,
parsedValue: {
type: definePropType<Dayjs[]>(Array),
type: definePropType<[Dayjs, Dayjs]>(Array),
},
format: {
type: String,

View File

@ -8,7 +8,7 @@
:arrow-control="arrowControl"
:show-seconds="showSeconds"
:am-pm-mode="amPmMode"
:spinner-date="parsedValue"
:spinner-date="(parsedValue as any)"
:disabled-hours="disabledHours"
:disabled-minutes="disabledMinutes"
:disabled-seconds="disabledSeconds"
@ -44,6 +44,7 @@ import { EVENT_CODE } from '@element-plus/constants'
import { useLocale, useNamespace } from '@element-plus/hooks'
import { isUndefined } from '@element-plus/utils'
import { panelTimePickerProps } from '../props/panel-time-picker'
import { useTimePanel } from '../composables/use-time-panel'
import TimeSpinner from './basic-time-spinner.vue'
import { getAvailableArrs, useOldValue } from './useTimePicker'
@ -52,6 +53,18 @@ import type { Dayjs } from 'dayjs'
const props = defineProps(panelTimePickerProps)
const emit = defineEmits(['pick', 'select-range', 'set-picker-option'])
// Injections
const pickerBase = inject('EP_PICKER_BASE') as any
const {
arrowControl,
disabledHours,
disabledMinutes,
disabledSeconds,
defaultValue,
} = pickerBase.props
const { getAvailableHours, getAvailableMinutes, getAvailableSeconds } =
getAvailableArrs(disabledHours, disabledMinutes, disabledSeconds)
const ns = useNamespace('time')
const { t, lang } = useLocale()
// data
@ -111,65 +124,31 @@ const changeSelectionRange = (step: number) => {
const handleKeydown = (event: KeyboardEvent) => {
const code = event.code
if (code === EVENT_CODE.left || code === EVENT_CODE.right) {
const step = code === EVENT_CODE.left ? -1 : 1
const { left, right, up, down } = EVENT_CODE
if ([left, right].includes(code)) {
const step = code === left ? -1 : 1
changeSelectionRange(step)
event.preventDefault()
return
}
if (code === EVENT_CODE.up || code === EVENT_CODE.down) {
const step = code === EVENT_CODE.up ? -1 : 1
if ([up, down].includes(code)) {
const step = code === up ? -1 : 1
timePickerOptions['start_scrollDown'](step)
event.preventDefault()
return
}
}
const getRangeAvailableTime = (date: Dayjs) => {
const availableTimeGetters = {
hour: getAvailableHours,
minute: getAvailableMinutes,
second: getAvailableSeconds,
} as const
let result = date
;(['hour', 'minute', 'second'] as const).forEach((type) => {
if (availableTimeGetters[type]) {
let availableTimeSlots
const method = availableTimeGetters[type]
switch (type) {
case 'minute': {
availableTimeSlots = (method as typeof getAvailableMinutes)(
result.hour(),
props.datetimeRole
)
break
}
case 'second': {
availableTimeSlots = (method as typeof getAvailableSeconds)(
result.hour(),
result.minute(),
props.datetimeRole
)
break
}
default: {
availableTimeSlots = (method as typeof getAvailableHours)(
props.datetimeRole
)
break
}
}
const { timePickerOptions, onSetOption, getAvailableTime } = useTimePanel({
getAvailableHours,
getAvailableMinutes,
getAvailableSeconds,
})
if (
availableTimeSlots?.length &&
!availableTimeSlots.includes(result[type]())
) {
result = result[type](availableTimeSlots[0]) as unknown as Dayjs
}
}
})
return result
const getRangeAvailableTime = (date: Dayjs) => {
return getAvailableTime(date, props.datetimeRole || '', true)
}
const parseUserInput = (value: Dayjs) => {
@ -192,18 +171,4 @@ emit('set-picker-option', ['parseUserInput', parseUserInput])
emit('set-picker-option', ['handleKeydownInput', handleKeydown])
emit('set-picker-option', ['getRangeAvailableTime', getRangeAvailableTime])
emit('set-picker-option', ['getDefaultValue', getDefaultValue])
const timePickerOptions = {} as any
const onSetOption = (e: [string, number]) => {
timePickerOptions[e[0]] = e[1]
}
const pickerBase = inject('EP_PICKER_BASE') as any
const {
arrowControl,
disabledHours,
disabledMinutes,
disabledSeconds,
defaultValue,
} = pickerBase.props
const { getAvailableHours, getAvailableMinutes, getAvailableSeconds } =
getAvailableArrs(disabledHours, disabledMinutes, disabledSeconds)
</script>

View File

@ -22,7 +22,7 @@
:show-seconds="showSeconds"
:am-pm-mode="amPmMode"
:arrow-control="arrowControl"
:spinner-date="minDate"
:spinner-date="startTime"
:disabled-hours="disabledHours_"
:disabled-minutes="disabledMinutes_"
:disabled-seconds="disabledSeconds_"
@ -50,7 +50,7 @@
:show-seconds="showSeconds"
:am-pm-mode="amPmMode"
:arrow-control="arrowControl"
:spinner-date="maxDate"
:spinner-date="endTime"
:disabled-hours="disabledHours_"
:disabled-minutes="disabledMinutes_"
:disabled-seconds="disabledSeconds_"
@ -81,18 +81,23 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, inject, ref } from 'vue'
<script lang="ts" setup>
import { computed, inject, ref, unref } from 'vue'
import dayjs from 'dayjs'
import { union } from 'lodash-unified'
import { useLocale, useNamespace } from '@element-plus/hooks'
import { isArray } from '@element-plus/utils'
import { EVENT_CODE } from '@element-plus/constants'
import { panelTimeRangeProps } from '../props/panel-time-range'
import { useTimePanel } from '../composables/use-time-panel'
import TimeSpinner from './basic-time-spinner.vue'
import { getAvailableArrs, useOldValue } from './useTimePicker'
import type { Dayjs } from 'dayjs'
const props = defineProps(panelTimeRangeProps)
const emit = defineEmits(['pick', 'select-range', 'set-picker-option'])
const makeSelectRange = (start: number, end: number) => {
const result = []
for (let i = start; i <= end; i++) {
@ -100,274 +105,204 @@ const makeSelectRange = (start: number, end: number) => {
}
return result
}
export default defineComponent({
components: { TimeSpinner },
props: panelTimeRangeProps,
const { t, lang } = useLocale()
const nsTime = useNamespace('time')
const nsPicker = useNamespace('picker')
const pickerBase = inject('EP_PICKER_BASE') as any
const {
arrowControl,
disabledHours,
disabledMinutes,
disabledSeconds,
defaultValue,
} = pickerBase.props
emits: ['pick', 'select-range', 'set-picker-option'],
setup(props, ctx) {
const { t, lang } = useLocale()
const nsTime = useNamespace('time')
const nsPicker = useNamespace('picker')
const minDate = computed(() => props.parsedValue[0])
const maxDate = computed(() => props.parsedValue[1])
const oldValue = useOldValue(props)
const handleCancel = () => {
ctx.emit('pick', oldValue.value, false)
}
const showSeconds = computed(() => {
return props.format.includes('ss')
})
const amPmMode = computed(() => {
if (props.format.includes('A')) return 'A'
if (props.format.includes('a')) return 'a'
return ''
})
const minSelectableRange = ref([])
const maxSelectableRange = ref([])
const handleConfirm = (visible = false) => {
ctx.emit('pick', [minDate.value, maxDate.value], visible)
}
const handleMinChange = (date) => {
handleChange(date.millisecond(0), maxDate.value)
}
const handleMaxChange = (date) => {
handleChange(minDate.value, date.millisecond(0))
}
const isValidValue = (_date: Dayjs[]) => {
const parsedDate = _date.map((_) => dayjs(_).locale(lang.value))
const result = getRangeAvailableTime(parsedDate)
return parsedDate[0].isSame(result[0]) && parsedDate[1].isSame(result[1])
}
const handleChange = (_minDate, _maxDate) => {
// todo getRangeAvailableTime(_date).millisecond(0)
ctx.emit('pick', [_minDate, _maxDate], true)
}
const btnConfirmDisabled = computed(() => {
return minDate.value > maxDate.value
})
const selectionRange = ref([0, 2])
const setMinSelectionRange = (start, end) => {
ctx.emit('select-range', start, end, 'min')
selectionRange.value = [start, end]
}
const offset = computed(() => (showSeconds.value ? 11 : 8))
const setMaxSelectionRange = (start, end) => {
ctx.emit('select-range', start, end, 'max')
selectionRange.value = [start + offset.value, end + offset.value]
}
const changeSelectionRange = (step) => {
const list = showSeconds.value ? [0, 3, 6, 11, 14, 17] : [0, 3, 8, 11]
const mapping = ['hours', 'minutes'].concat(
showSeconds.value ? ['seconds'] : []
)
const index = list.indexOf(selectionRange.value[0])
const next = (index + step + list.length) % list.length
const half = list.length / 2
if (next < half) {
timePickerOptions['start_emitSelectRange'](mapping[next])
} else {
timePickerOptions['end_emitSelectRange'](mapping[next - half])
}
}
const handleKeydown = (event: KeyboardEvent) => {
const code = event.code
if (code === EVENT_CODE.left || code === EVENT_CODE.right) {
const step = code === EVENT_CODE.left ? -1 : 1
changeSelectionRange(step)
event.preventDefault()
return
}
if (code === EVENT_CODE.up || code === EVENT_CODE.down) {
const step = code === EVENT_CODE.up ? -1 : 1
const role = selectionRange.value[0] < offset.value ? 'start' : 'end'
timePickerOptions[`${role}_scrollDown`](step)
event.preventDefault()
return
}
}
const disabledHours_ = (role, compare) => {
const defaultDisable = disabledHours ? disabledHours(role) : []
const isStart = role === 'start'
const compareDate = compare || (isStart ? maxDate.value : minDate.value)
const compareHour = compareDate.hour()
const nextDisable = isStart
? makeSelectRange(compareHour + 1, 23)
: makeSelectRange(0, compareHour - 1)
return union(defaultDisable, nextDisable)
}
const disabledMinutes_ = (hour, role, compare) => {
const defaultDisable = disabledMinutes ? disabledMinutes(hour, role) : []
const isStart = role === 'start'
const compareDate = compare || (isStart ? maxDate.value : minDate.value)
const compareHour = compareDate.hour()
if (hour !== compareHour) {
return defaultDisable
}
const compareMinute = compareDate.minute()
const nextDisable = isStart
? makeSelectRange(compareMinute + 1, 59)
: makeSelectRange(0, compareMinute - 1)
return union(defaultDisable, nextDisable)
}
const disabledSeconds_ = (hour, minute, role, compare) => {
const defaultDisable = disabledSeconds
? disabledSeconds(hour, minute, role)
: []
const isStart = role === 'start'
const compareDate = compare || (isStart ? maxDate.value : minDate.value)
const compareHour = compareDate.hour()
const compareMinute = compareDate.minute()
if (hour !== compareHour || minute !== compareMinute) {
return defaultDisable
}
const compareSecond = compareDate.second()
const nextDisable = isStart
? makeSelectRange(compareSecond + 1, 59)
: makeSelectRange(0, compareSecond - 1)
return union(defaultDisable, nextDisable)
}
const getRangeAvailableTime = (dates: Array<Dayjs>) => {
return dates.map((_, index) =>
getRangeAvailableTimeEach(
dates[0],
dates[1],
index === 0 ? 'start' : 'end'
)
)
}
const { getAvailableHours, getAvailableMinutes, getAvailableSeconds } =
getAvailableArrs(disabledHours_, disabledMinutes_, disabledSeconds_)
const getRangeAvailableTimeEach = (
startDate: Dayjs,
endDate: Dayjs,
role
) => {
const availableMap = {
hour: getAvailableHours,
minute: getAvailableMinutes,
second: getAvailableSeconds,
}
const isStart = role === 'start'
let result = isStart ? startDate : endDate
const compareDate = isStart ? endDate : startDate
;['hour', 'minute', 'second'].forEach((_) => {
if (availableMap[_]) {
let availableArr
const method = availableMap[_]
if (_ === 'minute') {
availableArr = method(result.hour(), role, compareDate)
} else if (_ === 'second') {
availableArr = method(
result.hour(),
result.minute(),
role,
compareDate
)
} else {
availableArr = method(role, compareDate)
}
if (
availableArr &&
availableArr.length &&
!availableArr.includes(result[_]())
) {
const pos = isStart ? 0 : availableArr.length - 1
result = result[_](availableArr[pos])
}
}
})
return result
}
const parseUserInput = (value: Dayjs[] | Dayjs) => {
if (!value) return null
if (Array.isArray(value)) {
return value.map((_) => dayjs(_, props.format).locale(lang.value))
}
return dayjs(value, props.format).locale(lang.value)
}
const formatToString = (value: Dayjs[] | Dayjs) => {
if (!value) return null
if (Array.isArray(value)) {
return value.map((_) => _.format(props.format))
}
return value.format(props.format)
}
const getDefaultValue = () => {
if (Array.isArray(defaultValue)) {
return defaultValue.map((_) => dayjs(_).locale(lang.value))
}
const defaultDay = dayjs(defaultValue).locale(lang.value)
return [defaultDay, defaultDay.add(60, 'm')]
}
ctx.emit('set-picker-option', ['formatToString', formatToString])
ctx.emit('set-picker-option', ['parseUserInput', parseUserInput])
ctx.emit('set-picker-option', ['isValidValue', isValidValue])
ctx.emit('set-picker-option', ['handleKeydownInput', handleKeydown])
ctx.emit('set-picker-option', ['getDefaultValue', getDefaultValue])
ctx.emit('set-picker-option', [
'getRangeAvailableTime',
getRangeAvailableTime,
])
const timePickerOptions = {} as any
const onSetOption = (e) => {
timePickerOptions[e[0]] = e[1]
}
const pickerBase = inject('EP_PICKER_BASE') as any
const {
arrowControl,
disabledHours,
disabledMinutes,
disabledSeconds,
defaultValue,
} = pickerBase.props
return {
nsTime,
nsPicker,
arrowControl,
onSetOption,
setMaxSelectionRange,
setMinSelectionRange,
btnConfirmDisabled,
handleCancel,
handleConfirm,
t,
showSeconds,
minDate,
maxDate,
amPmMode,
handleMinChange,
handleMaxChange,
minSelectableRange,
maxSelectableRange,
disabledHours_,
disabledMinutes_,
disabledSeconds_,
}
},
const startTime = computed(() => props.parsedValue![0])
const endTime = computed(() => props.parsedValue![1])
const oldValue = useOldValue(props)
const handleCancel = () => {
emit('pick', oldValue.value, false)
}
const showSeconds = computed(() => {
return props.format.includes('ss')
})
const amPmMode = computed(() => {
if (props.format.includes('A')) return 'A'
if (props.format.includes('a')) return 'a'
return ''
})
const handleConfirm = (visible = false) => {
emit('pick', [startTime.value, endTime.value], visible)
}
const handleMinChange = (date: Dayjs) => {
handleChange(date.millisecond(0), endTime.value)
}
const handleMaxChange = (date: Dayjs) => {
handleChange(startTime.value, date.millisecond(0))
}
const isValidValue = (_date: Dayjs[]) => {
const parsedDate = _date.map((_) => dayjs(_).locale(lang.value))
const result = getRangeAvailableTime(parsedDate)
return parsedDate[0].isSame(result[0]) && parsedDate[1].isSame(result[1])
}
const handleChange = (start: Dayjs, end: Dayjs) => {
// todo getRangeAvailableTime(_date).millisecond(0)
emit('pick', [start, end], true)
}
const btnConfirmDisabled = computed(() => {
return startTime.value > endTime.value
})
const selectionRange = ref([0, 2])
const setMinSelectionRange = (start: number, end: number) => {
emit('select-range', start, end, 'min')
selectionRange.value = [start, end]
}
const offset = computed(() => (showSeconds.value ? 11 : 8))
const setMaxSelectionRange = (start: number, end: number) => {
emit('select-range', start, end, 'max')
const _offset = unref(offset)
selectionRange.value = [start + _offset, end + _offset]
}
const changeSelectionRange = (step: number) => {
const list = showSeconds.value ? [0, 3, 6, 11, 14, 17] : [0, 3, 8, 11]
const mapping = ['hours', 'minutes'].concat(
showSeconds.value ? ['seconds'] : []
)
const index = list.indexOf(selectionRange.value[0])
const next = (index + step + list.length) % list.length
const half = list.length / 2
if (next < half) {
timePickerOptions['start_emitSelectRange'](mapping[next])
} else {
timePickerOptions['end_emitSelectRange'](mapping[next - half])
}
}
const handleKeydown = (event: KeyboardEvent) => {
const code = event.code
const { left, right, up, down } = EVENT_CODE
if ([left, right].includes(code)) {
const step = code === left ? -1 : 1
changeSelectionRange(step)
event.preventDefault()
return
}
if ([up, down].includes(code)) {
const step = code === up ? -1 : 1
const role = selectionRange.value[0] < offset.value ? 'start' : 'end'
timePickerOptions[`${role}_scrollDown`](step)
event.preventDefault()
return
}
}
const disabledHours_ = (role: string, compare?: Dayjs) => {
const defaultDisable = disabledHours ? disabledHours(role) : []
const isStart = role === 'start'
const compareDate = compare || (isStart ? endTime.value : startTime.value)
const compareHour = compareDate.hour()
const nextDisable = isStart
? makeSelectRange(compareHour + 1, 23)
: makeSelectRange(0, compareHour - 1)
return union(defaultDisable, nextDisable)
}
const disabledMinutes_ = (hour: number, role: string, compare?: Dayjs) => {
const defaultDisable = disabledMinutes ? disabledMinutes(hour, role) : []
const isStart = role === 'start'
const compareDate = compare || (isStart ? endTime.value : startTime.value)
const compareHour = compareDate.hour()
if (hour !== compareHour) {
return defaultDisable
}
const compareMinute = compareDate.minute()
const nextDisable = isStart
? makeSelectRange(compareMinute + 1, 59)
: makeSelectRange(0, compareMinute - 1)
return union(defaultDisable, nextDisable)
}
const disabledSeconds_ = (
hour: number,
minute: number,
role: string,
compare?: Dayjs
) => {
const defaultDisable = disabledSeconds
? disabledSeconds(hour, minute, role)
: []
const isStart = role === 'start'
const compareDate = compare || (isStart ? endTime.value : startTime.value)
const compareHour = compareDate.hour()
const compareMinute = compareDate.minute()
if (hour !== compareHour || minute !== compareMinute) {
return defaultDisable
}
const compareSecond = compareDate.second()
const nextDisable = isStart
? makeSelectRange(compareSecond + 1, 59)
: makeSelectRange(0, compareSecond - 1)
return union(defaultDisable, nextDisable)
}
const getRangeAvailableTime = ([start, end]: Array<Dayjs>) => {
return [
getAvailableTime(start, 'start', true, end),
getAvailableTime(end, 'end', false, start),
]
}
const { getAvailableHours, getAvailableMinutes, getAvailableSeconds } =
getAvailableArrs(disabledHours_, disabledMinutes_, disabledSeconds_)
const {
timePickerOptions,
getAvailableTime,
onSetOption,
} = useTimePanel({
getAvailableHours,
getAvailableMinutes,
getAvailableSeconds,
})
const parseUserInput = (days: Dayjs[] | Dayjs) => {
if (!days) return null
if (isArray(days)) {
return days.map((d) => dayjs(d, props.format).locale(lang.value))
}
return dayjs(days, props.format).locale(lang.value)
}
const formatToString = (days: Dayjs[] | Dayjs) => {
if (!days) return null
if (isArray(days)) {
return days.map((d) => d.format(props.format))
}
return days.format(props.format)
}
const getDefaultValue = () => {
if (isArray(defaultValue)) {
return defaultValue.map((d: Date) => dayjs(d).locale(lang.value))
}
const defaultDay = dayjs(defaultValue).locale(lang.value)
return [defaultDay, defaultDay.add(60, 'm')]
}
emit('set-picker-option', ['formatToString', formatToString])
emit('set-picker-option', ['parseUserInput', parseUserInput])
emit('set-picker-option', ['isValidValue', isValidValue])
emit('set-picker-option', ['handleKeydownInput', handleKeydown])
emit('set-picker-option', ['getDefaultValue', getDefaultValue])
emit('set-picker-option', ['getRangeAvailableTime', getRangeAvailableTime])
</script>