element-plus/packages/components/time-picker/src/common/picker.vue
Aex f78407a409
fix(components): empty icon component judgment (#4178)
* fix(components): empty icon component judgment

* revert: globals components

* fix(components): el-icon missing import

* fix: use shallowRef for icon components

* refactor: remove shallowRef

* fix: remove unused code

* fix: social-link icon size

* fix: time picker icon

* fix: v-if judge
2021-11-05 17:44:02 +08:00

619 lines
17 KiB
Vue

<template>
<el-popper
ref="refPopper"
v-model:visible="pickerVisible"
manual-mode
:effect="Effect.LIGHT"
pure
trigger="click"
v-bind="$attrs"
:popper-class="`el-picker__popper ${popperClass}`"
:popper-options="elPopperOptions"
:fallback-placements="['bottom', 'top', 'right', 'left']"
transition="el-zoom-in-top"
:gpu-acceleration="false"
:stop-popper-mouse-event="false"
append-to-body
@before-enter="pickerActualVisible = true"
@after-leave="pickerActualVisible = false"
>
<template #trigger>
<el-input
v-if="!isRangeInput"
v-clickoutside:[popperPaneRef]="onClickOutside"
:model-value="displayValue"
:name="name"
:size="pickerSize"
:disabled="pickerDisabled"
:placeholder="placeholder"
class="el-date-editor"
:class="'el-date-editor--' + type"
:readonly="!editable || readonly || isDatesPicker || type === 'week'"
@input="onUserInput"
@focus="handleFocus"
@keydown="handleKeydown"
@change="handleChange"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
>
<template #prefix>
<el-icon
v-if="triggerIcon"
class="el-input__icon"
@click="handleFocus"
>
<component :is="triggerIcon"></component>
</el-icon>
</template>
<template #suffix>
<el-icon
v-if="showClose && clearIcon"
class="el-input__icon clear-icon"
@click="onClearIconClick"
>
<component :is="clearIcon" />
</el-icon>
</template>
</el-input>
<div
v-else
v-clickoutside:[popperPaneRef]="onClickOutside"
class="el-date-editor el-range-editor el-input__inner"
:class="[
'el-date-editor--' + type,
pickerSize ? `el-range-editor--${pickerSize}` : '',
pickerDisabled ? 'is-disabled' : '',
pickerVisible ? 'is-active' : '',
]"
@click="handleFocus"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
@keydown="handleKeydown"
>
<el-icon
v-if="triggerIcon"
class="el-input__icon el-range__icon"
@click="handleFocus"
>
<component :is="triggerIcon"></component>
</el-icon>
<input
autocomplete="off"
:name="name && name[0]"
:placeholder="startPlaceholder"
:value="displayValue && displayValue[0]"
:disabled="pickerDisabled"
:readonly="!editable || readonly"
class="el-range-input"
@input="handleStartInput"
@change="handleStartChange"
@focus="handleFocus"
/>
<slot name="range-separator">
<span class="el-range-separator">{{ rangeSeparator }}</span>
</slot>
<input
autocomplete="off"
:name="name && name[1]"
:placeholder="endPlaceholder"
:value="displayValue && displayValue[1]"
:disabled="pickerDisabled"
:readonly="!editable || readonly"
class="el-range-input"
@focus="handleFocus"
@input="handleEndInput"
@change="handleEndChange"
/>
<el-icon
v-if="clearIcon"
class="el-input__icon el-range__close-icon"
:class="{
'el-range__close-icon--hidden': !showClose,
}"
@click="onClearIconClick"
>
<component :is="clearIcon" />
</el-icon>
</div>
</template>
<template #default>
<slot
:visible="pickerVisible"
:actual-visible="pickerActualVisible"
:parsed-value="parsedValue"
:format="format"
:unlink-panels="unlinkPanels"
:type="type"
:default-value="defaultValue"
@pick="onPick"
@select-range="setSelectionRange"
@set-picker-option="onSetPickerOption"
@calendar-change="onCalendarChange"
@mousedown.stop
></slot>
</template>
</el-popper>
</template>
<script lang="ts">
import {
defineComponent,
ref,
computed,
nextTick,
inject,
watch,
provide,
} from 'vue'
import dayjs from 'dayjs'
import isEqual from 'lodash/isEqual'
import { useLocaleInject } from '@element-plus/hooks'
import { ClickOutside } from '@element-plus/directives'
import { elFormKey, elFormItemKey } from '@element-plus/tokens'
import ElInput from '@element-plus/components/input'
import ElIcon from '@element-plus/components/icon'
import ElPopper, { Effect } from '@element-plus/components/popper'
import { EVENT_CODE } from '@element-plus/utils/aria'
import { useGlobalConfig, isEmpty } from '@element-plus/utils/util'
import { Clock, Calendar } from '@element-plus/icons'
import { timePickerDefaultProps } from './props'
import type { Dayjs } from 'dayjs'
import type { ElFormContext, ElFormItemContext } from '@element-plus/tokens'
import type { Options } from '@popperjs/core'
interface PickerOptions {
isValidValue: (date: Dayjs) => boolean
handleKeydown: (event: KeyboardEvent) => void
parseUserInput: (value: Dayjs) => dayjs.Dayjs
formatToString: (value: Dayjs) => string | string[]
getRangeAvailableTime: (date: Dayjs) => dayjs.Dayjs
getDefaultValue: () => Dayjs
panelReady: boolean
handleClear: () => void
}
// Date object and string
const dateEquals = function (a: Date | any, b: Date | any) {
const aIsDate = a instanceof Date
const bIsDate = b instanceof Date
if (aIsDate && bIsDate) {
return a.getTime() === b.getTime()
}
if (!aIsDate && !bIsDate) {
return a === b
}
return false
}
const valueEquals = function (a: Array<Date> | any, b: Array<Date> | any) {
const aIsArray = a instanceof Array
const bIsArray = b instanceof Array
if (aIsArray && bIsArray) {
if (a.length !== b.length) {
return false
}
return (a as Array<Date>).every((item, index) => dateEquals(item, b[index]))
}
if (!aIsArray && !bIsArray) {
return dateEquals(a, b)
}
return false
}
const parser = function (
date: Date | string,
format: string,
lang: string
): Dayjs {
const day = isEmpty(format)
? dayjs(date).locale(lang)
: dayjs(date, format).locale(lang)
return day.isValid() ? day : undefined
}
const formatter = function (date: Date, format: string, lang: string) {
return isEmpty(format) ? date : dayjs(date).locale(lang).format(format)
}
export default defineComponent({
name: 'Picker',
components: {
ElInput,
ElPopper,
ElIcon,
},
directives: { clickoutside: ClickOutside },
props: timePickerDefaultProps,
emits: ['update:modelValue', 'change', 'focus', 'blur', 'calendar-change'],
setup(props, ctx) {
const ELEMENT = useGlobalConfig()
const { lang } = useLocaleInject()
const elForm = inject(elFormKey, {} as ElFormContext)
const elFormItem = inject(elFormItemKey, {} as ElFormItemContext)
const elPopperOptions = inject('ElPopperOptions', {} as Options)
const refPopper = ref(null)
const pickerVisible = ref(false)
const pickerActualVisible = ref(false)
const valueOnOpen = ref(null)
watch(pickerVisible, (val) => {
if (!val) {
userInput.value = null
nextTick(() => {
emitChange(props.modelValue)
})
ctx.emit('blur')
blurInput()
props.validateEvent && elFormItem.validate?.('blur')
} else {
valueOnOpen.value = props.modelValue
}
})
const emitChange = (val, isClear?: boolean) => {
// determine user real change only
if (isClear || !valueEquals(val, valueOnOpen.value)) {
ctx.emit('change', val)
props.validateEvent && elFormItem.validate?.('change')
}
}
const emitInput = (val) => {
if (!valueEquals(props.modelValue, val)) {
let formatValue
if (Array.isArray(val)) {
formatValue = val.map((_) =>
formatter(_, props.valueFormat, lang.value)
)
} else if (val) {
formatValue = formatter(val, props.valueFormat, lang.value)
}
ctx.emit('update:modelValue', val ? formatValue : val, lang.value)
}
}
const refInput = computed(() => {
if (refPopper.value.triggerRef) {
const _r = isRangeInput.value
? refPopper.value.triggerRef
: refPopper.value.triggerRef.$el
return [].slice.call(_r.querySelectorAll('input'))
}
return []
})
const setSelectionRange = (start, end, pos) => {
const _inputs = refInput.value
if (!_inputs.length) return
if (!pos || pos === 'min') {
_inputs[0].setSelectionRange(start, end)
_inputs[0].focus()
} else if (pos === 'max') {
_inputs[1].setSelectionRange(start, end)
_inputs[1].focus()
}
}
const onPick = (date: any = '', visible = false) => {
pickerVisible.value = visible
let result
if (Array.isArray(date)) {
result = date.map((_) => _.toDate())
} else {
// clear btn emit null
result = date ? date.toDate() : date
}
userInput.value = null
emitInput(result)
}
const handleFocus = (e) => {
if (props.readonly || pickerDisabled.value || pickerVisible.value) return
pickerVisible.value = true
ctx.emit('focus', e)
}
const handleBlur = () => {
pickerVisible.value = false
blurInput()
}
const pickerDisabled = computed(() => {
return props.disabled || elForm.disabled
})
const parsedValue = computed(() => {
let result
if (valueIsEmpty.value) {
if (pickerOptions.value.getDefaultValue) {
result = pickerOptions.value.getDefaultValue()
}
} else {
if (Array.isArray(props.modelValue)) {
result = props.modelValue.map((_) =>
parser(_, props.valueFormat, lang.value)
)
} else {
result = parser(props.modelValue, props.valueFormat, lang.value)
}
}
if (pickerOptions.value.getRangeAvailableTime) {
const availableResult =
pickerOptions.value.getRangeAvailableTime(result)
if (!isEqual(availableResult, result)) {
result = availableResult
emitInput(
Array.isArray(result)
? result.map((_) => _.toDate())
: result.toDate()
)
}
}
if (Array.isArray(result) && result.some((_) => !_)) {
result = []
}
return result
})
const displayValue = computed(() => {
if (!pickerOptions.value.panelReady) return
const formattedValue = formatDayjsToString(parsedValue.value)
if (Array.isArray(userInput.value)) {
return [
userInput.value[0] || (formattedValue && formattedValue[0]) || '',
userInput.value[1] || (formattedValue && formattedValue[1]) || '',
]
} else if (userInput.value !== null) {
return userInput.value
}
if (!isTimePicker.value && valueIsEmpty.value) return
if (!pickerVisible.value && valueIsEmpty.value) return
if (formattedValue) {
return isDatesPicker.value
? (formattedValue as Array<string>).join(', ')
: formattedValue
}
return ''
})
const isTimeLikePicker = computed(() => props.type.includes('time'))
const isTimePicker = computed(() => props.type.startsWith('time'))
const isDatesPicker = computed(() => props.type === 'dates')
const triggerIcon = computed(
() => props.prefixIcon || (isTimeLikePicker.value ? Clock : Calendar)
)
const showClose = ref(false)
const onClearIconClick = (event) => {
if (props.readonly || pickerDisabled.value) return
if (showClose.value) {
event.stopPropagation()
emitInput(null)
emitChange(null, true)
showClose.value = false
pickerVisible.value = false
pickerOptions.value.handleClear && pickerOptions.value.handleClear()
}
}
const valueIsEmpty = computed(() => {
return (
!props.modelValue ||
(Array.isArray(props.modelValue) && !props.modelValue.length)
)
})
const onMouseEnter = () => {
if (props.readonly || pickerDisabled.value) return
if (!valueIsEmpty.value && props.clearable) {
showClose.value = true
}
}
const onMouseLeave = () => {
showClose.value = false
}
const isRangeInput = computed(() => {
return props.type.indexOf('range') > -1
})
const pickerSize = computed(() => {
return props.size || elFormItem.size || ELEMENT.size
})
const popperPaneRef = computed(() => {
return refPopper.value?.popperRef
})
const onClickOutside = () => {
if (!pickerVisible.value) return
pickerVisible.value = false
}
const userInput = ref(null)
const handleChange = () => {
if (userInput.value) {
const value = parseUserInputToDayjs(displayValue.value)
if (value) {
if (isValidValue(value)) {
emitInput(
Array.isArray(value)
? value.map((_) => _.toDate())
: value.toDate()
)
userInput.value = null
}
}
}
if (userInput.value === '') {
emitInput(null)
emitChange(null)
userInput.value = null
}
}
const blurInput = () => {
refInput.value.forEach((input) => input.blur())
}
const parseUserInputToDayjs = (value) => {
if (!value) return null
return pickerOptions.value.parseUserInput(value)
}
const formatDayjsToString = (value) => {
if (!value) return null
return pickerOptions.value.formatToString(value)
}
const isValidValue = (value) => {
return pickerOptions.value.isValidValue(value)
}
const handleKeydown = (event) => {
const code = event.code
if (code === EVENT_CODE.esc) {
pickerVisible.value = false
event.stopPropagation()
return
}
if (code === EVENT_CODE.tab) {
if (!isRangeInput.value) {
handleChange()
pickerVisible.value = false
event.stopPropagation()
} else {
// user may change focus between two input
setTimeout(() => {
if (refInput.value.indexOf(document.activeElement) === -1) {
pickerVisible.value = false
blurInput()
}
}, 0)
}
return
}
if (code === EVENT_CODE.enter) {
if (
userInput.value === '' ||
isValidValue(parseUserInputToDayjs(displayValue.value))
) {
handleChange()
pickerVisible.value = false
}
event.stopPropagation()
return
}
// if user is typing, do not let picker handle key input
if (userInput.value) {
event.stopPropagation()
return
}
if (pickerOptions.value.handleKeydown) {
pickerOptions.value.handleKeydown(event)
}
}
const onUserInput = (e) => {
userInput.value = e
}
const handleStartInput = (event) => {
if (userInput.value) {
userInput.value = [event.target.value, userInput.value[1]]
} else {
userInput.value = [event.target.value, null]
}
}
const handleEndInput = (event) => {
if (userInput.value) {
userInput.value = [userInput.value[0], event.target.value]
} else {
userInput.value = [null, event.target.value]
}
}
const handleStartChange = () => {
const value = parseUserInputToDayjs(userInput.value && userInput.value[0])
if (value && value.isValid()) {
userInput.value = [formatDayjsToString(value), displayValue.value[1]]
const newValue = [value, parsedValue.value && parsedValue.value[1]]
if (isValidValue(newValue)) {
emitInput(newValue)
userInput.value = null
}
}
}
const handleEndChange = () => {
const value = parseUserInputToDayjs(userInput.value && userInput.value[1])
if (value && value.isValid()) {
userInput.value = [displayValue.value[0], formatDayjsToString(value)]
const newValue = [parsedValue.value && parsedValue.value[0], value]
if (isValidValue(newValue)) {
emitInput(newValue)
userInput.value = null
}
}
}
const pickerOptions = ref<Partial<PickerOptions>>({})
const onSetPickerOption = <T extends keyof PickerOptions>(
e: [T, PickerOptions[T]]
) => {
pickerOptions.value[e[0]] = e[1]
pickerOptions.value.panelReady = true
}
const onCalendarChange = (e) => {
ctx.emit('calendar-change', e)
}
provide('EP_PICKER_BASE', {
props,
})
return {
Effect,
// injected popper options
elPopperOptions,
isDatesPicker,
handleEndChange,
handleStartChange,
handleStartInput,
handleEndInput,
onUserInput,
handleChange,
handleKeydown,
popperPaneRef,
onClickOutside,
pickerSize,
isRangeInput,
onMouseLeave,
onMouseEnter,
onClearIconClick,
showClose,
triggerIcon,
onPick,
handleFocus,
handleBlur,
pickerVisible,
pickerActualVisible,
displayValue,
parsedValue,
setSelectionRange,
refPopper,
pickerDisabled,
onSetPickerOption,
onCalendarChange,
}
},
})
</script>